├── firmware ├── rust-toolchain.toml ├── Cargo.toml ├── microgroove_app │ ├── memory.x │ ├── Embed.toml │ ├── .cargo │ │ └── config.toml │ ├── src │ │ ├── midi.rs │ │ ├── encoder.rs │ │ ├── peripherals.rs │ │ ├── input.rs │ │ └── display.rs │ └── Cargo.toml └── microgroove_sequencer │ ├── Cargo.toml │ └── src │ ├── machine_resources.rs │ ├── machine │ ├── unit_machine.rs │ └── rand_melody_machine.rs │ ├── machine.rs │ ├── sequence_generator.rs │ ├── part.rs │ ├── quantizer.rs │ ├── lib.rs │ ├── midi.rs │ ├── sequencer.rs │ └── param.rs ├── hardware ├── microgroove.kicad_pcb ├── microgroove-circuit.fzz ├── microgroove-circuit-breadboard.png ├── microgroove-circuit-breadboard-photo.jpg ├── lib │ ├── NHD-2.7-12864WDW3 │ │ ├── NHD-2.7-12864WDW3.dcm │ │ ├── NHD-2.7-12864WDW3.lib │ │ ├── NHD2712864WDW3.kicad_mod │ │ ├── NHD-2.7-12864WDW3.mod │ │ └── NHD-2.7-12864WDW3.kicad_sym │ ├── SJ1_3513 │ │ ├── SJ1-3513.lib │ │ ├── CUI_SJ1-3513.kicad_mod │ │ └── SJ1-3513.kicad_sym │ ├── rp_pico │ │ └── MCU_RaspberryPi_and_Boards.pretty │ │ │ ├── Crystal_SMD_HC49-US.kicad_mod │ │ │ └── RP2040-QFN-56.kicad_mod │ ├── H11L1 │ │ └── DIP762W46P254L889H508Q6B.kicad_mod │ ├── SW4_TL1100F160Q6JBLK_EWI │ │ ├── SW4_TL1100F160Q6JBLK_EWI.kicad_sym │ │ └── footprints.pretty │ │ │ └── SW4_TL1100F160Q6JBLK_EWI.kicad_mod │ ├── PEC11R_4215F_S0024 │ │ ├── XDCR_PEC11R-4215F-S0024.kicad_mod │ │ └── PEC11R-4215F-S0024.kicad_sym │ └── UJ2-MIBH-4-SMT-TR │ │ ├── CUIDEVICES_UJ2-MIBH-4-SMT-TR.kicad_mod │ │ └── UJ2-MIBH-4-SMT-TR.kicad_sym ├── fp-lib-table ├── sym-lib-table ├── bom.csv └── microgroove.kicad_prl ├── .gitignore ├── LICENSE └── README.md /firmware/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /firmware/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "microgroove_sequencer" 4 | ] 5 | -------------------------------------------------------------------------------- /hardware/microgroove.kicad_pcb: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 20240108) (generator "pcbnew") (generator_version "8.0") 2 | ) -------------------------------------------------------------------------------- /hardware/microgroove-circuit.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afternoon/microgroove/HEAD/hardware/microgroove-circuit.fzz -------------------------------------------------------------------------------- /hardware/microgroove-circuit-breadboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afternoon/microgroove/HEAD/hardware/microgroove-circuit-breadboard.png -------------------------------------------------------------------------------- /hardware/microgroove-circuit-breadboard-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afternoon/microgroove/HEAD/hardware/microgroove-circuit-breadboard-photo.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | **/Cargo.lock 3 | **/.DS_Store 4 | hardware/microgroove-backups/ 5 | hardware/fp-info-cache 6 | hardware/#auto_saved_files# 7 | hardware/*.lck 8 | -------------------------------------------------------------------------------- /hardware/lib/NHD-2.7-12864WDW3/NHD-2.7-12864WDW3.dcm: -------------------------------------------------------------------------------- 1 | EESchema-DOCLIB Version 2.0 2 | # 3 | $CMP NHD-2.7-12864WDW3 4 | D LCD OLED GRAPHIC 128X64 WHT 5 | K 6 | F https://componentsearchengine.com/Datasheets/1/NHD-2.7-12864WDW3.pdf 7 | $ENDCMP 8 | # 9 | #End Doc Library 10 | -------------------------------------------------------------------------------- /firmware/microgroove_app/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 5 | } 6 | 7 | EXTERN(BOOT2_FIRMWARE) 8 | 9 | SECTIONS { 10 | /* ### Boot loader */ 11 | .boot2 ORIGIN(BOOT2) : 12 | { 13 | KEEP(*(.boot2)); 14 | } > BOOT2 15 | } INSERT BEFORE .text; -------------------------------------------------------------------------------- /firmware/microgroove_app/Embed.toml: -------------------------------------------------------------------------------- 1 | [default.general] 2 | chip = "RP2040" 3 | log_level = "WARN" 4 | connect_under_reset = false # RP2040 does not support connect_under_reset 5 | 6 | [default.flashing] 7 | enabled = true 8 | 9 | [default.reset] 10 | enabled = true 11 | halt_afterwards = false 12 | 13 | [default.probe] 14 | protocol = "Swd" 15 | speed = 1000 16 | 17 | [default.rtt] 18 | enabled = true 19 | up_mode = "NoBlockSkip" 20 | channels = [ 21 | { up = 0, down = 0, name = "debug", up_mode = "NoBlockSkip", format = "Defmt" }, 22 | ] 23 | timeout = 3000 24 | show_timestamps = true 25 | 26 | [default.gdb] 27 | enabled = true 28 | gdb_connection_string = "127.0.0.1:2345" 29 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "microgroove_sequencer" 3 | version = "0.1.0" 4 | authors = ["afternoon "] 5 | edition = "2021" 6 | license = "MIT" 7 | homepage = "https://github.com/afternoon/microgroove" 8 | repository = "https://github.com/afternoon/microgroove" 9 | 10 | [dependencies] 11 | embedded-midi = "0.1.2" 12 | fugit = "0.3.6" 13 | heapless = "0.7.16" 14 | midi-types = "0.1.2" 15 | rp2040-hal = { version = "0.7.0", optional = true } 16 | rand = { version = "0.8.5", optional = true } 17 | rand_core = { version = "0.6.4", optional = true } 18 | 19 | [features] 20 | default = ["host_testing"] 21 | host_testing = ["dep:rand"] 22 | target_release = ["dep:rp2040-hal", "dep:rand_core"] 23 | -------------------------------------------------------------------------------- /hardware/fp-lib-table: -------------------------------------------------------------------------------- 1 | (fp_lib_table 2 | (version 7) 3 | (lib (name "UJ2-MIBH-4-SMT-TR")(type "KiCad")(uri "${KIPRJMOD}/lib/UJ2-MIBH-4-SMT-TR")(options "")(descr "")) 4 | (lib (name "PEC11R_4215F_S0024")(type "KiCad")(uri "${KIPRJMOD}/lib/PEC11R_4215F_S0024")(options "")(descr "")) 5 | (lib (name "MCU_RaspberryPi_and_Boards")(type "KiCad")(uri "${KIPRJMOD}/lib/rp_pico/MCU_RaspberryPi_and_Boards.pretty")(options "")(descr "")) 6 | (lib (name "NHD-2.7-12864WDW3")(type "KiCad")(uri "${KIPRJMOD}/lib/NHD-2.7-12864WDW3")(options "")(descr "")) 7 | (lib (name "SJ1_3513")(type "KiCad")(uri "${KIPRJMOD}/lib/SJ1_3513")(options "")(descr "")) 8 | (lib (name "footprints")(type "KiCad")(uri "${KIPRJMOD}/lib/SW4_TL1100F160Q6JBLK_EWI/footprints.pretty")(options "")(descr "")) 9 | ) 10 | -------------------------------------------------------------------------------- /hardware/sym-lib-table: -------------------------------------------------------------------------------- 1 | (sym_lib_table 2 | (version 7) 3 | (lib (name "MCU_RaspberryPi_and_Boards")(type "KiCad")(uri "${KIPRJMOD}/lib/rp_pico/MCU_RaspberryPi_and_Boards.kicad_sym")(options "")(descr "")) 4 | (lib (name "UJ2-MIBH-4-SMT-TR")(type "KiCad")(uri "${KIPRJMOD}/lib/UJ2-MIBH-4-SMT-TR/UJ2-MIBH-4-SMT-TR.kicad_sym")(options "")(descr "")) 5 | (lib (name "PEC11R-4215F-S0024")(type "KiCad")(uri "${KIPRJMOD}/lib/PEC11R_4215F_S0024/PEC11R-4215F-S0024.kicad_sym")(options "")(descr "")) 6 | (lib (name "SJ1-3513")(type "KiCad")(uri "${KIPRJMOD}/lib/SJ1_3513/SJ1-3513.kicad_sym")(options "")(descr "")) 7 | (lib (name "NHD-2.7-12864WDW3")(type "KiCad")(uri "${KIPRJMOD}/lib/NHD-2.7-12864WDW3/NHD-2.7-12864WDW3.kicad_sym")(options "")(descr "")) 8 | (lib (name "SW4_TL1100F160Q6JBLK_EWI")(type "KiCad")(uri "${KIPRJMOD}/lib/SW4_TL1100F160Q6JBLK_EWI/SW4_TL1100F160Q6JBLK_EWI.kicad_sym")(options "")(descr "")) 9 | ) 10 | -------------------------------------------------------------------------------- /firmware/microgroove_app/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Choose a default "cargo run" tool: 2 | # - probe-run provides flashing and defmt via a hardware debugger 3 | # - cargo embed offers flashing, rtt, defmt and a gdb server via a hardware debugger 4 | # it is configured via the Embed.toml in the root of this project 5 | # - elf2uf2-rs loads firmware over USB when the rp2040 is in boot mode 6 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 7 | runner = "probe-run --chip RP2040" 8 | # runner = "cargo embed" 9 | # # runner = "elf2uf2-rs -d" 10 | 11 | rustflags = [ 12 | "-C", "linker=flip-link", 13 | "-C", "link-arg=--nmagic", 14 | "-C", "link-arg=-Tlink.x", 15 | "-C", "link-arg=-Tdefmt.x", 16 | 17 | # Code-size optimizations. 18 | # trap unreachable can save a lot of space, but requires nightly compiler. 19 | # uncomment the next line if you wish to enable it 20 | # "-Z", "trap-unreachable=no", 21 | "-C", "inline-threshold=5", 22 | "-C", "no-vectorize-loops", 23 | ] 24 | 25 | [build] 26 | target = "thumbv6m-none-eabi" 27 | 28 | [env] 29 | DEFMT_LOG = "debug" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ben Godfrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/machine_resources.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "target_release")] 2 | use rp2040_hal::rosc::{Enabled, RingOscillator}; 3 | 4 | #[cfg(feature = "host_testing")] 5 | use rand::prelude::*; 6 | 7 | #[cfg(feature = "target_release")] 8 | use rand_core::RngCore; 9 | 10 | /// `MachineResources` defines a set of methods that machines can use when generating sequences, 11 | /// e.g a source of random numbers. 12 | pub struct MachineResources { 13 | #[cfg(feature = "target_release")] 14 | rosc: RingOscillator, 15 | } 16 | 17 | impl MachineResources { 18 | #[cfg(feature = "target_release")] 19 | pub fn new(rosc: RingOscillator) -> MachineResources { 20 | MachineResources { rosc } 21 | } 22 | 23 | #[cfg(feature = "host_testing")] 24 | pub fn new() -> MachineResources { 25 | MachineResources {} 26 | } 27 | 28 | #[cfg(feature = "target_release")] 29 | pub fn random_u64(&mut self) -> u64 { 30 | self.rosc.next_u64() 31 | } 32 | 33 | #[cfg(feature = "host_testing")] 34 | pub fn random_u64(&mut self) -> u64 { 35 | random() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /firmware/microgroove_app/src/midi.rs: -------------------------------------------------------------------------------- 1 | use defmt::trace; 2 | use midi_types::MidiMessage; 3 | 4 | pub fn log_message(message: &MidiMessage) { 5 | match message { 6 | MidiMessage::TimingClock => trace!("[midi_send] clock"), 7 | MidiMessage::Start => trace!("[midi_send] start"), 8 | MidiMessage::Stop => trace!("[midi_send] stop"), 9 | MidiMessage::Continue => trace!("[midi_send] continue"), 10 | MidiMessage::NoteOn(midi_channel, note, velocity) => { 11 | let midi_channel: u8 = (*midi_channel).into(); 12 | let note: u8 = (*note).into(); 13 | let velocity: u8 = (*velocity).into(); 14 | trace!( 15 | "[midi_send] note on midi_channel={} note={} velocity={}", 16 | midi_channel, 17 | note, 18 | velocity 19 | ); 20 | } 21 | MidiMessage::NoteOff(midi_channel, note, _velocity) => { 22 | let midi_channel: u8 = (*midi_channel).into(); 23 | let note: u8 = (*note).into(); 24 | trace!( 25 | "[midi_send] note off midi_channel={} note={}", 26 | midi_channel, 27 | note 28 | ); 29 | } 30 | _ => trace!("[midi_send] UNKNOWN"), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hardware/lib/SJ1_3513/SJ1-3513.lib: -------------------------------------------------------------------------------- 1 | EESchema-LIBRARY Version 2.3 2 | #encoding utf-8 3 | #(c) SnapEDA 2016 (snapeda.com) 4 | #This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA) with Design Exception 1.0 5 | # 6 | # SJ1-3513 7 | # 8 | DEF SJ1-3513 J 0 40 Y N 1 L N 9 | F0 "J" -500 300 50 H V L BNN 10 | F1 "SJ1-3513" -501 -501 50 H V L BNN 11 | F2 "CUI_SJ1-3513" 0 0 50 H I L BNN 12 | F3 "" 0 0 50 H I L BNN 13 | F4 "1.03" 0 0 50 H I L BNN "PARTREV" 14 | F5 "Manufacturer recommendation" 0 0 50 H I L BNN "STANDARD" 15 | F6 "CUI" 0 0 50 H I L BNN "MANUFACTURER" 16 | DRAW 17 | P 2 0 0 10 -500 200 -500 -300 N 18 | P 2 0 0 10 500 -300 500 200 N 19 | P 2 0 0 6 500 100 -325 100 N 20 | P 2 0 0 6 -325 100 -325 -25 N 21 | P 2 0 0 6 -325 -25 -350 -25 N 22 | P 2 0 0 6 -350 -25 -350 -175 N 23 | P 2 0 0 6 -350 -175 -300 -175 N 24 | P 2 0 0 6 -300 -175 -300 -25 N 25 | P 2 0 0 6 -300 -25 -325 -25 N 26 | P 2 0 0 6 500 0 -100 0 N 27 | P 2 0 0 6 -100 0 -150 -50 N 28 | P 2 0 0 6 -150 -50 -200 0 N 29 | P 2 0 0 6 500 -200 25 -200 N 30 | P 2 0 0 6 25 -200 -25 -150 N 31 | P 2 0 0 6 -25 -150 -75 -200 N 32 | S -500 -300 500 200 0 0 10 f 33 | X ~ 1 700 100 200 L 40 40 0 0 P 34 | X ~ 3 700 0 200 L 40 40 0 0 P 35 | X ~ 2 700 -200 200 L 40 40 0 0 P 36 | ENDDRAW 37 | ENDDEF 38 | # 39 | # End Library -------------------------------------------------------------------------------- /hardware/bom.csv: -------------------------------------------------------------------------------- 1 | "Reference","Value","Datasheet","Footprint","Qty","DNP" 2 | "D1","1N914","http://www.vishay.com/docs/85622/1n914.pdf","Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal","1","" 3 | "DS1","NHD-2.7-12864WDW3","https://componentsearchengine.com/Datasheets/1/NHD-2.7-12864WDW3.pdf","NHD-2.7-12864WDW3:NHD2712864WDW3","1","" 4 | "J1","UJ2-MIBH-4-SMT-TR","https://www.cuidevices.com/product/resource/uj2-mibh-4-smt.pdf","UJ2-MIBH-4-SMT-TR:CUIDEVICES_UJ2-MIBH-4-SMT-TR","1","" 5 | "J2,J3","SJ1-3513","https://www.cuidevices.com/product/resource/sj1-351x.pdf","SJ1_3513:CUI_SJ1-3513","2","" 6 | "R1","470","~","Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal","1","" 7 | "R2","220","~","Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal","1","" 8 | "R3","33","~","Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal","1","" 9 | "R4","10","~","Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal","1","" 10 | "SW1,SW2,SW3,SW4,SW5,SW6","RotaryEncoder","https://www.bourns.com/docs/Product-Datasheets/PEC11R.pdf","PEC11R_4215F_S0024:XDCR_PEC11R-4215F-S0024","6","" 11 | "SW7,SW8,SW9,SW10,SW11","TL1100F160Q6JBLK","TL1100F160Q6JBLK","footprints:SW4_TL1100F160Q6JBLK_EWI","5","" 12 | "U1","Pico","https://datasheets.raspberrypi.com/pico/pico-datasheet.pdf","MCU_RaspberryPi_and_Boards:RPi_Pico_SMD_TH","1","" 13 | "U2","H11L1","https://www.onsemi.com/pub/Collateral/H11L3M-D.PDF","Package_DIP:DIP-6_W7.62mm","1","" 14 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/machine/unit_machine.rs: -------------------------------------------------------------------------------- 1 | /// Reference machine which passes sequence input through unmodified. 2 | use super::Machine; 3 | use crate::{machine_resources::MachineResources, param::ParamList, Sequence}; 4 | 5 | #[derive(Debug)] 6 | pub struct UnitMachine { 7 | params: ParamList, 8 | } 9 | 10 | impl UnitMachine { 11 | pub fn new() -> UnitMachine { 12 | UnitMachine { 13 | params: ParamList::new(), 14 | } 15 | } 16 | } 17 | 18 | impl Machine for UnitMachine { 19 | fn name(&self) -> &str { 20 | "UNIT" 21 | } 22 | 23 | fn params(&self) -> &ParamList { 24 | &self.params 25 | } 26 | 27 | fn params_mut(&mut self) -> &mut ParamList { 28 | &mut self.params 29 | } 30 | 31 | fn generate(&mut self, _machine_resources: &mut MachineResources) {} 32 | 33 | fn apply(&self, sequence: Sequence) -> Sequence { 34 | sequence 35 | } 36 | } 37 | 38 | unsafe impl Send for UnitMachine {} 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | use crate::sequence_generator::SequenceGenerator; 44 | 45 | #[test] 46 | fn unitmachine_should_passthrough_sequence_unmodified() { 47 | let machine = UnitMachine::new(); 48 | let input_sequence = SequenceGenerator::initial_sequence(8); 49 | let output_sequence = machine.apply(SequenceGenerator::initial_sequence(8)); 50 | assert_eq!(output_sequence, input_sequence); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /hardware/microgroove.kicad_prl: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "active_layer": 0, 4 | "active_layer_preset": "", 5 | "auto_track_width": true, 6 | "hidden_netclasses": [], 7 | "hidden_nets": [], 8 | "high_contrast_mode": 0, 9 | "net_color_mode": 1, 10 | "opacity": { 11 | "images": 0.6, 12 | "pads": 1.0, 13 | "tracks": 1.0, 14 | "vias": 1.0, 15 | "zones": 0.6 16 | }, 17 | "selection_filter": { 18 | "dimensions": true, 19 | "footprints": true, 20 | "graphics": true, 21 | "keepouts": true, 22 | "lockedItems": false, 23 | "otherItems": true, 24 | "pads": true, 25 | "text": true, 26 | "tracks": true, 27 | "vias": true, 28 | "zones": true 29 | }, 30 | "visible_items": [ 31 | 0, 32 | 1, 33 | 2, 34 | 3, 35 | 4, 36 | 5, 37 | 8, 38 | 9, 39 | 10, 40 | 11, 41 | 12, 42 | 13, 43 | 15, 44 | 16, 45 | 17, 46 | 18, 47 | 19, 48 | 20, 49 | 21, 50 | 22, 51 | 23, 52 | 24, 53 | 25, 54 | 26, 55 | 27, 56 | 28, 57 | 29, 58 | 30, 59 | 32, 60 | 33, 61 | 34, 62 | 35, 63 | 36, 64 | 39, 65 | 40 66 | ], 67 | "visible_layers": "fffffff_ffffffff", 68 | "zone_display_mode": 0 69 | }, 70 | "git": { 71 | "repo_password": "", 72 | "repo_type": "", 73 | "repo_username": "", 74 | "ssh_key": "" 75 | }, 76 | "meta": { 77 | "filename": "microgroove.kicad_prl", 78 | "version": 3 79 | }, 80 | "project": { 81 | "files": [] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /hardware/lib/NHD-2.7-12864WDW3/NHD-2.7-12864WDW3.lib: -------------------------------------------------------------------------------- 1 | EESchema-LIBRARY Version 2.3 2 | #encoding utf-8 3 | #SamacSys ECAD Model NHD-2.7-12864WDW3 4 | #/990652/1365383/2.50/24/4/Display 5 | DEF NHD-2.7-12864WDW3 DS 0 30 Y Y 1 F N 6 | F0 "DS" 1350 300 50 H V L CNN 7 | F1 "NHD-2.7-12864WDW3" 1350 200 50 H V L CNN 8 | F2 "NHD2712864WDW3" 1350 100 50 H I L CNN 9 | F3 "https://componentsearchengine.com/Datasheets/1/NHD-2.7-12864WDW3.pdf" 1350 0 50 H I L CNN 10 | F4 "LCD OLED GRAPHIC 128X64 WHT" 1350 -100 50 H I L CNN "Description" 11 | F5 "6" 1350 -200 50 H I L CNN "Height" 12 | F6 "763-NHD2712864WDW3" 1350 -300 50 H I L CNN "Mouser Part Number" 13 | F7 "https://www.mouser.co.uk/ProductDetail/Newhaven-Display/NHD-2.7-12864WDW3?qs=EU6FO9ffTwf2tAy5%2FRPmRA%3D%3D" 1350 -400 50 H I L CNN "Mouser Price/Stock" 14 | F8 "Newhaven Display" 1350 -500 50 H I L CNN "Manufacturer_Name" 15 | F9 "NHD-2.7-12864WDW3" 1350 -600 50 H I L CNN "Manufacturer_Part_Number" 16 | DRAW 17 | X VSS 1 1500 -2000 200 L 50 50 0 0 P 18 | X VDD 2 1500 -1900 200 L 50 50 0 0 P 19 | X N.C._(BC_VDD) 3 1500 -1800 200 L 50 50 0 0 P 20 | X D/C 4 1500 -1700 200 L 50 50 0 0 P 21 | X R/W_OR_/WR 5 1500 -1600 200 L 50 50 0 0 P 22 | X E_OR_/RD 6 1500 -1500 200 L 50 50 0 0 P 23 | X DB0 7 1500 -1400 200 L 50 50 0 0 P 24 | X DB1 8 1500 -1300 200 L 50 50 0 0 P 25 | X DB2 9 1500 -1200 200 L 50 50 0 0 P 26 | X DB3 10 1500 -1100 200 L 50 50 0 0 P 27 | X DB4 11 1500 -1000 200 L 50 50 0 0 P 28 | X DB5 12 1500 -900 200 L 50 50 0 0 P 29 | X DB6 13 1500 -800 200 L 50 50 0 0 P 30 | X DB7 14 1500 -700 200 L 50 50 0 0 P 31 | X N.C._(VCC) 15 1500 -600 200 L 50 50 0 0 P 32 | X /RES 16 1500 -500 200 L 50 50 0 0 P 33 | X /CS 17 1500 -400 200 L 50 50 0 0 P 34 | X /SHDN_(N.C.) 18 1500 -300 200 L 50 50 0 0 P 35 | X BS1 19 1500 -200 200 L 50 50 0 0 P 36 | X BS0 20 1500 -100 200 L 50 50 0 0 P 37 | X MH1 MH1 1500 -2100 200 L 50 50 0 0 P 38 | X MH2 MH2 1500 0 200 L 50 50 0 0 P 39 | X MH3 MH3 0 0 200 R 50 50 0 0 P 40 | X MH4 MH4 0 -100 200 R 50 50 0 0 P 41 | P 5 0 1 6 200 100 1300 100 1300 -2200 200 -2200 200 100 N 42 | ENDDRAW 43 | ENDDEF 44 | # 45 | #End Library 46 | -------------------------------------------------------------------------------- /hardware/lib/rp_pico/MCU_RaspberryPi_and_Boards.pretty/Crystal_SMD_HC49-US.kicad_mod: -------------------------------------------------------------------------------- 1 | (module Crystal_SMD_HC49-US (layer F.Cu) (tedit 5F0C7995) 2 | (descr "SMD Crystal HC-49-SD http://cdn-reichelt.de/documents/datenblatt/B400/xxx-HC49-SMD.pdf, 11.4x4.7mm^2 package") 3 | (tags "SMD SMT crystal") 4 | (attr smd) 5 | (fp_text reference Y1 (at 0 -3.55) (layer F.SilkS) 6 | (effects (font (size 1 1) (thickness 0.15))) 7 | ) 8 | (fp_text value ABLS-12.000MHZ-B4-T (at 0 3.55) (layer F.Fab) 9 | (effects (font (size 1 1) (thickness 0.15))) 10 | ) 11 | (fp_line (start -6.7 1.3) (end -6.7 2.55) (layer F.SilkS) (width 0.12)) 12 | (fp_text user %R (at 0 0) (layer F.Fab) 13 | (effects (font (size 1 1) (thickness 0.15))) 14 | ) 15 | (fp_line (start -5.7 -2.35) (end -5.7 2.35) (layer F.Fab) (width 0.1)) 16 | (fp_line (start -5.7 2.35) (end 5.7 2.35) (layer F.Fab) (width 0.1)) 17 | (fp_line (start 5.7 2.35) (end 5.7 -2.35) (layer F.Fab) (width 0.1)) 18 | (fp_line (start 5.7 -2.35) (end -5.7 -2.35) (layer F.Fab) (width 0.1)) 19 | (fp_line (start -3.015 -2.115) (end 3.015 -2.115) (layer F.Fab) (width 0.1)) 20 | (fp_line (start -3.015 2.115) (end 3.015 2.115) (layer F.Fab) (width 0.1)) 21 | (fp_line (start 5.9 -2.55) (end -6.7 -2.55) (layer F.SilkS) (width 0.12)) 22 | (fp_line (start -6.7 -2.55) (end -6.7 -1.3) (layer F.SilkS) (width 0.12)) 23 | (fp_line (start -6.7 2.55) (end 5.9 2.55) (layer F.SilkS) (width 0.12)) 24 | (fp_line (start -6.8 -2.6) (end -6.8 2.6) (layer F.CrtYd) (width 0.05)) 25 | (fp_line (start -6.8 2.6) (end 6.8 2.6) (layer F.CrtYd) (width 0.05)) 26 | (fp_line (start 6.8 2.6) (end 6.8 -2.6) (layer F.CrtYd) (width 0.05)) 27 | (fp_line (start 6.8 -2.6) (end -6.8 -2.6) (layer F.CrtYd) (width 0.05)) 28 | (fp_arc (start -3.015 0) (end -3.015 -2.115) (angle -180) (layer F.Fab) (width 0.1)) 29 | (fp_arc (start 3.015 0) (end 3.015 -2.115) (angle 180) (layer F.Fab) (width 0.1)) 30 | (pad 1 smd rect (at -4.5 0) (size 5.6 2.1) (layers F.Cu F.Paste F.Mask)) 31 | (pad 2 smd rect (at 4.5 0) (size 5.6 2.1) (layers F.Cu F.Paste F.Mask)) 32 | (model ${KISYS3DMOD}/Crystal.3dshapes/Crystal_SMD_HC49-SD.wrl 33 | (at (xyz 0 0 0)) 34 | (scale (xyz 1 1 1)) 35 | (rotate (xyz 0 0 0)) 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /firmware/microgroove_app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "microgroove_app" 3 | version = "0.3.0" 4 | authors = ["afternoon "] 5 | edition = "2021" 6 | license = "MIT" 7 | homepage = "https://github.com/afternoon/microgroove" 8 | repository = "https://github.com/afternoon/microgroove" 9 | 10 | [workspace] 11 | 12 | [dependencies] 13 | alloc-cortex-m = "0.4.3" 14 | cortex-m = "0.7.6" 15 | cortex-m-rt = "0.7.1" 16 | cortex-m-rtic = "1.1.3" 17 | defmt = "0.3.2" 18 | defmt-rtt = "0.4.0" 19 | display-interface = "0.4.1" 20 | embedded-graphics = "0.7.1" 21 | embedded-hal = "0.2.7" 22 | embedded-midi = "0.1.2" 23 | fugit = "0.3.6" 24 | heapless = "0.7.16" 25 | midi-types = "0.1.2" 26 | microgroove_sequencer = { path = "../microgroove_sequencer", default-features = false, features = ["target_release"] } 27 | nb = "1.0.0" 28 | panic-probe = { version = "0.3.0", features = ["print-defmt"] } 29 | rand_core = "0.6.4" 30 | rotary-encoder-hal = { version = "0.5.0", features = ["table-decoder"] } 31 | rp-pico = { version = "0.6.0" } 32 | rp2040-hal = { version = "0.7.0", features = ["eh1_0_alpha", "rtic-monotonic", "defmt"] } 33 | ssd1306 = "0.7.1" 34 | debouncr = "0.2.2" 35 | 36 | # cargo build/run 37 | [profile.dev] 38 | codegen-units = 1 39 | debug = 2 40 | debug-assertions = true 41 | incremental = false 42 | opt-level = 3 43 | overflow-checks = true 44 | 45 | # cargo build/run --release 46 | [profile.release] 47 | codegen-units = 1 48 | debug = 2 49 | debug-assertions = false 50 | incremental = false 51 | lto = 'fat' 52 | opt-level = 3 53 | overflow-checks = false 54 | 55 | # do not optimize proc-macro crates = faster builds from scratch 56 | [profile.dev.build-override] 57 | codegen-units = 8 58 | debug = false 59 | debug-assertions = false 60 | opt-level = 0 61 | overflow-checks = false 62 | 63 | [profile.release.build-override] 64 | codegen-units = 8 65 | debug = false 66 | debug-assertions = false 67 | opt-level = 0 68 | overflow-checks = false 69 | 70 | # cargo test 71 | [profile.test] 72 | codegen-units = 1 73 | debug = 2 74 | debug-assertions = true 75 | incremental = false 76 | opt-level = 3 77 | overflow-checks = true 78 | 79 | # cargo test --release 80 | [profile.bench] 81 | codegen-units = 1 82 | debug = 2 83 | debug-assertions = false 84 | incremental = false 85 | lto = 'fat' 86 | opt-level = 3 87 | -------------------------------------------------------------------------------- /hardware/lib/H11L1/DIP762W46P254L889H508Q6B.kicad_mod: -------------------------------------------------------------------------------- 1 | 2 | (footprint DIP762W46P254L889H508Q6B (layer F.Cu) (tedit 660DD3B6) 3 | (descr "") 4 | (fp_text reference REF** (at -1.57611 -5.63899 0) (layer F.SilkS) 5 | (effects (font (size 1.00070866142 1.00070866142) (thickness 0.15))) 6 | ) 7 | (fp_text value DIP762W46P254L889H508Q6B (at 10.49207 5.616095 0) (layer F.Fab) 8 | (effects (font (size 1.00019685039 1.00019685039) (thickness 0.15))) 9 | ) 10 | (pad 1 thru_hole rect (at -3.81 -2.54) (size 1.14 1.14) (drill 0.74) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 11 | (pad 2 thru_hole circle (at -3.81 0.0) (size 1.14 1.14) (drill 0.74) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 12 | (pad 3 thru_hole circle (at -3.81 2.54) (size 1.14 1.14) (drill 0.74) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 13 | (pad 4 thru_hole circle (at 3.81 2.54) (size 1.14 1.14) (drill 0.74) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 14 | (pad 5 thru_hole circle (at 3.81 0.0) (size 1.14 1.14) (drill 0.74) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 15 | (pad 6 thru_hole circle (at 3.81 -2.54) (size 1.14 1.14) (drill 0.74) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 16 | (fp_line (start -3.3 -4.445) (end 3.3 -4.445) (layer F.Fab) (width 0.127)) 17 | (fp_line (start 3.3 -4.445) (end 3.3 4.445) (layer F.Fab) (width 0.127)) 18 | (fp_line (start 3.3 4.445) (end -3.3 4.445) (layer F.Fab) (width 0.127)) 19 | (fp_line (start -3.3 4.445) (end -3.3 -4.445) (layer F.Fab) (width 0.127)) 20 | (fp_line (start -3.3 -4.445) (end 3.3 -4.445) (layer F.SilkS) (width 0.127)) 21 | (fp_line (start 3.3 4.445) (end -3.3 4.445) (layer F.SilkS) (width 0.127)) 22 | (fp_line (start -3.3 -3.29) (end -3.3 -4.445) (layer F.SilkS) (width 0.127)) 23 | (fp_line (start 3.3 -3.29) (end 3.3 -4.445) (layer F.SilkS) (width 0.127)) 24 | (fp_line (start -3.3 3.29) (end -3.3 4.445) (layer F.SilkS) (width 0.127)) 25 | (fp_line (start 3.3 3.29) (end 3.3 4.445) (layer F.SilkS) (width 0.127)) 26 | (fp_line (start -4.5 -4.75) (end 4.5 -4.75) (layer F.CrtYd) (width 0.05)) 27 | (fp_line (start 4.5 -4.75) (end 4.5 4.75) (layer F.CrtYd) (width 0.05)) 28 | (fp_line (start 4.5 4.75) (end -4.5 4.75) (layer F.CrtYd) (width 0.05)) 29 | (fp_line (start -4.5 4.75) (end -4.5 -4.75) (layer F.CrtYd) (width 0.05)) 30 | (fp_circle (center -5.0 -2.54) (end -4.9 -2.54) (layer F.SilkS) (width 0.2)) 31 | (fp_circle (center -2.25 -2.54) (end -2.15 -2.54) (layer F.Fab) (width 0.2)) 32 | ) -------------------------------------------------------------------------------- /hardware/lib/SJ1_3513/CUI_SJ1-3513.kicad_mod: -------------------------------------------------------------------------------- 1 | 2 | (footprint CUI_SJ1-3513 (layer F.Cu) (tedit 65F37E1F) 3 | (descr "") 4 | (fp_text reference REF** (at 2.64547 -8.89061 0) (layer F.SilkS) 5 | (effects (font (size 1.4002519685 1.4002519685) (thickness 0.15))) 6 | ) 7 | (fp_text value CUI_SJ1-3513 (at 8.88311 8.904155 0) (layer F.Fab) 8 | (effects (font (size 1.40238582677 1.40238582677) (thickness 0.15))) 9 | ) 10 | (pad 1 thru_hole oval (at 0.0 0.0) (size 1.508 3.016) (drill oval 1.0 2.0) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 11 | (pad 2 thru_hole oval (at 3.5 4.5) (size 3.016 1.508) (drill oval 2.0 1.0) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 12 | (pad 3 thru_hole oval (at 8.3 -5.0) (size 3.016 1.508) (drill oval 2.0 1.0) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 13 | (fp_line (start -1.8 -5.95) (end -1.8 -3.0) (layer F.Fab) (width 0.127)) 14 | (fp_line (start -1.8 -3.0) (end -5.3 -3.0) (layer F.Fab) (width 0.127)) 15 | (fp_line (start -5.3 -3.0) (end -5.3 3.0) (layer F.Fab) (width 0.127)) 16 | (fp_line (start -5.3 3.0) (end -1.8 3.0) (layer F.Fab) (width 0.127)) 17 | (fp_line (start -1.8 3.0) (end -1.8 5.55) (layer F.Fab) (width 0.127)) 18 | (fp_line (start -1.8 5.55) (end 14.3 5.55) (layer F.Fab) (width 0.127)) 19 | (fp_line (start 14.3 5.55) (end 14.3 -5.95) (layer F.Fab) (width 0.127)) 20 | (fp_line (start 14.3 -5.95) (end -1.8 -5.95) (layer F.Fab) (width 0.127)) 21 | (fp_line (start -1.8 -3.0) (end -1.8 -5.95) (layer F.SilkS) (width 0.127)) 22 | (fp_line (start -1.8 -5.95) (end 14.3 -5.95) (layer F.SilkS) (width 0.127)) 23 | (fp_line (start 14.3 -5.95) (end 14.3 5.55) (layer F.SilkS) (width 0.127)) 24 | (fp_line (start 14.3 5.55) (end -1.8 5.55) (layer F.SilkS) (width 0.127)) 25 | (fp_line (start -1.8 5.55) (end -1.8 3.0) (layer F.SilkS) (width 0.127)) 26 | (fp_line (start 14.5 -6.2) (end -2.05 -6.2) (layer F.CrtYd) (width 0.05)) 27 | (fp_line (start -2.05 -6.2) (end -2.05 -3.25) (layer F.CrtYd) (width 0.05)) 28 | (fp_line (start -2.05 -3.25) (end -5.55 -3.25) (layer F.CrtYd) (width 0.05)) 29 | (fp_line (start -5.55 -3.25) (end -5.55 3.25) (layer F.CrtYd) (width 0.05)) 30 | (fp_line (start -5.55 3.25) (end -2.05 3.25) (layer F.CrtYd) (width 0.05)) 31 | (fp_line (start -2.05 3.25) (end -2.05 5.8) (layer F.CrtYd) (width 0.05)) 32 | (fp_line (start -2.05 5.8) (end 14.5 5.8) (layer F.CrtYd) (width 0.05)) 33 | (fp_line (start 14.5 5.8) (end 14.5 -6.2) (layer F.CrtYd) (width 0.05)) 34 | (fp_circle (center -0.1 -6.5) (end 0.07 -6.5) (layer F.SilkS) (width 0.34)) 35 | ) -------------------------------------------------------------------------------- /hardware/lib/SW4_TL1100F160Q6JBLK_EWI/SW4_TL1100F160Q6JBLK_EWI.kicad_sym: -------------------------------------------------------------------------------- 1 | (kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor) 2 | (symbol "TL1100F160Q6JBLK" (pin_names (offset 0.254)) (in_bom yes) (on_board yes) 3 | (property "Reference" "SW" (id 0) (at 0 3.81 0) 4 | (effects (font (size 1.524 1.524))) 5 | ) 6 | (property "Value" "TL1100F160Q6JBLK" (id 1) (at 0 0 0) 7 | (effects (font (size 1.524 1.524))) 8 | ) 9 | (property "Footprint" "SW4_TL1100F160Q6JBLK_EWI" (id 2) (at 0 0 0) 10 | (effects (font (size 1.27 1.27) italic) hide) 11 | ) 12 | (property "Datasheet" "TL1100F160Q6JBLK" (id 3) (at 0 0 0) 13 | (effects (font (size 1.27 1.27) italic) hide) 14 | ) 15 | (property "ki_keywords" "TL1100F160Q6JBLK" (id 4) (at 0 0 0) 16 | (effects (font (size 1.27 1.27)) hide) 17 | ) 18 | (property "ki_locked" "" (id 5) (at 0 0 0) 19 | (effects (font (size 1.27 1.27)) hide) 20 | ) 21 | (property "ki_fp_filters" "SW4_TL1100F160Q6JBLK_EWI" (id 6) (at 0 0 0) 22 | (effects (font (size 1.27 1.27)) hide) 23 | ) 24 | (symbol "TL1100F160Q6JBLK_0_1" 25 | (polyline 26 | (pts 27 | (xy -5.08 0) 28 | (xy -3.81 0) 29 | ) 30 | (stroke (width 0.2032) (type default) (color 0 0 0 0)) 31 | (fill (type none)) 32 | ) 33 | (circle (center 3.175 0) (radius 0.635) 34 | (stroke (width 0.254) (type default) (color 0 0 0 0)) 35 | (fill (type none)) 36 | ) 37 | (circle (center -3.175 0) (radius 0.635) 38 | (stroke (width 0.254) (type default) (color 0 0 0 0)) 39 | (fill (type none)) 40 | ) 41 | (polyline 42 | (pts 43 | (xy 5.08 0) 44 | (xy 3.81 0) 45 | ) 46 | (stroke (width 0.2032) (type default) (color 0 0 0 0)) 47 | (fill (type none)) 48 | ) 49 | (polyline 50 | (pts 51 | (xy -3.175 0) 52 | (xy 3.81 1.905) 53 | ) 54 | (stroke (width 0.2032) (type default) (color 0 0 0 0)) 55 | (fill (type none)) 56 | ) 57 | (pin unspecified line (at -7.62 0 0) (length 2.54) 58 | (name "1" (effects (font (size 1.27 1.27)))) 59 | (number "1" (effects (font (size 1.27 1.27)))) 60 | ) 61 | (pin unspecified line (at 7.62 0 180) (length 2.54) 62 | (name "2" (effects (font (size 1.27 1.27)))) 63 | (number "2" (effects (font (size 1.27 1.27)))) 64 | ) 65 | ) 66 | ) 67 | ) 68 | 69 | -------------------------------------------------------------------------------- /hardware/lib/PEC11R_4215F_S0024/XDCR_PEC11R-4215F-S0024.kicad_mod: -------------------------------------------------------------------------------- 1 | 2 | (footprint XDCR_PEC11R-4215F-S0024 (layer F.Cu) (tedit 65F37E0D) 3 | (descr "") 4 | (fp_text reference REF** (at -4.175 -8.835 0) (layer F.SilkS) 5 | (effects (font (size 1.0 1.0) (thickness 0.15))) 6 | ) 7 | (fp_text value XDCR_PEC11R-4215F-S0024 (at 7.255 9.315 0) (layer F.Fab) 8 | (effects (font (size 1.0 1.0) (thickness 0.15))) 9 | ) 10 | (pad 1 thru_hole rect (at -2.5 -7.0) (size 1.65 1.65) (drill 1.1) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 11 | (pad 2 thru_hole circle (at 2.5 -7.0) (size 1.65 1.65) (drill 1.1) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 12 | (pad A thru_hole circle (at -2.5 7.5) (size 1.65 1.65) (drill 1.1) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 13 | (pad COM thru_hole circle (at 0.0 7.5) (size 1.65 1.65) (drill 1.1) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 14 | (pad B thru_hole circle (at 2.5 7.5) (size 1.65 1.65) (drill 1.1) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 15 | (pad S1 thru_hole oval (at -5.7 0.0) (size 3.0 6.0) (drill oval 2.0 2.8) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 16 | (pad S2 thru_hole oval (at 5.7 0.0) (size 3.0 6.0) (drill oval 2.0 2.8) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 17 | (fp_line (start -6.25 -6.7) (end 6.25 -6.7) (layer F.Fab) (width 0.127)) 18 | (fp_line (start 6.25 -6.7) (end 6.25 6.7) (layer F.Fab) (width 0.127)) 19 | (fp_line (start 6.25 6.7) (end -6.25 6.7) (layer F.Fab) (width 0.127)) 20 | (fp_line (start -6.25 6.7) (end -6.25 -6.7) (layer F.Fab) (width 0.127)) 21 | (fp_line (start -6.25 -3.4) (end -6.25 -6.7) (layer F.SilkS) (width 0.127)) 22 | (fp_line (start -6.25 -6.7) (end -3.7 -6.7) (layer F.SilkS) (width 0.127)) 23 | (fp_line (start 3.7 -6.7) (end 6.25 -6.7) (layer F.SilkS) (width 0.127)) 24 | (fp_line (start 6.25 -6.7) (end 6.25 -3.4) (layer F.SilkS) (width 0.127)) 25 | (fp_line (start -6.25 3.4) (end -6.25 6.7) (layer F.SilkS) (width 0.127)) 26 | (fp_line (start -6.25 6.7) (end -3.7 6.7) (layer F.SilkS) (width 0.127)) 27 | (fp_line (start 3.7 6.7) (end 6.25 6.7) (layer F.SilkS) (width 0.127)) 28 | (fp_line (start 6.25 6.7) (end 6.25 3.4) (layer F.SilkS) (width 0.127)) 29 | (fp_circle (center 0.0 0.0) (end 3.0 0.0) (layer F.Fab) (width 0.127)) 30 | (fp_line (start -7.45 -8.1) (end -7.45 8.6) (layer F.CrtYd) (width 0.05)) 31 | (fp_line (start -7.45 8.6) (end 7.45 8.6) (layer F.CrtYd) (width 0.05)) 32 | (fp_line (start 7.45 8.6) (end 7.45 -8.1) (layer F.CrtYd) (width 0.05)) 33 | (fp_line (start 7.45 -8.1) (end -7.45 -8.1) (layer F.CrtYd) (width 0.05)) 34 | (fp_circle (center -4.2 -7.6) (end -4.1 -7.6) (layer F.SilkS) (width 0.2)) 35 | (fp_circle (center -4.2 -7.6) (end -4.1 -7.6) (layer F.Fab) (width 0.2)) 36 | ) -------------------------------------------------------------------------------- /hardware/lib/SW4_TL1100F160Q6JBLK_EWI/footprints.pretty/SW4_TL1100F160Q6JBLK_EWI.kicad_mod: -------------------------------------------------------------------------------- 1 | (footprint "SW4_TL1100F160Q6JBLK_EWI" (version 20211014) (generator pcbnew) 2 | (layer "F.Cu") 3 | (tags "TL1100F160Q6JBLK ") 4 | (attr through_hole) 5 | (fp_text reference "REF**" (at 6.25 -2.5 unlocked) (layer F.SilkS) 6 | (effects (font (size 1 1) (thickness 0.15))) 7 | ) 8 | (fp_text value SW4_TL1100F160Q6JBLK_EWI (at 6.25 -2.5 unlocked) (layer F.Fab) 9 | (effects (font (size 1 1) (thickness 0.15))) 10 | ) 11 | (fp_text user "${REFERENCE}" (at 6.25 -2.5 unlocked) (layer F.Fab) 12 | (effects (font (size 1 1) (thickness 0.15))) 13 | ) 14 | (fp_line (start -1.1557 -8.7484) (end -1.1557 3.7484) (layer "F.CrtYd") (width 0.1524)) 15 | (fp_line (start -1.1557 3.7484) (end 13.6557 3.7484) (layer "F.CrtYd") (width 0.1524)) 16 | (fp_line (start 13.6557 3.7484) (end 13.6557 -8.7484) (layer "F.CrtYd") (width 0.1524)) 17 | (fp_line (start 13.6557 -8.7484) (end -1.1557 -8.7484) (layer "F.CrtYd") (width 0.1524)) 18 | (fp_arc (start -1.385286 0.2746) (mid -2.0304 0) (end -1.385286 -0.2746) (layer "F.SilkS") (width 0.508)) 19 | (fp_circle (center -1.6494 0) (end -1.2684 0) (layer "B.SilkS") (width 0.508) (fill none)) 20 | (fp_circle (center 0 1.905) (end 0.381 1.905) (layer "F.Fab") (width 0.508) (fill none)) 21 | (fp_line (start 0.1286 3.6214) (end 12.3714 3.6214) (layer "F.SilkS") (width 0.1524)) 22 | (fp_line (start 12.3714 3.6214) (end 12.3714 1.227723) (layer "F.SilkS") (width 0.1524)) 23 | (fp_line (start 12.3714 -8.6214) (end 0.1286 -8.6214) (layer "F.SilkS") (width 0.1524)) 24 | (fp_line (start 0.1286 -8.6214) (end 0.1286 -6.227723) (layer "F.SilkS") (width 0.1524)) 25 | (fp_line (start 0.2556 3.4944) (end 12.2444 3.4944) (layer "F.Fab") (width 0.0254)) 26 | (fp_line (start 12.2444 3.4944) (end 12.2444 -8.4944) (layer "F.Fab") (width 0.0254)) 27 | (fp_line (start 12.2444 -8.4944) (end 0.2556 -8.4944) (layer "F.Fab") (width 0.0254)) 28 | (fp_line (start 0.2556 -8.4944) (end 0.2556 3.4944) (layer "F.Fab") (width 0.0254)) 29 | (fp_line (start 0.1286 1.227723) (end 0.1286 3.6214) (layer "F.SilkS") (width 0.1524)) 30 | (fp_line (start 12.3714 -1.227723) (end 12.3714 -3.772277) (layer "F.SilkS") (width 0.1524)) 31 | (fp_line (start 0.1286 -3.772277) (end 0.1286 -1.227723) (layer "F.SilkS") (width 0.1524)) 32 | (fp_line (start 12.3714 -6.227723) (end 12.3714 -8.6214) (layer "F.SilkS") (width 0.1524)) 33 | (pad "1" thru_hole circle (at 0 0) (size 1.8034 1.8034) (drill 1.2954) (layers *.Cu *.Mask)) 34 | (pad "2" thru_hole circle (at 12.5 0) (size 1.8034 1.8034) (drill 1.2954) (layers *.Cu *.Mask)) 35 | (pad "3" thru_hole circle (at 0 -5) (size 1.8034 1.8034) (drill 1.2954) (layers *.Cu *.Mask)) 36 | (pad "4" thru_hole circle (at 12.5 -5) (size 1.8034 1.8034) (drill 1.2954) (layers *.Cu *.Mask)) 37 | ) 38 | -------------------------------------------------------------------------------- /hardware/lib/SJ1_3513/SJ1-3513.kicad_sym: -------------------------------------------------------------------------------- 1 | 2 | (kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor) 3 | (symbol "SJ1-3513" (pin_names (offset 1.016)) (in_bom yes) (on_board yes) 4 | (property "Reference" "J" (id 0) (at -12.7049 7.62294 0) 5 | (effects (font (size 1.27 1.27)) (justify bottom left)) 6 | ) 7 | (property "Value" "SJ1-3513" (id 1) (at -12.7216 -12.7216 0) 8 | (effects (font (size 1.27 1.27)) (justify bottom left)) 9 | ) 10 | (property "Footprint" "SJ1-3513:CUI_SJ1-3513" (id 2) (at 0 0 0) 11 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 12 | ) 13 | (property "PARTREV" "1.03" (id 4) (at 0 0 0) 14 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 15 | ) 16 | (property "STANDARD" "Manufacturer recommendation" (id 5) (at 0 0 0) 17 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 18 | ) 19 | (property "MANUFACTURER" "CUI" (id 6) (at 0 0 0) 20 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 21 | ) 22 | (symbol "SJ1-3513_0_0" 23 | (polyline 24 | (pts (xy 12.7 2.54) (xy -8.255 2.54)) (stroke (width 0.1524)) 25 | ) 26 | (polyline 27 | (pts (xy -8.255 2.54) (xy -8.255 -0.635)) (stroke (width 0.1524)) 28 | ) 29 | (polyline 30 | (pts (xy -8.255 -0.635) (xy -8.89 -0.635)) (stroke (width 0.1524)) 31 | ) 32 | (polyline 33 | (pts (xy -8.89 -0.635) (xy -8.89 -4.445)) (stroke (width 0.1524)) 34 | ) 35 | (polyline 36 | (pts (xy -8.89 -4.445) (xy -7.62 -4.445)) (stroke (width 0.1524)) 37 | ) 38 | (polyline 39 | (pts (xy -7.62 -4.445) (xy -7.62 -0.635)) (stroke (width 0.1524)) 40 | ) 41 | (polyline 42 | (pts (xy -7.62 -0.635) (xy -8.255 -0.635)) (stroke (width 0.1524)) 43 | ) 44 | (polyline 45 | (pts (xy 12.7 0.0) (xy -2.54 0.0)) (stroke (width 0.1524)) 46 | ) 47 | (polyline 48 | (pts (xy -2.54 0.0) (xy -3.81 -1.27)) (stroke (width 0.1524)) 49 | ) 50 | (polyline 51 | (pts (xy -3.81 -1.27) (xy -5.08 0.0)) (stroke (width 0.1524)) 52 | ) 53 | (polyline 54 | (pts (xy 12.7 -5.08) (xy 0.635 -5.08)) (stroke (width 0.1524)) 55 | ) 56 | (polyline 57 | (pts (xy 0.635 -5.08) (xy -0.635 -3.81)) (stroke (width 0.1524)) 58 | ) 59 | (polyline 60 | (pts (xy -0.635 -3.81) (xy -1.905 -5.08)) (stroke (width 0.1524)) 61 | ) 62 | (rectangle (start -12.7 -7.62) (end 12.7 5.08) 63 | (stroke (width 0.254)) (fill (type background)) 64 | ) 65 | (pin passive line (at 17.78 2.54 180.0) (length 5.08) 66 | (name "~" 67 | (effects (font (size 1.016 1.016))) 68 | ) 69 | (number "1" 70 | (effects (font (size 1.016 1.016))) 71 | ) 72 | ) 73 | (pin passive line (at 17.78 0.0 180.0) (length 5.08) 74 | (name "~" 75 | (effects (font (size 1.016 1.016))) 76 | ) 77 | (number "3" 78 | (effects (font (size 1.016 1.016))) 79 | ) 80 | ) 81 | (pin passive line (at 17.78 -5.08 180.0) (length 5.08) 82 | (name "~" 83 | (effects (font (size 1.016 1.016))) 84 | ) 85 | (number "2" 86 | (effects (font (size 1.016 1.016))) 87 | ) 88 | ) 89 | ) 90 | ) 91 | ) -------------------------------------------------------------------------------- /hardware/lib/PEC11R_4215F_S0024/PEC11R-4215F-S0024.kicad_sym: -------------------------------------------------------------------------------- 1 | 2 | (kicad_symbol_lib (version 20211014) (generator kicad_symbol_editor) 3 | (symbol "PEC11R-4215F-S0024" (pin_names (offset 1.016)) (in_bom yes) (on_board yes) 4 | (property "Reference" "MT" (id 0) (at -7.62 8.89 0) 5 | (effects (font (size 1.27 1.27)) (justify bottom left)) 6 | ) 7 | (property "Value" "PEC11R-4215F-S0024" (id 1) (at -7.62 -11.43 0) 8 | (effects (font (size 1.27 1.27)) (justify top left)) 9 | ) 10 | (property "Footprint" "PEC11R-4215F-S0024:XDCR_PEC11R-4215F-S0024" (id 2) (at 0 0 0) 11 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 12 | ) 13 | (property "PARTREV" "Rev. 09/19" (id 4) (at 0 0 0) 14 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 15 | ) 16 | (property "MANUFACTURER" "J.W.Miller/Bourns" (id 5) (at 0 0 0) 17 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 18 | ) 19 | (property "MAXIMUM_PACKAGE_HEIGHT" "21.5 mm" (id 6) (at 0 0 0) 20 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 21 | ) 22 | (property "STANDARD" "Manufacturer Recommendations" (id 7) (at 0 0 0) 23 | (effects (font (size 1.27 1.27)) (justify bottom) hide) 24 | ) 25 | (symbol "PEC11R-4215F-S0024_0_0" 26 | (polyline 27 | (pts (xy -2.54 2.54) (xy 2.794 4.6736)) (stroke (width 0.1524)) 28 | ) 29 | (circle (center 2.54 2.54) (radius 0.3302) 30 | (stroke (width 0.1524)) (fill (type none)) 31 | ) 32 | (polyline 33 | (pts (xy 7.62 2.54) (xy 2.921 2.54)) (stroke (width 0.1524)) 34 | ) 35 | (polyline 36 | (pts (xy -2.54 2.54) (xy -7.62 2.54)) (stroke (width 0.1524)) 37 | ) 38 | (rectangle (start -7.62 -10.16) (end 7.62 7.62) 39 | (stroke (width 0.254)) (fill (type background)) 40 | ) 41 | (pin passive line (at -10.16 2.54 0) (length 2.54) 42 | (name "~" 43 | (effects (font (size 1.016 1.016))) 44 | ) 45 | (number "1" 46 | (effects (font (size 1.016 1.016))) 47 | ) 48 | ) 49 | (pin passive line (at 10.16 2.54 180.0) (length 2.54) 50 | (name "~" 51 | (effects (font (size 1.016 1.016))) 52 | ) 53 | (number "2" 54 | (effects (font (size 1.016 1.016))) 55 | ) 56 | ) 57 | (pin passive line (at 10.16 -2.54 180.0) (length 2.54) 58 | (name "A" 59 | (effects (font (size 1.016 1.016))) 60 | ) 61 | (number "A" 62 | (effects (font (size 1.016 1.016))) 63 | ) 64 | ) 65 | (pin passive line (at 10.16 -5.08 180.0) (length 2.54) 66 | (name "B" 67 | (effects (font (size 1.016 1.016))) 68 | ) 69 | (number "B" 70 | (effects (font (size 1.016 1.016))) 71 | ) 72 | ) 73 | (pin passive line (at -10.16 -2.54 0) (length 2.54) 74 | (name "COM" 75 | (effects (font (size 1.016 1.016))) 76 | ) 77 | (number "COM" 78 | (effects (font (size 1.016 1.016))) 79 | ) 80 | ) 81 | (pin passive line (at -10.16 -5.08 0) (length 2.54) 82 | (name "SHIELD" 83 | (effects (font (size 1.016 1.016))) 84 | ) 85 | (number "S1" 86 | (effects (font (size 1.016 1.016))) 87 | ) 88 | ) 89 | (pin passive line (at -10.16 -7.62 0) (length 2.54) 90 | (name "SHIELD__1" 91 | (effects (font (size 1.016 1.016))) 92 | ) 93 | (number "S2" 94 | (effects (font (size 1.016 1.016))) 95 | ) 96 | ) 97 | ) 98 | ) 99 | ) -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/machine.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | use core::fmt::{Debug, Display, Formatter, Result as FmtResult}; 3 | use heapless::String; 4 | 5 | use crate::{machine_resources::MachineResources, param::ParamList, Sequence}; 6 | 7 | pub mod euclidean_rhythm_machine; 8 | pub mod grids_rhythm_machine; 9 | pub mod rand_melody_machine; 10 | pub mod unit_machine; 11 | 12 | use euclidean_rhythm_machine::EuclideanRhythmMachine; 13 | use grids_rhythm_machine::GridsRhythmMachine; 14 | use rand_melody_machine::RandMelodyMachine; 15 | use unit_machine::UnitMachine; 16 | 17 | #[derive(Debug)] 18 | pub enum MachineError { 19 | UnknowMachine(String<6>), 20 | } 21 | 22 | /// A `Machine` represents a sequence generator that can be controlled via a list of parameters. In 23 | /// Microgroove, each `Track` has 2 machines, one to generate the rhythm, one for the melody. 24 | pub trait Machine: Debug + Send { 25 | fn name(&self) -> &str; // TODO redundant because Display implmented for machine IDs 26 | fn generate(&mut self, machine_resources: &mut MachineResources); 27 | fn apply(&self, sequence: Sequence) -> Sequence; 28 | fn params(&self) -> &ParamList; 29 | fn params_mut(&mut self) -> &mut ParamList; 30 | } 31 | 32 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 33 | pub enum RhythmMachineId { 34 | Unit, 35 | #[default] 36 | Euclid, 37 | Grids, 38 | } 39 | 40 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 41 | pub enum MelodyMachineId { 42 | Unit, 43 | #[default] 44 | Rand, 45 | } 46 | 47 | impl From for Box { 48 | fn from(value: RhythmMachineId) -> Self { 49 | match value { 50 | RhythmMachineId::Unit => Box::new(UnitMachine::new()), 51 | RhythmMachineId::Euclid => Box::new(EuclideanRhythmMachine::new()), 52 | RhythmMachineId::Grids => Box::new(GridsRhythmMachine::new()), 53 | } 54 | } 55 | } 56 | 57 | impl From for Box { 58 | fn from(value: MelodyMachineId) -> Self { 59 | match value { 60 | MelodyMachineId::Unit => Box::new(UnitMachine::new()), 61 | MelodyMachineId::Rand => Box::new(RandMelodyMachine::new()), 62 | } 63 | } 64 | } 65 | 66 | impl Display for RhythmMachineId { 67 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 68 | match self { 69 | RhythmMachineId::Unit => Display::fmt("UNIT", f), 70 | RhythmMachineId::Euclid => Display::fmt("EUCLID", f), 71 | RhythmMachineId::Grids => Display::fmt("GRIDS", f), 72 | } 73 | } 74 | } 75 | 76 | impl Display for MelodyMachineId { 77 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 78 | match self { 79 | MelodyMachineId::Unit => Display::fmt("UNIT", f), 80 | MelodyMachineId::Rand => Display::fmt("RAND", f), 81 | } 82 | } 83 | } 84 | 85 | impl TryFrom for RhythmMachineId { 86 | type Error = (); 87 | 88 | fn try_from(value: u8) -> Result { 89 | match value { 90 | 0 => Ok(RhythmMachineId::Unit), 91 | 1 => Ok(RhythmMachineId::Euclid), 92 | 2 => Ok(RhythmMachineId::Grids), 93 | _ => Err(()), 94 | } 95 | } 96 | } 97 | 98 | impl TryFrom for MelodyMachineId { 99 | type Error = (); 100 | 101 | fn try_from(value: u8) -> Result { 102 | match value { 103 | 0 => Ok(MelodyMachineId::Unit), 104 | 1 => Ok(MelodyMachineId::Rand), 105 | _ => Err(()), 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /hardware/lib/UJ2-MIBH-4-SMT-TR/CUIDEVICES_UJ2-MIBH-4-SMT-TR.kicad_mod: -------------------------------------------------------------------------------- 1 | 2 | (module CUIDEVICES_UJ2-MIBH-4-SMT-TR (layer F.Cu) (tedit 65F23479) 3 | (descr "") 4 | (attr smd) 5 | (fp_text reference REF** (at -3.4904475 -7.0637665 0) (layer F.SilkS) 6 | (effects (font (size 0.320592913386 0.320592913386) (thickness 0.15))) 7 | ) 8 | (fp_text value CUIDEVICES_UJ2-MIBH-4-SMT-TR (at 1.241114 6.456702 0) (layer F.Fab) 9 | (effects (font (size 0.320406299213 0.320406299213) (thickness 0.15))) 10 | ) 11 | (pad 1 smd rect (at -1.3 -4.95 270.0) (size 1.7 0.4) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 12 | (pad 2 smd rect (at -0.65 -4.95 270.0) (size 1.7 0.4) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 13 | (pad 3 smd rect (at 0.0 -4.95 270.0) (size 1.7 0.4) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 14 | (pad 4 smd rect (at 0.65 -4.95 270.0) (size 1.7 0.4) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 15 | (pad 5 smd rect (at 1.3 -4.95 270.0) (size 1.7 0.4) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 16 | (pad P5 smd rect (at 1.15 -0.75 270.0) (size 3.9 1.8) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 17 | (pad P6 smd rect (at -1.15 -0.75 270.0) (size 3.9 1.8) (layers F.Cu F.Mask F.Paste) (solder_mask_margin 0.102)) 18 | (pad S2 thru_hole oval (at -3.6 -1.75) (size 0.95 1.9) (drill oval 0.6 1.7001617514) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 19 | (pad S1 thru_hole oval (at -3.6 -5.3) (size 0.95 1.9) (drill oval 0.6 1.7001617514) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 20 | (pad S3 thru_hole oval (at 3.6 -5.3) (size 0.95 1.9) (drill oval 0.6 1.7001617514) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 21 | (pad S4 thru_hole oval (at 3.6 -1.75) (size 0.95 1.9) (drill oval 0.6 1.7001617514) (layers *.Cu *.Mask) (solder_mask_margin 0.102)) 22 | (fp_line (start 3.75 -5.9) (end -3.75 -5.9) (layer F.Fab) (width 0.127)) 23 | (fp_line (start -3.75 -5.9) (end -3.75 5.9) (layer F.Fab) (width 0.127)) 24 | (fp_line (start -3.75 5.9) (end 3.75 5.9) (layer F.Fab) (width 0.127)) 25 | (fp_line (start 3.75 5.9) (end 3.75 -5.9) (layer F.Fab) (width 0.127)) 26 | (fp_line (start 4.5 6.15) (end -4.5 6.15) (layer F.CrtYd) (width 0.05)) 27 | (fp_line (start -4.5 6.15) (end -4.5 -6.6) (layer F.CrtYd) (width 0.05)) 28 | (fp_line (start -4.5 -6.6) (end 4.5 -6.6) (layer F.CrtYd) (width 0.05)) 29 | (fp_line (start 4.5 -6.6) (end 4.5 6.15) (layer F.CrtYd) (width 0.05)) 30 | (fp_circle (center -1.35 -6.35) (end -1.3 -6.35) (layer F.SilkS) (width 0.1)) 31 | (fp_line (start -5.0 2.9) (end 5.0 2.9) (layer F.Fab) (width 0.127)) 32 | (fp_arc (start 3.59999952355 -2.2998382486) (end 3.9 -2.29) (angle -91.8782) (layer Edge.Cuts) (width 0.01)) 33 | (fp_arc (start -3.60000047645 -2.2998382486) (end -3.3 -2.29) (angle -91.8782) (layer Edge.Cuts) (width 0.01)) 34 | (fp_arc (start 3.59999952355 -5.8498382486) (end 3.9 -5.84) (angle -91.8782) (layer Edge.Cuts) (width 0.01)) 35 | (fp_arc (start -3.60000047645 -5.8498382486) (end -3.3 -5.84) (angle -91.8782) (layer Edge.Cuts) (width 0.01)) 36 | (fp_circle (center -1.35 -6.35) (end -1.3 -6.35) (layer F.Fab) (width 0.1)) 37 | (fp_line (start 3.75 2.9) (end 3.75 -0.48) (layer F.SilkS) (width 0.127)) 38 | (fp_line (start -3.75 -0.48) (end -3.75 2.9) (layer F.SilkS) (width 0.127)) 39 | (fp_line (start -1.82 -5.9) (end -2.805 -5.9) (layer F.SilkS) (width 0.127)) 40 | (fp_line (start 1.82 -5.9) (end 2.805 -5.9) (layer F.SilkS) (width 0.127)) 41 | (fp_line (start 3.75 -3.02) (end 3.75 -4.03) (layer F.SilkS) (width 0.127)) 42 | (fp_line (start -3.75 -4.03) (end -3.75 -3.02) (layer F.SilkS) (width 0.127)) 43 | (fp_text user "PCB Edge" (at -3.0 3.2) (layer F.Fab) 44 | (effects (font (size 0.64 0.64) (thickness 0.15))) 45 | ) 46 | ) -------------------------------------------------------------------------------- /firmware/microgroove_app/src/encoder.rs: -------------------------------------------------------------------------------- 1 | pub mod positional_encoder { 2 | use core::fmt::Debug; 3 | use defmt::{error, trace}; 4 | use rotary_encoder_hal::{Direction, Rotary}; 5 | use rp_pico::hal::gpio::DynPin; 6 | 7 | pub struct PositionalEncoder { 8 | encoder: Rotary, 9 | value: i8, 10 | } 11 | 12 | impl PositionalEncoder { 13 | pub fn new(mut pin_a: DynPin, mut pin_b: DynPin) -> PositionalEncoder { 14 | pin_a.into_pull_up_input(); 15 | pin_b.into_pull_up_input(); 16 | PositionalEncoder { 17 | encoder: Rotary::new(pin_a.into(), pin_b.into()), 18 | value: 0, 19 | } 20 | } 21 | 22 | /// Check the encoder state for changes. This should be called frequently, e.g. 23 | /// every 1ms. Returns a `Some` containing the encoder value if there have been 24 | /// changes, `None` otherwise. 25 | pub fn update(&mut self) -> Option { 26 | match self.encoder.update() { 27 | Ok(Direction::Clockwise) => { 28 | trace!("[PositionalEncoder::update] Direction::Clockwise"); 29 | self.value += 1; 30 | Some(self.value) 31 | } 32 | Ok(Direction::CounterClockwise) => { 33 | trace!("[PositionalEncoder::update] Direction::CounterClockwise"); 34 | self.value -= 1; 35 | Some(self.value) 36 | } 37 | Ok(Direction::None) => None, 38 | Err(_error) => { 39 | error!("[PositionalEncoder::update] could not update encoder"); 40 | None 41 | } 42 | } 43 | } 44 | 45 | /// Get the value of the encoder, and then reset that to zero. This has the 46 | /// semantics of "I would like to know your value, which I will use to update my 47 | /// state, so you can then discard it." 48 | pub fn take_value(&mut self) -> Option { 49 | let value = self.value; 50 | if value == 0 { 51 | None 52 | } else { 53 | self.value = 0; 54 | Some(value) 55 | } 56 | } 57 | } 58 | 59 | impl Debug for PositionalEncoder { 60 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 61 | write!(f, "encoder") 62 | } 63 | } 64 | } 65 | 66 | pub mod encoder_array { 67 | use super::positional_encoder::PositionalEncoder; 68 | use heapless::Vec; 69 | 70 | pub const ENCODER_COUNT: usize = 6; 71 | 72 | /// An array of multiple `PositionalEncoders`. 73 | pub struct EncoderArray { 74 | encoders: Vec, 75 | } 76 | 77 | impl EncoderArray { 78 | pub fn new(encoders: Vec) -> EncoderArray { 79 | EncoderArray { encoders } 80 | } 81 | 82 | pub fn update(&mut self) -> Option<()> { 83 | let any_changes = self 84 | .encoders 85 | .iter_mut() 86 | .map(|enc| enc.update()) 87 | .any(|opt| opt.is_some()); 88 | if any_changes { 89 | Some(()) 90 | } else { 91 | None 92 | } 93 | } 94 | 95 | pub fn take_values(&mut self) -> Vec, ENCODER_COUNT> { 96 | self.encoders 97 | .iter_mut() 98 | .map(|enc| enc.take_value()) 99 | .collect() 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /hardware/lib/NHD-2.7-12864WDW3/NHD2712864WDW3.kicad_mod: -------------------------------------------------------------------------------- 1 | (module "NHD2712864WDW3" (layer F.Cu) 2 | (descr "NHD-2.7-12864WDW3-1") 3 | (tags "Display") 4 | (fp_text reference DS** (at -24.130 20.8) (layer F.SilkS) 5 | (effects (font (size 1.27 1.27) (thickness 0.254))) 6 | ) 7 | (fp_text user %R (at -24.130 20.8) (layer F.Fab) 8 | (effects (font (size 1.27 1.27) (thickness 0.254))) 9 | ) 10 | (fp_text value "NHD2712864WDW3" (at -24.130 20.8) (layer F.SilkS) hide 11 | (effects (font (size 1.27 1.27) (thickness 0.254))) 12 | ) 13 | (fp_line (start -65.13 45) (end 16.87 45) (layer F.Fab) (width 0.2)) 14 | (fp_line (start 16.87 45) (end 16.87 -2.5) (layer F.Fab) (width 0.2)) 15 | (fp_line (start 16.87 -2.5) (end -65.13 -2.5) (layer F.Fab) (width 0.2)) 16 | (fp_line (start -65.13 -2.5) (end -65.13 45) (layer F.Fab) (width 0.2)) 17 | (fp_line (start -65.13 -2.5) (end 16.87 -2.5) (layer F.SilkS) (width 0.1)) 18 | (fp_line (start 16.87 -2.5) (end 16.87 45) (layer F.SilkS) (width 0.1)) 19 | (fp_line (start 16.87 45) (end -65.13 45) (layer F.SilkS) (width 0.1)) 20 | (fp_line (start -65.13 45) (end -65.13 -2.5) (layer F.SilkS) (width 0.1)) 21 | (fp_line (start -66.13 -4.4) (end 17.87 -4.4) (layer F.CrtYd) (width 0.1)) 22 | (fp_line (start 17.87 -4.4) (end 17.87 46) (layer F.CrtYd) (width 0.1)) 23 | (fp_line (start 17.87 46) (end -66.13 46) (layer F.CrtYd) (width 0.1)) 24 | (fp_line (start -66.13 46) (end -66.13 -4.4) (layer F.CrtYd) (width 0.1)) 25 | (fp_line (start 0 -3.4) (end 0 -3.4) (layer F.SilkS) (width 0.2)) 26 | (fp_line (start 0 -3.3) (end 0 -3.3) (layer F.SilkS) (width 0.2)) 27 | (fp_line (start 0 -3.4) (end 0 -3.4) (layer F.SilkS) (width 0.2)) 28 | (fp_arc (start 0 -3.35) (end 0.000 -3.4) (angle -180) (layer F.SilkS) (width 0.2)) 29 | (fp_arc (start 0 -3.35) (end 0.000 -3.3) (angle -180) (layer F.SilkS) (width 0.2)) 30 | (fp_arc (start 0 -3.35) (end 0.000 -3.4) (angle -180) (layer F.SilkS) (width 0.2)) 31 | (pad 1 thru_hole circle (at 0.000 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 32 | (pad 2 thru_hole circle (at -2.540 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 33 | (pad 3 thru_hole circle (at -5.080 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 34 | (pad 4 thru_hole circle (at -7.620 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 35 | (pad 5 thru_hole circle (at -10.160 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 36 | (pad 6 thru_hole circle (at -12.700 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 37 | (pad 7 thru_hole circle (at -15.240 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 38 | (pad 8 thru_hole circle (at -17.780 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 39 | (pad 9 thru_hole circle (at -20.320 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 40 | (pad 10 thru_hole circle (at -22.860 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 41 | (pad 11 thru_hole circle (at -25.400 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 42 | (pad 12 thru_hole circle (at -27.940 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 43 | (pad 13 thru_hole circle (at -30.480 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 44 | (pad 14 thru_hole circle (at -33.020 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 45 | (pad 15 thru_hole circle (at -35.560 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 46 | (pad 16 thru_hole circle (at -38.100 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 47 | (pad 17 thru_hole circle (at -40.640 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 48 | (pad 18 thru_hole circle (at -43.180 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 49 | (pad 19 thru_hole circle (at -45.720 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 50 | (pad 20 thru_hole circle (at -48.260 -0) (size 1.800 1.800) (drill 1.2) (layers *.Cu *.Mask)) 51 | (pad MH1 thru_hole circle (at 14.370 -0) (size 4.050 4.050) (drill 2.7) (layers *.Cu *.Mask)) 52 | (pad MH2 thru_hole circle (at -62.630 -0) (size 4.050 4.050) (drill 2.7) (layers *.Cu *.Mask)) 53 | (pad MH3 thru_hole circle (at -62.630 42.5) (size 4.050 4.050) (drill 2.7) (layers *.Cu *.Mask)) 54 | (pad MH4 thru_hole circle (at 14.370 42.5) (size 4.050 4.050) (drill 2.7) (layers *.Cu *.Mask)) 55 | (model NHD-2.7-12864WDW3.stp 56 | (at (xyz 0 0 0)) 57 | (scale (xyz 1 1 1)) 58 | (rotate (xyz 0 0 0)) 59 | ) 60 | ) 61 | -------------------------------------------------------------------------------- /hardware/lib/NHD-2.7-12864WDW3/NHD-2.7-12864WDW3.mod: -------------------------------------------------------------------------------- 1 | PCBNEW-LibModule-V1 2024-04-03 22:24:49 2 | # encoding utf-8 3 | Units mm 4 | $INDEX 5 | NHD2712864WDW3 6 | $EndINDEX 7 | $MODULE NHD2712864WDW3 8 | Po 0 0 0 15 660dc921 00000000 ~~ 9 | Li NHD2712864WDW3 10 | Cd NHD-2.7-12864WDW3-1 11 | Kw Display 12 | Sc 0 13 | At STD 14 | AR 15 | Op 0 0 0 16 | T0 -24.130 20.8 1.27 1.27 0 0.254 N V 21 N "DS**" 17 | T1 -24.130 20.8 1.27 1.27 0 0.254 N I 21 N "NHD2712864WDW3" 18 | DS -65.13 45 16.87 45 0.2 24 19 | DS 16.87 45 16.87 -2.5 0.2 24 20 | DS 16.87 -2.5 -65.13 -2.5 0.2 24 21 | DS -65.13 -2.5 -65.13 45 0.2 24 22 | DS -65.13 -2.5 16.87 -2.5 0.1 21 23 | DS 16.87 -2.5 16.87 45 0.1 21 24 | DS 16.87 45 -65.13 45 0.1 21 25 | DS -65.13 45 -65.13 -2.5 0.1 21 26 | DS -66.13 -4.4 17.87 -4.4 0.1 24 27 | DS 17.87 -4.4 17.87 46 0.1 24 28 | DS 17.87 46 -66.13 46 0.1 24 29 | DS -66.13 46 -66.13 -4.4 0.1 24 30 | DS 0 -3.4 0 -3.4 0.2 21 31 | DS 0 -3.3 0 -3.3 0.2 21 32 | DS 0 -3.4 0 -3.4 0.2 21 33 | DA 4.1633363423443E-17 -3.35 0.000 -3.4 -1800 0.2 21 34 | DA 4.1633363423443E-17 -3.35 0.000 -3.3 -1800 0.2 21 35 | DA 4.1633363423443E-17 -3.35 0.000 -3.4 -1800 0.2 21 36 | $PAD 37 | Po 0.000 -0 38 | Sh "1" C 1.800 1.800 0 0 900 39 | Dr 1.2 0 0 40 | At STD N 00E0FFFF 41 | Ne 0 "" 42 | $EndPAD 43 | $PAD 44 | Po -2.540 -0 45 | Sh "2" C 1.800 1.800 0 0 900 46 | Dr 1.2 0 0 47 | At STD N 00E0FFFF 48 | Ne 0 "" 49 | $EndPAD 50 | $PAD 51 | Po -5.080 -0 52 | Sh "3" C 1.800 1.800 0 0 900 53 | Dr 1.2 0 0 54 | At STD N 00E0FFFF 55 | Ne 0 "" 56 | $EndPAD 57 | $PAD 58 | Po -7.620 -0 59 | Sh "4" C 1.800 1.800 0 0 900 60 | Dr 1.2 0 0 61 | At STD N 00E0FFFF 62 | Ne 0 "" 63 | $EndPAD 64 | $PAD 65 | Po -10.160 -0 66 | Sh "5" C 1.800 1.800 0 0 900 67 | Dr 1.2 0 0 68 | At STD N 00E0FFFF 69 | Ne 0 "" 70 | $EndPAD 71 | $PAD 72 | Po -12.700 -0 73 | Sh "6" C 1.800 1.800 0 0 900 74 | Dr 1.2 0 0 75 | At STD N 00E0FFFF 76 | Ne 0 "" 77 | $EndPAD 78 | $PAD 79 | Po -15.240 -0 80 | Sh "7" C 1.800 1.800 0 0 900 81 | Dr 1.2 0 0 82 | At STD N 00E0FFFF 83 | Ne 0 "" 84 | $EndPAD 85 | $PAD 86 | Po -17.780 -0 87 | Sh "8" C 1.800 1.800 0 0 900 88 | Dr 1.2 0 0 89 | At STD N 00E0FFFF 90 | Ne 0 "" 91 | $EndPAD 92 | $PAD 93 | Po -20.320 -0 94 | Sh "9" C 1.800 1.800 0 0 900 95 | Dr 1.2 0 0 96 | At STD N 00E0FFFF 97 | Ne 0 "" 98 | $EndPAD 99 | $PAD 100 | Po -22.860 -0 101 | Sh "10" C 1.800 1.800 0 0 900 102 | Dr 1.2 0 0 103 | At STD N 00E0FFFF 104 | Ne 0 "" 105 | $EndPAD 106 | $PAD 107 | Po -25.400 -0 108 | Sh "11" C 1.800 1.800 0 0 900 109 | Dr 1.2 0 0 110 | At STD N 00E0FFFF 111 | Ne 0 "" 112 | $EndPAD 113 | $PAD 114 | Po -27.940 -0 115 | Sh "12" C 1.800 1.800 0 0 900 116 | Dr 1.2 0 0 117 | At STD N 00E0FFFF 118 | Ne 0 "" 119 | $EndPAD 120 | $PAD 121 | Po -30.480 -0 122 | Sh "13" C 1.800 1.800 0 0 900 123 | Dr 1.2 0 0 124 | At STD N 00E0FFFF 125 | Ne 0 "" 126 | $EndPAD 127 | $PAD 128 | Po -33.020 -0 129 | Sh "14" C 1.800 1.800 0 0 900 130 | Dr 1.2 0 0 131 | At STD N 00E0FFFF 132 | Ne 0 "" 133 | $EndPAD 134 | $PAD 135 | Po -35.560 -0 136 | Sh "15" C 1.800 1.800 0 0 900 137 | Dr 1.2 0 0 138 | At STD N 00E0FFFF 139 | Ne 0 "" 140 | $EndPAD 141 | $PAD 142 | Po -38.100 -0 143 | Sh "16" C 1.800 1.800 0 0 900 144 | Dr 1.2 0 0 145 | At STD N 00E0FFFF 146 | Ne 0 "" 147 | $EndPAD 148 | $PAD 149 | Po -40.640 -0 150 | Sh "17" C 1.800 1.800 0 0 900 151 | Dr 1.2 0 0 152 | At STD N 00E0FFFF 153 | Ne 0 "" 154 | $EndPAD 155 | $PAD 156 | Po -43.180 -0 157 | Sh "18" C 1.800 1.800 0 0 900 158 | Dr 1.2 0 0 159 | At STD N 00E0FFFF 160 | Ne 0 "" 161 | $EndPAD 162 | $PAD 163 | Po -45.720 -0 164 | Sh "19" C 1.800 1.800 0 0 900 165 | Dr 1.2 0 0 166 | At STD N 00E0FFFF 167 | Ne 0 "" 168 | $EndPAD 169 | $PAD 170 | Po -48.260 -0 171 | Sh "20" C 1.800 1.800 0 0 900 172 | Dr 1.2 0 0 173 | At STD N 00E0FFFF 174 | Ne 0 "" 175 | $EndPAD 176 | $PAD 177 | Po 14.370 -0 178 | Sh "MH1" C 4.050 4.050 0 0 900 179 | Dr 2.7 0 0 180 | At STD N 00E0FFFF 181 | Ne 0 "" 182 | $EndPAD 183 | $PAD 184 | Po -62.630 -0 185 | Sh "MH2" C 4.050 4.050 0 0 900 186 | Dr 2.7 0 0 187 | At STD N 00E0FFFF 188 | Ne 0 "" 189 | $EndPAD 190 | $PAD 191 | Po -62.630 42.5 192 | Sh "MH3" C 4.050 4.050 0 0 900 193 | Dr 2.7 0 0 194 | At STD N 00E0FFFF 195 | Ne 0 "" 196 | $EndPAD 197 | $PAD 198 | Po 14.370 42.5 199 | Sh "MH4" C 4.050 4.050 0 0 900 200 | Dr 2.7 0 0 201 | At STD N 00E0FFFF 202 | Ne 0 "" 203 | $EndPAD 204 | $EndMODULE NHD2712864WDW3 205 | $EndLIBRARY 206 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/machine/rand_melody_machine.rs: -------------------------------------------------------------------------------- 1 | /// Machine which generates random note pitch values. 2 | use super::Machine; 3 | use crate::{ 4 | machine_resources::MachineResources, 5 | map_to_range, 6 | midi::Note, 7 | param::{Param, ParamList}, 8 | Sequence, 9 | }; 10 | 11 | use alloc::boxed::Box; 12 | 13 | #[derive(Debug)] 14 | pub struct RandMelodyMachine { 15 | params: ParamList, 16 | seed: u64, 17 | } 18 | 19 | impl RandMelodyMachine { 20 | pub fn new() -> RandMelodyMachine { 21 | let params = ParamList::from_slice(&[ 22 | Box::new(Param::new_note_param("ROOT")), 23 | Box::new(Param::new_number_param("RANGE", 1, 60, 12)), 24 | ]) 25 | .expect("should create rand melody machine param list from slice"); 26 | RandMelodyMachine { params, seed: 0 } 27 | } 28 | 29 | fn process(sequence: Sequence, root: Note, range: u8, seed: u64) -> Sequence { 30 | let min_note = Into::::into(root) as i32; 31 | let max_note: i32 = min_note + range as i32 - 1; 32 | let mut i = 0; 33 | sequence.map_notes(|_| { 34 | let rand_note_num = ((seed >> i) & 127) as i32; 35 | let note_num = map_to_range(rand_note_num, 0, 127, min_note, max_note) as u8; 36 | i += 1; 37 | note_num 38 | .try_into() 39 | .expect("note number should go into note") 40 | }) 41 | } 42 | } 43 | 44 | impl Machine for RandMelodyMachine { 45 | fn name(&self) -> &str { 46 | "RAND" 47 | } 48 | 49 | fn params(&self) -> &ParamList { 50 | &self.params 51 | } 52 | 53 | fn params_mut(&mut self) -> &mut ParamList { 54 | &mut self.params 55 | } 56 | 57 | fn generate(&mut self, machine_resources: &mut MachineResources) { 58 | self.seed = machine_resources.random_u64(); 59 | } 60 | 61 | fn apply(&self, sequence: Sequence) -> Sequence { 62 | let root = self.params[0] 63 | .value() 64 | .try_into() 65 | .expect("unexpected root param for RandMelodyMachine"); 66 | let range = self.params[1] 67 | .value() 68 | .try_into() 69 | .expect("unexpected range param for RandMelodyMachine"); 70 | Self::process(sequence, root, range, self.seed) 71 | } 72 | } 73 | 74 | unsafe impl Send for RandMelodyMachine {} 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | use crate::{machine_resources::MachineResources, sequence_generator::SequenceGenerator}; 80 | 81 | #[test] 82 | fn rand_melody_machine_should_generate_stable_sequence() { 83 | let mut machine_resources = MachineResources::new(); 84 | let mut machine = RandMelodyMachine::new(); 85 | machine.generate(&mut machine_resources); 86 | let input_sequence = SequenceGenerator::initial_sequence(8); 87 | let output_sequence = machine.apply(SequenceGenerator::initial_sequence(8)); 88 | let output_sequence2 = machine.apply(SequenceGenerator::initial_sequence(8)); 89 | assert_ne!(input_sequence, output_sequence); 90 | assert_eq!(output_sequence, output_sequence2); 91 | } 92 | 93 | #[test] 94 | fn rand_melody_machine_should_generate_different_sequences_if_generate_called_twice() { 95 | let mut machine_resources = MachineResources::new(); 96 | let mut machine = RandMelodyMachine::new(); 97 | machine.generate(&mut machine_resources); 98 | let input_sequence = SequenceGenerator::initial_sequence(8); 99 | let output_sequence = machine.apply(SequenceGenerator::initial_sequence(8)); 100 | machine.generate(&mut machine_resources); 101 | let output_sequence2 = machine.apply(SequenceGenerator::initial_sequence(8)); 102 | assert_ne!(input_sequence, output_sequence); 103 | assert_ne!(output_sequence, output_sequence2); 104 | } 105 | 106 | #[test] 107 | fn rand_melody_machine_should_generate_notes_in_specified_range() { 108 | let mut machine_resources = MachineResources::new(); 109 | let mut machine = RandMelodyMachine::new(); 110 | machine.generate(&mut machine_resources); 111 | let root_note: u8 = Note::default().into(); 112 | let max_note = root_note + 11; 113 | let output_sequence = machine.apply(SequenceGenerator::initial_sequence(8)); 114 | assert!(output_sequence.iter().all(|step| { 115 | let note: u8 = step.as_ref().unwrap().note.into(); 116 | note >= root_note && note <= max_note 117 | })); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /hardware/lib/UJ2-MIBH-4-SMT-TR/UJ2-MIBH-4-SMT-TR.kicad_sym: -------------------------------------------------------------------------------- 1 | (kicad_symbol_lib 2 | (version 20231120) 3 | (generator "kicad_symbol_editor") 4 | (generator_version "8.0") 5 | (symbol "UJ2-MIBH-4-SMT-TR" 6 | (pin_names 7 | (offset 1.016) 8 | ) 9 | (exclude_from_sim no) 10 | (in_bom yes) 11 | (on_board yes) 12 | (property "Reference" "J" 13 | (at -5.08 10.6934 0) 14 | (effects 15 | (font 16 | (size 1.27 1.27) 17 | ) 18 | (justify left bottom) 19 | ) 20 | ) 21 | (property "Value" "UJ2-MIBH-4-SMT-TR" 22 | (at -5.08 -25.4254 0) 23 | (effects 24 | (font 25 | (size 1.27 1.27) 26 | ) 27 | (justify left bottom) 28 | ) 29 | ) 30 | (property "Footprint" "CUIDEVICES_UJ2-MIBH-4-SMT-TR" 31 | (at 0 0 0) 32 | (effects 33 | (font 34 | (size 1.27 1.27) 35 | ) 36 | (justify left bottom) 37 | (hide yes) 38 | ) 39 | ) 40 | (property "Datasheet" "" 41 | (at 0 0 0) 42 | (effects 43 | (font 44 | (size 1.27 1.27) 45 | ) 46 | (justify left bottom) 47 | (hide yes) 48 | ) 49 | ) 50 | (property "Description" "1.01" 51 | (at 0 0 0) 52 | (effects 53 | (font 54 | (size 1.27 1.27) 55 | ) 56 | (justify left bottom) 57 | (hide yes) 58 | ) 59 | ) 60 | (property "STANDARD" "Manufacturer Recommendations" 61 | (at 0 0 0) 62 | (effects 63 | (font 64 | (size 1.27 1.27) 65 | ) 66 | (justify left bottom) 67 | (hide yes) 68 | ) 69 | ) 70 | (property "MAXIMUM_PACKAGE_HEIGHT" "2.45mm" 71 | (at 0 0 0) 72 | (effects 73 | (font 74 | (size 1.27 1.27) 75 | ) 76 | (justify left bottom) 77 | (hide yes) 78 | ) 79 | ) 80 | (property "MANUFACTURER" "CUI Devices" 81 | (at 0 0 0) 82 | (effects 83 | (font 84 | (size 1.27 1.27) 85 | ) 86 | (justify left bottom) 87 | (hide yes) 88 | ) 89 | ) 90 | (property "ki_locked" "" 91 | (at 0 0 0) 92 | (effects 93 | (font 94 | (size 1.27 1.27) 95 | ) 96 | ) 97 | ) 98 | (symbol "UJ2-MIBH-4-SMT-TR_0_0" 99 | (rectangle 100 | (start -5.08 -22.86) 101 | (end 5.08 10.16) 102 | (stroke 103 | (width 0.254) 104 | (type solid) 105 | ) 106 | (fill 107 | (type background) 108 | ) 109 | ) 110 | (pin power_in line 111 | (at 7.62 7.62 180) 112 | (length 2.54) 113 | (name "VCC" 114 | (effects 115 | (font 116 | (size 1.016 1.016) 117 | ) 118 | ) 119 | ) 120 | (number "1" 121 | (effects 122 | (font 123 | (size 1.016 1.016) 124 | ) 125 | ) 126 | ) 127 | ) 128 | (pin bidirectional line 129 | (at 7.62 5.08 180) 130 | (length 2.54) 131 | (name "D-" 132 | (effects 133 | (font 134 | (size 1.016 1.016) 135 | ) 136 | ) 137 | ) 138 | (number "2" 139 | (effects 140 | (font 141 | (size 1.016 1.016) 142 | ) 143 | ) 144 | ) 145 | ) 146 | (pin bidirectional line 147 | (at 7.62 2.54 180) 148 | (length 2.54) 149 | (name "D+" 150 | (effects 151 | (font 152 | (size 1.016 1.016) 153 | ) 154 | ) 155 | ) 156 | (number "3" 157 | (effects 158 | (font 159 | (size 1.016 1.016) 160 | ) 161 | ) 162 | ) 163 | ) 164 | (pin bidirectional line 165 | (at 7.62 0 180) 166 | (length 2.54) 167 | (name "ID" 168 | (effects 169 | (font 170 | (size 1.016 1.016) 171 | ) 172 | ) 173 | ) 174 | (number "4" 175 | (effects 176 | (font 177 | (size 1.016 1.016) 178 | ) 179 | ) 180 | ) 181 | ) 182 | (pin power_in line 183 | (at 7.62 -2.54 180) 184 | (length 2.54) 185 | (name "GND" 186 | (effects 187 | (font 188 | (size 1.016 1.016) 189 | ) 190 | ) 191 | ) 192 | (number "5" 193 | (effects 194 | (font 195 | (size 1.016 1.016) 196 | ) 197 | ) 198 | ) 199 | ) 200 | (pin passive line 201 | (at 7.62 -7.62 180) 202 | (length 2.54) 203 | (name "SHIELD" 204 | (effects 205 | (font 206 | (size 1.016 1.016) 207 | ) 208 | ) 209 | ) 210 | (number "P5" 211 | (effects 212 | (font 213 | (size 1.016 1.016) 214 | ) 215 | ) 216 | ) 217 | ) 218 | (pin passive line 219 | (at 7.62 -10.16 180) 220 | (length 2.54) 221 | (name "SHIELD__1" 222 | (effects 223 | (font 224 | (size 1.016 1.016) 225 | ) 226 | ) 227 | ) 228 | (number "P6" 229 | (effects 230 | (font 231 | (size 1.016 1.016) 232 | ) 233 | ) 234 | ) 235 | ) 236 | (pin passive line 237 | (at 7.62 -12.7 180) 238 | (length 2.54) 239 | (name "SHIELD__2" 240 | (effects 241 | (font 242 | (size 1.016 1.016) 243 | ) 244 | ) 245 | ) 246 | (number "S1" 247 | (effects 248 | (font 249 | (size 1.016 1.016) 250 | ) 251 | ) 252 | ) 253 | ) 254 | (pin passive line 255 | (at 7.62 -15.24 180) 256 | (length 2.54) 257 | (name "SHIELD__3" 258 | (effects 259 | (font 260 | (size 1.016 1.016) 261 | ) 262 | ) 263 | ) 264 | (number "S2" 265 | (effects 266 | (font 267 | (size 1.016 1.016) 268 | ) 269 | ) 270 | ) 271 | ) 272 | (pin passive line 273 | (at 7.62 -17.78 180) 274 | (length 2.54) 275 | (name "SHIELD__4" 276 | (effects 277 | (font 278 | (size 1.016 1.016) 279 | ) 280 | ) 281 | ) 282 | (number "S3" 283 | (effects 284 | (font 285 | (size 1.016 1.016) 286 | ) 287 | ) 288 | ) 289 | ) 290 | (pin passive line 291 | (at 7.62 -20.32 180) 292 | (length 2.54) 293 | (name "SHIELD__5" 294 | (effects 295 | (font 296 | (size 1.016 1.016) 297 | ) 298 | ) 299 | ) 300 | (number "S4" 301 | (effects 302 | (font 303 | (size 1.016 1.016) 304 | ) 305 | ) 306 | ) 307 | ) 308 | ) 309 | ) 310 | ) -------------------------------------------------------------------------------- /hardware/lib/NHD-2.7-12864WDW3/NHD-2.7-12864WDW3.kicad_sym: -------------------------------------------------------------------------------- 1 | (kicad_symbol_lib (version 20211014) (generator SamacSys_ECAD_Model) 2 | (symbol "NHD-2.7-12864WDW3" (in_bom yes) (on_board yes) 3 | (property "Reference" "DS" (at 34.29 7.62 0) 4 | (effects (font (size 1.27 1.27)) (justify left top)) 5 | ) 6 | (property "Value" "NHD-2.7-12864WDW3" (at 34.29 5.08 0) 7 | (effects (font (size 1.27 1.27)) (justify left top)) 8 | ) 9 | (property "Footprint" "NHD2712864WDW3" (at 34.29 -94.92 0) 10 | (effects (font (size 1.27 1.27)) (justify left top) hide) 11 | ) 12 | (property "Datasheet" "https://componentsearchengine.com/Datasheets/1/NHD-2.7-12864WDW3.pdf" (at 34.29 -194.92 0) 13 | (effects (font (size 1.27 1.27)) (justify left top) hide) 14 | ) 15 | (property "ki_description" "LCD OLED GRAPHIC 128X64 WHT" (at 34.29 -294.92 0) 16 | (effects (font (size 1.27 1.27)) (justify left top) hide) 17 | ) 18 | (property "Height" "6" (at 34.29 -394.92 0) 19 | (effects (font (size 1.27 1.27)) (justify left top) hide) 20 | ) 21 | (property "Mouser Part Number" "763-NHD2712864WDW3" (at 34.29 -494.92 0) 22 | (effects (font (size 1.27 1.27)) (justify left top) hide) 23 | ) 24 | (property "Mouser Price/Stock" "https://www.mouser.co.uk/ProductDetail/Newhaven-Display/NHD-2.7-12864WDW3?qs=EU6FO9ffTwf2tAy5%2FRPmRA%3D%3D" (at 34.29 -594.92 0) 25 | (effects (font (size 1.27 1.27)) (justify left top) hide) 26 | ) 27 | (property "Manufacturer_Name" "Newhaven Display" (at 34.29 -694.92 0) 28 | (effects (font (size 1.27 1.27)) (justify left top) hide) 29 | ) 30 | (property "Manufacturer_Part_Number" "NHD-2.7-12864WDW3" (at 34.29 -794.92 0) 31 | (effects (font (size 1.27 1.27)) (justify left top) hide) 32 | ) 33 | (rectangle 34 | (start 5.08 2.54) 35 | (end 33.02 -55.88) 36 | (stroke (width 0.254) (type default)) 37 | (fill (type background)) 38 | ) 39 | (pin passive line (at 38.1 -50.8 180) (length 5.08) 40 | (name "VSS" (effects (font (size 1.27 1.27)))) 41 | (number "1" (effects (font (size 1.27 1.27)))) 42 | ) 43 | (pin passive line (at 38.1 -48.26 180) (length 5.08) 44 | (name "VDD" (effects (font (size 1.27 1.27)))) 45 | (number "2" (effects (font (size 1.27 1.27)))) 46 | ) 47 | (pin passive line (at 38.1 -45.72 180) (length 5.08) 48 | (name "N.C._(BC_VDD)" (effects (font (size 1.27 1.27)))) 49 | (number "3" (effects (font (size 1.27 1.27)))) 50 | ) 51 | (pin passive line (at 38.1 -43.18 180) (length 5.08) 52 | (name "D/C" (effects (font (size 1.27 1.27)))) 53 | (number "4" (effects (font (size 1.27 1.27)))) 54 | ) 55 | (pin passive line (at 38.1 -40.64 180) (length 5.08) 56 | (name "R/W_OR_/WR" (effects (font (size 1.27 1.27)))) 57 | (number "5" (effects (font (size 1.27 1.27)))) 58 | ) 59 | (pin passive line (at 38.1 -38.1 180) (length 5.08) 60 | (name "E_OR_/RD" (effects (font (size 1.27 1.27)))) 61 | (number "6" (effects (font (size 1.27 1.27)))) 62 | ) 63 | (pin passive line (at 38.1 -35.56 180) (length 5.08) 64 | (name "DB0" (effects (font (size 1.27 1.27)))) 65 | (number "7" (effects (font (size 1.27 1.27)))) 66 | ) 67 | (pin passive line (at 38.1 -33.02 180) (length 5.08) 68 | (name "DB1" (effects (font (size 1.27 1.27)))) 69 | (number "8" (effects (font (size 1.27 1.27)))) 70 | ) 71 | (pin passive line (at 38.1 -30.48 180) (length 5.08) 72 | (name "DB2" (effects (font (size 1.27 1.27)))) 73 | (number "9" (effects (font (size 1.27 1.27)))) 74 | ) 75 | (pin passive line (at 38.1 -27.94 180) (length 5.08) 76 | (name "DB3" (effects (font (size 1.27 1.27)))) 77 | (number "10" (effects (font (size 1.27 1.27)))) 78 | ) 79 | (pin passive line (at 38.1 -25.4 180) (length 5.08) 80 | (name "DB4" (effects (font (size 1.27 1.27)))) 81 | (number "11" (effects (font (size 1.27 1.27)))) 82 | ) 83 | (pin passive line (at 38.1 -22.86 180) (length 5.08) 84 | (name "DB5" (effects (font (size 1.27 1.27)))) 85 | (number "12" (effects (font (size 1.27 1.27)))) 86 | ) 87 | (pin passive line (at 38.1 -20.32 180) (length 5.08) 88 | (name "DB6" (effects (font (size 1.27 1.27)))) 89 | (number "13" (effects (font (size 1.27 1.27)))) 90 | ) 91 | (pin passive line (at 38.1 -17.78 180) (length 5.08) 92 | (name "DB7" (effects (font (size 1.27 1.27)))) 93 | (number "14" (effects (font (size 1.27 1.27)))) 94 | ) 95 | (pin passive line (at 38.1 -15.24 180) (length 5.08) 96 | (name "N.C._(VCC)" (effects (font (size 1.27 1.27)))) 97 | (number "15" (effects (font (size 1.27 1.27)))) 98 | ) 99 | (pin passive line (at 38.1 -12.7 180) (length 5.08) 100 | (name "/RES" (effects (font (size 1.27 1.27)))) 101 | (number "16" (effects (font (size 1.27 1.27)))) 102 | ) 103 | (pin passive line (at 38.1 -10.16 180) (length 5.08) 104 | (name "/CS" (effects (font (size 1.27 1.27)))) 105 | (number "17" (effects (font (size 1.27 1.27)))) 106 | ) 107 | (pin passive line (at 38.1 -7.62 180) (length 5.08) 108 | (name "/SHDN_(N.C.)" (effects (font (size 1.27 1.27)))) 109 | (number "18" (effects (font (size 1.27 1.27)))) 110 | ) 111 | (pin passive line (at 38.1 -5.08 180) (length 5.08) 112 | (name "BS1" (effects (font (size 1.27 1.27)))) 113 | (number "19" (effects (font (size 1.27 1.27)))) 114 | ) 115 | (pin passive line (at 38.1 -2.54 180) (length 5.08) 116 | (name "BS0" (effects (font (size 1.27 1.27)))) 117 | (number "20" (effects (font (size 1.27 1.27)))) 118 | ) 119 | (pin passive line (at 38.1 -53.34 180) (length 5.08) 120 | (name "MH1" (effects (font (size 1.27 1.27)))) 121 | (number "MH1" (effects (font (size 1.27 1.27)))) 122 | ) 123 | (pin passive line (at 38.1 0 180) (length 5.08) 124 | (name "MH2" (effects (font (size 1.27 1.27)))) 125 | (number "MH2" (effects (font (size 1.27 1.27)))) 126 | ) 127 | (pin passive line (at 0 0 0) (length 5.08) 128 | (name "MH3" (effects (font (size 1.27 1.27)))) 129 | (number "MH3" (effects (font (size 1.27 1.27)))) 130 | ) 131 | (pin passive line (at 0 -2.54 0) (length 5.08) 132 | (name "MH4" (effects (font (size 1.27 1.27)))) 133 | (number "MH4" (effects (font (size 1.27 1.27)))) 134 | ) 135 | ) 136 | ) 137 | -------------------------------------------------------------------------------- /firmware/microgroove_app/src/peripherals.rs: -------------------------------------------------------------------------------- 1 | /// Device initialisation and interfacing. 2 | use super::encoder::{encoder_array::EncoderArray, positional_encoder::PositionalEncoder}; 3 | use embedded_midi; 4 | use fugit::{HertzU32, RateExtU32}; 5 | use heapless::Vec; 6 | use rp2040_hal::{clocks::PeripheralClock, rosc::Enabled}; 7 | use rp_pico::{ 8 | hal::{ 9 | clocks::{self, Clock}, 10 | gpio::{ 11 | pin::bank0::{Gpio0, Gpio1, Gpio16, Gpio17, Gpio2, Gpio26, Gpio27}, 12 | FunctionI2C, FunctionUart, Pin, PullUpInput, 13 | }, 14 | pac::{self, I2C1, RESETS, TIMER, UART0}, 15 | rosc::RingOscillator, 16 | sio::Sio, 17 | timer::{monotonic::Monotonic, Alarm0}, 18 | uart::{DataBits, Reader, StopBits, UartConfig, UartPeripheral, Writer}, 19 | Timer, Watchdog, I2C, 20 | }, 21 | Pins, XOSC_CRYSTAL_FREQ, 22 | }; 23 | use ssd1306::{mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306}; 24 | 25 | // type alias for UART pins 26 | type MidiOutUartPin = Pin; 27 | type MidiInUartPin = Pin; 28 | type MidiUartPins = (MidiOutUartPin, MidiInUartPin); 29 | 30 | // microgroove-specific midi in/out channel types 31 | pub type MidiIn = embedded_midi::MidiIn>; 32 | pub type MidiOut = embedded_midi::MidiOut>; 33 | 34 | // type alias for display pins 35 | type DisplaySdaPin = Pin; 36 | type DisplaySclPin = Pin; 37 | pub type DisplayPins = (DisplaySdaPin, DisplaySclPin); 38 | 39 | // microgroove-specific display type 40 | pub type Display = Ssd1306< 41 | I2CInterface>, 42 | DisplaySize128x64, 43 | BufferedGraphicsMode, 44 | >; 45 | 46 | // type alias for button pins 47 | pub type ButtonTrackPin = Pin; 48 | pub type ButtonRhythmPin = Pin; 49 | pub type ButtonMelodyPin = Pin; 50 | type ButtonArray = (ButtonTrackPin, ButtonRhythmPin, ButtonMelodyPin); 51 | 52 | pub fn setup( 53 | mut pac: pac::Peripherals, 54 | ) -> ( 55 | MidiIn, 56 | MidiOut, 57 | Display, 58 | ButtonArray, 59 | EncoderArray, 60 | RingOscillator, 61 | Monotonic, 62 | ) { 63 | // setup gpio pins 64 | let sio = Sio::new(pac.SIO); 65 | let pins = Pins::new( 66 | pac.IO_BANK0, 67 | pac.PADS_BANK0, 68 | sio.gpio_bank0, 69 | &mut pac.RESETS, 70 | ); 71 | 72 | // setup clocks 73 | let mut watchdog = Watchdog::new(pac.WATCHDOG); 74 | let clocks = clocks::init_clocks_and_plls( 75 | XOSC_CRYSTAL_FREQ, 76 | pac.XOSC, 77 | pac.CLOCKS, 78 | pac.PLL_SYS, 79 | pac.PLL_USB, 80 | &mut pac.RESETS, 81 | &mut watchdog, 82 | ) 83 | .ok() 84 | .expect("init_clocks_and_plls(...) should succeed"); 85 | 86 | // setup MIDI IO 87 | let (midi_in, midi_out) = new_midi_uart( 88 | pac.UART0, 89 | pins.gpio16.into_mode::(), 90 | pins.gpio17.into_mode::(), 91 | &mut pac.RESETS, 92 | clocks.peripheral_clock.freq(), 93 | ); 94 | 95 | // setup display 96 | let display = new_display( 97 | pac.I2C1, 98 | pins.gpio26.into_mode::(), 99 | pins.gpio27.into_mode::(), 100 | &mut pac.RESETS, 101 | &clocks.peripheral_clock, 102 | ); 103 | 104 | // setup buttons 105 | let button_track_pin = pins.gpio0.into_pull_up_input(); 106 | let button_rhythm_pin = pins.gpio1.into_pull_up_input(); 107 | let button_melody_pin = pins.gpio2.into_pull_up_input(); 108 | let buttons = (button_track_pin, button_rhythm_pin, button_melody_pin); 109 | 110 | // setup encoders 111 | let mut encoder_vec = Vec::new(); 112 | encoder_vec 113 | .push(PositionalEncoder::new( 114 | pins.gpio9.into(), 115 | pins.gpio10.into(), 116 | )) 117 | .expect("encoder_vec.push(...) should succeed"); 118 | encoder_vec 119 | .push(PositionalEncoder::new( 120 | pins.gpio11.into(), 121 | pins.gpio12.into(), 122 | )) 123 | .expect("encoder_vec.push(...) should succeed"); 124 | encoder_vec 125 | .push(PositionalEncoder::new( 126 | pins.gpio13.into(), 127 | pins.gpio14.into(), 128 | )) 129 | .expect("encoder_vec.push(...) should succeed"); 130 | encoder_vec 131 | .push(PositionalEncoder::new(pins.gpio3.into(), pins.gpio4.into())) 132 | .expect("encoder_vec.push(...) should succeed"); 133 | encoder_vec 134 | .push(PositionalEncoder::new(pins.gpio5.into(), pins.gpio6.into())) 135 | .expect("encoder_vec.push(...) should succeed"); 136 | encoder_vec 137 | .push(PositionalEncoder::new(pins.gpio7.into(), pins.gpio8.into())) 138 | .expect("encoder_vec.push(...) should succeed"); 139 | let encoders = EncoderArray::new(encoder_vec); 140 | 141 | // create a ring oscillator for random-number generation 142 | let rosc = RingOscillator::new(pac.ROSC).initialize(); 143 | 144 | ( 145 | midi_in, 146 | midi_out, 147 | display, 148 | buttons, 149 | encoders, 150 | rosc, 151 | new_monotonic_timer(pac.TIMER, &mut pac.RESETS), 152 | ) 153 | } 154 | 155 | fn new_monotonic_timer(timer: TIMER, resets: &mut RESETS) -> Monotonic { 156 | // setup monotonic timer for rtic 157 | let mut timer = Timer::new(timer, resets); 158 | let monotonic_alarm = timer.alarm_0().expect("should get alarm_0"); 159 | Monotonic::new(timer, monotonic_alarm) 160 | } 161 | 162 | fn new_midi_uart( 163 | uart: UART0, 164 | out_pin: MidiOutUartPin, 165 | in_pin: MidiInUartPin, 166 | resets: &mut RESETS, 167 | peripheral_clock_freq: HertzU32, 168 | ) -> (MidiIn, MidiOut) { 169 | let midi_uart_pins = (out_pin, in_pin); 170 | let uart_config = UartConfig::new(31_250.Hz(), DataBits::Eight, None, StopBits::One); 171 | let mut midi_uart = UartPeripheral::new(uart, midi_uart_pins, resets) 172 | .enable(uart_config, peripheral_clock_freq) 173 | .expect("enabling uart for midi should succeed"); 174 | midi_uart.enable_rx_interrupt(); 175 | let (midi_reader, midi_writer) = midi_uart.split(); 176 | ( 177 | embedded_midi::MidiIn::new(midi_reader), 178 | embedded_midi::MidiOut::new(midi_writer), 179 | ) 180 | } 181 | 182 | fn new_display( 183 | i2c: I2C1, 184 | sda_pin: DisplaySdaPin, 185 | scl_pin: DisplaySclPin, 186 | resets: &mut RESETS, 187 | peripheral_clock: &PeripheralClock, 188 | ) -> Display { 189 | let i2c_bus = I2C::i2c1(i2c, sda_pin, scl_pin, 1.MHz(), resets, peripheral_clock); 190 | 191 | let mut display = Ssd1306::new( 192 | I2CDisplayInterface::new_alternate_address(i2c_bus), 193 | DisplaySize128x64, 194 | DisplayRotation::Rotate0, 195 | ) 196 | .into_buffered_graphics_mode(); 197 | 198 | display.init().expect("display.init() should succeed"); 199 | 200 | display 201 | } 202 | -------------------------------------------------------------------------------- /firmware/microgroove_app/src/input.rs: -------------------------------------------------------------------------------- 1 | use crate::encoder::encoder_array::ENCODER_COUNT; 2 | use microgroove_sequencer::{ 3 | machine::{MelodyMachineId, RhythmMachineId}, 4 | param::{wrapping_add, ParamError, ParamList, ParamValue}, 5 | sequence_generator::SequenceGenerator, 6 | sequencer::Sequencer, 7 | Track, TRACK_COUNT, 8 | }; 9 | 10 | use core::iter::zip; 11 | use defmt::{debug, error, Format}; 12 | use heapless::Vec; 13 | 14 | type EncoderValues = Vec, ENCODER_COUNT>; 15 | 16 | const TRACK_NUM_PARAM_INDEX: usize = 2; 17 | 18 | #[derive(Clone, Copy, Debug, Default, Format)] 19 | pub enum InputMode { 20 | #[default] 21 | Track, 22 | Sequence, 23 | Rhythm, 24 | Groove, 25 | Melody, 26 | Harmony, 27 | } 28 | 29 | /// Iterate over `encoder_values` and pass to a destination set of `Param`s 30 | /// determined by `InputMode`. This may have side-effects, including that sequence data may need to be 31 | /// regenerated. 32 | pub fn apply_encoder_values( 33 | encoder_values: EncoderValues, 34 | input_mode: InputMode, 35 | current_track: &mut u8, 36 | sequencer: &mut Sequencer, 37 | sequence_generators: &mut Vec, 38 | ) -> Result<(), ParamError> { 39 | if track_num_has_changed(input_mode, &encoder_values) { 40 | update_current_track(&encoder_values, current_track); 41 | return Ok(()); 42 | } 43 | if track_disabled(sequencer, current_track) { 44 | enable_track(sequencer, current_track); 45 | return Ok(()); 46 | } 47 | let generator = sequence_generators 48 | .get_mut(*current_track as usize) 49 | .expect("should get mut ref to sequence generator for current track"); 50 | match input_mode { 51 | InputMode::Track => { 52 | let track = sequencer 53 | .tracks 54 | .get_mut(*current_track as usize) 55 | .expect("should get current track") 56 | .as_mut() 57 | .expect("should get current track as mut ref"); 58 | let params = track.params_mut(); 59 | update_params(&encoder_values, params)?; 60 | if rhythm_machine_changed(input_mode, &encoder_values) { 61 | update_rhythm_machine(generator, params[0].value()) 62 | } 63 | if melody_machine_changed(input_mode, &encoder_values) { 64 | update_melody_machine(generator, params[3].value()) 65 | } 66 | track.apply_params()?; 67 | } 68 | InputMode::Sequence => { 69 | update_params(&encoder_values, sequencer.params_mut())?; 70 | } 71 | InputMode::Rhythm => { 72 | update_params(&encoder_values, generator.rhythm_machine.params_mut())?; 73 | } 74 | InputMode::Groove => { 75 | update_params(&encoder_values, generator.groove_params_mut())?; 76 | } 77 | InputMode::Melody => { 78 | update_params(&encoder_values, generator.melody_machine.params_mut())?; 79 | } 80 | InputMode::Harmony => { 81 | update_params(&encoder_values, generator.harmony_params_mut())?; 82 | } 83 | } 84 | update_sequence(sequencer, current_track, generator); 85 | Ok(()) 86 | } 87 | 88 | fn update_current_track(encoder_values: &EncoderValues, current_track: &mut u8) { 89 | if let Some(track_num_increment) = encoder_values[TRACK_NUM_PARAM_INDEX] { 90 | let new_track_num = wrapping_add( 91 | *current_track as i32, 92 | track_num_increment as i32, 93 | TRACK_COUNT as i32 - 1, 94 | ) as u8; 95 | debug!("[map_encoder_input] current_track={}", new_track_num); 96 | *current_track = new_track_num; 97 | } 98 | } 99 | 100 | fn track_num_has_changed(input_mode: InputMode, encoder_values: &EncoderValues) -> bool { 101 | match input_mode { 102 | InputMode::Track => match encoder_values.as_slice() { 103 | [_, _, Some(_), _, _, _] => true, 104 | _ => false, 105 | }, 106 | _ => false, 107 | } 108 | } 109 | 110 | fn rhythm_machine_changed(input_mode: InputMode, encoder_values: &EncoderValues) -> bool { 111 | match input_mode { 112 | InputMode::Track => match encoder_values.as_slice() { 113 | [Some(_), _, _, _, _, _] => true, 114 | _ => false, 115 | }, 116 | _ => false, 117 | } 118 | } 119 | 120 | fn melody_machine_changed(input_mode: InputMode, encoder_values: &EncoderValues) -> bool { 121 | match input_mode { 122 | InputMode::Track => match encoder_values.as_slice() { 123 | [_, _, _, Some(_), _, _] => true, 124 | _ => false, 125 | }, 126 | _ => false, 127 | } 128 | } 129 | 130 | fn track_disabled(sequencer: &Sequencer, track_num: &u8) -> bool { 131 | sequencer 132 | .tracks 133 | .get(*track_num as usize) 134 | .expect("should get track") 135 | .is_none() 136 | } 137 | 138 | fn enable_track(sequencer: &mut Sequencer, track_num: &u8) { 139 | let mut new_track = Track::default(); 140 | new_track.midi_channel = (*track_num).into(); 141 | new_track.sequence = SequenceGenerator::initial_sequence(new_track.length); 142 | let _ = sequencer.enable_track(*track_num, new_track); 143 | } 144 | 145 | fn update_params(encoder_values: &EncoderValues, params: &mut ParamList) -> Result<(), ParamError> { 146 | let params_and_values = zip(params.iter_mut(), encoder_values); 147 | for (param, &value) in params_and_values { 148 | if let Some(value) = value { 149 | debug!( 150 | "[map_encoder_input] increment param={}, value={}", 151 | param.name(), 152 | value 153 | ); 154 | param.increment(value.into())?; 155 | } 156 | } 157 | Ok(()) 158 | } 159 | 160 | fn update_rhythm_machine(generator: &mut SequenceGenerator, param_value: ParamValue) { 161 | let id: RhythmMachineId = param_value 162 | .try_into() 163 | .expect("unexpected rhythm machine param"); 164 | generator.rhythm_machine = id.into(); 165 | } 166 | 167 | fn update_melody_machine(generator: &mut SequenceGenerator, param_value: ParamValue) { 168 | let id: MelodyMachineId = param_value 169 | .try_into() 170 | .expect("unexpected melody machine param"); 171 | generator.melody_machine = id.into(); 172 | } 173 | 174 | fn update_sequence( 175 | sequencer: &mut Sequencer, 176 | track_num: &u8, 177 | generator: &SequenceGenerator, 178 | ) { 179 | debug!("[update_sequence] track_num={}", track_num); 180 | match sequencer.tracks.get_mut(*track_num as usize) { 181 | Some(mut_track) => match mut_track.as_mut() { 182 | Some(track) => { 183 | track.sequence = generator.apply(track.length); 184 | } 185 | None => { 186 | error!("[update_sequence] tried to update sequence for disabled track"); 187 | } 188 | }, 189 | None => { 190 | error!( 191 | "[update_sequence] couldn't get track from sequencer, track_num={}", 192 | track_num 193 | ); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/sequence_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | machine::unit_machine::UnitMachine, 3 | machine::Machine, 4 | machine_resources::MachineResources, 5 | param::{Param, ParamList, ParamValue}, 6 | part::Part, 7 | quantizer::quantize, 8 | Sequence, Step, SEQUENCE_MAX_STEPS, 9 | }; 10 | 11 | use alloc::boxed::Box; 12 | use heapless::Vec; 13 | 14 | #[derive(Debug)] 15 | pub struct SequenceGenerator { 16 | pub rhythm_machine: Box, 17 | pub melody_machine: Box, 18 | groove_params: ParamList, 19 | harmony_params: ParamList, 20 | } 21 | 22 | impl Default for SequenceGenerator { 23 | fn default() -> SequenceGenerator { 24 | SequenceGenerator { 25 | rhythm_machine: Box::new(UnitMachine::new()), 26 | melody_machine: Box::new(UnitMachine::new()), 27 | groove_params: ParamList::from_slice(&[Box::new(Param::new_part_param("PART"))]) 28 | .expect("should create groove param list from slice"), 29 | harmony_params: ParamList::from_slice(&[ 30 | Box::new(Param::new_scale_param("SCALE")), 31 | Box::new(Param::new_key_param("KEY")), 32 | ]) 33 | .expect("should create harmony param list from slice"), 34 | } 35 | } 36 | } 37 | 38 | impl SequenceGenerator { 39 | pub fn initial_sequence(length: u8) -> Sequence { 40 | (0..length).map(|_i| Step::new(60).ok()).collect() 41 | } 42 | 43 | pub fn groove_params(&self) -> &ParamList { 44 | &self.groove_params 45 | } 46 | 47 | pub fn groove_params_mut(&mut self) -> &mut ParamList { 48 | &mut self.groove_params 49 | } 50 | 51 | pub fn harmony_params(&self) -> &ParamList { 52 | &self.harmony_params 53 | } 54 | 55 | pub fn harmony_params_mut(&mut self) -> &mut ParamList { 56 | &mut self.harmony_params 57 | } 58 | 59 | pub fn part(&self) -> Part { 60 | self.groove_params[0].value().try_into().unwrap() 61 | } 62 | 63 | pub fn set_part(&mut self, part: Part) { 64 | self.groove_params[0].set(ParamValue::Part(part)); 65 | } 66 | 67 | pub fn generate(&mut self, machine_resources: &mut MachineResources) { 68 | self.melody_machine.generate(machine_resources); 69 | self.rhythm_machine.generate(machine_resources); 70 | } 71 | 72 | /// Generate a sequence by piping the initial sequence through the set of configured machines. 73 | pub fn apply(&self, length: u8) -> Sequence { 74 | // a pipe operator would be nice to have here 75 | self.apply_part( 76 | self.apply_quantizer( 77 | self.melody_machine 78 | .apply(self.rhythm_machine.apply(Self::initial_sequence(length))), 79 | ), 80 | ) 81 | } 82 | 83 | fn apply_quantizer(&self, sequence: Sequence) -> Sequence { 84 | let scale = self.harmony_params[0] 85 | .value() 86 | .try_into() 87 | .expect("unexpected scale value for quantizer"); 88 | let key = self.harmony_params[1] 89 | .value() 90 | .try_into() 91 | .expect("unexpected key value for quantizer"); 92 | sequence.map_notes(|note| quantize(note.into(), scale, key).into()) 93 | } 94 | 95 | fn apply_part(&self, sequence: Sequence) -> Sequence { 96 | let part = self.part(); 97 | let step_mask = Part::new_mask(part, sequence.len()); 98 | match part { 99 | Part::A => { 100 | let sequence = sequence.mask_steps(step_mask); 101 | let prefix_len = sequence.len() / 2; 102 | let suffix_len = sequence.len() - prefix_len; 103 | let steps_clone = sequence.steps.clone(); 104 | let suffix = steps_clone.iter().take(suffix_len); 105 | let prefix = suffix.clone().take(prefix_len); 106 | sequence.set_steps(Vec::, SEQUENCE_MAX_STEPS>::from_iter( 107 | prefix.chain(suffix).cloned(), 108 | )) 109 | } 110 | _ => sequence.mask_steps(step_mask), 111 | } 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | 119 | use crate::{ 120 | machine::rand_melody_machine::RandMelodyMachine, 121 | machine_resources::MachineResources, 122 | midi::Note, 123 | param::ParamValue, 124 | quantizer::{Key, Scale}, 125 | }; 126 | 127 | #[test] 128 | fn sequence_generator_default_should_create_a_new_generator() { 129 | let generator = SequenceGenerator::default(); 130 | assert_eq!("UNIT", generator.rhythm_machine.name()); 131 | assert_eq!("UNIT", generator.melody_machine.name()); 132 | } 133 | 134 | #[test] 135 | fn sequence_generator_apply_should_generate_a_sequence() { 136 | let generator = SequenceGenerator::default(); 137 | let sequence = generator.apply(8); 138 | assert_eq!(8, sequence.len()); 139 | assert!(sequence.iter().all(|step| { 140 | match step { 141 | Some(step) => { 142 | let note_num: u8 = step.note.into(); 143 | note_num == 60 144 | } 145 | _ => false, 146 | } 147 | })); 148 | } 149 | 150 | #[test] 151 | fn sequence_generator_should_quantize_melodies_if_configured_to_do_so() { 152 | let mut generator = SequenceGenerator::default(); 153 | generator.set_part(Part::Sequence); 154 | let params = generator.harmony_params_mut(); 155 | params[0].set(ParamValue::Scale(Scale::Major)); 156 | params[1].set(ParamValue::Key(Key::B)); 157 | let sequence = generator.apply(8); 158 | assert!(sequence.steps[0].is_some()); 159 | let step0 = sequence.steps[0].as_ref().unwrap(); 160 | let step0_note_num: u8 = step0.note.into(); 161 | let expected: u8 = Note::CSharp3.into(); 162 | assert_eq!(expected, step0_note_num); // exp 163 | } 164 | 165 | #[test] 166 | fn sequence_generator_with_part_equal_call_should_only_have_active_steps_in_first_half_of_sequence( 167 | ) { 168 | let mut generator = SequenceGenerator::default(); 169 | generator.set_part(Part::Call); 170 | let sequence = generator.apply(8); 171 | let expected_active_steps = vec![true, true, true, true, false, false, false, false]; 172 | let actual_active_steps = sequence 173 | .iter() 174 | .map(|s| s.is_some()) 175 | .collect::>(); 176 | assert_eq!(expected_active_steps, actual_active_steps); 177 | } 178 | 179 | #[test] 180 | fn sequence_generator_with_part_equal_a_should_have_two_identical_halves() { 181 | let mut generator = SequenceGenerator::default(); 182 | generator.set_part(Part::A); 183 | generator.rhythm_machine = Box::new(RandMelodyMachine::new()); 184 | let sequence = generator.apply(12); 185 | let half1 = &sequence.steps[0..6]; 186 | let half2 = &sequence.steps[6..12]; 187 | assert_eq!(half1, half2); 188 | } 189 | 190 | #[test] 191 | fn sequence_generator_with_part_equal_a_and_odd_len_should_have_an_even_prefix_with_two_identical_halves( 192 | ) { 193 | let mut generator = SequenceGenerator::default(); 194 | generator.set_part(Part::A); 195 | let sequence = generator.apply(7); 196 | let half1 = &sequence.steps[0..3]; 197 | let half2 = &sequence.steps[3..6]; 198 | assert_eq!(half1, half2); 199 | } 200 | 201 | #[test] 202 | fn sequence_generator_generate_should_randomise_sequencer_when_stochastic_machine_used() { 203 | let mut generator = SequenceGenerator::default(); 204 | let mut machine_resources = MachineResources::new(); 205 | generator.rhythm_machine = Box::new(RandMelodyMachine::new()); 206 | generator.generate(&mut machine_resources); 207 | let sequence1 = generator.apply(8); 208 | generator.generate(&mut machine_resources); 209 | let sequence2 = generator.apply(8); 210 | assert_ne!(sequence1, sequence2); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/part.rs: -------------------------------------------------------------------------------- 1 | use crate::SEQUENCE_MAX_STEPS; 2 | 3 | use core::fmt::{Display, Formatter, Result as FmtResult}; 4 | use heapless::Vec; 5 | 6 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 7 | pub enum Part { 8 | #[default] 9 | Sequence, 10 | Call, 11 | Response, 12 | A, 13 | B, 14 | C, 15 | Hook, 16 | Turnaround, 17 | } 18 | 19 | impl Display for Part { 20 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 21 | write!( 22 | f, 23 | "{}", 24 | match *self { 25 | Part::Sequence => "SEQ", 26 | Part::Call => "CALL", 27 | Part::Response => "RESP", 28 | Part::A => "A_A_", 29 | Part::B => "_B__", 30 | Part::C => "___C", 31 | Part::Hook => "HOOK", 32 | Part::Turnaround => "TURN", 33 | } 34 | ) 35 | } 36 | } 37 | 38 | impl TryFrom for Part { 39 | type Error = (); 40 | 41 | fn try_from(value: u8) -> Result { 42 | match value { 43 | 0 => Ok(Part::Sequence), 44 | 1 => Ok(Part::Call), 45 | 2 => Ok(Part::Response), 46 | 3 => Ok(Part::A), 47 | 4 => Ok(Part::B), 48 | 5 => Ok(Part::C), 49 | 6 => Ok(Part::Hook), 50 | 7 => Ok(Part::Turnaround), 51 | _ => Err(()), 52 | } 53 | } 54 | } 55 | 56 | impl Part { 57 | pub fn new_mask(part: Part, mask_len: usize) -> Vec { 58 | let infinite_trues = [true].iter().cycle(); 59 | let infinite_falses = [false].iter().cycle(); 60 | match part { 61 | Part::Sequence => infinite_trues.take(mask_len).cloned().collect(), 62 | Part::Call => { 63 | let prefix_len = mask_len / 2; 64 | let prefix_mask = infinite_trues.take(prefix_len); 65 | let suffix_len = mask_len - prefix_len; 66 | let suffix_mask = infinite_falses.take(suffix_len); 67 | prefix_mask.chain(suffix_mask).cloned().collect() 68 | } 69 | Part::Response => { 70 | let prefix_len = mask_len / 2; 71 | let prefix_mask = infinite_falses.take(prefix_len); 72 | let suffix_len = mask_len - prefix_len; 73 | let suffix_mask = infinite_trues.take(suffix_len); 74 | prefix_mask.chain(suffix_mask).cloned().collect() 75 | } 76 | Part::A => { 77 | // A_A_ = XXXX____XXXX____ 78 | let section_len = mask_len / 4; 79 | let a_section = infinite_trues.take(section_len); 80 | let b_section = infinite_falses.take(section_len); 81 | let a2_section = [true].iter().cycle().take(section_len); 82 | let c_section_len = mask_len - (section_len * 3); 83 | let c_section = [false].iter().cycle().take(c_section_len); 84 | a_section 85 | .chain(b_section) 86 | .chain(a2_section) 87 | .chain(c_section) 88 | .cloned() 89 | .collect() 90 | } 91 | Part::B => { 92 | // _B__ = ____XXXX________ 93 | let section_len = mask_len / 4; 94 | let a_section = infinite_falses.take(section_len); 95 | let b_section = infinite_trues.take(section_len); 96 | let suffix_len = mask_len - section_len * 2; 97 | let suffix_mask = [false].iter().cycle().take(suffix_len); 98 | a_section 99 | .chain(b_section) 100 | .chain(suffix_mask) 101 | .cloned() 102 | .collect() 103 | } 104 | Part::C => { 105 | // ___C = ____________XXXX 106 | let prefix_len = mask_len / 4 * 3; 107 | let prefix_mask = infinite_falses.take(prefix_len); 108 | let suffix_len = mask_len - prefix_len; 109 | let suffix_mask = infinite_trues.take(suffix_len); 110 | prefix_mask.chain(suffix_mask).cloned().collect() 111 | } 112 | Part::Hook => { 113 | // Hook => XXXXXXXXXXXXXX__ 114 | let prefix_len = mask_len / 8 * 7; 115 | let prefix_mask = infinite_trues.take(prefix_len); 116 | let suffix_len = mask_len - prefix_len; 117 | let suffix_mask = infinite_falses.take(suffix_len); 118 | prefix_mask.chain(suffix_mask).cloned().collect() 119 | } 120 | Part::Turnaround => { 121 | // Turnaround => ______________XX 122 | let prefix_len = mask_len / 8 * 7; 123 | let prefix_mask = infinite_falses.take(prefix_len); 124 | let suffix_len = mask_len - prefix_len; 125 | let suffix_mask = infinite_trues.take(suffix_len); 126 | prefix_mask.chain(suffix_mask).cloned().collect() 127 | } 128 | } 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | 136 | #[test] 137 | fn part_mask_should_be_same_length_as_mask_len_parameter() { 138 | let mask = Part::new_mask(Part::Sequence, 27); 139 | assert_eq!(27, mask.len()); 140 | } 141 | 142 | #[test] 143 | fn part_sequence_mask_should_correct_for_len_16() { 144 | let expected: Vec = Vec::from_slice(&[ 145 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, 146 | true, true, 147 | ]) 148 | .unwrap(); 149 | let actual = Part::new_mask(Part::Sequence, 16); 150 | assert_eq!(expected, actual); 151 | } 152 | 153 | #[test] 154 | fn part_call_mask_should_correct_for_len_16() { 155 | let expected: Vec = Vec::from_slice(&[ 156 | true, true, true, true, true, true, true, true, false, false, false, false, false, 157 | false, false, false, 158 | ]) 159 | .unwrap(); 160 | let actual = Part::new_mask(Part::Call, 16); 161 | assert_eq!(expected, actual); 162 | } 163 | 164 | #[test] 165 | fn part_response_mask_should_correct_for_len_16() { 166 | let expected: Vec = Vec::from_slice(&[ 167 | false, false, false, false, false, false, false, false, true, true, true, true, true, 168 | true, true, true, 169 | ]) 170 | .unwrap(); 171 | let actual = Part::new_mask(Part::Response, 16); 172 | assert_eq!(expected, actual); 173 | } 174 | 175 | #[test] 176 | fn part_a_mask_should_correct_for_len_16() { 177 | let expected: Vec = Vec::from_slice(&[ 178 | true, true, true, true, false, false, false, false, true, true, true, true, false, 179 | false, false, false, 180 | ]) 181 | .unwrap(); 182 | let actual = Part::new_mask(Part::A, 16); 183 | assert_eq!(expected, actual); 184 | } 185 | 186 | #[test] 187 | fn part_b_mask_should_correct_for_len_16() { 188 | let expected: Vec = Vec::from_slice(&[ 189 | false, false, false, false, true, true, true, true, false, false, false, false, false, 190 | false, false, false, 191 | ]) 192 | .unwrap(); 193 | let actual = Part::new_mask(Part::B, 16); 194 | assert_eq!(expected, actual); 195 | } 196 | 197 | #[test] 198 | fn part_c_mask_should_correct_for_len_16() { 199 | let expected: Vec = Vec::from_slice(&[ 200 | false, false, false, false, false, false, false, false, false, false, false, false, 201 | true, true, true, true, 202 | ]) 203 | .unwrap(); 204 | let actual = Part::new_mask(Part::C, 16); 205 | assert_eq!(expected, actual); 206 | } 207 | 208 | #[test] 209 | fn part_hook_mask_should_correct_for_len_16() { 210 | let expected: Vec = Vec::from_slice(&[ 211 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, 212 | false, false, 213 | ]) 214 | .unwrap(); 215 | let actual = Part::new_mask(Part::Hook, 16); 216 | assert_eq!(expected, actual); 217 | } 218 | 219 | #[test] 220 | fn part_turnaround_mask_should_correct_for_len_16() { 221 | let expected: Vec = Vec::from_slice(&[ 222 | false, false, false, false, false, false, false, false, false, false, false, false, 223 | false, false, true, true, 224 | ]) 225 | .unwrap(); 226 | let actual = Part::new_mask(Part::Turnaround, 16); 227 | assert_eq!(expected, actual); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /hardware/lib/rp_pico/MCU_RaspberryPi_and_Boards.pretty/RP2040-QFN-56.kicad_mod: -------------------------------------------------------------------------------- 1 | (module RP2040-QFN-56 (layer F.Cu) (tedit 5EF32B43) 2 | (descr "QFN, 56 Pin (http://www.cypress.com/file/416486/download#page=40), generated with kicad-footprint-generator ipc_dfn_qfn_generator.py") 3 | (tags "QFN DFN_QFN") 4 | (attr smd) 5 | (fp_text reference REF** (at 0 -4.82) (layer F.SilkS) 6 | (effects (font (size 1 1) (thickness 0.15))) 7 | ) 8 | (fp_text value Pico2040-QFN-56 (at 0 4.82) (layer F.Fab) 9 | (effects (font (size 1 1) (thickness 0.15))) 10 | ) 11 | (fp_text user %R (at 0 0) (layer F.Fab) 12 | (effects (font (size 1 1) (thickness 0.15))) 13 | ) 14 | (fp_line (start 4.12 -4.12) (end -4.12 -4.12) (layer F.CrtYd) (width 0.05)) 15 | (fp_line (start 4.12 4.12) (end 4.12 -4.12) (layer F.CrtYd) (width 0.05)) 16 | (fp_line (start -4.12 4.12) (end 4.12 4.12) (layer F.CrtYd) (width 0.05)) 17 | (fp_line (start -4.12 -4.12) (end -4.12 4.12) (layer F.CrtYd) (width 0.05)) 18 | (fp_line (start -3.5 -2.5) (end -2.5 -3.5) (layer F.Fab) (width 0.1)) 19 | (fp_line (start -3.5 3.5) (end -3.5 -2.5) (layer F.Fab) (width 0.1)) 20 | (fp_line (start 3.5 3.5) (end -3.5 3.5) (layer F.Fab) (width 0.1)) 21 | (fp_line (start 3.5 -3.5) (end 3.5 3.5) (layer F.Fab) (width 0.1)) 22 | (fp_line (start -2.5 -3.5) (end 3.5 -3.5) (layer F.Fab) (width 0.1)) 23 | (fp_line (start -2.96 -3.61) (end -3.61 -3.61) (layer F.SilkS) (width 0.12)) 24 | (fp_line (start 3.61 3.61) (end 3.61 2.96) (layer F.SilkS) (width 0.12)) 25 | (fp_line (start 2.96 3.61) (end 3.61 3.61) (layer F.SilkS) (width 0.12)) 26 | (fp_line (start -3.61 3.61) (end -3.61 2.96) (layer F.SilkS) (width 0.12)) 27 | (fp_line (start -2.96 3.61) (end -3.61 3.61) (layer F.SilkS) (width 0.12)) 28 | (fp_line (start 3.61 -3.61) (end 3.61 -2.96) (layer F.SilkS) (width 0.12)) 29 | (fp_line (start 2.96 -3.61) (end 3.61 -3.61) (layer F.SilkS) (width 0.12)) 30 | (pad 56 smd roundrect (at -2.6 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 31 | (pad 55 smd roundrect (at -2.2 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 32 | (pad 54 smd roundrect (at -1.8 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 33 | (pad 53 smd roundrect (at -1.4 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 34 | (pad 52 smd roundrect (at -1 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 35 | (pad 51 smd roundrect (at -0.6 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 36 | (pad 50 smd roundrect (at -0.2 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 37 | (pad 49 smd roundrect (at 0.2 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 38 | (pad 48 smd roundrect (at 0.6 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 39 | (pad 47 smd roundrect (at 1 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 40 | (pad 46 smd roundrect (at 1.4 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 41 | (pad 45 smd roundrect (at 1.8 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 42 | (pad 44 smd roundrect (at 2.2 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 43 | (pad 43 smd roundrect (at 2.6 -3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 44 | (pad 42 smd roundrect (at 3.4375 -2.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 45 | (pad 41 smd roundrect (at 3.4375 -2.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 46 | (pad 40 smd roundrect (at 3.4375 -1.8) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 47 | (pad 39 smd roundrect (at 3.4375 -1.4) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 48 | (pad 38 smd roundrect (at 3.4375 -1) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 49 | (pad 37 smd roundrect (at 3.4375 -0.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 50 | (pad 36 smd roundrect (at 3.4375 -0.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 51 | (pad 35 smd roundrect (at 3.4375 0.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 52 | (pad 34 smd roundrect (at 3.4375 0.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 53 | (pad 33 smd roundrect (at 3.4375 1) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 54 | (pad 32 smd roundrect (at 3.4375 1.4) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 55 | (pad 31 smd roundrect (at 3.4375 1.8) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 56 | (pad 30 smd roundrect (at 3.4375 2.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 57 | (pad 29 smd roundrect (at 3.4375 2.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 58 | (pad 28 smd roundrect (at 2.6 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 59 | (pad 27 smd roundrect (at 2.2 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 60 | (pad 26 smd roundrect (at 1.8 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 61 | (pad 25 smd roundrect (at 1.4 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 62 | (pad 24 smd roundrect (at 1 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 63 | (pad 23 smd roundrect (at 0.6 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 64 | (pad 22 smd roundrect (at 0.2 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 65 | (pad 21 smd roundrect (at -0.2 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 66 | (pad 20 smd roundrect (at -0.6 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 67 | (pad 19 smd roundrect (at -1 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 68 | (pad 18 smd roundrect (at -1.4 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 69 | (pad 17 smd roundrect (at -1.8 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 70 | (pad 16 smd roundrect (at -2.2 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 71 | (pad 15 smd roundrect (at -2.6 3.4375) (size 0.2 0.875) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 72 | (pad 14 smd roundrect (at -3.4375 2.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 73 | (pad 13 smd roundrect (at -3.4375 2.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 74 | (pad 12 smd roundrect (at -3.4375 1.8) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 75 | (pad 11 smd roundrect (at -3.4375 1.4) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 76 | (pad 10 smd roundrect (at -3.4375 1) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 77 | (pad 9 smd roundrect (at -3.4375 0.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 78 | (pad 8 smd roundrect (at -3.4375 0.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 79 | (pad 7 smd roundrect (at -3.4375 -0.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 80 | (pad 6 smd roundrect (at -3.4375 -0.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 81 | (pad 5 smd roundrect (at -3.4375 -1) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 82 | (pad 4 smd roundrect (at -3.4375 -1.4) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 83 | (pad 3 smd roundrect (at -3.4375 -1.8) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 84 | (pad 2 smd roundrect (at -3.4375 -2.2) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 85 | (pad 1 smd roundrect (at -3.4375 -2.6) (size 0.875 0.2) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 86 | (pad "" smd roundrect (at 0.6375 0.6375) (size 1.084435 1.084435) (layers F.Paste) (roundrect_rratio 0.230535)) 87 | (pad "" smd roundrect (at 0.6375 -0.6375) (size 1.084435 1.084435) (layers F.Paste) (roundrect_rratio 0.230535)) 88 | (pad "" smd roundrect (at -0.6375 0.6375) (size 1.084435 1.084435) (layers F.Paste) (roundrect_rratio 0.230535)) 89 | (pad "" smd roundrect (at -0.6375 -0.6375) (size 1.084435 1.084435) (layers F.Paste) (roundrect_rratio 0.230535)) 90 | (pad 57 thru_hole circle (at 1.275 1.275) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 91 | (pad 57 thru_hole circle (at 0 1.275) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 92 | (pad 57 thru_hole circle (at -1.275 1.275) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 93 | (pad 57 thru_hole circle (at 1.275 0) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 94 | (pad 57 thru_hole circle (at 0 0) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 95 | (pad 57 thru_hole circle (at -1.275 0) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 96 | (pad 57 thru_hole circle (at 1.275 -1.275) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 97 | (pad 57 thru_hole circle (at 0 -1.275) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 98 | (pad 57 thru_hole circle (at -1.275 -1.275) (size 0.6 0.6) (drill 0.35) (layers *.Cu)) 99 | (pad 57 smd roundrect (at 0 0) (size 3.2 3.2) (layers F.Cu F.Mask) (roundrect_rratio 0.045)) 100 | (model ${KISYS3DMOD}/Package_DFN_QFN.3dshapes/QFN-56-1EP_7x7mm_P0.4mm_EP5.6x5.6mm.wrl 101 | (at (xyz 0 0 0)) 102 | (scale (xyz 1 1 1)) 103 | (rotate (xyz 0 0 0)) 104 | ) 105 | ) 106 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/quantizer.rs: -------------------------------------------------------------------------------- 1 | use crate::midi::Note; 2 | 3 | use core::fmt::{Display, Formatter, Result as FmtResult}; 4 | 5 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 6 | pub enum Scale { 7 | #[default] 8 | Chromatic, 9 | Major, 10 | NaturalMinor, 11 | HarmonicMinor, 12 | MelodicMinor, 13 | PentatonicMajor, 14 | PentatonicMinor, 15 | HexatonicBlues, 16 | WholeTone, 17 | MajorTriad, 18 | MinorTriad, 19 | DominantSeventh, 20 | DiminishedSeventh, 21 | Octave, 22 | OctaveAndFifth, 23 | Dorian, 24 | Phrygian, 25 | Lydian, 26 | Mixolydian, 27 | Locrian, 28 | } 29 | 30 | impl Into for Scale { 31 | fn into(self) -> u8 { 32 | self as u8 33 | } 34 | } 35 | 36 | impl TryFrom for Scale { 37 | type Error = (); 38 | 39 | fn try_from(value: u8) -> Result { 40 | match value { 41 | 0 => Ok(Scale::Chromatic), 42 | 1 => Ok(Scale::Major), 43 | 2 => Ok(Scale::NaturalMinor), 44 | 3 => Ok(Scale::HarmonicMinor), 45 | 4 => Ok(Scale::MelodicMinor), 46 | 5 => Ok(Scale::PentatonicMajor), 47 | 6 => Ok(Scale::PentatonicMinor), 48 | 7 => Ok(Scale::HexatonicBlues), 49 | 8 => Ok(Scale::WholeTone), 50 | 9 => Ok(Scale::MajorTriad), 51 | 10 => Ok(Scale::MinorTriad), 52 | 11 => Ok(Scale::DominantSeventh), 53 | 12 => Ok(Scale::DiminishedSeventh), 54 | 13 => Ok(Scale::Octave), 55 | 14 => Ok(Scale::OctaveAndFifth), 56 | 15 => Ok(Scale::Dorian), 57 | 16 => Ok(Scale::Phrygian), 58 | 17 => Ok(Scale::Lydian), 59 | 18 => Ok(Scale::Mixolydian), 60 | 19 => Ok(Scale::Locrian), 61 | _ => Err(()), 62 | } 63 | } 64 | } 65 | 66 | impl Display for Scale { 67 | #[rustfmt::skip] 68 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 69 | write!( 70 | f, 71 | "{}", 72 | match *self { 73 | Scale::Chromatic => "OFF", 74 | Scale::Major => "MAJ", 75 | Scale::NaturalMinor => "MIN", 76 | Scale::HarmonicMinor => "HMI", 77 | Scale::MelodicMinor => "MMI", 78 | Scale::PentatonicMajor => "PMA", 79 | Scale::PentatonicMinor => "PMI", 80 | Scale::HexatonicBlues => "BLU", 81 | Scale::WholeTone => "WHL", 82 | Scale::MajorTriad => "3MA", 83 | Scale::MinorTriad => "3MI", 84 | Scale::DominantSeventh => "7DO", 85 | Scale::DiminishedSeventh => "7DI", 86 | Scale::Octave => "OCT", 87 | Scale::OctaveAndFifth => "O+5", 88 | Scale::Dorian => "DOR", 89 | Scale::Phrygian => "PHR", 90 | Scale::Lydian => "LYD", 91 | Scale::Mixolydian => "MIX", 92 | Scale::Locrian => "LOC", 93 | } 94 | ) 95 | } 96 | } 97 | 98 | /// Type to capture the mapping of notes in a chromatic octave to the quantized equivalent of 99 | /// those notea in given scale. Each entry is an array of 12 values. The input note is used to 100 | /// index into the array. The array value returned is the quantized note. This format allows for 101 | /// some exciting scales (Reverse Phrygian anyone??). 102 | type ScaleMap = [u8; 12]; 103 | 104 | impl From for ScaleMap { 105 | #[rustfmt::skip] 106 | fn from(scale: Scale) -> Self { 107 | match scale { 108 | Scale::Chromatic => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 109 | Scale::Major => [0, 2, 2, 4, 4, 5, 7, 7, 9, 9, 11, 11], 110 | Scale::NaturalMinor => [0, 2, 2, 3, 5, 5, 7, 7, 8, 10, 10, 12], 111 | Scale::HarmonicMinor => [0, 2, 2, 3, 5, 5, 7, 7, 8, 8, 11, 11], 112 | Scale::MelodicMinor => [0, 2, 2, 3, 5, 5, 7, 7, 9, 9, 11, 11], 113 | Scale::PentatonicMajor => [0, 2, 2, 4, 4, 4, 7, 7, 9, 9, 9, 12], 114 | Scale::PentatonicMinor => [0, 0, 3, 3, 5, 5, 7, 7, 7, 10, 10, 10], 115 | Scale::HexatonicBlues => [0, 0, 3, 3, 5, 5, 6, 7, 7, 10, 10, 10], 116 | Scale::WholeTone => [0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10], 117 | Scale::MajorTriad => [0, 0, 0, 0, 4, 4, 4, 7, 7, 7, 7, 7 ], 118 | Scale::MinorTriad => [0, 0, 0, 3, 3, 3, 3, 7, 7, 7, 7, 7 ], 119 | Scale::DominantSeventh => [0, 0, 0, 0, 4, 4, 4, 7, 7, 7, 10, 10], 120 | Scale::DiminishedSeventh => [0, 0, 0, 3, 3, 3, 6, 6, 6, 9, 9, 9 ], 121 | Scale::Octave => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], 122 | Scale::OctaveAndFifth => [0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 7, 7 ], 123 | Scale::Dorian => [0, 2, 2, 3, 3, 5, 7, 7, 9, 9, 10, 10], 124 | Scale::Phrygian => [0, 1, 1, 3, 3, 5, 5, 7, 8, 8, 10, 10], 125 | Scale::Lydian => [0, 2, 2, 4, 4, 6, 6, 7, 9, 9, 11, 11], 126 | Scale::Mixolydian => [0, 2, 2, 4, 4, 5, 7, 7, 9, 9, 10, 10], 127 | Scale::Locrian => [0, 1, 1, 3, 3, 5, 6, 6, 8, 8, 10, 10], 128 | } 129 | } 130 | } 131 | 132 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 133 | pub enum Key { 134 | #[default] 135 | C, 136 | CSharp, 137 | D, 138 | DSharp, 139 | E, 140 | F, 141 | FSharp, 142 | G, 143 | GSharp, 144 | A, 145 | ASharp, 146 | B, 147 | } 148 | 149 | impl Into for Key { 150 | fn into(self) -> u8 { 151 | self as u8 152 | } 153 | } 154 | 155 | impl TryFrom for Key { 156 | type Error = (); 157 | 158 | fn try_from(value: u8) -> Result { 159 | match value { 160 | 0 => Ok(Key::C), 161 | 1 => Ok(Key::CSharp), 162 | 2 => Ok(Key::D), 163 | 3 => Ok(Key::DSharp), 164 | 4 => Ok(Key::E), 165 | 5 => Ok(Key::F), 166 | 6 => Ok(Key::FSharp), 167 | 7 => Ok(Key::G), 168 | 8 => Ok(Key::GSharp), 169 | 9 => Ok(Key::A), 170 | 10 => Ok(Key::ASharp), 171 | 11 => Ok(Key::B), 172 | _ => Err(()), 173 | } 174 | } 175 | } 176 | 177 | impl Display for Key { 178 | #[rustfmt::skip] 179 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 180 | write!( 181 | f, 182 | "{}", 183 | match *self { 184 | Key::C => "C", 185 | Key::CSharp => "C#", 186 | Key::D => "D", 187 | Key::DSharp => "D#", 188 | Key::E => "E", 189 | Key::F => "F", 190 | Key::FSharp => "F#", 191 | Key::G => "G", 192 | Key::GSharp => "G#", 193 | Key::A => "A", 194 | Key::ASharp => "A#", 195 | Key::B => "B", 196 | } 197 | ) 198 | } 199 | } 200 | 201 | pub fn quantize(note: Note, scale: Scale, key: Key) -> Note { 202 | let key_num: u8 = key.into(); 203 | let offset = 12 - key_num; 204 | let note_num: u8 = note.into(); 205 | let note_num_offset = note_num + offset; 206 | let octave = note_num_offset / 12; 207 | let degree = note_num_offset % 12; 208 | let interval_map: ScaleMap = scale.into(); 209 | let quantized_degree = interval_map[degree as usize] as u8; 210 | let quantized_note_num = ((quantized_degree + octave * 12) - offset) as u8; 211 | quantized_note_num 212 | .min(127) 213 | .try_into() 214 | .expect("note number should be valid note") 215 | } 216 | 217 | #[cfg(test)] 218 | pub mod tests { 219 | use super::*; 220 | 221 | #[test] 222 | fn quantize_should_quantize_c_major() { 223 | let expected_notes = [ 224 | Note::C3, 225 | Note::D3, 226 | Note::D3, 227 | Note::E3, 228 | Note::E3, 229 | Note::F3, 230 | Note::G3, 231 | Note::G3, 232 | Note::A3, 233 | Note::A3, 234 | Note::B3, 235 | Note::B3, 236 | ]; 237 | let quantized_notes = quantize_octave(input_notes(), Scale::Major, Key::C); 238 | assert_eq!(expected_notes, quantized_notes); 239 | } 240 | 241 | #[test] 242 | fn quantize_should_quantize_c_minor() { 243 | let expected_notes = [ 244 | Note::C3, 245 | Note::D3, 246 | Note::D3, 247 | Note::DSharp3, 248 | Note::F3, 249 | Note::F3, 250 | Note::G3, 251 | Note::G3, 252 | Note::GSharp3, 253 | Note::ASharp3, 254 | Note::ASharp3, 255 | Note::C4, 256 | ]; 257 | let quantized_notes = quantize_octave(input_notes(), Scale::NaturalMinor, Key::C); 258 | assert_eq!(expected_notes, quantized_notes); 259 | } 260 | 261 | #[test] 262 | fn quantize_should_quantize_g_sharp_minor() { 263 | let expected_notes = [ 264 | Note::CSharp3, 265 | Note::CSharp3, 266 | Note::DSharp3, 267 | Note::DSharp3, 268 | Note::E3, 269 | Note::FSharp3, 270 | Note::FSharp3, 271 | Note::GSharp3, 272 | Note::GSharp3, 273 | Note::ASharp3, 274 | Note::ASharp3, 275 | Note::B3, 276 | ]; 277 | let quantized_notes = quantize_octave(input_notes(), Scale::NaturalMinor, Key::GSharp); 278 | assert_eq!(expected_notes, quantized_notes); 279 | } 280 | 281 | fn input_notes() -> [Note; 12] { 282 | [ 283 | Note::C3, 284 | Note::CSharp3, 285 | Note::D3, 286 | Note::DSharp3, 287 | Note::E3, 288 | Note::F3, 289 | Note::FSharp3, 290 | Note::G3, 291 | Note::GSharp3, 292 | Note::A3, 293 | Note::ASharp3, 294 | Note::B3, 295 | ] 296 | } 297 | 298 | fn quantize_octave(input_notes: [Note; 12], scale: Scale, key: Key) -> [Note; 12] { 299 | input_notes 300 | .iter() 301 | .map(|¬e| quantize(note, scale, key)) 302 | .collect::>() 303 | .try_into() 304 | .unwrap() 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(test), no_std)] 2 | 3 | pub mod machine; 4 | pub mod machine_resources; 5 | pub mod midi; 6 | pub mod param; 7 | pub mod part; 8 | pub mod quantizer; 9 | pub mod sequence_generator; 10 | pub mod sequencer; 11 | 12 | extern crate alloc; 13 | 14 | use midi::{Note, NoteError}; 15 | use param::{Param, ParamError, ParamList}; 16 | use sequence_generator::SequenceGenerator; 17 | 18 | use alloc::boxed::Box; 19 | use core::{ 20 | cmp::Ordering, 21 | fmt::{Display, Formatter, Result as FmtResult}, 22 | slice::{Iter, IterMut}, 23 | }; 24 | use heapless::Vec; 25 | use midi_types::{Channel, Value14, Value7}; 26 | 27 | pub const TRACK_COUNT: usize = 8; 28 | 29 | const TRACK_MIN_LENGTH: u8 = 1; // because live performance effect of repeating a single step 30 | const TRACK_MAX_LENGTH: u8 = 32; 31 | const TRACK_DEFAULT_LENGTH: u8 = 8; // because techno 32 | 33 | const SEQUENCE_MAX_STEPS: usize = TRACK_MAX_LENGTH as usize; 34 | 35 | const TRACK_MIN_NUM: u8 = 1; 36 | 37 | const MIDI_MIN_CHANNEL: u8 = 1; 38 | const MIDI_MAX_CHANNEL: u8 = 16; 39 | 40 | pub fn map_to_range(x: i32, in_min: i32, in_max: i32, out_min: i32, out_max: i32) -> i32 { 41 | (x - in_min) * (out_max - out_min + 1) / (in_max - in_min + 1) + out_min 42 | } 43 | 44 | /// Represent a step in a musical sequence. 45 | #[derive(Clone, Debug)] 46 | pub struct Step { 47 | pub note: Note, 48 | pub velocity: Value7, 49 | pub pitch_bend: Value14, 50 | 51 | /// Note gate time as % of step time, e.g. 80 = 80%. Step time is defined by 52 | /// Track::time_division. 53 | pub length_step_cents: u8, 54 | 55 | /// Delay playing this step for % of track time division. Used for swing. Can be abused 56 | /// for general timing madness. Note that its not possible to play a step early. This 57 | /// is because Microgroove depends on an external clock. 58 | pub delay: u8, 59 | } 60 | 61 | impl Step { 62 | pub fn new(note: u8) -> Result { 63 | Ok(Step { 64 | note: note.try_into()?, 65 | velocity: 127.into(), 66 | pitch_bend: 0u16.into(), 67 | length_step_cents: 80, 68 | delay: 0, 69 | }) 70 | } 71 | } 72 | 73 | impl PartialEq for Step { 74 | fn eq(&self, other: &Self) -> bool { 75 | let self_note_num: u8 = self.note.into(); 76 | let other_note_num: u8 = other.note.into(); 77 | self_note_num == other_note_num 78 | } 79 | } 80 | 81 | impl Eq for Step {} 82 | 83 | impl PartialOrd for Step { 84 | fn partial_cmp(&self, other: &Self) -> Option { 85 | Some(self.cmp(other)) 86 | } 87 | } 88 | 89 | impl Ord for Step { 90 | fn cmp(&self, other: &Self) -> Ordering { 91 | let self_note_num: u8 = self.note.into(); 92 | let other_note_num: u8 = other.note.into(); 93 | self_note_num.cmp(&other_note_num) 94 | } 95 | } 96 | 97 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 98 | pub enum TimeDivision { 99 | ThirtySecond, 100 | #[default] 101 | Sixteenth, 102 | Eigth, 103 | Quarter, 104 | Whole, 105 | } 106 | 107 | impl TimeDivision { 108 | // TODO TryFrom 109 | pub fn from_id(id: &str) -> TimeDivision { 110 | match id { 111 | "1/32" => TimeDivision::ThirtySecond, 112 | "1/16" => TimeDivision::Sixteenth, 113 | "1/8" => TimeDivision::Eigth, 114 | "1/4" => TimeDivision::Quarter, 115 | "1" => TimeDivision::Whole, 116 | _ => TimeDivision::Sixteenth, 117 | } 118 | } 119 | 120 | pub fn division_length_24ppqn(time_div: TimeDivision) -> u8 { 121 | match time_div { 122 | TimeDivision::ThirtySecond => 3, 123 | TimeDivision::Sixteenth => 6, 124 | TimeDivision::Eigth => 12, 125 | TimeDivision::Quarter => 24, 126 | TimeDivision::Whole => 96, 127 | } 128 | } 129 | } 130 | 131 | impl Display for TimeDivision { 132 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 133 | write!( 134 | f, 135 | "{}", 136 | // TODO impl Into for TimeDivision 137 | match *self { 138 | TimeDivision::ThirtySecond => "1/32", 139 | TimeDivision::Sixteenth => "1/16", 140 | TimeDivision::Eigth => "1/8", 141 | TimeDivision::Quarter => "1/4", 142 | TimeDivision::Whole => "1", 143 | } 144 | ) 145 | } 146 | } 147 | 148 | impl TryFrom for TimeDivision { 149 | type Error = (); 150 | 151 | fn try_from(value: u8) -> Result { 152 | match value { 153 | 0 => Ok(TimeDivision::ThirtySecond), 154 | 1 => Ok(TimeDivision::Sixteenth), 155 | 2 => Ok(TimeDivision::Eigth), 156 | 3 => Ok(TimeDivision::Quarter), 157 | 4 => Ok(TimeDivision::Whole), 158 | _ => Err(()), 159 | } 160 | } 161 | } 162 | 163 | type StepVec = Vec, SEQUENCE_MAX_STEPS>; 164 | 165 | #[derive(Clone, Debug)] 166 | pub struct Sequence { 167 | pub steps: StepVec, 168 | } 169 | 170 | impl Sequence { 171 | pub fn new(steps: StepVec) -> Sequence { 172 | Sequence { steps } 173 | } 174 | 175 | pub fn len(&self) -> usize { 176 | self.steps.len() 177 | } 178 | pub fn iter(&self) -> Iter> { 179 | self.steps.iter() 180 | } 181 | pub fn iter_mut(&mut self) -> IterMut> { 182 | self.steps.iter_mut() 183 | } 184 | pub fn as_slice(&self) -> &[Option] { 185 | self.steps.as_slice() 186 | } 187 | 188 | pub fn set_steps(mut self, steps: Vec, SEQUENCE_MAX_STEPS>) -> Self { 189 | self.steps = steps; 190 | self 191 | } 192 | 193 | pub fn rotate_left(mut self, amount: usize) -> Sequence { 194 | self.steps.rotate_left(amount); 195 | self 196 | } 197 | 198 | pub fn rotate_right(mut self, amount: usize) -> Sequence { 199 | self.steps.rotate_right(amount); 200 | self 201 | } 202 | 203 | pub fn map_notes(mut self, mut f: impl FnMut(Note) -> Note) -> Self { 204 | for step in self.steps.iter_mut() { 205 | if let Some(step) = step { 206 | step.note = f(step.note); 207 | } 208 | } 209 | self 210 | } 211 | 212 | pub fn set_notes(mut self, notes: I) -> Self 213 | where 214 | I: IntoIterator, 215 | { 216 | let mut notes = notes.into_iter(); 217 | for step in self.steps.iter_mut() { 218 | let next_note = notes.next(); 219 | if let Some(step) = step { 220 | step.note = next_note.expect("should get next note"); 221 | } 222 | } 223 | self 224 | } 225 | 226 | pub fn mask_steps(mut self, step_mask: I) -> Self 227 | where 228 | I: IntoIterator, 229 | { 230 | let steps_unmasked_pairs = self.steps.iter_mut().zip(step_mask); 231 | for (step, unmasked) in steps_unmasked_pairs { 232 | if !unmasked { 233 | step.take(); 234 | } 235 | } 236 | self 237 | } 238 | } 239 | 240 | impl PartialEq for Sequence { 241 | fn eq(&self, other: &Self) -> bool { 242 | self.steps == other.steps 243 | } 244 | } 245 | 246 | impl FromIterator> for Sequence { 247 | fn from_iter(steps: T) -> Self 248 | where 249 | T: IntoIterator>, 250 | { 251 | Sequence::new(StepVec::from_iter(steps)) 252 | } 253 | } 254 | 255 | #[derive(Debug)] 256 | pub struct Track { 257 | pub time_division: TimeDivision, 258 | pub length: u8, 259 | pub midi_channel: Channel, 260 | pub sequence: Sequence, 261 | pub params: ParamList, 262 | } 263 | 264 | impl Default for Track { 265 | fn default() -> Track { 266 | let length = TRACK_DEFAULT_LENGTH; 267 | let sequence = SequenceGenerator::initial_sequence(length); 268 | let params = Track::param_defintions(); 269 | Track { 270 | time_division: Default::default(), 271 | length, 272 | midi_channel: 0.into(), 273 | sequence, 274 | params, 275 | } 276 | } 277 | } 278 | 279 | impl Track { 280 | fn param_defintions() -> ParamList { 281 | ParamList::from_slice(&[ 282 | Box::new(Param::new_rhythm_machine_id_param("RHYTHM")), 283 | Box::new(Param::new_number_param( 284 | "LEN", 285 | TRACK_MIN_LENGTH, 286 | TRACK_MAX_LENGTH, 287 | TRACK_DEFAULT_LENGTH, 288 | )), 289 | Box::new(Param::new_number_param( 290 | "TRACK", 291 | TRACK_MIN_NUM, 292 | TRACK_COUNT as u8, 293 | TRACK_MIN_NUM, 294 | )), 295 | Box::new(Param::new_melody_machine_id_param("MELODY")), 296 | Box::new(Param::new_time_division_param("SPD")), 297 | Box::new(Param::new_number_param( 298 | "CHAN", 299 | MIDI_MIN_CHANNEL, 300 | MIDI_MAX_CHANNEL, 301 | MIDI_MIN_CHANNEL, 302 | )), 303 | ]) 304 | .expect("should create track param list from slice") 305 | } 306 | 307 | pub fn params(&self) -> &ParamList { 308 | &self.params 309 | } 310 | 311 | pub fn params_mut(&mut self) -> &mut ParamList { 312 | &mut self.params 313 | } 314 | 315 | pub fn apply_params(&mut self) -> Result<(), ParamError> { 316 | // params 0 (rhythm machine), 2 (track number) and 3 (melody machine) are intentionally ignored 317 | // they are "virtual parameters" which don't actually relate to a `Track` at all. They're 318 | // handled by microgroove_app::input::map_encoder_values directly. 319 | self.length = self.params[1].value().try_into()?; 320 | self.time_division = self.params[4].value().try_into()?; 321 | let channel_num: u8 = self.params[5].value().try_into()?; 322 | self.midi_channel = channel_num.into(); 323 | Ok(()) 324 | } 325 | 326 | pub fn should_play_on_tick(&self, tick: u32) -> bool { 327 | tick % (TimeDivision::division_length_24ppqn(self.time_division) as u32) == 0 328 | } 329 | 330 | pub fn step_num(&self, tick: u32) -> u8 { 331 | (tick / (TimeDivision::division_length_24ppqn(self.time_division) as u32) 332 | % self.length as u32) as u8 333 | } 334 | 335 | pub fn step_at_tick(&self, tick: u32) -> Option<&Step> { 336 | if !self.should_play_on_tick(tick) { 337 | return None; 338 | } 339 | self.sequence 340 | .steps 341 | .get(self.step_num(tick) as usize) 342 | .expect("should get step at tick") 343 | .as_ref() 344 | } 345 | } 346 | 347 | #[cfg(test)] 348 | mod tests { 349 | use super::*; 350 | 351 | #[test] 352 | fn map_to_range_maps_to_range() { 353 | assert_eq!(10, map_to_range(100, 0, 100, 0, 10)); 354 | assert_eq!(5, map_to_range(50, 0, 100, 0, 10)); 355 | assert_eq!(0, map_to_range(0, 0, 100, 0, 10)); 356 | assert_eq!(1, map_to_range(10, 0, 100, 0, 10)); 357 | assert_eq!(66, map_to_range(63, 0, 127, 60, 72)); 358 | } 359 | 360 | #[test] 361 | fn steps_are_correctly_ordered() { 362 | let (s1, s2) = (Step::new(60).unwrap(), Step::new(61).unwrap()); 363 | assert!(s1 < s2); 364 | } 365 | 366 | #[test] 367 | fn track_default_generates_sequence_correctly() { 368 | let t = Track::default(); 369 | let expected: Sequence = Sequence::new((0..8).map(|_i| Step::new(60).ok()).collect()); 370 | assert_eq!(expected, t.sequence); 371 | } 372 | 373 | #[test] 374 | fn sequence_set_notes_should_set_note_values_from_intoiterator() { 375 | let seq = SequenceGenerator::initial_sequence(8); 376 | let notes: [Note; 8] = [60, 61, 62, 63, 64, 65, 66, 67].map(|i| i.try_into().unwrap()); 377 | let seq = seq.set_notes(notes); 378 | let result: Vec = seq.iter().map(|step| step.as_ref().unwrap().note).collect(); 379 | assert_eq!(notes, result); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/midi.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{Display, Formatter, Result as FmtResult}; 2 | use midi_types; 3 | 4 | #[rustfmt::skip] 5 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 6 | pub enum Note { 7 | CMinus2, CSharpMinus2, DMinus2, DSharpMinus2, EMinus2, FMinus2, FSharpMinus2, GMinus2, GSharpMinus2, AMinus2, ASharpMinus2, BMinus2, 8 | CMinus1, CSharpMinus1, DMinus1, DSharpMinus1, EMinus1, FMinus1, FSharpMinus1, GMinus1, GSharpMinus1, AMinus1, ASharpMinus1, BMinus1, 9 | C0, CSharp0, D0, DSharp0, E0, F0, FSharp0, G0, GSharp0, A0, ASharp0, B0, 10 | C1, CSharp1, D1, DSharp1, E1, F1, FSharp1, G1, GSharp1, A1, ASharp1, B1, 11 | C2, CSharp2, D2, DSharp2, E2, F2, FSharp2, G2, GSharp2, A2, ASharp2, B2, 12 | #[default] 13 | C3, CSharp3, D3, DSharp3, E3, F3, FSharp3, G3, GSharp3, A3, ASharp3, B3, 14 | C4, CSharp4, D4, DSharp4, E4, F4, FSharp4, G4, GSharp4, A4, ASharp4, B4, 15 | C5, CSharp5, D5, DSharp5, E5, F5, FSharp5, G5, GSharp5, A5, ASharp5, B5, 16 | C6, CSharp6, D6, DSharp6, E6, F6, FSharp6, G6, GSharp6, A6, ASharp6, B6, 17 | C7, CSharp7, D7, DSharp7, E7, F7, FSharp7, G7, GSharp7, A7, ASharp7, B7, 18 | C8, CSharp8, D8, DSharp8, E8, F8, FSharp8, G8, 19 | } 20 | 21 | impl Into for Note { 22 | fn into(self) -> u8 { 23 | self as u8 24 | } 25 | } 26 | 27 | #[derive(Debug)] 28 | pub enum NoteError { 29 | InvalidNoteNumber, 30 | } 31 | 32 | impl TryFrom for Note { 33 | type Error = NoteError; 34 | 35 | fn try_from(value: u8) -> Result { 36 | match value { 37 | 0 => Ok(Note::CMinus2), 38 | 1 => Ok(Note::CSharpMinus2), 39 | 2 => Ok(Note::DMinus2), 40 | 3 => Ok(Note::DSharpMinus2), 41 | 4 => Ok(Note::EMinus2), 42 | 5 => Ok(Note::FMinus2), 43 | 6 => Ok(Note::FSharpMinus2), 44 | 7 => Ok(Note::GMinus2), 45 | 8 => Ok(Note::GSharpMinus2), 46 | 9 => Ok(Note::AMinus2), 47 | 10 => Ok(Note::ASharpMinus2), 48 | 11 => Ok(Note::BMinus2), 49 | 12 => Ok(Note::CMinus1), 50 | 13 => Ok(Note::CSharpMinus1), 51 | 14 => Ok(Note::DMinus1), 52 | 15 => Ok(Note::DSharpMinus1), 53 | 16 => Ok(Note::EMinus1), 54 | 17 => Ok(Note::FMinus1), 55 | 18 => Ok(Note::FSharpMinus1), 56 | 19 => Ok(Note::GMinus1), 57 | 20 => Ok(Note::GSharpMinus1), 58 | 21 => Ok(Note::AMinus1), 59 | 22 => Ok(Note::ASharpMinus1), 60 | 23 => Ok(Note::BMinus1), 61 | 24 => Ok(Note::C0), 62 | 25 => Ok(Note::CSharp0), 63 | 26 => Ok(Note::D0), 64 | 27 => Ok(Note::DSharp0), 65 | 28 => Ok(Note::E0), 66 | 29 => Ok(Note::F0), 67 | 30 => Ok(Note::FSharp0), 68 | 31 => Ok(Note::G0), 69 | 32 => Ok(Note::GSharp0), 70 | 33 => Ok(Note::A0), 71 | 34 => Ok(Note::ASharp0), 72 | 35 => Ok(Note::B0), 73 | 36 => Ok(Note::C1), 74 | 37 => Ok(Note::CSharp1), 75 | 38 => Ok(Note::D1), 76 | 39 => Ok(Note::DSharp1), 77 | 40 => Ok(Note::E1), 78 | 41 => Ok(Note::F1), 79 | 42 => Ok(Note::FSharp1), 80 | 43 => Ok(Note::G1), 81 | 44 => Ok(Note::GSharp1), 82 | 45 => Ok(Note::A1), 83 | 46 => Ok(Note::ASharp1), 84 | 47 => Ok(Note::B1), 85 | 48 => Ok(Note::C2), 86 | 49 => Ok(Note::CSharp2), 87 | 50 => Ok(Note::D2), 88 | 51 => Ok(Note::DSharp2), 89 | 52 => Ok(Note::E2), 90 | 53 => Ok(Note::F2), 91 | 54 => Ok(Note::FSharp2), 92 | 55 => Ok(Note::G2), 93 | 56 => Ok(Note::GSharp2), 94 | 57 => Ok(Note::A2), 95 | 58 => Ok(Note::ASharp2), 96 | 59 => Ok(Note::B2), 97 | 60 => Ok(Note::C3), 98 | 61 => Ok(Note::CSharp3), 99 | 62 => Ok(Note::D3), 100 | 63 => Ok(Note::DSharp3), 101 | 64 => Ok(Note::E3), 102 | 65 => Ok(Note::F3), 103 | 66 => Ok(Note::FSharp3), 104 | 67 => Ok(Note::G3), 105 | 68 => Ok(Note::GSharp3), 106 | 69 => Ok(Note::A3), 107 | 70 => Ok(Note::ASharp3), 108 | 71 => Ok(Note::B3), 109 | 72 => Ok(Note::C4), 110 | 73 => Ok(Note::CSharp4), 111 | 74 => Ok(Note::D4), 112 | 75 => Ok(Note::DSharp4), 113 | 76 => Ok(Note::E4), 114 | 77 => Ok(Note::F4), 115 | 78 => Ok(Note::FSharp4), 116 | 79 => Ok(Note::G4), 117 | 80 => Ok(Note::GSharp4), 118 | 81 => Ok(Note::A4), 119 | 82 => Ok(Note::ASharp4), 120 | 83 => Ok(Note::B4), 121 | 84 => Ok(Note::C5), 122 | 85 => Ok(Note::CSharp5), 123 | 86 => Ok(Note::D5), 124 | 87 => Ok(Note::DSharp5), 125 | 88 => Ok(Note::E5), 126 | 89 => Ok(Note::F5), 127 | 90 => Ok(Note::FSharp5), 128 | 91 => Ok(Note::G5), 129 | 92 => Ok(Note::GSharp5), 130 | 93 => Ok(Note::A5), 131 | 94 => Ok(Note::ASharp5), 132 | 95 => Ok(Note::B5), 133 | 96 => Ok(Note::C6), 134 | 97 => Ok(Note::CSharp6), 135 | 98 => Ok(Note::D6), 136 | 99 => Ok(Note::DSharp6), 137 | 100 => Ok(Note::E6), 138 | 101 => Ok(Note::F6), 139 | 102 => Ok(Note::FSharp6), 140 | 103 => Ok(Note::G6), 141 | 104 => Ok(Note::GSharp6), 142 | 105 => Ok(Note::A6), 143 | 106 => Ok(Note::ASharp6), 144 | 107 => Ok(Note::B6), 145 | 108 => Ok(Note::C7), 146 | 109 => Ok(Note::CSharp7), 147 | 110 => Ok(Note::D7), 148 | 111 => Ok(Note::DSharp7), 149 | 112 => Ok(Note::E7), 150 | 113 => Ok(Note::F7), 151 | 114 => Ok(Note::FSharp7), 152 | 115 => Ok(Note::G7), 153 | 116 => Ok(Note::GSharp7), 154 | 117 => Ok(Note::A7), 155 | 118 => Ok(Note::ASharp7), 156 | 119 => Ok(Note::B7), 157 | 120 => Ok(Note::C8), 158 | 121 => Ok(Note::CSharp8), 159 | 122 => Ok(Note::D8), 160 | 123 => Ok(Note::DSharp8), 161 | 124 => Ok(Note::E8), 162 | 125 => Ok(Note::F8), 163 | 126 => Ok(Note::FSharp8), 164 | 127 => Ok(Note::G8), 165 | _ => Err(NoteError::InvalidNoteNumber), 166 | } 167 | } 168 | } 169 | 170 | impl Into for Note { 171 | fn into(self) -> midi_types::Note { 172 | let note_num: u8 = self.into(); 173 | note_num.into() 174 | } 175 | } 176 | 177 | impl From for Note { 178 | fn from(note: midi_types::Note) -> Self { 179 | let note_num: u8 = note.into(); 180 | note_num 181 | .try_into() 182 | .expect("should transform note num into note") 183 | } 184 | } 185 | 186 | impl Display for Note { 187 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 188 | write!( 189 | f, 190 | "{}", 191 | match *self { 192 | Note::CMinus2 => "C-2", 193 | Note::CSharpMinus2 => "C#-2", 194 | Note::DMinus2 => "D-2", 195 | Note::DSharpMinus2 => "D#-2", 196 | Note::EMinus2 => "E-2", 197 | Note::FMinus2 => "F-2", 198 | Note::FSharpMinus2 => "F#-2", 199 | Note::GMinus2 => "G-2", 200 | Note::GSharpMinus2 => "G#-2", 201 | Note::AMinus2 => "A-2", 202 | Note::ASharpMinus2 => "A#-2", 203 | Note::BMinus2 => "B-2", 204 | Note::CMinus1 => "C-1", 205 | Note::CSharpMinus1 => "C#-1", 206 | Note::DMinus1 => "D-1", 207 | Note::DSharpMinus1 => "D#-1", 208 | Note::EMinus1 => "E-1", 209 | Note::FMinus1 => "F-1", 210 | Note::FSharpMinus1 => "F#-1", 211 | Note::GMinus1 => "G-1", 212 | Note::GSharpMinus1 => "G#-1", 213 | Note::AMinus1 => "A-1", 214 | Note::ASharpMinus1 => "A#-1", 215 | Note::BMinus1 => "B-1", 216 | Note::C0 => "C0", 217 | Note::CSharp0 => "C#0", 218 | Note::D0 => "D0", 219 | Note::DSharp0 => "D#0", 220 | Note::E0 => "E0", 221 | Note::F0 => "F0", 222 | Note::FSharp0 => "F#0", 223 | Note::G0 => "G0", 224 | Note::GSharp0 => "G#0", 225 | Note::A0 => "A0", 226 | Note::ASharp0 => "A#0", 227 | Note::B0 => "B0", 228 | Note::C1 => "C1", 229 | Note::CSharp1 => "C#1", 230 | Note::D1 => "D1", 231 | Note::DSharp1 => "D#1", 232 | Note::E1 => "E1", 233 | Note::F1 => "F1", 234 | Note::FSharp1 => "F#1", 235 | Note::G1 => "G1", 236 | Note::GSharp1 => "G#1", 237 | Note::A1 => "A1", 238 | Note::ASharp1 => "A#1", 239 | Note::B1 => "B1", 240 | Note::C2 => "C2", 241 | Note::CSharp2 => "C#2", 242 | Note::D2 => "D2", 243 | Note::DSharp2 => "D#2", 244 | Note::E2 => "E2", 245 | Note::F2 => "F2", 246 | Note::FSharp2 => "F#2", 247 | Note::G2 => "G2", 248 | Note::GSharp2 => "G#2", 249 | Note::A2 => "A2", 250 | Note::ASharp2 => "A#2", 251 | Note::B2 => "B2", 252 | Note::C3 => "C3", 253 | Note::CSharp3 => "C#3", 254 | Note::D3 => "D3", 255 | Note::DSharp3 => "D#3", 256 | Note::E3 => "E3", 257 | Note::F3 => "F3", 258 | Note::FSharp3 => "F#3", 259 | Note::G3 => "G3", 260 | Note::GSharp3 => "G#3", 261 | Note::A3 => "A3", 262 | Note::ASharp3 => "A#3", 263 | Note::B3 => "B3", 264 | Note::C4 => "C4", 265 | Note::CSharp4 => "C#4", 266 | Note::D4 => "D4", 267 | Note::DSharp4 => "D#4", 268 | Note::E4 => "E4", 269 | Note::F4 => "F4", 270 | Note::FSharp4 => "F#4", 271 | Note::G4 => "G4", 272 | Note::GSharp4 => "G#4", 273 | Note::A4 => "A4", 274 | Note::ASharp4 => "A#4", 275 | Note::B4 => "B4", 276 | Note::C5 => "C5", 277 | Note::CSharp5 => "C#5", 278 | Note::D5 => "D5", 279 | Note::DSharp5 => "D#5", 280 | Note::E5 => "E5", 281 | Note::F5 => "F5", 282 | Note::FSharp5 => "F#5", 283 | Note::G5 => "G5", 284 | Note::GSharp5 => "G#5", 285 | Note::A5 => "A5", 286 | Note::ASharp5 => "A#5", 287 | Note::B5 => "B5", 288 | Note::C6 => "C6", 289 | Note::CSharp6 => "C#6", 290 | Note::D6 => "D6", 291 | Note::DSharp6 => "D#6", 292 | Note::E6 => "E6", 293 | Note::F6 => "F6", 294 | Note::FSharp6 => "F#6", 295 | Note::G6 => "G6", 296 | Note::GSharp6 => "G#6", 297 | Note::A6 => "A6", 298 | Note::ASharp6 => "A#6", 299 | Note::B6 => "B6", 300 | Note::C7 => "C7", 301 | Note::CSharp7 => "C#7", 302 | Note::D7 => "D7", 303 | Note::DSharp7 => "D#7", 304 | Note::E7 => "E7", 305 | Note::F7 => "F7", 306 | Note::FSharp7 => "F#7", 307 | Note::G7 => "G7", 308 | Note::GSharp7 => "G#7", 309 | Note::A7 => "A7", 310 | Note::ASharp7 => "A#7", 311 | Note::B7 => "B7", 312 | Note::C8 => "C8", 313 | Note::CSharp8 => "C#8", 314 | Note::D8 => "D8", 315 | Note::DSharp8 => "D#8", 316 | Note::E8 => "E8", 317 | Note::F8 => "F8", 318 | Note::FSharp8 => "F#8", 319 | Note::G8 => "G8", 320 | } 321 | ) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microgroove 2 | 3 | 8-track open-source hardware MIDI sequence generator. 4 | 5 | - Machines offer different ways to generate sequences: random melodies, Euclidean rhythms, rhythms 6 | from Mutable Instruments' Grids. 7 | - Tweak parameters to explore new ideas, or to perform live. 8 | - Quantize melodies to scales. 9 | - Add swing and groove. 10 | - Create interplay between sequences, like call-and-response, or ABAC structures. 11 | 12 | ## Why? 13 | 14 | The modular world has some amazing tools to create interesting sequences: Turing Machine, Mutable 15 | Instruments Grids, Mimetic Digitalis, Knight's Gallop, and so many more. The software ecosystem does 16 | too, plugins like Stepic, modules in VCV Rack, and so many Max for Live experiments. In the desktop 17 | MIDI hardware world, we're short on options (kudos to the Torso T1 for changing that), and that's 18 | where I like to jam. I built Microgroove to bring some interesting sequencing ideas into a desktop 19 | MIDI setup. 20 | 21 | Microgroove is also a platform for experimentation. Trying new sequencing ideas is fast: create a 22 | software change, flash it to the device next to you and start playing. Open sourcing the entire 23 | device means anyone can extend or adapt it. 24 | 25 | ## Caution 26 | 27 | Microgroove is a DIY electronic device that connects to other hardware. Please be careful if you're 28 | working on the electronics yourself, or connecting to expensive equipment. I can't guarantee that 29 | Microgroove won't break your gear. 30 | 31 | ## Quickstart 32 | 33 | Connect at least one instrument to MIDI out, for example, a synth or a drum machine. 34 | 35 | Connect a device with a sequencer to MIDI in. Microgroove doesn't have its own clock or transport 36 | controls. It expects another device to be the master clock. 37 | 38 | Microgroove's Track 1 is set to MIDI channel 1 by default. Set one of your instruments to listen to 39 | this channel, or change it from the track page. 40 | 41 | Press play on your master sequencer. Microgroove will start playing a 42 | randomly-generated 8-step sequence on MIDI channel 1. 43 | 44 | Microgroove's MIDI out provides soft MIDI thru. Any MIDI notes coming from your 45 | master sequencer will also be sent to your instruments. 46 | 47 | ### Tweak 48 | 49 | Microgroove's philosophy is that generating a sequence and tweaking it is a 50 | great way to create ideas. Use `[ENCODER1]` to `[ENCODER6]` to change 51 | parameters. Use the `[TRACK]`, `[RHYTHM]` and `[MELODY]` buttons to change 52 | between parameter pages. Press `[TRACK]` to cycle between the Track and 53 | Sequence pages, `[RHYTHM]` to cycle between Rhythm and Groove pages, `[MELODY]` 54 | for Melody and Harmony pages. 55 | 56 | Each page lets you control an aspect of the current track, or the overall sequence. 57 | 58 | - Track: Change rhythm and melody machines, length, time division and MIDI 59 | channel for the current track. Use `[ENCODER3]` to switch between tracks. 60 | - Sequence: Set swing for all tracks (MPC format). 61 | - Rhythm: Parameters for the selected rhythm machine. 62 | - Groove: Set a part for this track, masking areas of the pattern. 63 | - Melody: Parameters for the selected melody machine. 64 | - Harmony: Quantize the melody to scale and key. 65 | 66 | Choose rhythm and melody machines for each track, both are random by default. 67 | 68 | To switch tracks, press `[TRACK]` to go to the Track page and choose a track 69 | with `[ENCODER3]`. Tracks 2-8 are disabled by default. Choose a MIDI channel to 70 | enable them. 71 | 72 | Parts allow you to set up multiple tracks to play together in structures like 73 | call-and-response or ABAC. Try setting Track 1 to `CALL` and Track 2 to 74 | `RESP`, with all other parameters the same. 75 | 76 | ## Hardware 77 | 78 | Microgroove is a simple device based around the Raspberry Pi Pico microcontroller. Building your own 79 | should be straightforward (I knew nothing about electronics before I built it). The parts are fairly 80 | standard and easy to get hold of from a few different electronics vendors, for example Pi Hut in the 81 | UK or Adafruit in the US, and many more. You can build the device on a breadboard, or solder to 82 | something like a [protoboard](https://www.adafruit.com/product/4785) and mount inside a laser-cut 83 | case. 84 | 85 | ![Early Microgroove build on a breadboard](https://github.com/afternoon/microgroove/blob/main/hardware/microgroove-circuit-breadboard-photo.jpg) 86 | 87 | ### Components 88 | 89 | | Component | Quantity | 90 | |----------------------------------------------------------------------------------------------------------------- | -------- | 91 | | [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/) | 1 | 92 | | [Adafruit 128x64 1.3" Monochrome OLED](https://www.adafruit.com/product/938) | 1 | 93 | | [Cherry MX-compatible key](https://thepihut.com/products/kailh-mechanical-key-switches-clicky-white-10-pack) | 4 | 94 | | [Cherry MX keycap](https://thepihut.com/products/relegendable-plastic-keycaps-for-mx-compatible-switches-5-pack) | 4 | 95 | | [PEC11R rotary encoders](https://www.digikey.co.uk/en/products/detail/bourns-inc/PEC11R-4215F-S0024/4499665) | 6 | 96 | | [TRS minijacks](https://thepihut.com/products/breadboard-friendly-3-5mm-stereo-headphone-jack) | 2 | 97 | | [H11L1 optoisolator](https://www.digikey.co.uk/en/products/detail/onsemi/H11L1TVM/401266) | 1 | 98 | | [1N914 diode](https://www.digikey.co.uk/en/products/detail/onsemi/1N914/978749) | 1 | 99 | | 470Ω resistor | 1 | 100 | | 220Ω resistor | 1 | 101 | | 33Ω resistor | 1 | 102 | | 10Ω resistor | 1 | 103 | | Breadboard/protoboard | 2 | 104 | 105 | ### Build 106 | 107 | This diagram shows the breadboard layout for Microgroove. 108 | 109 | ![Microgroove components shown on a breadboard](https://github.com/afternoon/microgroove/blob/main/hardware/microgroove-circuit-breadboard.png) 110 | 111 | (If you want to build on a breadboard, you can get the Pico H, which has headers soldered on and 112 | slots right on.) 113 | 114 | See the [Fritzing 115 | file](https://github.com/afternoon/microgroove/blob/main/hardware/microgroove-circuit.fzz) to view 116 | the components and their connections. There is also a schematic view, but it is currently a mess. 117 | 118 | The OLED display, keys and encoders connect directly to pins on the Pico. 119 | 120 | The MIDI section is also fairly simple. They are based on diyelectromusic's 121 | [MIDI in](https://diyelectromusic.wordpress.com/2021/02/15/midi-in-for-3-3v-microcontrollers/) and 122 | [MIDI out](https://diyelectromusic.wordpress.com/2021/01/23/midi-micropython-and-the-raspberry-pi-pico/) 123 | circuits (thanks Kevin!). You can use TRS minijacks like me, or classic MIDI DIN jacks. Either way, 124 | check the pinouts for the components you purchase carefully. Wrong wiring here might damage your 125 | gear. 126 | 127 | ### Case 128 | 129 | The case is laser-cut. I used 3mm ply. You can find 130 | the [design as an SVG file 131 | here](https://github.com/afternoon/microgroove/blob/main/hardware/microgroove-case-lasercut.svg). 132 | 133 | The SVG file was creating in Tinkercad. You can [access the model 134 | here](https://www.tinkercad.com/things/e7vA3MJyz0E-microgroove-box). If you would like to make 135 | your own modifications, you can clone the model in Tinkercad and customise it. 136 | 137 | When cut, the case pieces slot together, and the components screw or glue to the case with 138 | standoffs and M2 or M3 screws. 139 | 140 | If you don't have access to a laser cutter, you should be able to find cutting services online. 141 | 142 | ## Firmware 143 | 144 | The Microgroove firmware is written in Rust using the [RTIC](https://rtic.rs) 145 | real-time framework. RTIC is truly wonderful. It lets us write clean Rust code 146 | which multitasks with timing accurate to a few microseconds. 147 | 148 | The app loosely follows the MVC architecture. The `microgroove_sequencer` library crate implements 149 | the model. In the `microgroove_app` binary crate, the `display` module implements the view, while 150 | the`app` module implements the controller. 151 | 152 | Conceptually Microgroove is inspired by Elektron's machines, which make it fast to create and 153 | manipulate musical ideas (the UI borrows Elektron's pages + encoders paradigm), and from modular, 154 | which allows different task-specific components to be composed into a system. Different `Machine`s 155 | can be combined to change how the sequence is generated. 156 | 157 | The Machine concept is somewhat inspired by modular, where different modules can generate the 158 | rhythm or the melody, or process it. 159 | 160 | The code is split across two crates to allow the model and logic code to be platform-independent, 161 | and therefore testable. 162 | 163 | ### Data model 164 | 165 | A set of `struct`s in the `microgroove_sequencer` crate implement the data model. 166 | 167 | - The top-level object is an instance of `Sequencer`. 168 | - A `Sequencer` has many `Track`s. `Track` has a length, time division, MIDI channel, etc, and a 169 | `Sequence`. 170 | - `Sequence` is a wrapper around a `Vec` of `Step`s, providing a grammar of methods for 171 | manipulating sequences in useful ways, e.g. setting the note numbers for steps from a `Vec` of 172 | `Note`s. 173 | - `Step` has a `Note`, velocity, length and delay. 174 | - `Note` is an `enum` of all MIDI note numbers. 175 | 176 | Sequence generation is implemented by the `SequenceGenerator` struct. This is exposed to the RTIC 177 | application separately from the data model, to allow the app to control how and when concrete 178 | sequences are generated. A `SequenceGenerator` object has two `Machine`s. One to generate the rhythm 179 | and a second to generate a melody. `Machine`s have an `apply` method which takes a `Sequence` and 180 | transforms it. The process of generating a sequence is implemented as a pipeline in 181 | `SequenceGenerator::generate`. A default `Sequence` is created and transformed by several 182 | `Machine`s in order. The `Sequence` is then passed to a quantizer and to logic which applies parts - 183 | removing steps from parts of the sequence. 184 | 185 | ### Building the firmware 186 | 187 | If you haven't already, [install Rust](https://www.rust-lang.org/tools/install). If you aren't yet 188 | familiar with Rust, I recommend reading the [Rust Book](https://doc.rust-lang.org/book/) to learn 189 | the language. 190 | 191 | Microgroove requires Rust nightly (it uses the 192 | [linked_list_allocator](https://crates.io/crates/linked_list_allocator) crate, which requires the 193 | `AllocRef` trait only in the nightly API). You'll also need to install the `thumbv6m-none-eabi` 194 | target, which allows compilation for the Pi Pico's ARM Cortex-M0+ CPU, and 195 | [cargo-embed](https://crates.io/crates/cargo-embed) which extends `cargo` with an `embed` command to 196 | flash binaries to embedded devices. 197 | 198 | ``` 199 | $ rustup toolchain install nightly 200 | $ rustup target add thumbv6m-none-eabi 201 | $ cargo install cargo-embed 202 | ``` 203 | 204 | You can check your setup by running the `microgroove_sequencer` crate's unit tests. 205 | 206 | ``` 207 | $ cd firmware/microgroove_sequencer 208 | $ cargo test 209 | ``` 210 | 211 | Connect the Pi Pico to your computer with USB and use `cargo-embed` to flash the app to your device. 212 | 213 | ``` 214 | $ cd ../microgroove_app 215 | $ cargo embed 216 | ``` 217 | 218 | Your Microgroove should now be ready to play! 219 | 220 | ### Debugging 221 | 222 | Serial output will be displayed on the console. See the 223 | [cargo-embed](https://crates.io/crates/cargo-embed) docs for information on how to run GDB. 224 | 225 | You can also use `probe-run` to flash binaries, but this requires a debug probe (which can be a 2nd 226 | Pi Pico). 227 | 228 | ## Get in touch 229 | 230 | Microgroove is still young and evolving fast. I'm be really interested to help out if you would like to build a device or contribute. I'd love to get your feedback on the process and also on how the device is to play, whether it’s fun, if you find the sequences in generates useful, what would make it more useful, and so on. 231 | 232 | If you have feedback, ideas or questions, 233 | head over to the [discussions section](https://github.com/afternoon/microgroove/discussions) 234 | or contact me at [ben@ben2.com](mailto:ben@ben2.com). 235 | -------------------------------------------------------------------------------- /firmware/microgroove_app/src/display.rs: -------------------------------------------------------------------------------- 1 | /// Rendering UI graphics to the display. 2 | use crate::{input::InputMode, peripherals::Display}; 3 | use microgroove_sequencer::{map_to_range, part::Part, Sequence}; 4 | 5 | use core::{fmt::Write, iter::zip, str::FromStr}; 6 | use display_interface::DisplayError; 7 | use embedded_graphics::{ 8 | mono_font::{ 9 | ascii::{FONT_4X6, FONT_6X10}, 10 | MonoTextStyle, 11 | }, 12 | pixelcolor::BinaryColor, 13 | prelude::*, 14 | primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle}, 15 | text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder}, 16 | }; 17 | use heapless::{String, Vec}; 18 | 19 | type DisplayResult = Result<(), DisplayError>; 20 | 21 | const DISPLAY_WIDTH: i32 = 128; 22 | const DISPLAY_HEIGHT: i32 = 64; 23 | const DISPLAY_CENTER: i32 = DISPLAY_WIDTH / 2; 24 | 25 | const CHAR_HEIGHT: u32 = 7; 26 | 27 | const WARNING_Y_POS: i32 = 21; 28 | const WARNING_PADDING: i32 = 4; 29 | const WARNING_BORDER: u32 = 1; 30 | 31 | const HEADER_HEIGHT: u32 = 6; 32 | const HEADER_PLAYING_ICON_X_POS: i32 = 24; 33 | 34 | const SEQUENCE_X_POS: i32 = 0; 35 | const SEQUENCE_Y_POS: i32 = HEADER_HEIGHT as i32 + 1; 36 | const SEQUENCE_WIDTH: u32 = DISPLAY_WIDTH as u32; 37 | const SEQUENCE_HEIGHT: u32 = 45; 38 | const SEQUENCE_UNDERLINE_Y_POS: i32 = 45; 39 | 40 | const PARAM_Y_POS: u32 = 51; 41 | 42 | /// Show snazzy splash screen. 43 | pub fn render_splash_screen_view(display: &mut Display) -> DisplayResult { 44 | display.clear(); 45 | Text::with_text_style( 46 | "MICROGROOVE", 47 | Point::new(DISPLAY_CENTER, WARNING_Y_POS), 48 | big_character_style(), 49 | centered(), 50 | ) 51 | .draw(display)?; 52 | Text::with_baseline( 53 | "I wanna go bang", 54 | Point::new(37, 42), 55 | default_character_style(), 56 | Baseline::Top, 57 | ) 58 | .draw(display)?; 59 | display.flush()?; 60 | Ok(()) 61 | } 62 | 63 | type ParamData = Vec<(String<6>, String<6>), 6>; 64 | 65 | #[derive(Debug)] 66 | pub struct PerformView { 67 | pub input_mode: InputMode, 68 | pub playing: bool, 69 | pub track_num: u8, 70 | pub sequence: Option, 71 | pub part: Part, 72 | pub active_step_num: Option, 73 | pub machine_name: Option>, 74 | pub param_data: Option, 75 | } 76 | 77 | impl PerformView { 78 | pub fn render(&self, display: &mut Display) -> DisplayResult { 79 | display.clear(); 80 | self.draw_header(display)?; 81 | if self.sequence.is_some() { 82 | self.draw_sequence(display)?; 83 | self.draw_params(display)?; 84 | } else { 85 | draw_disabled_track_warning(display)?; 86 | } 87 | display.flush()?; 88 | Ok(()) 89 | } 90 | 91 | fn draw_header(&self, display: &mut Display) -> DisplayResult { 92 | let mut track_num_str: String<5> = 93 | String::from_str("TRK").expect("track_num_str from_str should succeed"); 94 | write!(track_num_str, "{:02}", self.track_num) 95 | .expect("write! track_num_str should succeed"); 96 | Text::with_baseline( 97 | track_num_str.as_str(), 98 | Point::zero(), 99 | default_character_style(), 100 | Baseline::Top, 101 | ) 102 | .draw(display)?; 103 | if self.playing { 104 | Text::with_baseline( 105 | ">", 106 | Point::new(HEADER_PLAYING_ICON_X_POS, 0), 107 | default_character_style(), 108 | Baseline::Top, 109 | ) 110 | .draw(display)?; 111 | } 112 | let title = match self.input_mode { 113 | InputMode::Track => "TRACK", 114 | InputMode::Sequence => "SEQUENCE", 115 | InputMode::Rhythm => "RHYTHM", 116 | InputMode::Groove => "GROOVE", 117 | InputMode::Melody => "MELODY", 118 | InputMode::Harmony => "HARMONY", 119 | }; 120 | Text::with_text_style( 121 | title, 122 | Point::new(DISPLAY_CENTER, 0), 123 | default_character_style(), 124 | centered(), 125 | ) 126 | .draw(display)?; 127 | match self.input_mode { 128 | InputMode::Rhythm | InputMode::Melody => { 129 | Text::with_text_style( 130 | self.machine_name.as_ref().map(|s| s.as_str()).unwrap_or(""), 131 | Point::new(DISPLAY_WIDTH, 0), 132 | default_character_style(), 133 | right_align(), 134 | ) 135 | .draw(display)?; 136 | } 137 | _ => { /* don't do nuffink */ } 138 | } 139 | Ok(()) 140 | } 141 | 142 | fn draw_sequence(&self, display: &mut Display) -> DisplayResult { 143 | let sequence = self 144 | .sequence 145 | .as_ref() 146 | .expect("get sequence as_ref should succeed"); 147 | let length = sequence.len(); 148 | let part_mask = Part::new_mask(self.part, length); 149 | let step_width: u32 = if length <= 16 { 6 } else { 3 }; 150 | let step_height: u32 = step_width; 151 | let display_sequence_margin_left = 152 | (DISPLAY_WIDTH - ((length as i32) * ((step_width as i32) + 1))) / 2; 153 | let (note_min, note_max) = note_min_max_as_u8s(&sequence); 154 | let note_y_pos_min: u32 = 35; 155 | let note_y_pos_max: u32 = 9 + step_height as u32; 156 | let step_size = Size::new(step_width, step_height); 157 | let mut step_num: u8 = 0; 158 | let stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); 159 | 160 | for (step, &masked) in sequence.steps.iter().zip(part_mask.iter()) { 161 | let x = display_sequence_margin_left + (step_num as i32 * (step_width as i32 + 1)); 162 | let x2 = x + step_width as i32; 163 | 164 | // draw step 165 | if let Some(step) = step { 166 | // draw step indicator 167 | let note_num: u8 = step.note.into(); 168 | let y = map_to_range( 169 | note_num as i32, 170 | note_min as i32, 171 | note_max as i32, 172 | note_y_pos_min as i32, 173 | note_y_pos_max as i32, 174 | ); 175 | let step_style = 176 | if step_num == self.active_step_num.expect("should get active step num") { 177 | outline_style() 178 | } else { 179 | filled_style() 180 | }; 181 | Rectangle::new(Point::new(x as i32, y as i32), step_size) 182 | .into_styled(step_style) 183 | .draw(display)?; 184 | 185 | // draw velocity tick 186 | let velocity: u8 = step.velocity.into(); 187 | let velocity_tick_height = velocity >> 5; 188 | Line::new( 189 | Point::new(x, SEQUENCE_UNDERLINE_Y_POS), 190 | Point::new(x, SEQUENCE_UNDERLINE_Y_POS - velocity_tick_height as i32), 191 | ) 192 | .into_styled(stroke) 193 | .draw(display)?; 194 | } 195 | 196 | // draw step underline 197 | let (underline_start, underline_finish) = 198 | if masked { (x, x2 - 1) } else { (x + 2, x2 - 4) }; 199 | Line::new( 200 | Point::new(underline_start, SEQUENCE_UNDERLINE_Y_POS), 201 | Point::new(underline_finish, SEQUENCE_UNDERLINE_Y_POS), 202 | ) 203 | .into_styled(stroke) 204 | .draw(display)?; 205 | 206 | step_num += 1; 207 | } 208 | 209 | Ok(()) 210 | } 211 | 212 | fn draw_params(&self, display: &mut Display) -> DisplayResult { 213 | let is_track = match self.input_mode { 214 | InputMode::Track => true, 215 | _ => false, 216 | }; 217 | 218 | let col_content_width = 40; 219 | let col_padding = 8; 220 | let col_width = col_content_width + col_padding; 221 | 222 | let name0_x: i32 = 0; 223 | let name1_x: i32 = if is_track { 60 } else { col_width }; 224 | let name2_x: i32 = if is_track { 96 } else { col_width * 2 }; 225 | 226 | let value0_x: i32 = if is_track { 227 | 51 228 | } else { 229 | name0_x + col_content_width 230 | }; 231 | let value1_x: i32 = if is_track { 232 | 88 233 | } else { 234 | name1_x + col_content_width 235 | }; 236 | let value2_x: i32 = DISPLAY_WIDTH; 237 | 238 | let row0_y = PARAM_Y_POS as i32; 239 | let row1_y = (PARAM_Y_POS + CHAR_HEIGHT) as i32; 240 | 241 | let param_name_points = [ 242 | Point::new(name0_x, row0_y), 243 | Point::new(name1_x, row0_y), 244 | Point::new(name2_x, row0_y), 245 | Point::new(name0_x, row1_y), 246 | Point::new(name1_x, row1_y), 247 | Point::new(name2_x, row1_y), 248 | ]; 249 | let param_value_points = [ 250 | Point::new(value0_x, row0_y), 251 | Point::new(value1_x, row0_y), 252 | Point::new(value2_x, row0_y), 253 | Point::new(value0_x, row1_y), 254 | Point::new(value1_x, row1_y), 255 | Point::new(value2_x, row1_y), 256 | ]; 257 | let params = zip( 258 | self.param_data.as_ref().expect("should get param data"), 259 | zip(param_name_points, param_value_points), 260 | ); 261 | 262 | for ((param_name, param_value), (name_point, value_point)) in params { 263 | Text::with_baseline( 264 | param_name.as_str(), 265 | name_point, 266 | default_character_style(), 267 | Baseline::Top, 268 | ) 269 | .draw(display)?; 270 | Text::with_text_style( 271 | param_value.as_str(), 272 | value_point, 273 | default_character_style(), 274 | right_align(), 275 | ) 276 | .draw(display)?; 277 | } 278 | 279 | // HACK HACK HACK 280 | // track num isn't actually stored in a param, so here we just write the real track num over 281 | // the top of whatever junk value came from the param. 282 | if is_track { 283 | let mut track_num_str: String<5> = String::new(); 284 | write!(track_num_str, "{}", self.track_num).expect("write! track_num should succeed"); 285 | Rectangle::new(Point::new(116, row0_y), Size::new(13, 6)) 286 | .into_styled(background_style()) 287 | .draw(display)?; 288 | Text::with_text_style( 289 | track_num_str.as_str(), 290 | Point::new(DISPLAY_WIDTH, row0_y), 291 | default_character_style(), 292 | right_align(), 293 | ) 294 | .draw(display)?; 295 | } 296 | 297 | Ok(()) 298 | } 299 | } 300 | 301 | fn draw_disabled_track_warning(display: &mut Display) -> DisplayResult { 302 | Rectangle::new( 303 | Point::new(SEQUENCE_X_POS, SEQUENCE_Y_POS), 304 | Size::new( 305 | SEQUENCE_WIDTH, 306 | SEQUENCE_HEIGHT + (DISPLAY_HEIGHT as u32 - PARAM_Y_POS), 307 | ), 308 | ) 309 | .into_styled(background_style()) 310 | .draw(display)?; 311 | warning(display, "TRACK DISABLED") 312 | } 313 | 314 | fn note_min_max_as_u8s(sequence: &Sequence) -> (u8, u8) { 315 | let mut min = 127; 316 | let mut max = 0; 317 | for step in &sequence.steps { 318 | if let Some(step) = step { 319 | let note: u8 = step.note.into(); 320 | min = note.min(min); 321 | max = note.max(max); 322 | } 323 | } 324 | (min, max) 325 | } 326 | 327 | fn default_character_style<'a>() -> MonoTextStyle<'a, BinaryColor> { 328 | MonoTextStyle::new(&FONT_4X6, BinaryColor::On) 329 | } 330 | 331 | fn big_character_style<'a>() -> MonoTextStyle<'a, BinaryColor> { 332 | MonoTextStyle::new(&FONT_6X10, BinaryColor::On) 333 | } 334 | 335 | fn background_style() -> PrimitiveStyle { 336 | PrimitiveStyle::with_fill(BinaryColor::Off) 337 | } 338 | 339 | fn filled_style() -> PrimitiveStyle { 340 | PrimitiveStyle::with_fill(BinaryColor::On) 341 | } 342 | 343 | fn outline_style() -> PrimitiveStyle { 344 | PrimitiveStyleBuilder::new() 345 | .stroke_color(BinaryColor::On) 346 | .stroke_width(1) 347 | .fill_color(BinaryColor::Off) 348 | .build() 349 | } 350 | 351 | fn warning_style() -> PrimitiveStyle { 352 | PrimitiveStyleBuilder::new() 353 | .stroke_color(BinaryColor::On) 354 | .stroke_width(WARNING_BORDER) 355 | .fill_color(BinaryColor::Off) 356 | .build() 357 | } 358 | 359 | fn centered() -> TextStyle { 360 | TextStyleBuilder::new() 361 | .alignment(Alignment::Center) 362 | .baseline(Baseline::Top) 363 | .build() 364 | } 365 | 366 | fn right_align() -> TextStyle { 367 | TextStyleBuilder::new() 368 | .alignment(Alignment::Right) 369 | .baseline(Baseline::Top) 370 | .build() 371 | } 372 | 373 | fn warning(display: &mut Display, text: &str) -> DisplayResult { 374 | let char_width = 6; 375 | let char_height = 10; 376 | let space_width = 1; 377 | let text_width = ((text.len() * char_width) 378 | + ((text.len() - 1) * space_width) 379 | + (WARNING_PADDING as usize * 2)) as i32; 380 | let text_margin_left = (DISPLAY_WIDTH - text_width) / 2; 381 | let warning_width = DISPLAY_WIDTH - (text_margin_left * 2); 382 | let warning_height = char_height + WARNING_PADDING * 2 + WARNING_BORDER as i32 * 2; 383 | let warning_text_y_pos = WARNING_Y_POS + WARNING_PADDING + WARNING_BORDER as i32; 384 | Rectangle::new( 385 | Point::new(text_margin_left, WARNING_Y_POS), 386 | Size::new(warning_width as u32, warning_height as u32), 387 | ) 388 | .into_styled(warning_style()) 389 | .draw(display)?; 390 | Text::with_text_style( 391 | text, 392 | Point::new(DISPLAY_CENTER, warning_text_y_pos), 393 | big_character_style(), 394 | centered(), 395 | ) 396 | .draw(display)?; 397 | Ok(()) 398 | } 399 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/sequencer.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | use core::fmt::{Display, Formatter, Result as FmtResult}; 3 | use fugit::{ExtU64, MicrosDurationU64}; 4 | use heapless::{HistoryBuffer, Vec}; 5 | use midi_types::MidiMessage; 6 | 7 | use crate::{ 8 | param::{Param, ParamList, ParamValue}, 9 | TimeDivision, Track, TRACK_COUNT, 10 | }; 11 | 12 | // TODO will cause issues if polyphony 13 | const MAX_MESSAGES_PER_TICK: usize = TRACK_COUNT * 2; 14 | 15 | const MIDI_HISTORY_SAMPLE_COUNT: usize = 6; 16 | 17 | #[derive(Debug)] 18 | pub enum SequencerError { 19 | EnableTrackError(), 20 | } 21 | 22 | #[derive(Debug, PartialEq)] 23 | pub enum ScheduledMidiMessage { 24 | Immediate(MidiMessage), 25 | Delayed(MidiMessage, MicrosDurationU64), 26 | } 27 | 28 | const DEFAULT_BPM: u64 = 130; 29 | const DEFAULT_TICK_DURATION_US: u64 = (60_000_000 / DEFAULT_BPM) / 24; 30 | 31 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 32 | pub enum Swing { 33 | #[default] 34 | None, 35 | Mpc54, 36 | Mpc58, 37 | Mpc62, 38 | Mpc66, 39 | Mpc70, 40 | Mpc75, 41 | } 42 | 43 | impl Swing { 44 | pub fn as_percentage(&self) -> u8 { 45 | match self { 46 | Swing::None => 50, 47 | Swing::Mpc54 => 54, 48 | Swing::Mpc58 => 58, 49 | Swing::Mpc62 => 62, 50 | Swing::Mpc66 => 66, 51 | Swing::Mpc70 => 70, 52 | Swing::Mpc75 => 75, 53 | } 54 | } 55 | } 56 | 57 | impl Display for Swing { 58 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 59 | write!(f, "{}", self.as_percentage()) 60 | } 61 | } 62 | 63 | impl Into for Swing { 64 | fn into(self) -> u8 { 65 | self as u8 66 | } 67 | } 68 | 69 | impl TryFrom for Swing { 70 | type Error = (); 71 | 72 | fn try_from(value: u8) -> Result { 73 | match value { 74 | 0 => Ok(Swing::None), 75 | 1 => Ok(Swing::Mpc54), 76 | 2 => Ok(Swing::Mpc58), 77 | 3 => Ok(Swing::Mpc62), 78 | 4 => Ok(Swing::Mpc66), 79 | 5 => Ok(Swing::Mpc70), 80 | 6 => Ok(Swing::Mpc75), 81 | _ => Err(()), 82 | } 83 | } 84 | } 85 | 86 | pub struct Sequencer { 87 | pub tracks: Vec, TRACK_COUNT>, 88 | tick: u32, 89 | playing: bool, 90 | params: ParamList, 91 | last_tick_instant_us: Option, 92 | midi_tick_history: HistoryBuffer, 93 | } 94 | 95 | impl Default for Sequencer { 96 | fn default() -> Sequencer { 97 | // create a set of empty tracks 98 | let mut tracks = Vec::new(); 99 | for _ in 0..TRACK_COUNT { 100 | tracks 101 | .push(None) 102 | .expect("inserting track into tracks vector should succeed"); 103 | } 104 | Sequencer { 105 | tracks, 106 | tick: 0, 107 | playing: false, 108 | params: ParamList::from_slice(&[ 109 | // if ordering changes, need to update getters and setters, e.g. swing/set_swing 110 | Box::new(Param::new_swing_param("SWING")), 111 | ]) 112 | .expect("should create sequencer param list from slice"), 113 | last_tick_instant_us: None, 114 | midi_tick_history: HistoryBuffer::::new(), 115 | } 116 | } 117 | } 118 | 119 | impl Sequencer { 120 | pub fn playing(&self) -> bool { 121 | self.playing 122 | } 123 | 124 | pub fn params(&self) -> &ParamList { 125 | &self.params 126 | } 127 | 128 | pub fn params_mut(&mut self) -> &mut ParamList { 129 | &mut self.params 130 | } 131 | 132 | pub fn tick(&self) -> u32 { 133 | self.tick 134 | } 135 | 136 | pub fn start_playing(&mut self) { 137 | self.tick = 0; 138 | self.playing = true 139 | } 140 | 141 | pub fn stop_playing(&mut self) { 142 | self.playing = false; 143 | } 144 | 145 | pub fn continue_playing(&mut self) { 146 | self.playing = true 147 | } 148 | 149 | pub fn swing(&self) -> Swing { 150 | self.params[0] 151 | .value() 152 | .try_into() 153 | .expect("invalid swing parameter for sequencer") 154 | } 155 | 156 | pub fn set_swing(&mut self, swing: Swing) { 157 | self.params[0].set(ParamValue::Swing(swing)); 158 | } 159 | 160 | pub fn enable_track(&mut self, track_num: u8, new_track: Track) -> &mut Track { 161 | self.tracks[track_num as usize].insert(new_track) 162 | } 163 | 164 | pub fn advance(&mut self, now_us: u64) -> Vec { 165 | let tick_duration = self.average_tick_duration(now_us); 166 | 167 | let mut output_messages = Vec::new(); 168 | 169 | if !self.playing { 170 | return output_messages; 171 | } 172 | 173 | let apply_swing = self.swing() != Swing::None && self.tick % 12 == 6; 174 | let swing_delay = (tick_duration * (self.swing().as_percentage() - 50) as u32) / 8; 175 | 176 | for track in &self.tracks { 177 | if let Some(track) = track { 178 | if let Some(step) = track.step_at_tick(self.tick) { 179 | let note_on_message = 180 | MidiMessage::NoteOn(track.midi_channel, step.note.into(), step.velocity); 181 | if apply_swing { 182 | output_messages 183 | .push(ScheduledMidiMessage::Delayed(note_on_message, swing_delay)) 184 | .expect("should push message to output_messages vec"); 185 | } else { 186 | output_messages 187 | .push(ScheduledMidiMessage::Immediate(note_on_message)) 188 | .expect("should push message to output_messages vec"); 189 | } 190 | 191 | let note_off_message = 192 | MidiMessage::NoteOff(track.midi_channel, step.note.into(), 0.into()); 193 | let mut note_off_time = ((tick_duration.to_micros() 194 | * (TimeDivision::division_length_24ppqn(track.time_division) as u64) 195 | * step.length_step_cents as u64) 196 | / 100) 197 | .micros(); 198 | if apply_swing { 199 | note_off_time += swing_delay; 200 | } 201 | output_messages 202 | .push(ScheduledMidiMessage::Delayed( 203 | note_off_message, 204 | note_off_time, 205 | )) 206 | .expect("should push message to output_messages vec"); 207 | } 208 | } 209 | } 210 | 211 | self.tick += 1; 212 | 213 | output_messages 214 | } 215 | 216 | /// Calculate average time between last k MIDI ticks. Defaults to tick frequency of 217 | /// 19,230ms, which is equivalent to 130BPM. 218 | fn average_tick_duration(&mut self, now_us: u64) -> MicrosDurationU64 { 219 | let mut tick_duration = DEFAULT_TICK_DURATION_US.micros(); 220 | 221 | if let Some(last_tick_instant_us) = self.last_tick_instant_us { 222 | let last_tick_duration = now_us - last_tick_instant_us; 223 | self.midi_tick_history.write(last_tick_duration); 224 | tick_duration = (self.midi_tick_history.as_slice().iter().sum::() 225 | / self.midi_tick_history.len() as u64) 226 | .micros(); 227 | } 228 | 229 | self.last_tick_instant_us = Some(now_us); 230 | 231 | tick_duration 232 | } 233 | } 234 | 235 | #[cfg(test)] 236 | mod tests { 237 | use super::*; 238 | use crate::sequence_generator::SequenceGenerator; 239 | 240 | #[test] 241 | fn sequencer_default_should_have_empty_tracks() { 242 | let sequencer = Sequencer::default(); 243 | assert!(sequencer.tracks.iter().all(|track| track.is_none())); 244 | } 245 | 246 | #[test] 247 | fn sequencer_enable_track_should_insert_new_track() { 248 | let generator = SequenceGenerator::default(); 249 | let mut new_track = Track::default(); 250 | new_track.sequence = generator.apply(new_track.length); 251 | let mut sequencer = Sequencer::default(); 252 | sequencer.enable_track(0, new_track); 253 | assert!(sequencer.tracks[0].is_some()); 254 | assert!(sequencer.tracks[1..TRACK_COUNT] 255 | .iter() 256 | .all(|track| track.is_none())); 257 | } 258 | 259 | #[test] 260 | fn sequencer_should_start_stop_and_continue_playing() { 261 | let mut sequencer = Sequencer::default(); 262 | assert_eq!(false, sequencer.playing()); 263 | assert_eq!(0, sequencer.tick); 264 | sequencer.start_playing(); 265 | assert_eq!(true, sequencer.playing()); 266 | 267 | sequencer.advance(1); 268 | sequencer.stop_playing(); 269 | assert_eq!(false, sequencer.playing()); 270 | 271 | sequencer.advance(1); // should be ignored because sequencer stopped 272 | sequencer.continue_playing(); 273 | sequencer.advance(1); 274 | assert_eq!(true, sequencer.playing()); 275 | assert_eq!(2, sequencer.tick); 276 | 277 | sequencer.stop_playing(); 278 | assert_eq!(2, sequencer.tick); 279 | 280 | sequencer.start_playing(); 281 | assert_eq!(true, sequencer.playing()); 282 | assert_eq!(0, sequencer.tick); 283 | } 284 | 285 | #[test] 286 | fn sequencer_should_calculate_average_tick_duration() { 287 | let mut sequencer = Sequencer::default(); 288 | let tick_duration = sequencer.average_tick_duration(0); 289 | assert_eq!(DEFAULT_TICK_DURATION_US, tick_duration.to_micros()); 290 | 291 | let tick_duration = sequencer.average_tick_duration(100); 292 | assert_eq!(100, tick_duration.to_micros()); 293 | 294 | sequencer.average_tick_duration(200); 295 | sequencer.average_tick_duration(300); 296 | sequencer.average_tick_duration(350); 297 | sequencer.average_tick_duration(400); 298 | let tick_duration = sequencer.average_tick_duration(450); 299 | assert_eq!(75, tick_duration.to_micros()); 300 | } 301 | 302 | #[test] 303 | fn sequencer_advance_should_output_immediate_note_on_and_delayed_note_off_messages() { 304 | let mut now_us = 0; 305 | let mut sequencer = Sequencer::default(); 306 | let generator = SequenceGenerator::default(); 307 | let mut new_track = Track::default(); 308 | new_track.sequence = generator.apply(new_track.length); 309 | sequencer.enable_track(0, new_track); 310 | sequencer.start_playing(); 311 | let mut output_messages = vec![]; 312 | for _ in 0..48 { 313 | let step_messages = sequencer.advance(now_us); 314 | output_messages.extend(step_messages.into_iter()); 315 | now_us += DEFAULT_TICK_DURATION_US; 316 | } 317 | assert_eq!(16, output_messages.len()); // 8 note on/note off pairs 318 | let expected_note_on = 319 | ScheduledMidiMessage::Immediate(MidiMessage::NoteOn(0.into(), 60.into(), 127.into())); 320 | let expected_note_off = ScheduledMidiMessage::Delayed( 321 | MidiMessage::NoteOff(0.into(), 60.into(), 0.into()), 322 | 92304.micros(), 323 | ); 324 | assert_eq!(expected_note_on, output_messages[0]); 325 | assert_eq!(expected_note_off, output_messages[1]); 326 | assert_eq!(expected_note_on, output_messages[2]); 327 | assert_eq!(expected_note_off, output_messages[3]); 328 | assert_eq!(expected_note_on, output_messages[4]); 329 | assert_eq!(expected_note_off, output_messages[5]); 330 | assert_eq!(expected_note_on, output_messages[6]); 331 | assert_eq!(expected_note_off, output_messages[7]); 332 | assert_eq!(expected_note_on, output_messages[8]); 333 | assert_eq!(expected_note_off, output_messages[9]); 334 | assert_eq!(expected_note_on, output_messages[10]); 335 | assert_eq!(expected_note_off, output_messages[11]); 336 | assert_eq!(expected_note_on, output_messages[12]); 337 | assert_eq!(expected_note_off, output_messages[13]); 338 | assert_eq!(expected_note_on, output_messages[14]); 339 | assert_eq!(expected_note_off, output_messages[15]); 340 | } 341 | 342 | #[test] 343 | fn sequencer_advance_with_swing_enabled_should_output_delayed_note_on_messages_for_swung_steps() 344 | { 345 | let mut now_us = 0; 346 | let mut sequencer = Sequencer::default(); 347 | let generator = SequenceGenerator::default(); 348 | let mut new_track = Track::default(); 349 | new_track.sequence = generator.apply(new_track.length); 350 | sequencer.enable_track(0, new_track); 351 | sequencer.set_swing(Swing::Mpc54); 352 | sequencer.start_playing(); 353 | let mut output_messages = vec![]; 354 | for _ in 0..48 { 355 | let step_messages = sequencer.advance(now_us); 356 | output_messages.extend(step_messages.into_iter()); 357 | now_us += DEFAULT_TICK_DURATION_US; 358 | } 359 | assert_eq!(16, output_messages.len()); // 8 note on/note off pairs 360 | let expected_note_on = 361 | ScheduledMidiMessage::Immediate(MidiMessage::NoteOn(0.into(), 60.into(), 127.into())); 362 | let expected_note_on_with_swing = ScheduledMidiMessage::Delayed( 363 | MidiMessage::NoteOn(0.into(), 60.into(), 127.into()), 364 | 9615.micros(), 365 | ); 366 | let expected_note_off = ScheduledMidiMessage::Delayed( 367 | MidiMessage::NoteOff(0.into(), 60.into(), 0.into()), 368 | 92304.micros(), 369 | ); 370 | let expected_note_off_with_swing = ScheduledMidiMessage::Delayed( 371 | MidiMessage::NoteOff(0.into(), 60.into(), 0.into()), 372 | (92304 + 9615).micros(), 373 | ); 374 | assert_eq!(expected_note_on, output_messages[0]); 375 | assert_eq!(expected_note_off, output_messages[1]); 376 | assert_eq!(expected_note_on_with_swing, output_messages[2]); 377 | assert_eq!(expected_note_off_with_swing, output_messages[3]); 378 | assert_eq!(expected_note_on, output_messages[4]); 379 | assert_eq!(expected_note_off, output_messages[5]); 380 | assert_eq!(expected_note_on_with_swing, output_messages[6]); 381 | assert_eq!(expected_note_off_with_swing, output_messages[7]); 382 | assert_eq!(expected_note_on, output_messages[8]); 383 | assert_eq!(expected_note_off, output_messages[9]); 384 | assert_eq!(expected_note_on_with_swing, output_messages[10]); 385 | assert_eq!(expected_note_off_with_swing, output_messages[11]); 386 | assert_eq!(expected_note_on, output_messages[12]); 387 | assert_eq!(expected_note_off, output_messages[13]); 388 | assert_eq!(expected_note_on_with_swing, output_messages[14]); 389 | assert_eq!(expected_note_off_with_swing, output_messages[15]); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /firmware/microgroove_sequencer/src/param.rs: -------------------------------------------------------------------------------- 1 | /// Model parameters as mutable values with metadata (name). 2 | use alloc::boxed::Box; 3 | use core::cmp::PartialEq; 4 | use core::fmt::{Debug, Display, Formatter, Result as FmtResult}; 5 | use heapless::{String, Vec}; 6 | 7 | use crate::{ 8 | machine::{grids_rhythm_machine::Instrument, MelodyMachineId, RhythmMachineId}, 9 | midi::Note, 10 | part::Part, 11 | quantizer::{Key, Scale}, 12 | sequencer::Swing, 13 | TimeDivision, 14 | }; 15 | 16 | pub fn wrapping_add(a: i32, b: i32, max: i32) -> i32 { 17 | let size = max + 1; 18 | ((a + b % size) + size) % size 19 | } 20 | 21 | #[derive(Clone, Copy, Debug, PartialEq)] 22 | pub enum ParamValue { 23 | Number(u8), 24 | TimeDivision(TimeDivision), 25 | RhythmMachineId(RhythmMachineId), 26 | MelodyMachineId(MelodyMachineId), 27 | Note(Note), 28 | Scale(Scale), 29 | Key(Key), 30 | Swing(Swing), 31 | Instrument(Instrument), 32 | Part(Part), 33 | } 34 | 35 | impl Display for ParamValue { 36 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 37 | match self { 38 | ParamValue::Number(num) => Display::fmt(&num, f), 39 | ParamValue::TimeDivision(time_div) => Display::fmt(&time_div, f), 40 | ParamValue::RhythmMachineId(id) => Display::fmt(&id, f), 41 | ParamValue::MelodyMachineId(id) => Display::fmt(&id, f), 42 | ParamValue::Note(note) => Display::fmt(¬e, f), 43 | ParamValue::Scale(scale) => Display::fmt(&scale, f), 44 | ParamValue::Key(key) => Display::fmt(&key, f), 45 | ParamValue::Swing(swing) => Display::fmt(&swing, f), 46 | ParamValue::Instrument(instrument) => Display::fmt(&instrument, f), 47 | ParamValue::Part(part) => Display::fmt(&part, f), 48 | } 49 | } 50 | } 51 | 52 | impl From for i32 { 53 | fn from(value: ParamValue) -> i32 { 54 | match value { 55 | ParamValue::Number(num) => num as i32, 56 | ParamValue::TimeDivision(time_div) => time_div as i32, 57 | ParamValue::RhythmMachineId(id) => id as i32, 58 | ParamValue::MelodyMachineId(id) => id as i32, 59 | ParamValue::Note(note) => note as i32, 60 | ParamValue::Scale(scale) => scale as i32, 61 | ParamValue::Key(key) => key as i32, 62 | ParamValue::Swing(swing) => swing as i32, 63 | ParamValue::Instrument(instrument) => instrument as i32, 64 | ParamValue::Part(part) => part as i32, 65 | } 66 | } 67 | } 68 | 69 | type ParamName = String<6>; 70 | 71 | #[derive(Debug)] 72 | pub enum ParamError { 73 | ValueOutOfRange, 74 | UnexpectedValue(ParamValue), 75 | } 76 | 77 | #[derive(Clone, Debug)] 78 | pub struct Param { 79 | name: ParamName, 80 | value: ParamValue, 81 | min: ParamValue, 82 | max: ParamValue, 83 | } 84 | 85 | impl Param { 86 | pub fn new_number_param(name: &str, min: u8, max: u8, default: u8) -> Param { 87 | if default < min || default > max { 88 | panic!("param default out of bounds"); 89 | } 90 | Param { 91 | name: name.into(), 92 | value: ParamValue::Number(default), 93 | min: ParamValue::Number(min), 94 | max: ParamValue::Number(max), 95 | } 96 | } 97 | 98 | pub fn new_time_division_param(name: &str) -> Param { 99 | Param { 100 | name: name.into(), 101 | value: ParamValue::TimeDivision(TimeDivision::default()), 102 | min: ParamValue::TimeDivision(TimeDivision::ThirtySecond), 103 | max: ParamValue::TimeDivision(TimeDivision::Whole), 104 | } 105 | } 106 | 107 | pub fn new_rhythm_machine_id_param(name: &str) -> Param { 108 | Param { 109 | name: name.into(), 110 | value: ParamValue::RhythmMachineId(RhythmMachineId::default()), 111 | min: ParamValue::RhythmMachineId(RhythmMachineId::Unit), 112 | max: ParamValue::RhythmMachineId(RhythmMachineId::Euclid), 113 | } 114 | } 115 | 116 | pub fn new_melody_machine_id_param(name: &str) -> Param { 117 | Param { 118 | name: name.into(), 119 | value: ParamValue::MelodyMachineId(MelodyMachineId::default()), 120 | min: ParamValue::MelodyMachineId(MelodyMachineId::Unit), 121 | max: ParamValue::MelodyMachineId(MelodyMachineId::Rand), 122 | } 123 | } 124 | 125 | pub fn new_note_param(name: &str) -> Param { 126 | Param { 127 | name: name.into(), 128 | value: ParamValue::Note(Note::default()), 129 | min: ParamValue::Note(Note::CMinus2), 130 | max: ParamValue::Note(Note::G8), 131 | } 132 | } 133 | 134 | pub fn new_scale_param(name: &str) -> Param { 135 | Param { 136 | name: name.into(), 137 | value: ParamValue::Scale(Scale::default()), 138 | min: ParamValue::Scale(Scale::Chromatic), 139 | max: ParamValue::Scale(Scale::OctaveAndFifth), 140 | } 141 | } 142 | 143 | pub fn new_key_param(name: &str) -> Param { 144 | Param { 145 | name: name.into(), 146 | value: ParamValue::Key(Key::default()), 147 | min: ParamValue::Key(Key::C), 148 | max: ParamValue::Key(Key::B), 149 | } 150 | } 151 | 152 | pub fn new_swing_param(name: &str) -> Param { 153 | Param { 154 | name: name.into(), 155 | value: ParamValue::Swing(Swing::default()), 156 | min: ParamValue::Swing(Swing::None), 157 | max: ParamValue::Swing(Swing::Mpc75), 158 | } 159 | } 160 | 161 | pub fn new_instrument_param(name: &str) -> Param { 162 | Param { 163 | name: name.into(), 164 | value: ParamValue::Instrument(Instrument::default()), 165 | min: ParamValue::Instrument(Instrument::BD), 166 | max: ParamValue::Instrument(Instrument::HH), 167 | } 168 | } 169 | 170 | pub fn new_part_param(name: &str) -> Param { 171 | Param { 172 | name: name.into(), 173 | value: ParamValue::Part(Part::default()), 174 | min: ParamValue::Part(Part::Sequence), 175 | max: ParamValue::Part(Part::Turnaround), 176 | } 177 | } 178 | 179 | pub fn name(&self) -> &str { 180 | self.name.as_str() 181 | } 182 | 183 | pub fn value(&self) -> ParamValue { 184 | self.value.clone() 185 | } 186 | 187 | pub fn set(&mut self, new_value: ParamValue) { 188 | // panic!("unexpected ParamValue variant"); 189 | // if new_value < self.min || new_value > self.max { 190 | // panic!("param default out of bounds"); 191 | // } 192 | self.value = new_value; 193 | } 194 | 195 | pub fn set_from_u8(&mut self, new_value: u8) -> Result<(), ParamError> { 196 | match self.value { 197 | ParamValue::Number(_) => self.value = ParamValue::Number(new_value), 198 | ParamValue::TimeDivision(_) => new_value 199 | .try_into() 200 | .map(|val| self.value = ParamValue::TimeDivision(val)) 201 | .map_err(|_| ParamError::ValueOutOfRange)?, 202 | ParamValue::RhythmMachineId(_) => new_value 203 | .try_into() 204 | .map(|val| self.value = ParamValue::RhythmMachineId(val)) 205 | .map_err(|_| ParamError::ValueOutOfRange)?, 206 | ParamValue::MelodyMachineId(_) => new_value 207 | .try_into() 208 | .map(|val| self.value = ParamValue::MelodyMachineId(val)) 209 | .map_err(|_| ParamError::ValueOutOfRange)?, 210 | ParamValue::Note(_) => new_value 211 | .try_into() 212 | .map(|val| self.value = ParamValue::Note(val)) 213 | .map_err(|_| ParamError::ValueOutOfRange)?, 214 | ParamValue::Scale(_) => new_value 215 | .try_into() 216 | .map(|val| self.value = ParamValue::Scale(val)) 217 | .map_err(|_| ParamError::ValueOutOfRange)?, 218 | ParamValue::Key(_) => new_value 219 | .try_into() 220 | .map(|val| self.value = ParamValue::Key(val)) 221 | .map_err(|_| ParamError::ValueOutOfRange)?, 222 | ParamValue::Swing(_) => new_value 223 | .try_into() 224 | .map(|val| self.value = ParamValue::Swing(val)) 225 | .map_err(|_| ParamError::ValueOutOfRange)?, 226 | ParamValue::Instrument(_) => new_value 227 | .try_into() 228 | .map(|val| self.value = ParamValue::Instrument(val)) 229 | .map_err(|_| ParamError::ValueOutOfRange)?, 230 | ParamValue::Part(_) => new_value 231 | .try_into() 232 | .map(|val| self.value = ParamValue::Part(val)) 233 | .map_err(|_| ParamError::ValueOutOfRange)?, 234 | }; 235 | Ok(()) 236 | } 237 | 238 | pub fn increment(&mut self, n: i32) -> Result<(), ParamError> { 239 | let value_i32: i32 = self.value.into(); 240 | let min_i32: i32 = self.min.into(); 241 | let max_i32: i32 = self.max.into(); 242 | let new_value = (wrapping_add(value_i32 - min_i32, n, max_i32 - min_i32) + min_i32) as u8; 243 | self.set_from_u8(new_value) 244 | } 245 | } 246 | 247 | impl TryInto for ParamValue { 248 | type Error = ParamError; 249 | 250 | fn try_into(self) -> Result { 251 | match self { 252 | ParamValue::Number(num) => Ok(num), 253 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 254 | } 255 | } 256 | } 257 | 258 | impl TryInto for ParamValue { 259 | type Error = ParamError; 260 | 261 | fn try_into(self) -> Result { 262 | match self { 263 | ParamValue::TimeDivision(time_div) => Ok(time_div), 264 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 265 | } 266 | } 267 | } 268 | 269 | impl TryInto for ParamValue { 270 | type Error = ParamError; 271 | 272 | fn try_into(self) -> Result { 273 | match self { 274 | ParamValue::RhythmMachineId(id) => Ok(id), 275 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 276 | } 277 | } 278 | } 279 | 280 | impl TryInto for ParamValue { 281 | type Error = ParamError; 282 | 283 | fn try_into(self) -> Result { 284 | match self { 285 | ParamValue::MelodyMachineId(id) => Ok(id), 286 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 287 | } 288 | } 289 | } 290 | 291 | impl TryInto for ParamValue { 292 | type Error = ParamError; 293 | 294 | fn try_into(self) -> Result { 295 | match self { 296 | ParamValue::Note(note) => Ok(note), 297 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 298 | } 299 | } 300 | } 301 | 302 | impl TryInto for ParamValue { 303 | type Error = ParamError; 304 | 305 | fn try_into(self) -> Result { 306 | match self { 307 | ParamValue::Scale(scale) => Ok(scale), 308 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 309 | } 310 | } 311 | } 312 | 313 | impl TryInto for ParamValue { 314 | type Error = ParamError; 315 | 316 | fn try_into(self) -> Result { 317 | match self { 318 | ParamValue::Key(key) => Ok(key), 319 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 320 | } 321 | } 322 | } 323 | 324 | impl TryInto for ParamValue { 325 | type Error = ParamError; 326 | 327 | fn try_into(self) -> Result { 328 | match self { 329 | ParamValue::Swing(swing) => Ok(swing), 330 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 331 | } 332 | } 333 | } 334 | 335 | impl TryInto for ParamValue { 336 | type Error = ParamError; 337 | 338 | fn try_into(self) -> Result { 339 | match self { 340 | ParamValue::Instrument(instrument) => Ok(instrument), 341 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 342 | } 343 | } 344 | } 345 | 346 | impl TryInto for ParamValue { 347 | type Error = ParamError; 348 | 349 | fn try_into(self) -> Result { 350 | match self { 351 | ParamValue::Part(part) => Ok(part), 352 | unexpected => Err(ParamError::UnexpectedValue(unexpected)), 353 | } 354 | } 355 | } 356 | 357 | pub type ParamList = Vec, 6>; 358 | 359 | #[cfg(test)] 360 | mod tests { 361 | use super::*; 362 | 363 | #[test] 364 | #[should_panic] 365 | fn param_cant_have_out_of_bounds_default() { 366 | let _ = Param::new_number_param("NUM", 1, 10, 0); 367 | } 368 | 369 | #[test] 370 | fn param_number_should_increment() { 371 | let mut param_number = Param::new_number_param("NUM", 0, 10, 0); 372 | param_number.increment(1).unwrap(); 373 | assert_eq!(1, param_number.value().try_into().unwrap()) 374 | } 375 | 376 | #[test] 377 | fn param_number_starting_at_1_should_increment() { 378 | let mut param_number = Param::new_number_param("NUM", 1, 10, 1); 379 | param_number.increment(1).unwrap(); 380 | assert_eq!(2, param_number.value().try_into().unwrap()); 381 | param_number.increment(10).unwrap(); 382 | assert_eq!(2, param_number.value().try_into().unwrap()); 383 | param_number.increment(-5).unwrap(); 384 | assert_eq!(7, param_number.value().try_into().unwrap()); 385 | } 386 | 387 | #[test] 388 | fn param_time_division_should_increment() { 389 | let mut param_time_div = Param::new_time_division_param("SPD"); 390 | param_time_div.increment(1).unwrap(); 391 | assert_eq!( 392 | TimeDivision::Eigth, 393 | param_time_div.value().try_into().unwrap() 394 | ); 395 | param_time_div.increment(9).unwrap(); 396 | assert_eq!( 397 | TimeDivision::Sixteenth, 398 | param_time_div.value().try_into().unwrap() 399 | ); 400 | param_time_div.increment(-1).unwrap(); 401 | assert_eq!( 402 | TimeDivision::ThirtySecond, 403 | param_time_div.value().try_into().unwrap() 404 | ); 405 | param_time_div.increment(-11).unwrap(); 406 | assert_eq!( 407 | TimeDivision::Whole, 408 | param_time_div.value().try_into().unwrap() 409 | ); 410 | } 411 | 412 | #[test] 413 | fn param_enum_value_should_have_to_string() { 414 | let param_time_div = Param::new_time_division_param("SPD"); 415 | let value: TimeDivision = param_time_div.value().try_into().unwrap(); 416 | assert_eq!("1/16", value.to_string()); 417 | } 418 | 419 | #[test] 420 | fn param_list_can_store_different_param_types() { 421 | let param_number = Param::new_number_param("NUM", 0, 10, 0); 422 | let param_time_div = Param::new_time_division_param("SPD"); 423 | let _param_list = 424 | ParamList::from_slice(&[Box::new(param_number), Box::new(param_time_div)]); 425 | } 426 | 427 | #[test] 428 | fn param_value_can_be_set() { 429 | let mut param_number = Param::new_number_param("NUM", 0, 10, 0); 430 | param_number.set(ParamValue::Number(1)); 431 | assert_eq!(1, param_number.value().try_into().unwrap()) 432 | } 433 | 434 | #[test] 435 | #[should_panic] 436 | #[ignore = "unimplemented"] 437 | fn param_value_cant_be_set_to_value_out_of_range() { 438 | let mut param_number = Param::new_number_param("NUM", 0, 10, 0); 439 | param_number.set(ParamValue::Number(11)); 440 | } 441 | 442 | #[test] 443 | #[should_panic] 444 | #[ignore = "unimplemented"] 445 | fn param_value_cant_be_set_to_different_paramvalue_variant() { 446 | let mut param_number = Param::new_number_param("NUM", 0, 10, 0); 447 | param_number.set(ParamValue::TimeDivision(TimeDivision::Sixteenth)); 448 | } 449 | } 450 | --------------------------------------------------------------------------------