diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index ca598d8..d27de3a 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -21,7 +21,32 @@ pub const SPLASH_DURATION_MS: u32 = 3000; pub const DIAG_LOGGING: bool = false; // set true to see raw ADC / calibration logs pub const LOG_INTERVAL_MS: u32 = 500; -pub const LOG_SWITCH: usize = 0; // HE1 — first switch +pub const LOG_SWITCH: usize = 89; // HE90 — debugging spurious triggers + +// ── Baseline drift tracking ─────────────────────────────────────────────────── +// IIR: baseline += (filtered - baseline) / ALPHA each idle tick. +// At 1 kHz this gives a ~1 s time constant — fast enough to absorb boot drift, +// slow enough that held notes never corrupt the baseline. +pub const BASELINE_TRACKING_ALPHA: u32 = 1024; +// Hard cap on how far the tracked baseline may wander from the boot-calibrated +// value. Must stay well under RELEASE_DELTA so sustained crosstalk from a held +// chord on neighboring switches can never, by itself, drift a baseline far +// enough to cross FIRST_DELTA/RELEASE_DELTA once the chord releases. +pub const BASELINE_DRIFT_MAX: u16 = 40; + +// ── Special function keys ───────────────────────────────────────────────────── +pub const RECALIBRATE_KEY: usize = 74; // HE75 → snapshot recalibration +pub const ALL_NOTES_OFF_KEY: usize = 75; // HE76 → CC 123 all channels +pub const RECALIBRATE_FLASH_MS: u32 = 1500; + +// ── Voice select keys (HE71–HE73, AM10 decoder) ─────────────────────────────── +// PC sent on melody_channel. Mapping: key A→triangle, B→square, C→saw. +pub const VOICE_KEY_A: usize = 70; // HE71 +pub const VOICE_KEY_B: usize = 71; // HE72 +pub const VOICE_KEY_C: usize = 72; // HE73 +pub const VOICE_PC_A: u8 = 0; // triangle +pub const VOICE_PC_B: u8 = 1; // square +pub const VOICE_PC_C: u8 = 2; // saw // ── Settings screen ─────────────────────────────────────────────────────────── pub const SETTINGS_OPEN: usize = 73; // HE74 → open / close settings @@ -133,7 +158,7 @@ pub const SWITCH_TO_NOTE: [u8; NUM_SWITCHES] = [ 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, // Row 5 — HE59–70 (base 54) 54, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, - 80, // HE71 — skipped 56 and 78 (those keys don't exist on the board) + 80, // HE70 — skipped 56 and 78 (those keys don't exist on the board) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; diff --git a/keyboard_keyboard/code/src/display/mod.rs b/keyboard_keyboard/code/src/display/mod.rs index d2a936b..409faa4 100644 --- a/keyboard_keyboard/code/src/display/mod.rs +++ b/keyboard_keyboard/code/src/display/mod.rs @@ -1,5 +1,5 @@ pub mod screens; -pub use screens::{draw_main, draw_settings, draw_splash}; +pub use screens::{draw_main, draw_recalibrating, draw_settings, draw_splash}; const NOTE_NAMES: [&str; 12] = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", diff --git a/keyboard_keyboard/code/src/display/screens.rs b/keyboard_keyboard/code/src/display/screens.rs index 840e098..7f34454 100644 --- a/keyboard_keyboard/code/src/display/screens.rs +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -17,6 +17,19 @@ use heapless::String; const SPRITE_ATLAS: &[u8] = include_bytes!("../../../../images/Keyboard-Keyboard-Spritesheet.raw"); +pub fn draw_recalibrating(disp: &mut LcdDisplay) { + disp.clear(); + Text::with_alignment( + "RECALIBRATING", + Point::new(64, 20), + MonoTextStyle::new(&FONT_6X9, BinaryColor::On), + Alignment::Center, + ) + .draw(disp) + .ok(); + disp.flush().ok(); +} + pub fn draw_splash(disp: &mut LcdDisplay) { disp.clear(); let atlas = ImageRawBE::::new(SPRITE_ATLAS, 128); @@ -102,6 +115,31 @@ pub fn draw_main(disp: &mut LcdDisplay, state: &DisplayState) { .draw(disp) .ok(); + // Bottom-right volume level (0–10) + if let Some(vol) = state.volume_level { + let mut vol_str: String<4> = String::new(); + write!(vol_str, "{}", vol).ok(); + Text::with_alignment( + vol_str.as_str(), + center + Point::new(54, 15), + MonoTextStyle::new(&FONT_6X9, off), + Alignment::Center, + ) + .draw(disp) + .ok(); + } + + // Bottom-left waveform icon (20×8 px at display position 5,24) + if let Some(voice) = state.current_voice { + let atlas_x = match voice { + 0 => 40, // triangle + 1 => 20, // square + _ => 0, // saw + }; + let icon = atlas.sub_image(&Rectangle::new(Point::new(atlas_x, 64), Size::new(20, 8))); + Image::new(&icon, Point::new(5, 24)).draw(disp).ok(); + } + disp.flush().ok(); } diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index be2a0ce..c79df75 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -67,6 +67,7 @@ mod app { settings: Settings, settings_active: bool, settings_selected: usize, + recalibrate_pending: bool, } // ── Local resources ─────────────────────────────────────────────────────── @@ -94,6 +95,7 @@ mod app { midi_sender: MidiSender, midi_rx: Rx, filters: [ChannelFilter; NUM_SWITCHES], + dynamic_baselines: [u16; NUM_SWITCHES], last_pitch_bend: u16, last_vibrato_cc: u8, led1_off_at: u32, @@ -336,6 +338,7 @@ mod app { settings: Settings::default(), settings_active: false, settings_selected: 0, + recalibrate_pending: false, }, Local { audio: system.audio, @@ -359,6 +362,7 @@ mod app { midi_sender, midi_rx, filters, + dynamic_baselines: baselines, last_pitch_bend: 0x2000, last_vibrato_cc: 0, led1_off_at: 0, @@ -400,13 +404,18 @@ mod app { return; } + let state = ctx.shared.display_state.lock(|s| *s); + if state.recalibrating { + crate::display::draw_recalibrating(disp); + return; + } + let active = ctx.shared.settings_active.lock(|a| *a); if active { let selected = ctx.shared.settings_selected.lock(|s| *s); let settings = ctx.shared.settings.lock(|s| *s); crate::display::draw_settings(disp, selected, &settings); } else { - let state = ctx.shared.display_state.lock(|s| *s); crate::display::draw_main(disp, &state); } } @@ -420,10 +429,13 @@ mod app { #[task( binds = TIM2, local = [timer2, adc, adc_pins, enb_a, enb_b, enb_c, adc_pin_a9, adc_pin_a10, - adc_pin_a11, pot_last_cc, s0, s1, s2, mux_raw, filters, last_pitch_bend, - last_vibrato_cc, led1, led2, led3, midi_rx, led1_off_at, led2_off_at], + adc_pin_a11, pot_last_cc, s0, s1, s2, mux_raw, filters, dynamic_baselines, + last_pitch_bend, last_vibrato_cc, led1, led2, led3, midi_rx, + led1_off_at, led2_off_at, + recalibrate_show_until: u32 = 0, + recalibrate_flashing: bool = false], shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag, splash_done, - settings_active], + settings_active, recalibrate_pending, display_state], priority = 15 )] fn timer_handler(mut ctx: timer_handler::Context) { @@ -443,7 +455,9 @@ mod app { display_update::spawn().ok(); } - let baselines = ctx.shared.baselines.lock(|b| *b); + // Anchor for drift clamping — the last boot/recalibration snapshot, never + // touched by per-tick tracking. + let anchor_baselines = ctx.shared.baselines.lock(|b| *b); let settings_active = ctx.shared.settings_active.lock(|a| *a); let mut pending: heapless::Vec<(usize, SwitchEvent), 32> = heapless::Vec::new(); @@ -473,10 +487,10 @@ mod app { } // ── Switch state machine ─────────────────────────────────────────────── - let mut pb_filt_down = baselines[PITCH_BEND_DOWN]; - let mut pb_filt_up = baselines[PITCH_BEND_UP]; - let mut vib_filt_a = baselines[VIBRATO_A]; - let mut vib_filt_b = baselines[VIBRATO_B]; + let mut pb_filt_down = ctx.local.dynamic_baselines[PITCH_BEND_DOWN]; + let mut pb_filt_up = ctx.local.dynamic_baselines[PITCH_BEND_UP]; + let mut vib_filt_a = ctx.local.dynamic_baselines[VIBRATO_A]; + let mut vib_filt_b = ctx.local.dynamic_baselines[VIBRATO_B]; ctx.shared.switch_states.lock(|states| { for (switch_idx, &(mux, ch)) in SWITCH_MAP.iter().enumerate() { @@ -499,18 +513,44 @@ mod app { } else { vib_filt_b = filtered; } - } else if let Some(event) = - states[switch_idx].update(filtered, baselines[switch_idx], now, switch_idx) - { + } else if let Some(event) = states[switch_idx].update( + filtered, + ctx.local.dynamic_baselines[switch_idx], + now, + switch_idx, + ) { pending.push((switch_idx, event)).ok(); } + + // Idle-state baseline tracking: absorb slow drift on floating / + // unconnected inputs without ever corrupting an active-press baseline. + let at_rest = if is_analog && !settings_active { + filtered.abs_diff(ctx.local.dynamic_baselines[switch_idx]) < RELEASE_DELTA + } else { + states[switch_idx].is_idle() + && filtered.abs_diff(ctx.local.dynamic_baselines[switch_idx]) + < RELEASE_DELTA + }; + if at_rest { + let db = &mut ctx.local.dynamic_baselines[switch_idx]; + let tracked = ((*db as u32 * (BASELINE_TRACKING_ALPHA - 1) + filtered as u32) + / BASELINE_TRACKING_ALPHA) as u16; + // Clamp to the boot/recalibration anchor so sustained crosstalk from a + // held chord can never drift a neighboring baseline far enough to cause + // a phantom trigger once the chord releases. + let anchor = anchor_baselines[switch_idx]; + *db = tracked.clamp( + anchor.saturating_sub(BASELINE_DRIFT_MAX), + anchor.saturating_add(BASELINE_DRIFT_MAX), + ); + } } }); // ── Pitch bend (rate-limited, only when settings is closed) ─────────── if !settings_active && now % PITCH_BEND_INTERVAL_MS == 0 { - let delta_down = pb_filt_down.abs_diff(baselines[PITCH_BEND_DOWN]); - let delta_up = pb_filt_up.abs_diff(baselines[PITCH_BEND_UP]); + let delta_down = pb_filt_down.abs_diff(ctx.local.dynamic_baselines[PITCH_BEND_DOWN]); + let delta_up = pb_filt_up.abs_diff(ctx.local.dynamic_baselines[PITCH_BEND_UP]); let pb_value = if delta_down < RELEASE_DELTA && delta_up < RELEASE_DELTA { 0x2000u16 } else { @@ -534,7 +574,7 @@ mod app { if DIAG_LOGGING && now % LOG_INTERVAL_MS == 0 { let (lk_mux, lk_ch) = SWITCH_MAP[LOG_SWITCH]; let lk_filt = ctx.local.filters[LOG_SWITCH].last_output(); - let lk_base = baselines[LOG_SWITCH]; + let lk_base = ctx.local.dynamic_baselines[LOG_SWITCH]; info!( "DIAG HE{} raw={} filt={} base={} delta={:+}", HE_NUM[LOG_SWITCH], @@ -556,8 +596,8 @@ mod app { // ── Vibrato → CC1 (dead zone + rate-limited, only when settings closed) ─ if !settings_active && now % VIBRATO_INTERVAL_MS == 0 { let max_delta = vib_filt_a - .abs_diff(baselines[VIBRATO_A]) - .max(vib_filt_b.abs_diff(baselines[VIBRATO_B])) + .abs_diff(ctx.local.dynamic_baselines[VIBRATO_A]) + .max(vib_filt_b.abs_diff(ctx.local.dynamic_baselines[VIBRATO_B])) .saturating_sub(VIBRATO_DEAD_ZONE); let cc_val = ((max_delta.min(VIBRATO_MAX_DELTA) as u32 * 127 / VIBRATO_MAX_DELTA as u32) as u8) @@ -623,6 +663,34 @@ mod app { ctx.local.led3.set_high(); } + // ── Snapshot recalibration ──────────────────────────────────────────── + let do_recal = ctx + .shared + .recalibrate_pending + .lock(|p| core::mem::replace(p, false)); + if do_recal { + for i in 0..NUM_SWITCHES { + let current = ctx.local.filters[i].last_output(); + ctx.local.dynamic_baselines[i] = current; + ctx.local.filters[i].prime(current); + } + ctx.shared + .baselines + .lock(|b| *b = *ctx.local.dynamic_baselines); + *ctx.local.recalibrate_show_until = now.wrapping_add(RECALIBRATE_FLASH_MS); + *ctx.local.recalibrate_flashing = true; + info!("recalibration done tick={}", now); + } + + // Clear recalibrating display after timeout + if *ctx.local.recalibrate_flashing + && now.wrapping_sub(*ctx.local.recalibrate_show_until) < 0x8000_0000u32 + { + *ctx.local.recalibrate_flashing = false; + ctx.shared.display_state.lock(|s| s.recalibrating = false); + display_update::spawn().ok(); + } + if !pending.is_empty() { ctx.shared.event_queue.lock(|queue| { for item in pending { @@ -636,7 +704,7 @@ mod app { // ── MIDI output ─────────────────────────────────────────────────────────── #[task( shared = [event_queue, midi_tx_flag, display_state, splash_done, - settings, settings_active, settings_selected], + settings, settings_active, settings_selected, recalibrate_pending], local = [midi_sender], priority = 2, capacity = 32 @@ -652,6 +720,7 @@ mod app { let mut nav_dirty = false; let mut did_send = false; let mut new_display_event: Option = None; + let mut new_volume: Option = None; ctx.shared.event_queue.lock(|queue| { while let Some((switch_idx, event)) = queue.dequeue() { @@ -671,6 +740,56 @@ mod app { continue; } + // ── HE71–73: voice select → Program Change on melody channel ── + if switch_idx == VOICE_KEY_A + || switch_idx == VOICE_KEY_B + || switch_idx == VOICE_KEY_C + { + if matches!(event, SwitchEvent::NoteOn { .. }) { + let pc = if switch_idx == VOICE_KEY_A { + VOICE_PC_A + } else if switch_idx == VOICE_KEY_B { + VOICE_PC_B + } else { + VOICE_PC_C + }; + info!("Voice select PC={} ch={}", pc, settings.melody_channel + 1); + ctx.local.midi_sender.set_channel(settings.melody_channel); + ctx.local.midi_sender.all_notes_off(); + ctx.local.midi_sender.program_change(pc); + ctx.shared + .display_state + .lock(|s| s.current_voice = Some(pc)); + display_update::spawn().ok(); + did_send = true; + } + continue; + } + + // ── HE75: snapshot recalibration ────────────────────────────── + if switch_idx == RECALIBRATE_KEY { + if matches!(event, SwitchEvent::NoteOn { .. }) { + ctx.shared.recalibrate_pending.lock(|p| *p = true); + ctx.shared.display_state.lock(|s| s.recalibrating = true); + display_update::spawn().ok(); + info!("recalibration requested"); + } + continue; + } + + // ── HE76: all notes off (CC 123) on all 16 channels ─────────── + if switch_idx == ALL_NOTES_OFF_KEY { + if matches!(event, SwitchEvent::NoteOn { .. }) { + info!("All Notes Off — all 16 channels"); + for ch in 0..16u8 { + ctx.local.midi_sender.set_channel(ch); + ctx.local.midi_sender.all_notes_off(); + } + did_send = true; + } + continue; + } + // ── Settings navigation (arrow keys hijacked) ───────────────── if active { if matches!(event, SwitchEvent::NoteOn { .. }) { @@ -745,6 +864,9 @@ mod app { info!("CC{} = {}", cc, value); ctx.local.midi_sender.control_change(cc, value); new_display_event = Some(LastEvent::Cc { num: cc, value }); + if cc == 7 { + new_volume = Some((value as u32 * 10 / 127) as u8); + } } SwitchEvent::PitchBend { value } => { info!("PitchBend value={}", value); @@ -766,20 +888,27 @@ mod app { // ── Handle open / close transition ──────────────────────────────────── if was_active != active { - // Kill any held notes and reset pitch bend on both channels. + // Kill held notes on every channel — covers old channel, new channel, and any + // channel that was active before settings was opened. + for ch in 0..16u8 { + ctx.local.midi_sender.set_channel(ch); + ctx.local.midi_sender.all_notes_off(); + } ctx.local.midi_sender.set_channel(settings.melody_channel); - ctx.local.midi_sender.all_notes_off(); ctx.local.midi_sender.pitch_bend(0x2000); - ctx.local.midi_sender.set_channel(settings.drum_channel); - ctx.local.midi_sender.all_notes_off(); did_send = true; if !active { - // Settings just closed — inform the synth of the new pitch bend range. + // Settings just closed — push updated parameters to the synth. ctx.local.midi_sender.set_channel(settings.melody_channel); ctx.local .midi_sender .set_pitch_bend_range(settings.pitch_bend_range); + ctx.local + .midi_sender + .program_change(settings.melody_program); + ctx.local.midi_sender.set_channel(settings.drum_channel); + ctx.local.midi_sender.program_change(settings.drum_program); } } @@ -795,6 +924,9 @@ mod app { Some(ev) => s.last_event = Some(ev), None => {} } + if let Some(vol) = new_volume { + s.volume_level = Some(vol); + } }); display_update::spawn().ok(); } diff --git a/keyboard_keyboard/code/src/midi.rs b/keyboard_keyboard/code/src/midi.rs index aa0d2ab..69da583 100644 --- a/keyboard_keyboard/code/src/midi.rs +++ b/keyboard_keyboard/code/src/midi.rs @@ -48,6 +48,12 @@ impl MidiSender { self.control_change(123, 0); } + pub fn program_change(&mut self, program: u8) { + let status = 0xC0 | self.channel; + self.send_byte(status); + self.send_byte(program & 0x7F); + } + /// Sets pitch bend range on the receiving synth via Registered Parameter Number 0. pub fn set_pitch_bend_range(&mut self, semitones: u8) { // Select RPN 0 (pitch bend range): MSB then LSB both = 0. diff --git a/keyboard_keyboard/code/src/settings.rs b/keyboard_keyboard/code/src/settings.rs index e85c01f..1ebaf1d 100644 --- a/keyboard_keyboard/code/src/settings.rs +++ b/keyboard_keyboard/code/src/settings.rs @@ -1,4 +1,4 @@ -pub const NUM_SETTINGS_ITEMS: usize = 4; +pub const NUM_SETTINGS_ITEMS: usize = 6; pub struct SettingsItem { pub name: &'static str, @@ -8,12 +8,12 @@ pub struct SettingsItem { pub const SETTINGS_ITEMS: [SettingsItem; NUM_SETTINGS_ITEMS] = [ SettingsItem { - name: "MELODY CH", + name: "MELODY CHANNEL", min: 1, max: 16, }, SettingsItem { - name: "DRUM CH", + name: "DRUM CHANNEL", min: 1, max: 16, }, @@ -23,10 +23,20 @@ pub const SETTINGS_ITEMS: [SettingsItem; NUM_SETTINGS_ITEMS] = [ max: 5, }, SettingsItem { - name: "PB RANGE", + name: "BEND RANGE", min: 1, max: 12, }, + SettingsItem { + name: "MELODY PROGRAM", + min: 0, + max: 127, + }, + SettingsItem { + name: "DRUM PROGRAM", + min: 0, + max: 127, + }, ]; #[derive(Clone, Copy, Debug)] @@ -35,6 +45,8 @@ pub struct Settings { pub drum_channel: u8, // 0-indexed (0–15), displayed as 1–16 pub octave: i8, // 0–8; offset = (octave - 4) * 12 pub pitch_bend_range: u8, // semitones 1–12 + pub melody_program: u8, // 0–127, sent as PC on melody channel when settings closes + pub drum_program: u8, // 0–127, sent as PC on drum channel when settings closes } impl Settings { @@ -44,6 +56,8 @@ impl Settings { drum_channel: 9, octave: 2, pitch_bend_range: 2, + melody_program: 0, + drum_program: 0, } } @@ -54,6 +68,8 @@ impl Settings { 1 => self.drum_channel as i16 + 1, 2 => self.octave as i16, 3 => self.pitch_bend_range as i16, + 4 => self.melody_program as i16, + 5 => self.drum_program as i16, _ => 0, } } @@ -67,6 +83,8 @@ impl Settings { 1 => self.drum_channel = (v - 1) as u8, 2 => self.octave = v as i8, 3 => self.pitch_bend_range = v as u8, + 4 => self.melody_program = v as u8, + 5 => self.drum_program = v as u8, _ => {} } } diff --git a/keyboard_keyboard/code/src/switch.rs b/keyboard_keyboard/code/src/switch.rs index 02d6bf3..f86ac6a 100644 --- a/keyboard_keyboard/code/src/switch.rs +++ b/keyboard_keyboard/code/src/switch.rs @@ -67,6 +67,10 @@ impl SwitchState { } } + pub fn is_idle(&self) -> bool { + matches!(self.phase, SwitchPhase::Idle) + } + pub fn update( &mut self, adc_value: u16, diff --git a/keyboard_keyboard/code/src/types.rs b/keyboard_keyboard/code/src/types.rs index c7c29bd..3aae4a7 100644 --- a/keyboard_keyboard/code/src/types.rs +++ b/keyboard_keyboard/code/src/types.rs @@ -23,6 +23,9 @@ pub struct DisplayState { pub last_event: Option, pub melody_channel: u8, pub drum_channel: u8, + pub recalibrating: bool, + pub current_voice: Option, + pub volume_level: Option, // 0–10, mapped from CC7 (0–127) } impl DisplayState { @@ -31,6 +34,9 @@ impl DisplayState { last_event: None, melody_channel: 0, drum_channel: 9, + recalibrating: false, + current_voice: None, + volume_level: None, } } }