diff --git a/rust/pm-kit/Cargo.toml b/rust/pm-kit/Cargo.toml index 4955673..7db251e 100644 --- a/rust/pm-kit/Cargo.toml +++ b/rust/pm-kit/Cargo.toml @@ -14,6 +14,8 @@ embedded-hal-bus = "0.2" mipidsi = "0.9" embedded-graphics = "0.8" pm-ui = { path = "../pm-ui" } +track-format = { path = "../track-format" } +embedded-alloc = "0.6" [profile.release] opt-level = "s" diff --git a/rust/pm-kit/src/main.rs b/rust/pm-kit/src/main.rs index 31dccb0..0e0e95c 100644 --- a/rust/pm-kit/src/main.rs +++ b/rust/pm-kit/src/main.rs @@ -7,6 +7,13 @@ #![no_std] #![no_main] +extern crate alloc; +use alloc::vec::Vec; +use embedded_alloc::LlffHeap as Heap; + +#[global_allocator] +static HEAP: Heap = Heap::empty(); + use embedded_hal::delay::DelayNs; use embedded_hal::digital::{InputPin, OutputPin}; use embedded_hal::pwm::SetDutyCycle; @@ -49,6 +56,14 @@ impl OutputPin for NoCs { #[hal::entry] fn main() -> ! { + // heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM. + { + use core::mem::MaybeUninit; + const HEAP_SIZE: usize = 96 * 1024; + static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; + unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) } + } + let mut pac = hal::pac::Peripherals::take().unwrap(); let mut watchdog = hal::Watchdog::new(pac.WATCHDOG); let clocks = hal::clocks::init_clocks_and_plls( @@ -135,36 +150,148 @@ fn main() -> ! { .init(&mut timer) .unwrap(); - // ---- Peripheral self-test: buttons (GP15/GP14), joystick (GP26/GP27 ADC), speaker (GP13 PWM). - // Verifies the inputs + speaker actually work on the device; reads live and draws state. - let mut btn_a = pins.gpio15.into_pull_up_input(); - let mut btn_b = pins.gpio14.into_pull_up_input(); - + // ---- inputs + speaker ---- + let mut btn_a = pins.gpio15.into_pull_up_input(); // A = play/stop + let mut btn_b = pins.gpio14.into_pull_up_input(); // B = grid/notation view let mut adc = hal::adc::Adc::new(pac.ADC, &mut pac.RESETS); let mut joy_x = hal::adc::AdcPin::new(pins.gpio26).unwrap(); let mut joy_y = hal::adc::AdcPin::new(pins.gpio27).unwrap(); - let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS); let mut spk = pwm_slices.pwm6; // GP13 = PWM slice 6, channel B spk.set_div_int(125); - spk.set_top(600); // ~2 kHz click tone + spk.set_top(600); // ~2 kHz click spk.enable(); spk.channel_b.output_to(pins.gpio13); + // ---- built-in grooves ---- + const GROOVES: [&str; 4] = [ + "t120;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2=xxxxxxxx", + "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x", + "t140;kick:4=X..x;snare:4=.X.X;hatClosed:4/4=xxxxxxxxxxxxxxxx", + "t100;kick:4=Xxxx;claves:5=Xxxxx~", + ]; + const NAMES: [&str; 4] = ["Four on the floor", "Half-time shuffle", "Driving 16ths", "5 over 4"]; + + let mut idx = 0usize; + let mut track = track_format::parse(GROOVES[idx]); + let mut tempo: i64 = track.bpm; + let mut playing = true; + let mut notation = false; + + let mut bar_start = timer.get_counter().ticks(); + let mut last_step = usize::MAX; + let mut click_off_us: u64 = 0; + let (mut pa, mut pb) = (false, false); + let mut joy_zone = 0i8; + let mut full_redraw = true; let mut hb = false; + let mut hb_us = 0u64; + led.set_low().unwrap(); + loop { - let a = btn_a.is_low().unwrap(); - let b = btn_b.is_low().unwrap(); - let x: u16 = adc.read(&mut joy_x).unwrap(); - let y: u16 = adc.read(&mut joy_y).unwrap(); - let beep = a || b; - let _ = spk.channel_b.set_duty_cycle(if beep { 300 } else { 0 }); + let now = timer.get_counter().ticks(); - pm_ui::draw_peripheral_test(&mut display, &pm_ui::PeriphState { a, b, x, y, beep }).ok(); + // ---- inputs ---- + let a = btn_a.is_low().unwrap_or(false); + let b = btn_b.is_low().unwrap_or(false); + if a && !pa { + playing = !playing; + if playing { + bar_start = now; + last_step = usize::MAX; + } + full_redraw = true; + } + if b && !pb { + notation = !notation; + full_redraw = true; + } + pa = a; + pb = b; - hb = !hb; // LED heartbeat - let _ = if hb { led.set_high() } else { led.set_low() }; - timer.delay_ms(40); + let jx = adc.read(&mut joy_x).unwrap_or(2048); + let jy = adc.read(&mut joy_y).unwrap_or(2048); + // rotate the joystick reading 90° CCW for control + let rx = jy as i32; + let ry = 4095 - jx as i32; + let zone: i8 = if ry > 3200 { 1 } else if ry < 900 { 2 } else if rx > 3200 { 3 } else if rx < 900 { 4 } else { 0 }; + if zone != 0 && joy_zone == 0 { + match zone { + 1 => tempo = (tempo + 4).min(300), // up → tempo+ + 2 => tempo = (tempo - 4).max(30), // down → tempo- + 3 => { + idx = (idx + 1) % GROOVES.len(); // right → next groove + track = track_format::parse(GROOVES[idx]); + tempo = track.bpm; + bar_start = now; + last_step = usize::MAX; + } + 4 => { + idx = (idx + GROOVES.len() - 1) % GROOVES.len(); // left → prev + track = track_format::parse(GROOVES[idx]); + tempo = track.bpm; + bar_start = now; + last_step = usize::MAX; + } + _ => {} + } + full_redraw = true; + } + joy_zone = zone; + + // ---- clock ---- + let master = &track.lanes[0]; + let msteps = master.levels.len().max(1) as u64; + let beats = master.groups.iter().map(|&g| g as u64).sum::().max(1); + let bar_us = 60_000_000u64 * beats / tempo.max(1) as u64; + let step_us = (bar_us / msteps).max(1); + let elapsed = now.wrapping_sub(bar_start) % bar_us; + let cur_step = (elapsed / step_us) as usize; + let phase = elapsed as f32 / bar_us as f32; + + // ---- audio: click on a new step that has a hit ---- + if playing && cur_step != last_step { + let lvl = master.levels[cur_step.min(msteps as usize - 1)]; + if lvl > 0 { + let _ = spk.channel_b.set_duty_cycle(if lvl == 2 { 380 } else { 240 }); + click_off_us = now + if lvl == 2 { 28_000 } else { 14_000 }; + } + last_step = cur_step; + } + if now >= click_off_us { + let _ = spk.channel_b.set_duty_cycle(0); + } + + // ---- draw: full screen only on change; the bar-position strip every frame ---- + if full_redraw { + let lanes: Vec = track + .lanes + .iter() + .map(|l| pm_ui::LaneView { + name: &l.sound, + levels: &l.levels, + beats: l.groups.iter().map(|&g| g as u32).sum::().min(255) as u8, + poly: l.poly, + muted: l.mute, + }) + .collect(); + let screen = pm_ui::Screen { name: NAMES[idx], bpm: tempo, playing, phase, lanes: &lanes }; + if notation { + pm_ui::draw_notation(&mut display, &screen).ok(); + } else { + pm_ui::draw_metronome(&mut display, &screen).ok(); + } + full_redraw = false; + } + let _ = pm_ui::draw_progress(&mut display, phase, playing, 62); + + // heartbeat LED (~1 Hz) + if now.wrapping_sub(hb_us) > 500_000 { + hb = !hb; + let _ = if hb { led.set_high() } else { led.set_low() }; + hb_us = now; + } + timer.delay_ms(8); } } diff --git a/rust/pm-ui/src/lib.rs b/rust/pm-ui/src/lib.rs index 4d72c6d..7172633 100644 --- a/rust/pm-ui/src/lib.rs +++ b/rust/pm-ui/src/lib.rs @@ -182,6 +182,24 @@ where Ok(()) } +/// Thin bar-position strip the firmware can update cheaply every frame (no full redraw → no +/// flicker). `phase` 0..1 through the bar. Draw it at `top` (draw_metronome leaves y≈60 free). +pub fn draw_progress(d: &mut D, phase: f32, playing: bool, top: i32) -> Result<(), D::Error> +where + D: DrawTarget, +{ + let bb = d.bounding_box(); + let w = bb.size.width as i32; + const M: i32 = 12; + let bw = w - 2 * M; + Rectangle::new(Point::new(M, top), Size::new(bw as u32, 5)).into_styled(PrimitiveStyle::with_fill(PANEL)).draw(d)?; + if playing { + let fw = ((bw as f32) * phase.clamp(0.0, 1.0)) as u32; + Rectangle::new(Point::new(M, top), Size::new(fw, 5)).into_styled(PrimitiveStyle::with_fill(GREEN)).draw(d)?; + } + Ok(()) +} + /// Live state for the peripheral self-test. pub struct PeriphState { pub a: bool,