diff --git a/keyboard_keyboard/code/src/constants.rs b/keyboard_keyboard/code/src/constants.rs index 1ec6a9e..ca598d8 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..840e098 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, NUM_SETTINGS_ITEMS, 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,55 @@ 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..be2a0ce 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,94 @@ 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 +755,46 @@ 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..e85c01f --- /dev/null +++ b/keyboard_keyboard/code/src/settings.rs @@ -0,0 +1,78 @@ +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, } } }