灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-kernel/src/drivers/daw.rs
//! LingStation for LingOS: the DAW window.
//!
//! A tracker-style song editor on top of the freestanding `ling-osdaw` engine: eight channels, 64 rows, fourteen built-in instruments, a master
//! effect rack (drive / echo / verb / crush), note scripts (arpeggiator, humanize, reverse, euclid, repeat, snap to key, transpose), Mix AI,
//! undo, Standard MIDI File export / import, a sample-accurate sequencer that the mixer pulls once per audio block (`block`), save / load to
//! lingfs, and a WAV export that lands in `music/` where the Media Player finds it. Tools are mouse buttons; `A` cycles the arpeggio style and
//! `F` the euclid density. Keyboard: the two piano rows enter notes (Z S X D C V G B H N J M , / Q 2 W 3 E R 5 T 6 Y 7 U I), arrows
//! move, Space plays, Tab / Shift+Tab change channel, K changes the channel's instrument, L mutes it, O writes a note-off, Backspace clears,
//! `,` `.` change the velocity, `[` `]` the octave, `-` `=` the tempo. The mouse works on every button, cell and channel header.

use crate::drivers::{font8x8, framebuffer, keyboard, mixer, theme};
use crate::fs::lingfs;
use alloc::boxed::Box;
use alloc::format;
use alloc::vec::Vec;
use ling_osdaw::{midi, mix, script, Cell, Engine, Song, CHANNELS, FX_NAMES, INSTRUMENTS, NOTE_OFF};

static mut ENGINE: Option<Box<Engine>> = None;
static mut CUR_ROW: usize = 0;
static mut CUR_CH: usize = 0;
static mut OCTAVE: u8 = 4;
static mut STATUS: [u8; 96] = [0; 96];
static mut STATUS_LEN: usize = 0;
static mut OUT: [i16; 8192] = [0; 8192];
static mut LAST_PLAY_ROW: usize = usize::MAX;

static mut VIS: usize = 16;
static mut UNDO: Vec<Song> = Vec::new();
static mut ARP_I: usize = 0;
static mut EU_I: usize = 0;
const EU_HITS: [usize; 5] = [3, 5, 7, 9, 11];

/// (label, button row): transport, then scripts, then tools, then the effect rack (labels of the last four show their level)
const BUTTONS: [(&[u8], u8); 29] = [
    (b"Play", 0),
    (b"Loop", 0),
    (b"-", 0),
    (b"+", 0),
    (b"Save", 0),
    (b"Load", 0),
    (b"WAV", 0),
    (b"Demo", 0),
    (b"Clear", 0),
    (b"Undo", 1),
    (b"Arp", 1),
    (b"Human", 1),
    (b"Rev", 1),
    (b"Euclid", 1),
    (b"Repeat", 1),
    (b"Snap", 1),
    (b"Xp-", 1),
    (b"Xp+", 1),
    (b"Mix", 2),
    (b"MIDI", 2),
    (b"LoadMid", 2),
    (b"Vol-", 2),
    (b"Vol+", 2),
    (b"Pan<", 2),
    (b"Pan>", 2),
    (b"Drive", 3),
    (b"Echo", 3),
    (b"Verb", 3),
    (b"Crush", 3),
];
const FX_BTN: usize = 25;
const BTN_ROWS: u32 = 4;

fn rows_vis() -> usize {
    unsafe { VIS }
}

/// remember the song before an edit so Undo can bring it back (the last eight)
fn snap() {
    unsafe {
        let u = &mut *&raw mut UNDO;
        if u.len() >= 8 {
            u.remove(0);
        }
        u.push(eng().song.clone());
    }
}

fn pitch_name(root: u8, minor: bool) -> alloc::string::String {
    const N: [&str; 12] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
    format!("{} {}", N[(root % 12) as usize], if minor { "minor" } else { "major" })
}

fn eng() -> &'static mut Engine {
    unsafe {
        let e = &mut *&raw mut ENGINE;
        if e.is_none() {
            let mut en = Box::new(Engine::new(Song::demo()));
            en.master = 0.9;
            *e = Some(en);
        }
        e.as_mut().unwrap()
    }
}

