From cb08a0dc23f0dad67cdc302961f12d88e06fdb82 Mon Sep 17 00:00:00 2001 From: Nathan Bradshaw Date: Sat, 13 Jun 2026 11:00:11 -0500 Subject: [PATCH 1/3] Add pitch-bend sensor support Introduce pitch-bend handling from two ADC sensors: add constants for sensor indices, max delta, hysteresis and rate limit. Add a PitchBend event variant and last_pitch_bend state, initialize it to center, and include it in the TIM2 task locals. Sample and filter the two bend sensors, compute a 14-bit combined pitch-bend value (center snap when idle), apply hysteresis and rate limiting, enqueue PitchBend events, and send MIDI pitch-bend messages when changes exceed the threshold. Also wire diagnostic logging for pitch-bend messages. --- keyboard_keyboard/code/src/main.rs | 47 ++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 4ba52c2..d1491be 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -63,6 +63,13 @@ mod app { 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, // GM Open Hi-Hat → Splash Cymbal ]; + // ── Pitch bend sensors ─────────────────────────────────────────────────────── + const PITCH_BEND_DOWN: usize = 77; // HE78 → bends pitch down + const PITCH_BEND_UP: usize = 79; // HE80 → bends pitch up + const PITCH_BEND_MAX_DELTA: u16 = 400; // ADC counts for full bend travel + const PITCH_BEND_HYSTERESIS: u16 = 32; // min 14-bit change to send a message + const PITCH_BEND_INTERVAL_MS: u32 = 5; // send at most every 5 ms (200 Hz) + // ── Potentiometers ─────────────────────────────────────────────────────────── const NUM_POTS: usize = 12; const POT_SCAN_MS: u32 = 10; // scan pots at 100 Hz @@ -430,6 +437,7 @@ mod app { NoteOn { velocity: u8 }, NoteOff, PotChange { cc: u8, value: u8 }, + PitchBend { value: u16 }, } type MuxRaw = [[u16; MUX_CHANNELS]; NUM_MUXES]; @@ -468,6 +476,7 @@ mod app { mux_raw: MuxRaw, midi_sender: MidiSender, filters: [ChannelFilter; NUM_SWITCHES], + last_pitch_bend: u16, display: Option, } @@ -746,6 +755,7 @@ mod app { mux_raw: [[0u16; MUX_CHANNELS]; NUM_MUXES], midi_sender, filters, + last_pitch_bend: 0x2000, display, }, init::Monotonics(), @@ -794,7 +804,7 @@ 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, led1, led2, led3], + 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, led1, led2, led3], shared = [tick_ms, switch_states, baselines, event_queue], priority = 15 )] @@ -839,12 +849,19 @@ mod app { } } + let mut pb_filt_down = baselines[PITCH_BEND_DOWN]; + let mut pb_filt_up = baselines[PITCH_BEND_UP]; + ctx.shared.switch_states.lock(|states| { for (switch_idx, &(mux, ch)) in SWITCH_MAP.iter().enumerate() { let raw = ctx.local.mux_raw[mux as usize][ch as usize]; let filtered = ctx.local.filters[switch_idx].feed(raw); - if let Some(event) = + if switch_idx == PITCH_BEND_DOWN { + pb_filt_down = filtered; + } else if switch_idx == PITCH_BEND_UP { + pb_filt_up = filtered; + } else if let Some(event) = states[switch_idx].update(filtered, baselines[switch_idx], now, switch_idx) { pending.push((switch_idx, event)).ok(); @@ -852,6 +869,28 @@ mod app { } }); + // Pitch bend from HE71 (bend down) and HE73 (bend up), rate-limited. + if 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 { + 0x2000u16 // snap to center when both sensors at rest + } else { + let d_down = delta_down.min(PITCH_BEND_MAX_DELTA); + let d_up = delta_up.min(PITCH_BEND_MAX_DELTA); + let bend_down = (d_down as u32 * 0x2000 / PITCH_BEND_MAX_DELTA as u32) as u16; + let bend_up = (d_up as u32 * 0x1FFF / PITCH_BEND_MAX_DELTA as u32) as u16; + (0x2000u16.saturating_sub(bend_down)) + .saturating_add(bend_up) + .min(0x3FFF) + }; + let prev_pb = *ctx.local.last_pitch_bend; + if pb_value.abs_diff(prev_pb) >= PITCH_BEND_HYSTERESIS { + *ctx.local.last_pitch_bend = pb_value; + pending.push((0, SwitchEvent::PitchBend { value: pb_value })).ok(); + } + } + if DIAG_LOGGING && now % LOG_INTERVAL_MS == 0 { // Print raw ADC for all 5 mux outputs at the current mux channel state. // Row format: AM1..AM5 raw values + signed delta from baseline for LOG_SWITCH. @@ -966,6 +1005,10 @@ mod app { info!("CC{} = {}", cc, value); ctx.local.midi_sender.control_change(cc, value); } + SwitchEvent::PitchBend { value } => { + info!("PitchBend value={}", value); + ctx.local.midi_sender.pitch_bend(value); + } } } }); From 47e2b6c00f58c59ce5f7d7e19acdc620b1ca2b76 Mon Sep 17 00:00:00 2001 From: Nathan Bradshaw Date: Sat, 13 Jun 2026 12:29:19 -0500 Subject: [PATCH 2/3] feat: vibrado --- keyboard_keyboard/code/src/main.rs | 40 +++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index d1491be..6ec1c4c 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -63,13 +63,20 @@ mod app { 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, // GM Open Hi-Hat → Splash Cymbal ]; - // ── Pitch bend sensors ─────────────────────────────────────────────────────── - const PITCH_BEND_DOWN: usize = 77; // HE78 → bends pitch down - const PITCH_BEND_UP: usize = 79; // HE80 → bends pitch up + // ── Pitch bend sensors (left/right arrow keys) ─────────────────────────────── + const PITCH_BEND_DOWN: usize = 77; // HE78 (left arrow) → bends pitch down + const PITCH_BEND_UP: usize = 79; // HE80 (right arrow) → bends pitch up const PITCH_BEND_MAX_DELTA: u16 = 400; // ADC counts for full bend travel const PITCH_BEND_HYSTERESIS: u16 = 32; // min 14-bit change to send a message const PITCH_BEND_INTERVAL_MS: u32 = 5; // send at most every 5 ms (200 Hz) + // ── Vibrato sensors (up/down arrow keys) → CC1 (mod wheel) ────────────────── + const VIBRATO_A: usize = 76; // HE77 (up arrow) → vibrato depth + const VIBRATO_B: usize = 78; // HE79 (down arrow) → vibrato depth + const VIBRATO_MAX_DELTA: u16 = 300; // ADC counts for full vibrato depth + const VIBRATO_HYSTERESIS: u8 = 2; // min CC change to send + const VIBRATO_INTERVAL_MS: u32 = 10; // send at most every 10 ms (100 Hz) + // ── Potentiometers ─────────────────────────────────────────────────────────── const NUM_POTS: usize = 12; const POT_SCAN_MS: u32 = 10; // scan pots at 100 Hz @@ -477,6 +484,7 @@ mod app { midi_sender: MidiSender, filters: [ChannelFilter; NUM_SWITCHES], last_pitch_bend: u16, + last_vibrato_cc: u8, display: Option, } @@ -756,6 +764,7 @@ mod app { midi_sender, filters, last_pitch_bend: 0x2000, + last_vibrato_cc: 0, display, }, init::Monotonics(), @@ -804,7 +813,7 @@ 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, led1, led2, led3], + 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], shared = [tick_ms, switch_states, baselines, event_queue], priority = 15 )] @@ -851,6 +860,8 @@ mod app { 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]; ctx.shared.switch_states.lock(|states| { for (switch_idx, &(mux, ch)) in SWITCH_MAP.iter().enumerate() { @@ -861,6 +872,10 @@ mod app { 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; } else if let Some(event) = states[switch_idx].update(filtered, baselines[switch_idx], now, switch_idx) { @@ -915,6 +930,23 @@ mod app { ); } + // Vibrato depth from HE77/HE79 (up/down arrows) → CC1, rate-limited. + if now % VIBRATO_INTERVAL_MS == 0 { + let delta_a = vib_filt_a.abs_diff(baselines[VIBRATO_A]); + let delta_b = vib_filt_b.abs_diff(baselines[VIBRATO_B]); + let max_delta = delta_a.max(delta_b); + let cc_val = ((max_delta.min(VIBRATO_MAX_DELTA) as u32 * 127 + / VIBRATO_MAX_DELTA as u32) as u8) + .min(127); + let prev_vib = *ctx.local.last_vibrato_cc; + if cc_val.abs_diff(prev_vib) >= VIBRATO_HYSTERESIS { + *ctx.local.last_vibrato_cc = cc_val; + pending + .push((0, SwitchEvent::PotChange { cc: 1, value: cc_val })) + .ok(); + } + } + // ── Pot scan (100 Hz) ───────────────────────────────────────────────────────── // Reads via Daisy28 (pad 35 / A11 / ADC11). Needs wire: pad 33 → pad 35. if now % POT_SCAN_MS == 0 { From e8829ea0342ff1055fd574f9f2dea3a55cc2baa5 Mon Sep 17 00:00:00 2001 From: Nathan Bradshaw Date: Sat, 13 Jun 2026 15:46:47 -0500 Subject: [PATCH 3/3] Update main.rs --- keyboard_keyboard/code/src/main.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/keyboard_keyboard/code/src/main.rs b/keyboard_keyboard/code/src/main.rs index 6ec1c4c..24fecd8 100644 --- a/keyboard_keyboard/code/src/main.rs +++ b/keyboard_keyboard/code/src/main.rs @@ -65,7 +65,7 @@ mod app { // ── Pitch bend sensors (left/right arrow keys) ─────────────────────────────── const PITCH_BEND_DOWN: usize = 77; // HE78 (left arrow) → bends pitch down - const PITCH_BEND_UP: usize = 79; // HE80 (right arrow) → bends pitch up + const PITCH_BEND_UP: usize = 79; // HE80 (right arrow) → bends pitch up const PITCH_BEND_MAX_DELTA: u16 = 400; // ADC counts for full bend travel const PITCH_BEND_HYSTERESIS: u16 = 32; // min 14-bit change to send a message const PITCH_BEND_INTERVAL_MS: u32 = 5; // send at most every 5 ms (200 Hz) @@ -74,7 +74,7 @@ mod app { const VIBRATO_A: usize = 76; // HE77 (up arrow) → vibrato depth const VIBRATO_B: usize = 78; // HE79 (down arrow) → vibrato depth const VIBRATO_MAX_DELTA: u16 = 300; // ADC counts for full vibrato depth - const VIBRATO_HYSTERESIS: u8 = 2; // min CC change to send + const VIBRATO_HYSTERESIS: u8 = 2; // min CC change to send const VIBRATO_INTERVAL_MS: u32 = 10; // send at most every 10 ms (100 Hz) // ── Potentiometers ─────────────────────────────────────────────────────────── @@ -902,7 +902,9 @@ mod app { let prev_pb = *ctx.local.last_pitch_bend; if pb_value.abs_diff(prev_pb) >= PITCH_BEND_HYSTERESIS { *ctx.local.last_pitch_bend = pb_value; - pending.push((0, SwitchEvent::PitchBend { value: pb_value })).ok(); + pending + .push((0, SwitchEvent::PitchBend { value: pb_value })) + .ok(); } } @@ -935,14 +937,20 @@ mod app { let delta_a = vib_filt_a.abs_diff(baselines[VIBRATO_A]); let delta_b = vib_filt_b.abs_diff(baselines[VIBRATO_B]); let max_delta = delta_a.max(delta_b); - let cc_val = ((max_delta.min(VIBRATO_MAX_DELTA) as u32 * 127 - / VIBRATO_MAX_DELTA as u32) as u8) - .min(127); + let cc_val = + ((max_delta.min(VIBRATO_MAX_DELTA) as u32 * 127 / VIBRATO_MAX_DELTA as u32) as u8) + .min(127); let prev_vib = *ctx.local.last_vibrato_cc; if cc_val.abs_diff(prev_vib) >= VIBRATO_HYSTERESIS { *ctx.local.last_vibrato_cc = cc_val; pending - .push((0, SwitchEvent::PotChange { cc: 1, value: cc_val })) + .push(( + 0, + SwitchEvent::PotChange { + cc: 1, + value: cc_val, + }, + )) .ok(); } }