From 0081fde81cc0fe646d3d8bf7495ed77ded7e716c Mon Sep 17 00:00:00 2001 From: Enoch Date: Tue, 23 Jun 2026 22:59:29 -0500 Subject: [PATCH 1/7] add settings --- keyboard_keyboard/code/src/constants.rs | 12 +- keyboard_keyboard/code/src/display/mod.rs | 2 +- keyboard_keyboard/code/src/display/screens.rs | 63 ++++++- keyboard_keyboard/code/src/main.rs | 174 ++++++++++++++---- keyboard_keyboard/code/src/midi.rs | 13 ++ keyboard_keyboard/code/src/settings.rs | 62 +++++++ keyboard_keyboard/code/src/types.rs | 2 + 7 files changed, 281 insertions(+), 47 deletions(-) create mode 100644 keyboard_keyboard/code/src/settings.rs diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index 1ec6a9e..2625f1a 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -23,12 +23,14 @@ pub const DIAG_LOGGING: bool = false; // set true to see raw ADC / calibration l pub const LOG_INTERVAL_MS: u32 = 500; pub const LOG_SWITCH: usize = 0; // HE1 — first switch -// ── Settings buttons ────────────────────────────────────────────────────────── -pub const SETTINGS_CHAN1: usize = 72; // HE73 → melody MIDI ch 1 -pub const SETTINGS_CHAN2: usize = 75; // HE76 → melody MIDI ch 2 +// ── Settings screen ─────────────────────────────────────────────────────────── +pub const SETTINGS_OPEN: usize = 73; // HE74 → open / close settings +pub const SETTINGS_NAV_PREV: usize = 76; // HE77 → previous item +pub const SETTINGS_VAL_DOWN: usize = 77; // HE78 → value −1 +pub const SETTINGS_NAV_NEXT: usize = 78; // HE79 → next item +pub const SETTINGS_VAL_UP: usize = 79; // HE80 → value +1 -// ── Drum pads (switches 81–100, MIDI ch 10) ─────────────────────────────────── -pub const DRUM_CHANNEL: u8 = 9; // 0-indexed +// ── Drum pads (switches 81–100) ─────────────────────────────────────────────── pub const DRUM_SWITCH_START: usize = 80; #[rustfmt::skip] pub const DRUM_NOTE: [u8; 20] = [ diff --git a/keyboard_keyboard/code/src/display/mod.rs b/keyboard_keyboard/code/src/display/mod.rs index d22f171..d2a936b 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_splash}; +pub use screens::{draw_main, 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 b4e32cb..8974058 100644 --- a/keyboard_keyboard/code/src/display/screens.rs +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -1,4 +1,5 @@ use crate::display::note_name; +use crate::settings::{Settings, SETTINGS_ITEMS, NUM_SETTINGS_ITEMS}; use crate::types::{DisplayState, LastEvent, LcdDisplay}; use core::fmt::Write; use embedded_graphics::{ @@ -9,7 +10,7 @@ use embedded_graphics::{ }, pixelcolor::BinaryColor, prelude::*, - primitives::Rectangle, + primitives::{Line, PrimitiveStyle, Rectangle}, text::{Alignment, Text}, }; use heapless::String; @@ -45,8 +46,8 @@ pub fn draw_main(disp: &mut LcdDisplay, state: &DisplayState) { match state.last_event { Some(LastEvent::Note { note }) => { let (name, oct) = note_name(note); - write!(label, "{}{}", name, oct).ok(); - write!(number, "{}", note).ok(); + write!(label, "{}", note).ok(); + write!(number, "{}{}", name, oct).ok(); } Some(LastEvent::Cc { num, value }) => { write!(label, "CC {}", num).ok(); @@ -89,9 +90,11 @@ pub fn draw_main(disp: &mut LcdDisplay, state: &DisplayState) { .draw(disp) .ok(); - // Drums channel — top-right, always 10 + // Drums channel — top-right + let mut drum_ch: String<4> = String::new(); + write!(drum_ch, "{}", state.drum_channel + 1).ok(); Text::with_alignment( - "10", + drum_ch.as_str(), center + Point::new(54, -9), MonoTextStyle::new(&FONT_6X9, off), Alignment::Center, @@ -101,3 +104,53 @@ pub fn draw_main(disp: &mut LcdDisplay, state: &DisplayState) { disp.flush().ok(); } + +/// Draws the settings screen: 3 items (prev / current / next) with name left, +/// value right. The current item has an underline under its value. +pub fn draw_settings(disp: &mut LcdDisplay, selected: usize, settings: &Settings) { + disp.clear(); + + let style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); + let underline_style = PrimitiveStyle::with_stroke(BinaryColor::On, 1); + + // Baseline y-coordinates for the three rows (font is 9px tall). + let y_rows: [i32; 3] = [9, 20, 30]; + + let prev = (selected + NUM_SETTINGS_ITEMS - 1) % NUM_SETTINGS_ITEMS; + let next = (selected + 1) % NUM_SETTINGS_ITEMS; + let slots = [prev, selected, next]; + + for (row, &item_idx) in slots.iter().enumerate() { + let y = y_rows[row]; + let item = &SETTINGS_ITEMS[item_idx]; + let value = settings.get(item_idx); + + // Name: left-aligned + Text::new(item.name, Point::new(0, y), style).draw(disp).ok(); + + // Value: right-aligned + let mut val_str: String<8> = String::new(); + write!(val_str, "{}", value).ok(); + Text::with_alignment( + val_str.as_str(), + Point::new(127, y), + style, + Alignment::Right, + ) + .draw(disp) + .ok(); + + // Underline under the value of the current (middle) row + if row == 1 { + let val_px_width = val_str.len() as i32 * 6; + let x0 = 127 - val_px_width + 1; + let uy = y + 1; + Line::new(Point::new(x0, uy), Point::new(127, uy)) + .into_styled(underline_style) + .draw(disp) + .ok(); + } + } + + disp.flush().ok(); +} diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 5a30cfa..d1d8603 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -10,6 +10,7 @@ mod constants; mod display; mod hardware; mod midi; +mod settings; mod switch; mod types; @@ -24,6 +25,7 @@ mod app { i2c_bus_recovery, read_all_adcs, set_decoder, set_mux_channel, AdcPins, MuxRaw, }; use crate::midi::MidiSender; + use crate::settings::{Settings, NUM_SETTINGS_ITEMS}; use crate::switch::{ChannelFilter, SwitchEvent, SwitchState}; use crate::types::{DisplayState, LastEvent, LcdDisplay}; @@ -62,6 +64,9 @@ mod app { midi_tx_flag: bool, display_state: DisplayState, splash_done: bool, + settings: Settings, + settings_active: bool, + settings_selected: usize, } // ── Local resources ─────────────────────────────────────────────────────── @@ -91,7 +96,6 @@ mod app { filters: [ChannelFilter; NUM_SWITCHES], last_pitch_bend: u16, last_vibrato_cc: u8, - melody_channel: u8, led1_off_at: u32, led2_off_at: u32, display: Option, @@ -329,6 +333,9 @@ mod app { midi_tx_flag: false, display_state: DisplayState::new(), splash_done: false, + settings: Settings::default(), + settings_active: false, + settings_selected: 0, }, Local { audio: system.audio, @@ -354,7 +361,6 @@ mod app { filters, last_pitch_bend: 0x2000, last_vibrato_cc: 0, - melody_channel: 0, led1_off_at: 0, led2_off_at: 0, display, @@ -371,8 +377,13 @@ mod app { } // Priority 1 — below process_events so a slow display never blocks key events. - // First spawn: init hardware + show splash. Subsequent spawns: redraw main screen. - #[task(local = [display, initialized: bool = false], shared = [display_state], priority = 1, capacity = 2)] + // First spawn: init hardware + show splash. Subsequent spawns: redraw. + #[task( + local = [display, initialized: bool = false], + shared = [display_state, settings, settings_active, settings_selected], + priority = 1, + capacity = 2 + )] fn display_update(mut ctx: display_update::Context) { let Some(disp) = ctx.local.display.as_mut() else { return; @@ -388,8 +399,16 @@ mod app { } return; } - let state = ctx.shared.display_state.lock(|s| *s); - crate::display::draw_main(disp, &state); + + 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); + } } #[task(binds = DMA1_STR1, priority = 8, local = [audio])] @@ -403,7 +422,8 @@ mod app { 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], - shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag, splash_done], + shared = [tick_ms, switch_states, baselines, event_queue, midi_tx_flag, splash_done, + settings_active], priority = 15 )] fn timer_handler(mut ctx: timer_handler::Context) { @@ -424,6 +444,7 @@ mod app { } let 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(); // ── ADC scan ────────────────────────────────────────────────────────── @@ -462,14 +483,22 @@ mod app { let raw = ctx.local.mux_raw[mux as usize][ch as usize]; let filtered = ctx.local.filters[switch_idx].feed(raw); - if switch_idx == PITCH_BEND_DOWN { - pb_filt_down = filtered; - } else if switch_idx == PITCH_BEND_UP { - pb_filt_up = filtered; - } else if switch_idx == VIBRATO_A { - vib_filt_a = filtered; - } else if switch_idx == VIBRATO_B { - vib_filt_b = filtered; + // When settings is open the four arrow keys become digital plunger switches; + let is_analog = switch_idx == PITCH_BEND_DOWN + || switch_idx == PITCH_BEND_UP + || switch_idx == VIBRATO_A + || switch_idx == VIBRATO_B; + + if is_analog && !settings_active { + if switch_idx == PITCH_BEND_DOWN { + pb_filt_down = filtered; + } else if switch_idx == PITCH_BEND_UP { + pb_filt_up = filtered; + } else if switch_idx == VIBRATO_A { + vib_filt_a = filtered; + } else { + vib_filt_b = filtered; + } } else if let Some(event) = states[switch_idx].update(filtered, baselines[switch_idx], now, switch_idx) { @@ -478,8 +507,8 @@ mod app { } }); - // ── Pitch bend (rate-limited) ───────────────────────────────────────── - if now % PITCH_BEND_INTERVAL_MS == 0 { + // ── 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 pb_value = if delta_down < RELEASE_DELTA && delta_up < RELEASE_DELTA { @@ -524,8 +553,8 @@ mod app { ); } - // ── Vibrato → CC1 (dead zone + rate-limited) ────────────────────────── - if now % VIBRATO_INTERVAL_MS == 0 { + // ── 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])) @@ -606,40 +635,86 @@ mod app { // ── MIDI output ─────────────────────────────────────────────────────────── #[task( - shared = [event_queue, midi_tx_flag, display_state, splash_done], - local = [midi_sender, melody_channel], + shared = [event_queue, midi_tx_flag, display_state, splash_done, + settings, settings_active, settings_selected], + local = [midi_sender], priority = 2, capacity = 32 )] fn process_events(mut ctx: process_events::Context) { + // Snapshot shared settings state — write back mutations after draining the queue. + let mut settings = ctx.shared.settings.lock(|s| *s); + let mut active = ctx.shared.settings_active.lock(|a| *a); + let mut selected = ctx.shared.settings_selected.lock(|s| *s); + + let was_active = active; + let mut settings_dirty = false; + let mut nav_dirty = false; let mut did_send = false; let mut new_display_event: Option = None; - let mut melody_changed = false; ctx.shared.event_queue.lock(|queue| { while let Some((switch_idx, event)) = queue.dequeue() { - if switch_idx == SETTINGS_CHAN1 || switch_idx == SETTINGS_CHAN2 { - if let SwitchEvent::NoteOn { .. } = event { - *ctx.local.melody_channel = - if switch_idx == SETTINGS_CHAN1 { 0 } else { 1 }; - info!("melody ch → {}", *ctx.local.melody_channel + 1); - melody_changed = true; + // ── Settings open / close (hold HE74) ──────────────────────── + if switch_idx == SETTINGS_OPEN { + match event { + SwitchEvent::NoteOn { .. } => { active = true; nav_dirty = true; } + SwitchEvent::NoteOff => { active = false; nav_dirty = true; } + _ => {} + } + continue; + } + + // ── Settings navigation (arrow keys hijacked) ───────────────── + if active { + if matches!(event, SwitchEvent::NoteOn { .. }) { + match switch_idx { + SETTINGS_NAV_PREV => { + selected = + (selected + NUM_SETTINGS_ITEMS - 1) % NUM_SETTINGS_ITEMS; + nav_dirty = true; + } + SETTINGS_NAV_NEXT => { + selected = (selected + 1) % NUM_SETTINGS_ITEMS; + nav_dirty = true; + } + SETTINGS_VAL_UP => { + settings.adjust(selected, 1); + settings_dirty = true; + nav_dirty = true; + } + SETTINGS_VAL_DOWN => { + settings.adjust(selected, -1); + settings_dirty = true; + nav_dirty = true; + } + _ => {} + } } continue; } + // ── Normal MIDI routing ──────────────────────────────────────── let he = HE_NUM[switch_idx]; let is_drum = switch_idx >= DRUM_SWITCH_START && switch_idx < DRUM_SWITCH_START + DRUM_NOTE.len(); - let (note, channel) = if is_drum { - (DRUM_NOTE[switch_idx - DRUM_SWITCH_START], DRUM_CHANNEL) + let (raw_note, channel) = if is_drum { + (DRUM_NOTE[switch_idx - DRUM_SWITCH_START], settings.drum_channel) } else { - (SWITCH_TO_NOTE[switch_idx], *ctx.local.melody_channel) + (SWITCH_TO_NOTE[switch_idx], settings.melody_channel) }; - if note == 0 { + if raw_note == 0 { continue; } + // Apply octave offset to melody keys; clamp to valid MIDI range. + let note = if is_drum { + raw_note + } else { + let offset = (settings.octave as i16 - 4) * 12; + (raw_note as i16 + offset).clamp(1, 127) as u8 + }; + ctx.local.midi_sender.set_channel(channel); match event { SwitchEvent::NoteOn { velocity } => { @@ -672,17 +747,44 @@ mod app { } }); + // ── Write back mutations ─────────────────────────────────────────────── + if settings_dirty { + ctx.shared.settings.lock(|s| *s = settings); + } + if nav_dirty { + ctx.shared.settings_active.lock(|a| *a = active); + ctx.shared.settings_selected.lock(|s| *s = selected); + } + + // ── Handle open / close transition ──────────────────────────────────── + if was_active != active { + // Kill any held notes and reset pitch bend on both channels. + 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. + ctx.local.midi_sender.set_channel(settings.melody_channel); + ctx.local.midi_sender.set_pitch_bend_range(settings.pitch_bend_range); + } + } + + // ── Update display ───────────────────────────────────────────────────── let done = ctx.shared.splash_done.lock(|d| *d); - if (new_display_event.is_some() || melody_changed) && done { + if done && (new_display_event.is_some() || nav_dirty || was_active != active) { ctx.shared.display_state.lock(|s| { + // Keep main-screen channel labels in sync with settings. + s.melody_channel = settings.melody_channel; + s.drum_channel = settings.drum_channel; match new_display_event { Some(LastEvent::Clear) => s.last_event = None, Some(ev) => s.last_event = Some(ev), None => {} } - if melody_changed { - s.melody_channel = *ctx.local.melody_channel; - } }); display_update::spawn().ok(); } diff --git a/keyboard_keyboard/code/src/midi.rs b/keyboard_keyboard/code/src/midi.rs index d48e330..aa0d2ab 100644 --- a/keyboard_keyboard/code/src/midi.rs +++ b/keyboard_keyboard/code/src/midi.rs @@ -48,6 +48,19 @@ impl MidiSender { self.control_change(123, 0); } + /// 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. + self.control_change(101, 0); + self.control_change(100, 0); + // Write the value: CC 6 = semitones (MSB), CC 38 = 0 cents (LSB). + self.control_change(6, semitones); + self.control_change(38, 0); + // Deselect — set RPN to null (127, 127). + self.control_change(101, 127); + self.control_change(100, 127); + } + pub fn set_channel(&mut self, channel: u8) { self.channel = channel & 0x0F; } diff --git a/keyboard_keyboard/code/src/settings.rs b/keyboard_keyboard/code/src/settings.rs new file mode 100644 index 0000000..b11144b --- /dev/null +++ b/keyboard_keyboard/code/src/settings.rs @@ -0,0 +1,62 @@ +pub const NUM_SETTINGS_ITEMS: usize = 4; + +pub struct SettingsItem { + pub name: &'static str, + pub min: i16, + pub max: i16, +} + +pub const SETTINGS_ITEMS: [SettingsItem; NUM_SETTINGS_ITEMS] = [ + SettingsItem { name: "MELODY CH", min: 1, max: 16 }, + SettingsItem { name: "DRUM CH", min: 1, max: 16 }, + SettingsItem { name: "OCTAVE", min: 2, max: 5 }, + SettingsItem { name: "PB RANGE", min: 1, max: 12 }, +]; + +#[derive(Clone, Copy, Debug)] +pub struct Settings { + pub melody_channel: u8, // 0-indexed (0–15), displayed as 1–16 + 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 +} + +impl Settings { + pub const fn default() -> Self { + Self { + melody_channel: 0, + drum_channel: 9, + octave: 2, + pitch_bend_range: 2, + } + } + + /// Returns the display value for menu item `idx` (channels are 1-indexed). + pub fn get(&self, idx: usize) -> i16 { + match idx { + 0 => self.melody_channel as i16 + 1, + 1 => self.drum_channel as i16 + 1, + 2 => self.octave as i16, + 3 => self.pitch_bend_range as i16, + _ => 0, + } + } + + /// Sets menu item `idx` from a display value, clamping to the item's range. + pub fn set(&mut self, idx: usize, value: i16) { + let item = &SETTINGS_ITEMS[idx]; + let v = value.clamp(item.min, item.max); + match idx { + 0 => self.melody_channel = (v - 1) as u8, + 1 => self.drum_channel = (v - 1) as u8, + 2 => self.octave = v as i8, + 3 => self.pitch_bend_range = v as u8, + _ => {} + } + } + + pub fn adjust(&mut self, idx: usize, delta: i16) { + let current = self.get(idx); + self.set(idx, current + delta); + } +} diff --git a/keyboard_keyboard/code/src/types.rs b/keyboard_keyboard/code/src/types.rs index 25784cd..c7c29bd 100644 --- a/keyboard_keyboard/code/src/types.rs +++ b/keyboard_keyboard/code/src/types.rs @@ -22,6 +22,7 @@ pub enum LastEvent { pub struct DisplayState { pub last_event: Option, pub melody_channel: u8, + pub drum_channel: u8, } impl DisplayState { @@ -29,6 +30,7 @@ impl DisplayState { Self { last_event: None, melody_channel: 0, + drum_channel: 9, } } } From fd31088c8cc1e0e4c2fe71cd5bf421e3c00741f7 Mon Sep 17 00:00:00 2001 From: Enoch Date: Tue, 23 Jun 2026 23:06:14 -0500 Subject: [PATCH 2/7] fmt more like fml --- keyboard_keyboard/code/src/constants.rs | 4 ++-- keyboard_keyboard/code/src/display/screens.rs | 6 +++-- keyboard_keyboard/code/src/main.rs | 24 +++++++++++++------ keyboard_keyboard/code/src/settings.rs | 24 +++++++++++++++---- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index 2625f1a..ca598d8 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -24,11 +24,11 @@ pub const LOG_INTERVAL_MS: u32 = 500; pub const LOG_SWITCH: usize = 0; // HE1 — first switch // ── Settings screen ─────────────────────────────────────────────────────────── -pub const SETTINGS_OPEN: usize = 73; // HE74 → open / close settings +pub const SETTINGS_OPEN: usize = 73; // HE74 → open / close settings pub const SETTINGS_NAV_PREV: usize = 76; // HE77 → previous item pub const SETTINGS_VAL_DOWN: usize = 77; // HE78 → value −1 pub const SETTINGS_NAV_NEXT: usize = 78; // HE79 → next item -pub const SETTINGS_VAL_UP: usize = 79; // HE80 → value +1 +pub const SETTINGS_VAL_UP: usize = 79; // HE80 → value +1 // ── Drum pads (switches 81–100) ─────────────────────────────────────────────── pub const DRUM_SWITCH_START: usize = 80; diff --git a/keyboard_keyboard/code/src/display/screens.rs b/keyboard_keyboard/code/src/display/screens.rs index 8974058..840e098 100644 --- a/keyboard_keyboard/code/src/display/screens.rs +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -1,5 +1,5 @@ use crate::display::note_name; -use crate::settings::{Settings, SETTINGS_ITEMS, NUM_SETTINGS_ITEMS}; +use crate::settings::{Settings, NUM_SETTINGS_ITEMS, SETTINGS_ITEMS}; use crate::types::{DisplayState, LastEvent, LcdDisplay}; use core::fmt::Write; use embedded_graphics::{ @@ -126,7 +126,9 @@ pub fn draw_settings(disp: &mut LcdDisplay, selected: usize, settings: &Settings let value = settings.get(item_idx); // Name: left-aligned - Text::new(item.name, Point::new(0, y), style).draw(disp).ok(); + Text::new(item.name, Point::new(0, y), style) + .draw(disp) + .ok(); // Value: right-aligned let mut val_str: String<8> = String::new(); diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index d1d8603..be2a0ce 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -483,7 +483,7 @@ mod app { let raw = ctx.local.mux_raw[mux as usize][ch as usize]; let filtered = ctx.local.filters[switch_idx].feed(raw); - // When settings is open the four arrow keys become digital plunger switches; + // When settings is open the four arrow keys become digital plunger switches; let is_analog = switch_idx == PITCH_BEND_DOWN || switch_idx == PITCH_BEND_UP || switch_idx == VIBRATO_A @@ -658,8 +658,14 @@ mod app { // ── Settings open / close (hold HE74) ──────────────────────── if switch_idx == SETTINGS_OPEN { match event { - SwitchEvent::NoteOn { .. } => { active = true; nav_dirty = true; } - SwitchEvent::NoteOff => { active = false; nav_dirty = true; } + SwitchEvent::NoteOn { .. } => { + active = true; + nav_dirty = true; + } + SwitchEvent::NoteOff => { + active = false; + nav_dirty = true; + } _ => {} } continue; @@ -670,8 +676,7 @@ mod app { if matches!(event, SwitchEvent::NoteOn { .. }) { match switch_idx { SETTINGS_NAV_PREV => { - selected = - (selected + NUM_SETTINGS_ITEMS - 1) % NUM_SETTINGS_ITEMS; + selected = (selected + NUM_SETTINGS_ITEMS - 1) % NUM_SETTINGS_ITEMS; nav_dirty = true; } SETTINGS_NAV_NEXT => { @@ -699,7 +704,10 @@ mod app { let is_drum = switch_idx >= DRUM_SWITCH_START && switch_idx < DRUM_SWITCH_START + DRUM_NOTE.len(); let (raw_note, channel) = if is_drum { - (DRUM_NOTE[switch_idx - DRUM_SWITCH_START], settings.drum_channel) + ( + DRUM_NOTE[switch_idx - DRUM_SWITCH_START], + settings.drum_channel, + ) } else { (SWITCH_TO_NOTE[switch_idx], settings.melody_channel) }; @@ -769,7 +777,9 @@ mod app { if !active { // Settings just closed — inform the synth of the new pitch bend range. 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 + .set_pitch_bend_range(settings.pitch_bend_range); } } diff --git a/keyboard_keyboard/code/src/settings.rs b/keyboard_keyboard/code/src/settings.rs index b11144b..e85c01f 100644 --- a/keyboard_keyboard/code/src/settings.rs +++ b/keyboard_keyboard/code/src/settings.rs @@ -7,10 +7,26 @@ pub struct SettingsItem { } pub const SETTINGS_ITEMS: [SettingsItem; NUM_SETTINGS_ITEMS] = [ - SettingsItem { name: "MELODY CH", min: 1, max: 16 }, - SettingsItem { name: "DRUM CH", min: 1, max: 16 }, - SettingsItem { name: "OCTAVE", min: 2, max: 5 }, - SettingsItem { name: "PB RANGE", min: 1, max: 12 }, + SettingsItem { + name: "MELODY CH", + min: 1, + max: 16, + }, + SettingsItem { + name: "DRUM CH", + min: 1, + max: 16, + }, + SettingsItem { + name: "OCTAVE", + min: 2, + max: 5, + }, + SettingsItem { + name: "PB RANGE", + min: 1, + max: 12, + }, ]; #[derive(Clone, Copy, Debug)] From 6f0c888afd5d89e5f4dd9b8212e683d589195043 Mon Sep 17 00:00:00 2001 From: Enoch Date: Thu, 25 Jun 2026 12:20:15 -0500 Subject: [PATCH 3/7] settings: program change and recalibration --- keyboard_keyboard/code/src/constants.rs | 26 ++- keyboard_keyboard/code/src/display/mod.rs | 2 +- keyboard_keyboard/code/src/display/screens.rs | 39 +++++ keyboard_keyboard/code/src/main.rs | 154 +++++++++++++++--- keyboard_keyboard/code/src/midi.rs | 6 + keyboard_keyboard/code/src/settings.rs | 26 ++- keyboard_keyboard/code/src/switch.rs | 4 + keyboard_keyboard/code/src/types.rs | 6 + 8 files changed, 236 insertions(+), 27 deletions(-) diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index ca598d8..db3d04c 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -19,9 +19,29 @@ pub const CALIBRATION_SAMPLES: usize = 64; pub const SPLASH_DURATION_MS: u32 = 3000; -pub const DIAG_LOGGING: bool = false; // set true to see raw ADC / calibration logs +pub const DIAG_LOGGING: bool = true; // 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; + +// ── 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 +153,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..6a0a0bc 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,32 @@ 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..6f61308 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,6 @@ mod app { display_update::spawn().ok(); } - let 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 +484,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 +510,34 @@ 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() + }; + if at_rest { + let db = &mut ctx.local.dynamic_baselines[switch_idx]; + *db = ((*db as u32 * (BASELINE_TRACKING_ALPHA - 1) + filtered as u32) + / BASELINE_TRACKING_ALPHA) as u16; + } } }); // ── 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 +561,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 +583,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 +650,36 @@ 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 +693,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 +709,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 +729,55 @@ 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.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 +852,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); @@ -775,11 +885,14 @@ mod app { 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 +908,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, } } } From 5877eb29d8da247fa2cc1d4257771bc3a95e3b93 Mon Sep 17 00:00:00 2001 From: Enoch Date: Thu, 25 Jun 2026 19:01:42 -0500 Subject: [PATCH 4/7] format --- keyboard_keyboard/code/src/constants.rs | 4 ++-- keyboard_keyboard/code/src/display/screens.rs | 3 +-- keyboard_keyboard/code/src/main.rs | 9 +++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index db3d04c..5cf616e 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -30,8 +30,8 @@ pub const LOG_SWITCH: usize = 89; // HE90 — debugging spurious triggers pub const BASELINE_TRACKING_ALPHA: u32 = 1024; // ── 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_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) ─────────────────────────────── diff --git a/keyboard_keyboard/code/src/display/screens.rs b/keyboard_keyboard/code/src/display/screens.rs index 6a0a0bc..7f34454 100644 --- a/keyboard_keyboard/code/src/display/screens.rs +++ b/keyboard_keyboard/code/src/display/screens.rs @@ -136,8 +136,7 @@ pub fn draw_main(disp: &mut LcdDisplay, state: &DisplayState) { 1 => 20, // square _ => 0, // saw }; - let icon = - atlas.sub_image(&Rectangle::new(Point::new(atlas_x, 64), Size::new(20, 8))); + 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(); } diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 6f61308..27d942e 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -674,9 +674,7 @@ mod app { && now.wrapping_sub(*ctx.local.recalibrate_show_until) < 0x8000_0000u32 { *ctx.local.recalibrate_flashing = false; - ctx.shared - .display_state - .lock(|s| s.recalibrating = false); + ctx.shared.display_state.lock(|s| s.recalibrating = false); display_update::spawn().ok(); } @@ -744,6 +742,7 @@ mod app { }; 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 @@ -890,7 +889,9 @@ mod app { 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 + .program_change(settings.melody_program); ctx.local.midi_sender.set_channel(settings.drum_channel); ctx.local.midi_sender.program_change(settings.drum_program); } From 07373e5ed8402675fcf43812dce8cd52da05c93c Mon Sep 17 00:00:00 2001 From: Enoch Date: Thu, 25 Jun 2026 19:03:38 -0500 Subject: [PATCH 5/7] oosp - turn back off logging --- keyboard_keyboard/code/src/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index 5cf616e..0a4a3fb 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -19,7 +19,7 @@ pub const CALIBRATION_SAMPLES: usize = 64; pub const SPLASH_DURATION_MS: u32 = 3000; -pub const DIAG_LOGGING: bool = true; // set true to see raw ADC / calibration logs +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 = 89; // HE90 — debugging spurious triggers From c53e5e1bce64d911086cd215b522f18a11a49f3c Mon Sep 17 00:00:00 2001 From: Enoch Date: Thu, 25 Jun 2026 19:22:17 -0500 Subject: [PATCH 6/7] kill notes for prev cxchannel --- keyboard_keyboard/code/src/main.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 27d942e..dab9359 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -875,12 +875,14 @@ 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 { From 79ac5cdd56c07636388ef661b0ad3669da0d0727 Mon Sep 17 00:00:00 2001 From: Nathan Bradshaw Date: Thu, 2 Jul 2026 11:33:43 -0500 Subject: [PATCH 7/7] Clamp baseline drift to prevent phantom triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sustained crosstalk from held chords can drift neighboring switch baselines far enough to cause phantom key triggers when the chord releases. This commit adds `BASELINE_DRIFT_MAX` to hard-cap how far tracked baselines may wander from their boot-calibrated anchor values. Tracked baselines are now clamped within ±40 ADC counts of the anchor, preventing drift-induced false triggers while still allowing fast adaptive tracking. --- keyboard_keyboard/code/src/constants.rs | 5 +++++ keyboard_keyboard/code/src/main.rs | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index 0a4a3fb..d27de3a 100644 --- a/keyboard_keyboard/code/src/constants.rs +++ b/keyboard_keyboard/code/src/constants.rs @@ -28,6 +28,11 @@ pub const LOG_SWITCH: usize = 89; // HE90 — debugging spurious triggers // 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 diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index dab9359..c79df75 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -455,6 +455,9 @@ mod app { display_update::spawn().ok(); } + // 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(); @@ -525,11 +528,21 @@ mod app { 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]; - *db = ((*db as u32 * (BASELINE_TRACKING_ALPHA - 1) + filtered as u32) + 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), + ); } } });