fn set_status(s: &str) {
    unsafe {
        let n = s.len().min(95);
        let st = &mut *&raw mut STATUS;
        st[..n].copy_from_slice(&s.as_bytes()[..n]);
        STATUS_LEN = n;
    }
}

/// The mixer's hook: the next `frames` stereo frames of the song, or None when nothing is sounding (so an idle window costs no CPU).
pub fn block(frames: usize) -> Option<&'static [i16]> {
    unsafe {
        let e = (*&raw mut ENGINE).as_mut()?;
        if !e.playing && e.peak < 0.0008 {
            return None;
        }
        let n = frames.min(4096);
        let out = &mut *&raw mut OUT;
        e.render(&mut out[..n * 2]);
        Some(&out[..n * 2])
    }
}

/// true while the song is playing (the Media Player and the mixer UI can show it)
pub fn playing() -> bool {
    unsafe { (*&raw const ENGINE).as_ref().map(|e| e.playing).unwrap_or(false) }
}

fn wake(e: &mut Engine) {
    e.peak = 1.0;
}

fn toggle_play() {
    let e = eng();
    if e.playing {
        e.stop();
        set_status("stopped");
    } else {
        e.row = unsafe { CUR_ROW };
        e.play();
        wake(e);
        set_status("playing");
    }
}

fn audition(note: u8) {
    let e = eng();
    let ch = unsafe { CUR_CH };
    let vel = e.song.cells[unsafe { CUR_ROW }][ch].vel;
    e.trigger(ch, note, vel);
    wake(e);
}

fn piano_offset(k: u8) -> Option<u8> {
    Some(match k {
        b'z' => 0,
        b's' => 1,
        b'x' => 2,
        b'd' => 3,
        b'c' => 4,
        b'v' => 5,
        b'g' => 6,
        b'b' => 7,
        b'h' => 8,
        b'n' => 9,
        b'j' => 10,
        b'm' => 11,
        b',' => 12,
        b'q' => 12,
        b'2' => 13,
        b'w' => 14,
        b'3' => 15,
        b'e' => 16,
        b'r' => 17,
        b'5' => 18,
        b't' => 19,
        b'6' => 20,
        b'y' => 21,
        b'7' => 22,
        b'u' => 23,
        b'i' => 24,
        _ => return None,
    })
}

fn move_cursor(dr: i32, dc: i32) {
    unsafe {
        let len = eng().song.length as i32;
        CUR_ROW = ((CUR_ROW as i32 + dr).rem_euclid(len)) as usize;
        CUR_CH = ((CUR_CH as i32 + dc).rem_euclid(CHANNELS as i32)) as usize;
    }
}

fn save() {
    let bytes = eng().song.to_bytes();
    match lingfs::write_in_dir("music", "song.lsx", &bytes) {
        Ok(()) => set_status("saved music/song.lsx"),
        Err(()) => set_status("could not save (no writable disk?)"),
    }
}

fn load() {
    let mut buf = [0u8; 2048];
    match lingfs::read_file_all("music/song.lsx", &mut buf) {
        Ok(Some(n)) => match Song::from_bytes(&buf[..n]) {
            Some(s) => {
                let e = eng();
                e.song = s;
                e.all_off();
                set_status("loaded music/song.lsx");
            },
            None => set_status("music/song.lsx is not a LingStation song"),
        },
        _ => set_status("no saved song yet (Save first)"),
    }
}

fn export_wav() {
    let song = eng().song.clone();
    if song.seconds() > 40.0 {
        set_status("song too long to export here (40 s max)");
        return;
    }
    let wav = Engine::render_wav(&song);
    match lingfs::write_in_dir_any("music", "LingStation.wav", &wav) {
        Ok(()) => set_status("wrote music/LingStation.wav (open it in Files)"),
        Err(()) => set_status("could not write the WAV"),
    }
}

