A step sequencer for Adafruit's RP2040-based macropad

revamp menus

+271 -123
-5
src/main.rs
··· 44 44 const ROWS: usize = 4; 45 45 type KeyGrid<T> = [[T; COLS]; ROWS]; 46 46 47 - static PLAY: AtomicBool = AtomicBool::new(false); 48 - static BPM: AtomicU32 = AtomicU32::new(120); 49 - static TIMING: AtomicU8 = AtomicU8::new(0); 50 - static SWING: AtomicU32 = AtomicU32::new(0); 51 - 52 47 #[embassy_executor::main] 53 48 async fn main(spawner: Spawner) { 54 49 let p = embassy_rp::init(Default::default());
+89 -24
src/menus/mod.rs
··· 1 1 mod render; 2 2 mod sequencer; 3 - use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering}; 3 + mod step; 4 4 5 5 pub use render::*; 6 - pub use sequencer::SequencerMenu; 6 + pub use sequencer::{SEQUENCER_MENU, SequencerMenuItems, SequencerMenuValue}; 7 7 8 8 use crate::display::MonoDisplay; 9 9 10 10 const WIDTH: u32 = 128; 11 11 const HEIGHT: u32 = 64; 12 12 13 - pub trait Menu { 14 - fn title(&self) -> &str; 15 - fn on_change(&mut self, step: i32); 16 - fn on_select(&mut self); 17 - fn render(&mut self, display: &mut MonoDisplay); 13 + pub struct Menu<'a, const SIZE: usize> { 14 + title: &'a str, 15 + index: usize, 16 + items: [&'a mut dyn MenuItem; SIZE], 17 + selecting: bool, 18 + } 19 + 20 + impl<'a, const SIZE: usize> Menu<'a, SIZE> { 21 + pub fn new(title: &'a str, items: [&'a mut dyn MenuItem; SIZE]) -> Self { 22 + let index = 0; 23 + let selecting = false; 24 + 25 + Self { 26 + title, 27 + index, 28 + items, 29 + selecting, 30 + } 31 + } 32 + 33 + pub fn on_change(&mut self, step: i32) { 34 + if self.selecting { 35 + let next = (self.index as i32 + step).rem_euclid(SIZE as i32); 36 + self.index = next as usize; 37 + } else { 38 + self.items[self.index].on_change(step); 39 + } 40 + } 41 + 42 + pub fn on_select(&mut self) { 43 + self.selecting = !self.selecting; 44 + } 45 + 46 + pub fn render(&mut self, display: &mut MonoDisplay) { 47 + render_menu_heading(display, self.title); 48 + 49 + for (i, item) in self.items.iter_mut().enumerate() { 50 + let (title, value) = item.as_str(); 51 + 52 + let state = if i == self.index { 53 + if self.selecting { 54 + MenuItemState::Selecting 55 + } else { 56 + MenuItemState::Selected 57 + } 58 + } else { 59 + MenuItemState::None 60 + }; 61 + 62 + render_menu_item( 63 + display, 64 + &MenuItemRender { 65 + position: i, 66 + title, 67 + value, 68 + state, 69 + }, 70 + ); 71 + } 72 + } 18 73 } 19 74 20 75 pub trait MenuItem { ··· 24 79 25 80 pub struct NumericMenuItem<'a> { 26 81 title: &'a str, 27 - value: &'a AtomicU32, 82 + value: u32, 28 83 buffer: itoa::Buffer, 84 + callback: &'a dyn Fn(u32), 29 85 } 30 86 31 87 impl<'a> NumericMenuItem<'a> { 32 - pub fn new(title: &'a str, value: &'a AtomicU32) -> Self { 88 + pub fn new(title: &'a str, value: u32, on_change: &'a dyn Fn(u32)) -> Self { 33 89 let buffer = itoa::Buffer::new(); 34 90 35 91 Self { 36 92 title, 37 93 value, 38 94 buffer, 95 + callback: on_change, 39 96 } 40 97 } 41 98 } 42 99 43 100 impl<'a> MenuItem for NumericMenuItem<'a> { 44 101 fn as_str(&mut self) -> (&str, &str) { 45 - ( 46 - self.title, 47 - self.buffer.format(self.value.load(Ordering::Relaxed)), 48 - ) 102 + (self.title, self.buffer.format(self.value)) 49 103 } 50 104 51 105 fn on_change(&mut self, step: i32) { 52 - let mut intermediate = self.value.load(Ordering::Relaxed) as i32; 106 + let mut intermediate = self.value as i32; 53 107 intermediate += step; 54 108 if intermediate < 0 { 55 109 intermediate = 0; 56 110 } 57 111 58 - self.value.store(intermediate as u32, Ordering::Relaxed); 112 + self.value = intermediate as u32; 113 + (*self.callback)(self.value); 59 114 } 60 115 } 61 116 62 117 pub struct BooleanMenuItem<'a> { 63 118 title: &'a str, 64 - value: &'a AtomicBool, 119 + value: bool, 65 120 on_str: &'a str, 66 121 off_str: &'a str, 122 + callback: &'a dyn Fn(bool), 67 123 } 68 124 69 125 impl<'a> BooleanMenuItem<'a> { 70 - pub fn new(title: &'a str, on_str: &'a str, off_str: &'a str, value: &'a AtomicBool) -> Self { 126 + pub fn new( 127 + title: &'a str, 128 + on_str: &'a str, 129 + off_str: &'a str, 130 + value: bool, 131 + on_change: &'a dyn Fn(bool), 132 + ) -> Self { 71 133 Self { 72 134 title, 73 135 value, 74 136 on_str, 75 137 off_str, 138 + callback: on_change, 76 139 } 77 140 } 78 141 } 79 142 80 143 impl MenuItem for BooleanMenuItem<'static> { 81 144 fn as_str(&mut self) -> (&str, &str) { 82 - let value = if self.value.load(Ordering::Relaxed) { 145 + let value = if self.value { 83 146 self.on_str 84 147 } else { 85 148 self.off_str ··· 88 151 } 89 152 90 153 fn on_change(&mut self, _step: i32) { 91 - self.value 92 - .store(!self.value.load(Ordering::Relaxed), Ordering::Relaxed); 154 + self.value = !self.value; 155 + (*self.callback)(self.value); 93 156 } 94 157 } 95 158 ··· 104 167 title: &'a str, 105 168 options: [T; SIZE], 106 169 index: usize, 107 - value: &'a AtomicU8, 170 + value: T, 171 + callback: &'a dyn Fn(T), 108 172 } 109 173 110 174 impl<'a, const SIZE: usize, T> EnumMenuItem<'a, SIZE, T> 111 175 where 112 176 T: Stringable, 113 177 { 114 - pub fn new(title: &'a str, options: [T; SIZE], value: &'a AtomicU8) -> Self { 178 + pub fn new(title: &'a str, options: [T; SIZE], value: T, on_change: &'a dyn Fn(T)) -> Self { 115 179 // TODO: set index to currently selected! 116 180 117 181 Self { ··· 119 183 options, 120 184 index: 0, 121 185 value, 186 + callback: on_change, 122 187 } 123 188 } 124 189 } ··· 134 199 fn on_change(&mut self, step: i32) { 135 200 let next = (self.index as i32 + step).rem_euclid(SIZE as i32); 136 201 self.index = next as usize; 137 - self.value 138 - .store(self.options[self.index].into(), Ordering::Relaxed); 202 + self.value = self.options[self.index]; 203 + (*self.callback)(self.value); 139 204 } 140 205 }
+87 -54
src/menus/sequencer.rs
··· 1 + use core::cell::RefCell; 2 + 3 + use embassy_sync::blocking_mutex::{Mutex, raw::ThreadModeRawMutex}; 4 + 1 5 use crate::{ 2 - display::MonoDisplay, 3 - menus::{ 4 - Menu, MenuItem, MenuItemRender, MenuItemState, Stringable, render_menu_heading, 5 - render_menu_item, 6 - }, 6 + menus::{BooleanMenuItem, EnumMenuItem, NumericMenuItem, Stringable}, 7 7 sequencer_timer::TimingOption, 8 8 }; 9 9 ··· 20 20 } 21 21 } 22 22 23 - pub struct SequencerMenu<'a, const SIZE: usize> { 24 - index: usize, 25 - items: [&'a mut dyn MenuItem; SIZE], 26 - selecting: bool, 23 + #[derive(Clone, Copy)] 24 + pub struct SequencerMenuValue { 25 + pub play: bool, 26 + pub bpm: u32, 27 + pub timing: TimingOption, 28 + pub swing: u32, 27 29 } 28 30 29 - impl<'a, const SIZE: usize> SequencerMenu<'a, SIZE> { 30 - pub fn new(items: [&'a mut dyn MenuItem; SIZE]) -> Self { 31 - let index = 0; 32 - let selecting = false; 33 - 31 + impl Default for SequencerMenuValue { 32 + fn default() -> Self { 34 33 Self { 35 - index, 36 - items, 37 - selecting, 34 + play: false, 35 + bpm: 120, 36 + timing: TimingOption::Eighth, 37 + swing: 0, 38 38 } 39 39 } 40 40 } 41 41 42 - impl<'a, const SIZE: usize> Menu for SequencerMenu<'a, SIZE> { 43 - fn title(&self) -> &str { 44 - "Sequencer" 45 - } 42 + type SequencerMenuMutex = Mutex<ThreadModeRawMutex, Option<SequencerMenuValue>>; 43 + pub static SEQUENCER_MENU: SequencerMenuMutex = Mutex::new(None); 46 44 47 - fn on_change(&mut self, step: i32) { 48 - if self.selecting { 49 - let next = (self.index as i32 + step).rem_euclid(SIZE as i32); 50 - self.index = next as usize; 51 - } else { 52 - self.items[self.index].on_change(step); 53 - } 54 - } 45 + pub struct SequencerMenuItems<'a> { 46 + pub play_menu: BooleanMenuItem<'a>, 47 + pub bpm_menu: NumericMenuItem<'a>, 48 + pub timing_menu: EnumMenuItem<'a, 6, TimingOption>, 49 + pub swing_menu: NumericMenuItem<'a>, 50 + } 55 51 56 - fn on_select(&mut self) { 57 - self.selecting = !self.selecting; 58 - } 52 + impl<'a> SequencerMenuItems<'a> { 53 + pub fn new() -> Self { 54 + let defaults = SequencerMenuValue::default(); 55 + let play_menu = 56 + BooleanMenuItem::new("STATUS", "PLAYING", "PAUSED", defaults.play, &|value| { 57 + unsafe { 58 + SEQUENCER_MENU.lock_mut(|inner| { 59 + if let Some(menu_value) = inner { 60 + menu_value.play = value; 61 + } 62 + }) 63 + }; 64 + }); 59 65 60 - fn render(&mut self, display: &mut MonoDisplay) { 61 - render_menu_heading(display, self.title()); 66 + let bpm_menu = NumericMenuItem::new("BPM", defaults.bpm, &|value| { 67 + unsafe { 68 + SEQUENCER_MENU.lock_mut(|inner| { 69 + if let Some(menu_value) = inner { 70 + menu_value.bpm = value; 71 + } 72 + }) 73 + }; 74 + }); 62 75 63 - for (i, item) in self.items.iter_mut().enumerate() { 64 - let (title, value) = item.as_str(); 65 - 66 - let state = if i == self.index { 67 - if self.selecting { 68 - MenuItemState::Selecting 69 - } else { 70 - MenuItemState::Selected 71 - } 72 - } else { 73 - MenuItemState::None 76 + let timing_menu = EnumMenuItem::new( 77 + "TIMING", 78 + [ 79 + TimingOption::Quarter, 80 + TimingOption::QuarterTriplet, 81 + TimingOption::Eighth, 82 + TimingOption::EighthTriplet, 83 + TimingOption::Sixteenth, 84 + TimingOption::SixteenthTriplet, 85 + ], 86 + defaults.timing, 87 + &|value| { 88 + unsafe { 89 + SEQUENCER_MENU.lock_mut(|inner| { 90 + if let Some(menu_value) = inner { 91 + menu_value.timing = value; 92 + } 93 + }) 94 + }; 95 + }, 96 + ); 97 + let swing_menu = NumericMenuItem::new("SWING", defaults.swing, &|value| { 98 + unsafe { 99 + SEQUENCER_MENU.lock_mut(|inner| { 100 + if let Some(menu_value) = inner { 101 + menu_value.swing = value; 102 + } 103 + }) 74 104 }; 105 + }); 75 106 76 - render_menu_item( 77 - display, 78 - &MenuItemRender { 79 - position: i, 80 - title, 81 - value, 82 - state, 83 - }, 84 - ); 107 + unsafe { 108 + SEQUENCER_MENU.lock_mut(|value| { 109 + *value = Some(defaults); 110 + }); 111 + } 112 + 113 + Self { 114 + play_menu, 115 + bpm_menu, 116 + timing_menu, 117 + swing_menu, 85 118 } 86 119 } 87 120 }
+69
src/menus/step.rs
··· 1 + use crate::{ 2 + menus::{BooleanMenuItem, EnumMenuItem, NumericMenuItem, Stringable}, 3 + sequencer_timer::TimingOption, 4 + }; 5 + 6 + enum Note { 7 + A, 8 + BFlat, 9 + B, 10 + C, 11 + CSharp, 12 + D, 13 + EFlat, 14 + E, 15 + F, 16 + FSharp, 17 + G, 18 + AFlat, 19 + } 20 + 21 + impl Stringable for Note { 22 + fn as_str(&self) -> &str { 23 + match self { 24 + Note::A => "A", 25 + Note::BFlat => "Bb", 26 + Note::B => "B", 27 + Note::C => "C", 28 + Note::CSharp => "C#", 29 + Note::D => "D", 30 + Note::EFlat => "Eb", 31 + Note::E => "E", 32 + Note::F => "F", 33 + Note::FSharp => "F#", 34 + Note::G => "G", 35 + Note::AFlat => "Ab", 36 + } 37 + } 38 + } 39 + 40 + //pub struct StepMenuItems<'a> { 41 + // pub note_menu: EnumMenuItem<'a, 12>, 42 + // pub octave_menu: NumericMenuItem<'a>, 43 + // pub velocity_menu: NumericMenuItem<'a>, 44 + //} 45 + // 46 + //impl<'a> StepMenuItems<'a> { 47 + // pub fn new() -> Self { 48 + // 49 + // let note_menu = EnumMenuItem::new("NOTE", &[ 50 + // Note::BFlat 51 + // Note::B, 52 + // Note::C, 53 + // Note::CSharp, 54 + // Note::D, 55 + // Note::EFlat, 56 + // Note::E, 57 + // Note::F, 58 + // Note::FSharp, 59 + // Note::G, 60 + // Note::AFlat, 61 + // ]); 62 + // 63 + // let octave_menu = NumericMenuItem::new() 64 + // 65 + // Self { 66 + // note_menu, 67 + // } 68 + // } 69 + //}
+1 -1
src/sequencer_timer.rs
··· 3 3 4 4 use crate::{COLS, ROWS}; 5 5 6 - #[derive(Clone, Copy, IntoPrimitive, FromPrimitive)] 6 + #[derive(Clone, Copy, IntoPrimitive, FromPrimitive, Default)] 7 7 #[repr(u8)] 8 8 pub enum TimingOption { 9 9 #[default]
+11 -29
src/tasks/display.rs
··· 1 - use core::sync::atomic::Ordering; 2 - 3 - use crate::{ 4 - BPM, PLAY, SWING, TIMING, 5 - menus::{BooleanMenuItem, EnumMenuItem, Menu, NumericMenuItem, SequencerMenu}, 6 - sequencer_timer::TimingOption, 7 - }; 1 + use crate::menus::{Menu, SequencerMenuItems}; 8 2 use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel}; 9 3 10 4 use crate::display::Display; ··· 17 11 } 18 12 19 13 struct Menus<'a> { 20 - pub sequencer: SequencerMenu<'a, 4>, 14 + pub sequencer: Menu<'a, 4>, 21 15 } 22 16 23 17 impl<'a> Menus<'a> { 24 - pub fn new(sequencer: SequencerMenu<'a, 4>) -> Self { 18 + pub fn new(sequencer: Menu<'a, 4>) -> Self { 25 19 Self { sequencer } 26 20 } 27 21 } ··· 29 23 #[embassy_executor::task] 30 24 pub async fn drive_display(mut display: Display) { 31 25 display.init(); 32 - TIMING.store(TimingOption::Quarter.into(), Ordering::Relaxed); 33 - let mut play_menu = BooleanMenuItem::new("STATUS", "PLAYING", "PAUSED", &PLAY); 34 - let mut bpm_menu = NumericMenuItem::new("BPM", &BPM); 35 - let mut timing_menu = EnumMenuItem::new( 36 - "TIMING", 26 + 27 + let mut sequencer_items = SequencerMenuItems::new(); 28 + let sequencer = Menu::new( 29 + "Sequencer", 37 30 [ 38 - TimingOption::Quarter, 39 - TimingOption::QuarterTriplet, 40 - TimingOption::Eighth, 41 - TimingOption::EighthTriplet, 42 - TimingOption::Sixteenth, 43 - TimingOption::SixteenthTriplet, 31 + &mut sequencer_items.play_menu, 32 + &mut sequencer_items.bpm_menu, 33 + &mut sequencer_items.timing_menu, 34 + &mut sequencer_items.swing_menu, 44 35 ], 45 - &TIMING, 46 36 ); 47 - let mut swing_menu = NumericMenuItem::new("SWING", &SWING); 48 - 49 - let sequencer = SequencerMenu::new([ 50 - &mut play_menu, 51 - &mut bpm_menu, 52 - &mut timing_menu, 53 - &mut swing_menu, 54 - ]); 55 37 56 38 let mut menus = Menus::new(sequencer); 57 39 menus.sequencer.render(&mut display.display);
+14 -10
src/tasks/sequencer.rs
··· 1 - use core::sync::atomic::Ordering; 2 - 3 1 use crate::{ 4 - BPM, COLS, PLAY, SWING, TIMING, 2 + COLS, 3 + menus::{SEQUENCER_MENU, SequencerMenuValue}, 5 4 sequencer_timer::{SequencerConfig, SequencerTimer}, 6 5 tasks::{CONTROLS_CHANNEL, controls::ControlEvent}, 7 6 }; ··· 12 11 let cols = COLS as u8; 13 12 14 13 let mut timer = SequencerTimer::new(); 14 + let mut play = false; 15 15 loop { 16 - let play = PLAY.load(Ordering::Relaxed); 16 + SEQUENCER_MENU.lock(|value| { 17 + let SequencerMenuValue { 18 + play: temp_play, 19 + bpm, 20 + timing, 21 + swing, 22 + } = value.unwrap(); 23 + timer.set(SequencerConfig { bpm, timing, swing }); 24 + play = temp_play; 25 + }); 26 + 17 27 if play { 18 28 let coord = (step % cols, step / cols); 19 29 CONTROLS_CHANNEL ··· 21 31 .await; 22 32 23 33 step = (step + 1).rem_euclid(12); 24 - 25 - timer.set(SequencerConfig { 26 - bpm: BPM.load(Ordering::Relaxed), 27 - timing: TIMING.load(Ordering::Relaxed).into(), 28 - swing: SWING.load(Ordering::Relaxed), 29 - }); 30 34 } 31 35 32 36 timer.next_step().await;