fn read_midi() -> Result<Song, alloc::string::String> {
    let mut buf: Vec<u8> = alloc::vec![0u8; 48 * 1024];
    for path in ["music/LingStation.mid", "music/song.mid"] {
        match lingfs::read_file_all(path, &mut buf) {
            Ok(Some(n)) => return midi::from_midi(&buf[..n]).ok_or_else(|| format!("{} is not a usable MIDI file ({} bytes)", path, n)),
            Ok(None) => {},
            Err(()) => return Err(format!("could not read {}", path)),
        }
    }
    Err(alloc::string::String::from("no MIDI file in music/ (export one first)"))
}

fn tool(i: usize) {
    let e = eng();
    let ch = unsafe { CUR_CH };
    match i {
        9 => {
            let prev = unsafe { (*&raw mut UNDO).pop() };
            match prev {
                Some(s) => {
                    e.song = s;
                    e.all_off();
                    set_status("undone");
                },
                None => set_status("nothing to undo"),
            }
        },
        10 => {
            snap();
            let (shape, dir) = unsafe { (ARP_I % 4, (ARP_I / 4) % 3) };
            let n = script::arpeggiate(&mut e.song, ch, shape, dir as u8);
            set_status(&format!("arp {} {}: {} notes (A changes style)", script::ARP_CHORDS[shape], ["up", "down", "up-down"][dir], n));
        },
        11 => {
            snap();
            let n = script::humanize(&mut e.song, ch, 3);
            set_status(&format!("humanize: {} velocities", n));
        },
        12 => {
            snap();
            let n = script::reverse(&mut e.song, ch);
            set_status(&format!("reversed channel {} ({} cells)", ch + 1, n));
        },
        13 => {
            snap();
            let hits = EU_HITS[unsafe { EU_I } % EU_HITS.len()];
            let steps = 16;
            let cur = e.song.cells[unsafe { CUR_ROW }][ch].note;
            let key = if cur > 0 && cur != NOTE_OFF {
                cur
            } else {
                match e.song.chans[ch].inst {
                    5 => 36,
                    6 => 38,
                    7 => 42,
                    12 => 39,
                    13 => 45,
                    _ => 60,
                }
            };
            let n = script::euclid(&mut e.song, ch, hits, steps, key, 12);
            set_status(&format!("euclid {} of {}: {} hits (F changes density)", hits, steps, n));
        },
        14 => {
            snap();
            let n = script::repeat_half(&mut e.song, ch);
            set_status(&format!("repeated the first half ({} notes)", n));
        },
        15 => {
            snap();
            let (root, minor) = script::detect_key(&e.song);
            let n = script::snap_to_scale(&mut e.song, ch, root, minor);
            set_status(&format!("snapped {} notes to {}", n, pitch_name(root, minor)));
        },
        16 | 17 => {
            snap();
            let d = if i == 16 { -1 } else { 1 };
            let n = script::transpose_melodic(&mut e.song, d);
            set_status(&format!("transposed {} notes {}", n, if d < 0 { "down" } else { "up" }));
        },
        18 => {
            snap();
            set_status("mixing...");
            let r = mix::auto_mix(&mut e.song);
            let n = r.roles.iter().filter(|x| **x != mix::Role::Silent).count();
            set_status(&format!("Mix AI: balanced {} channels, peak {}%", n, (r.peak * 100.0) as u32));
        },
        19 => {
            let m = midi::to_midi(&e.song);
            match lingfs::write_in_dir_any("music", "LingStation.mid", &m) {
                Ok(()) => set_status(&format!("wrote music/LingStation.mid ({} bytes)", m.len())),
                Err(()) => set_status("could not write the MIDI file"),
            }
        },
        20 => match read_midi() {
            Ok(s) => {
                snap();
                let l = e.looping;
                e.song = s;
                e.looping = l;
                e.all_off();
                set_status("imported the MIDI file from music/");
            },
            Err(m) => set_status(&m),
        },
        21 | 22 => {
            let c = &mut e.song.chans[ch];
            c.vol = (c.vol as i32 + if i == 21 { -8 } else { 8 }).clamp(0, 100) as u8;
            set_status(&format!("channel {} volume {}", ch + 1, c.vol));
        },
        23 | 24 => {
            let c = &mut e.song.chans[ch];
            c.pan = (c.pan as i32 + if i == 23 { -10 } else { 10 }).clamp(-100, 100) as i8;
            set_status(&format!("channel {} pan {}", ch + 1, c.pan));
        },
        _ => {
            let k = i - FX_BTN;
            let l = &mut e.song.fx[k];
            *l = if *l >= 10 { 0 } else { *l + 2 };
            let (name, lv) = (FX_NAMES[k], *l);
            set_status(&format!("{} {}", name, lv));
            wake(e);
        },
    }
}

fn button(i: usize) {
    let e = eng();
    if i >= 9 {
        return tool(i);
    }
    match i {
        0 => toggle_play(),
        1 => {
            e.looping = !e.looping;
            set_status(if e.looping { "loop on" } else { "loop off" });
        },
        2 => e.song.bpm = e.song.bpm.saturating_sub(2).max(50),
        3 => e.song.bpm = (e.song.bpm + 2).min(220),
        4 => save(),
        5 => {
            snap();
            load()
        },
        6 => export_wav(),
        7 => {
            snap();
            let l = e.looping;
            e.song = Song::demo();
            e.looping = l;
            e.all_off();
            set_status("the demo groove");
        },
        _ => {
            snap();
            e.song = Song::empty();
            e.all_off();
            set_status("empty song");
        },
    }
}

pub fn key(k: u8) {
    let e = eng();
    unsafe {
        match k {
            b' ' => toggle_play(),
            keyboard::UP_ARROW => move_cursor(-1, 0),
            keyboard::DOWN_ARROW => move_cursor(1, 0),
            keyboard::LEFT_ARROW => move_cursor(0, -1),
            keyboard::RIGHT_ARROW => move_cursor(0, 1),
            keyboard::PGUP_KEY => move_cursor(-(rows_vis() as i32), 0),
            keyboard::PGDN_KEY => move_cursor(rows_vis() as i32, 0),
            keyboard::HOME_KEY => CUR_ROW = 0,
            9 => move_cursor(0, if keyboard::shift_down() { -1 } else { 1 }),
            b'1' => tool(9),
            b'4' => tool(10),
            b'8' => tool(11),
            b'9' => tool(12),
            b'0' => tool(13),
            b';' => tool(14),
            39 => tool(15), // '
            b'`' => tool(18),
            92 => tool(19), // backslash
            b'/' => tool(20),
            b'a' | b'A' => {
                ARP_I = (ARP_I + 1) % 12;
                set_status(&format!("arp style: {} {}", script::ARP_CHORDS[ARP_I % 4], ["up", "down", "up-down"][(ARP_I / 4) % 3]));
            },
            b'f' | b'F' => {
                EU_I = (EU_I + 1) % EU_HITS.len();
                set_status(&format!("euclid density: {} of 16", EU_HITS[EU_I]));
            },
            8 | 127 => {
                snap();
                e.song.cells[CUR_ROW][CUR_CH] = Cell::EMPTY;
                move_cursor(1, 0);
            },
            b'o' | b'O' => {
                snap();
                e.song.cells[CUR_ROW][CUR_CH] = Cell { note: NOTE_OFF, vel: 0 };
                move_cursor(1, 0);
            },
            b'k' | b'K' => {
                let c = &mut e.song.chans[CUR_CH];
                c.inst = (c.inst + 1) % INSTRUMENTS.len() as u8;
                let name = INSTRUMENTS[c.inst as usize];
                e.all_off();
                set_status(name);
            },
            b'l' | b'L' => {
                let c = &mut e.song.chans[CUR_CH];
                c.mute = !c.mute;
                set_status(if c.mute { "channel muted" } else { "channel on" });
            },
            b'.' | b'>' => {
                let c = &mut e.song.cells[CUR_ROW][CUR_CH];
                if c.note > 0 && c.note != NOTE_OFF {
                    c.vel = (if c.vel == 0 { 11 } else { c.vel } + 1).min(15);
                }
            },
            b'<' => {
                let c = &mut e.song.cells[CUR_ROW][CUR_CH];
                if c.note > 0 && c.note != NOTE_OFF {
                    c.vel = (if c.vel == 0 { 11 } else { c.vel }).saturating_sub(1).max(1);
                }
            },
            b'[' => OCTAVE = OCTAVE.saturating_sub(1).max(1),
            b']' => OCTAVE = (OCTAVE + 1).min(8),
            b'-' => button(2),
            b'=' | b'+' => button(3),
            b'p' | b'P' => toggle_play(),
            _ => {
                if let Some(off) = piano_offset(k) {
                    let note = (12 * (OCTAVE as u32 + 1) + off as u32).min(127) as u8;
                    let vel = e.song.cells[CUR_ROW][CUR_CH].vel;
                    snap();
                    e.song.cells[CUR_ROW][CUR_CH] = Cell { note, vel: if vel == 0 { 11 } else { vel } };
                    audition(note);
                    move_cursor(1, 0);
                }
            },
        }
    }
}

// ── layout shared by drawing and the mouse ────────────────────────────────────

struct Lay {
    adv: u32,
    rowh: u32,
    /// height of the four button rows
    br: u32,
    grid_y: u32,
    rn_w: u32,
    cell_w: u32,
}

/// text scale: the 8x8 font, doubled on a big screen so the grid is readable
fn ts() -> u32 {
    (framebuffer::height() / 540).max(font8x8::scale()).max(1)
}

fn dtext(x: u32, y: u32, s: &[u8], fg: u32, bg: u32) {
    let sc = ts();
    for (i, c) in s.iter().enumerate() {
        font8x8::draw_char_scaled(x + i as u32 * 8 * sc, y, *c, fg, bg, sc);
    }
}

fn lay(y: u32) -> Lay {
    let adv = 8 * ts();
    let rowh = 8 * ts() + 3 * ts();
    let br = BTN_ROWS * (rowh + 8);
    Lay { adv, rowh, br, grid_y: y + br + 2 * rowh + 12, rn_w: adv * 3 + 6, cell_w: adv * 7 + 4 }
}

/// the text on a button; the effect knobs show their level
fn label(i: usize, buf: &mut [u8; 12]) -> usize {
    let t = BUTTONS[i].0;
    let mut n = t.len().min(8);
    buf[..n].copy_from_slice(&t[..n]);
    if i >= FX_BTN {
        let lv = eng().song.fx[i - FX_BTN];
        buf[n] = b' ';
        n += 1;
        if lv >= 10 {
            buf[n] = b'1';
            buf[n + 1] = b'0';
            n += 2;
        } else {
            buf[n] = b'0' + lv;
            n += 1;
        }
    }
    n
}

fn button_rect(x: u32, y: u32, i: usize) -> (u32, u32, u32, u32) {
    let l = lay(y);
    let row = BUTTONS[i].1;
    let mut bx = x;
    for b in 0..i {
        if BUTTONS[b].1 == row {
            let mut lb = [0u8; 12];
            bx += (label(b, &mut lb) as u32 + 2) * l.adv + 6;
        }
    }
    let mut lb = [0u8; 12];
    let n = label(i, &mut lb);
    (bx, y + row as u32 * (l.rowh + 8), (n as u32 + 2) * l.adv, l.rowh + 4)
}

fn inside(mx: i64, my: i64, r: (u32, u32, u32, u32)) -> bool {
    mx >= r.0 as i64 && mx < (r.0 + r.2) as i64 && my >= r.1 as i64 && my < (r.1 + r.3) as i64
}

/// A click inside the window body; (x, y) is the content origin `draw` was given.
pub fn click(mx: i64, my: i64, x: u32, y: u32) -> bool {
    for i in 0..BUTTONS.len() {
        if inside(mx, my, button_rect(x, y, i)) {
            button(i);
            mixer::jingle(mixer::EVENT_CLICK);
            return true;
        }
    }
    let l = lay(y);
    let page = unsafe { CUR_ROW } / rows_vis();
    // channel headers cycle the instrument (right half) or toggle mute (left half)
    let hy = l.grid_y - l.rowh - 2;
    if my >= hy as i64 && my < (hy + l.rowh) as i64 {
        for c in 0..CHANNELS {
            let cx = x + l.rn_w + c as u32 * l.cell_w;
            if mx >= cx as i64 && mx < (cx + l.cell_w) as i64 {
                let e = eng();
                unsafe { CUR_CH = c };
                if mx < (cx + l.cell_w / 2) as i64 {
                    e.song.chans[c].mute = !e.song.chans[c].mute;
                } else {
                    e.song.chans[c].inst = (e.song.chans[c].inst + 1) % INSTRUMENTS.len() as u8;
                    e.all_off();
                    set_status(INSTRUMENTS[e.song.chans[c].inst as usize]);
                }
                return true;
            }
        }
    }
    // grid cells
    for r in 0..rows_vis() {
        let row = page * rows_vis() + r;
        if row >= eng().song.length as usize {
            break;
        }
        let ry = l.grid_y + r as u32 * l.rowh;
        if my >= ry as i64 && my < (ry + l.rowh) as i64 {
            for c in 0..CHANNELS {
                let cx = x + l.rn_w + c as u32 * l.cell_w;
                if mx >= cx as i64 && mx < (cx + l.cell_w) as i64 {
                    unsafe {
                        CUR_ROW = row;
                        CUR_CH = c;
                    }
                    return true;
                }
            }
            // the row number: move the play position
            if mx < (x + l.rn_w) as i64 {
                unsafe { CUR_ROW = row };
                let e = eng();
                e.seek(row);
                return true;
            }
        }
    }
    false
}

fn put_u32(buf: &mut [u8], mut n: usize, v: u32, width: usize) -> usize {
    let mut digits = [0u8; 10];
    let mut i = 0;
    let mut x = v;
    loop {
        digits[i] = b'0' + (x % 10) as u8;
        i += 1;
        x /= 10;
        if x == 0 {
            break;
        }
    }
    for _ in i..width {
        buf[n] = b' ';
        n += 1;
    }
    while i > 0 {
        i -= 1;
        buf[n] = digits[i];
        n += 1;
    }
    n
}

fn put_str(buf: &mut [u8], n: usize, s: &[u8]) -> usize {
    let k = s.len().min(buf.len() - n);
    buf[n..n + k].copy_from_slice(&s[..k]);
    n + k
}

pub fn draw(x: u32, y: u32, w: u32, h: u32) {
    let e = eng();
    let panel = theme::color(theme::SLOT_PANEL);
    let border = theme::color(theme::SLOT_PANEL_BORDER);
    let text = theme::color(theme::SLOT_TEXT);
    let dim = theme::color(theme::SLOT_DIM);
    let accent = theme::color(theme::SLOT_ACCENT);
    let beat = theme::color(theme::SLOT_DOT_DIM);
    let l = lay(y);
    // as many grid rows as the window has room for (two help lines below when they fit)
    let room = h.saturating_sub(l.grid_y - y) / l.rowh.max(1);
    unsafe { VIS = (room.saturating_sub(2) as usize).clamp(6, 16) };
    // the buttons: transport, scripts, tools, effect rack
    for i in 0..BUTTONS.len() {
        let r = button_rect(x, y, i);
        let on = (i == 0 && e.playing) || (i == 1 && e.looping) || (i >= FX_BTN && e.song.fx[i - FX_BTN] > 0);
        framebuffer::back_fill_rounded_rect(r.0, r.1, r.2, r.3, 4, if on { accent } else { border });
        let mut lb = [0u8; 12];
        let n = label(i, &mut lb);
        dtext(r.0 + l.adv, r.1 + 3, &lb[..n], if on { panel } else { text }, if on { accent } else { border });
    }
    // the state line
    let mut line = [0u8; 120];
    let mut n = 0;
    n = put_str(&mut line, n, b"BPM ");
    n = put_u32(&mut line, n, e.song.bpm as u32, 0);
    n = put_str(&mut line, n, b"  oct ");
    n = put_u32(&mut line, n, unsafe { OCTAVE } as u32, 0);
    n = put_str(&mut line, n, b"  row ");
    n = put_u32(&mut line, n, unsafe { CUR_ROW } as u32, 2);
    n = put_str(&mut line, n, b"/");
    n = put_u32(&mut line, n, e.song.length as u32, 0);
    n = put_str(&mut line, n, b"  ch ");
    n = put_u32(&mut line, n, unsafe { CUR_CH } as u32 + 1, 0);
    n = put_str(&mut line, n, b" ");
    n = put_str(&mut line, n, INSTRUMENTS[e.song.chans[unsafe { CUR_CH }].inst as usize].as_bytes());
    dtext(x, y + l.br, &line[..n], text, panel);
    let st: &[u8] = unsafe {
        let all = &*&raw const STATUS;
        &all[..STATUS_LEN]
    };
    if !st.is_empty() {
        dtext(x + w.saturating_sub((st.len() as u32 + 1) * l.adv), y + l.br, st, accent, panel);
    }
    // meter
    let m = ((e.peak.min(1.0)) * (w as f32 * 0.25)) as u32;
    framebuffer::back_fill_rect(x, y + l.br + l.rowh + 3, w / 4, 3, beat);
    framebuffer::back_fill_rect(x, y + l.br + l.rowh + 3, m, 3, accent);
    // channel headers
    let hy = l.grid_y - l.rowh - 2;
    for c in 0..CHANNELS {
        let cx = x + l.rn_w + c as u32 * l.cell_w;
        let ch = e.song.chans[c];
        let cur = c == unsafe { CUR_CH };
        let mut hb = [0u8; 8];
        let name = INSTRUMENTS[ch.inst as usize].as_bytes();
        let k = name.len().min(7);
        hb[..k].copy_from_slice(&name[..k]);
        let colr = if ch.mute { dim } else if cur { accent } else { text };
        dtext(cx, hy, &hb[..k], colr, panel);
        if ch.mute {
            framebuffer::back_fill_rect(cx, hy + 3 * ts(), (k as u32) * l.adv, ts(), dim);
        }
    }
    // the grid: one page of 16 rows (the page follows the cursor and the playhead while playing)
    let follow = if e.playing { e.row } else { unsafe { CUR_ROW } };
    let page = follow / rows_vis();
    for r in 0..rows_vis() {
        let row = page * rows_vis() + r;
        if row >= e.song.length as usize {
            break;
        }
        let ry = l.grid_y + r as u32 * l.rowh;
        let playing_row = e.playing && row == e.row;
        let rowbg = if playing_row { border } else if row % (e.song.rpb.max(1) as usize) == 0 { beat } else { panel };
        framebuffer::back_fill_rect(x, ry, l.rn_w + l.cell_w * CHANNELS as u32, l.rowh, rowbg);
        let mut rb = [0u8; 3];
        let rn = put_u32(&mut rb, 0, row as u32, 2);
        dtext(x + 2, ry + 1, &rb[..rn], if playing_row { accent } else { dim }, rowbg);
        for c in 0..CHANNELS {
            let cx = x + l.rn_w + c as u32 * l.cell_w;
            let cell = e.song.cells[row][c];
            let (nt, v) = Song::cell_text(cell);
            let is_cur = row == unsafe { CUR_ROW } && c == unsafe { CUR_CH };
            let (fg, bg) = if is_cur { (panel, accent) } else if cell.note > 0 { (text, rowbg) } else { (dim, rowbg) };
            if is_cur {
                framebuffer::back_fill_rect(cx, ry, l.cell_w - 2, l.rowh, accent);
            }
            let mut cb = [b' '; 5];
            cb[..3].copy_from_slice(&nt);
            cb[4] = v;
            dtext(cx + 1, ry + 1, &cb, fg, bg);
        }
    }
    // help
    let hy = l.grid_y + rows_vis() as u32 * l.rowh + 8;
    if h > hy.saturating_sub(y) + 2 * l.rowh {
        dtext(x, hy, b"piano Z S X D C V G B H N J M  [ ] octave  Space play  A arp style  F euclid density  Tab channel", dim, panel);
        dtext(x, hy + l.rowh, b"1 undo  4 arp  8 human  9 rev  0 euclid  ; repeat  ' snap  ` Mix AI  \\ MIDI out  / MIDI in", dim, panel);
    }
    // follow-up: keep the cursor on the playhead page when stopped is the user's own
    unsafe {
        if e.playing && e.row != LAST_PLAY_ROW {
            LAST_PLAY_ROW = e.row;
        }
    }
}

← Back to index