Skip to content

Instantly share code, notes, and snippets.

@afternoon
Created October 31, 2022 23:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save afternoon/51aa5688cc9bf047a9e9711b231c62d0 to your computer and use it in GitHub Desktop.
Save afternoon/51aa5688cc9bf047a9e9711b231c62d0 to your computer and use it in GitHub Desktop.
#![no_std]
#![no_main]
use panic_probe as _;
mod microgroove {
mod sequencer {
use heapless::Vec;
use midi_types::{Channel, Note, Value14, Value7};
// represent a step in a musical sequence
// TODO polyphonic steps - note: Note -> notes: Vec<Note, _>
#[derive(Clone, Debug)]
pub struct Step {
pub note: Note,
pub velocity: Value7,
pub pitch_bend: Value14,
pub length_step_cents: u8,
}
impl Step {
pub fn new(note: Note) -> Step {
Step {
note,
velocity: 127.into(),
pitch_bend: 0u16.into(),
length_step_cents: 85, // == gate time is 85% of step length
}
}
}
#[derive(Debug)]
pub enum TimeDivision {
NinetySixth = 1, // corresponds to midi standard of 24 clock pulses per quarter note
ThirtySecond = 3,
Sixteenth = 6,
Eigth = 12,
Quarter = 24,
Whole = 96
}
pub type Sequence = Vec<Option<Step>, 32>;
#[derive(Debug)]
pub struct Track {
pub time_division: TimeDivision,
pub length: u8,
pub midi_channel: Channel,
pub steps: Sequence,
}
impl Track {
pub fn new() -> Track {
let steps = [57, 59, 60, 62, 64, 65, 67, 69, 57, 59, 60, 62, 64, 65, 67, 69]
// let steps = [60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60]
.map(|note_num| { Some(Step::new(note_num.into())) });
Track {
time_division: TimeDivision::Sixteenth,
length: 16,
midi_channel: 0.into(),
steps: Vec::from_slice(steps.as_slice()).unwrap(),
}
}
}
}
// RTIC app module
#[rtic::app(
device = rp_pico::hal::pac,
peripherals = true,
dispatchers = [USBCTRL_IRQ, DMA_IRQ_0, DMA_IRQ_1, PWM_IRQ_WRAP]
)]
mod app {
// hal aliases - turns out we have a big dependency on the hardware 😀
use rp_pico::{
hal::{
Clock,
I2C,
Timer,
Watchdog,
clocks,
gpio::{
FunctionI2C,
FunctionUart,
Interrupt::EdgeLow,
Pin,
DynPin,
PullUpInput,
pin::bank0::{Gpio0, Gpio1, Gpio2, Gpio16, Gpio17, Gpio26, Gpio27},
},
pac::{
I2C1,
UART0,
},
sio::{
self,
Sio
},
timer::{
Alarm0,
monotonic::Monotonic,
},
uart::{
DataBits,
Reader,
StopBits,
UartConfig,
UartPeripheral,
Writer,
},
},
Pins,
XOSC_CRYSTAL_FREQ,
};
// driver for rotary encoders
use rotary_encoder_hal::{Direction, Rotary};
// midi stuff
use midi_types::MidiMessage;
use embedded_midi::{MidiIn, MidiOut};
// ssd1306 oled display driver
use ssd1306::{
I2CDisplayInterface,
Ssd1306,
prelude::*,
mode::BufferedGraphicsMode,
};
// graphics APIs
use embedded_graphics::{
mono_font::{
MonoTextStyleBuilder,
ascii::{FONT_4X6, FONT_8X13_ITALIC},
},
pixelcolor::BinaryColor,
prelude::*,
text::{Baseline, Text},
};
// non-blocking io
use nb::block;
// rtic Mutex trait for passing shared resources to functions
use rtic::Mutex;
// defmt rtt logging (read the logs with cargo embed, etc)
use defmt;
use defmt::{error, info, debug, trace};
use defmt_rtt as _;
// alloc-free data structures
use heapless::{Vec, HistoryBuffer, String};
// Write trait to allow formatting heapless Strings
use core::fmt::Write;
// time manipulation
use fugit::{ExtU64, MicrosDurationU64, RateExtU32};
// crate imports
use super::sequencer::Track;
// time between each display render
// this is the practical upper bound for drawing and flushing a frame to the oled
// at 40ms, the frame rate will be 25 FPS
// we want the lowest frame rate that looks acceptable, to provide the largest budget for
// render times
// TODO move to UI/input module
const DISPLAY_UPDATE_INTERVAL: MicrosDurationU64 = MicrosDurationU64::millis(40);
// how often to poll encoders for position updates
const ENCODER_READ_INTERVAL: MicrosDurationU64 = MicrosDurationU64::millis(1);
// monotonic clock for RTIC and defmt
#[monotonic(binds = TIMER_IRQ_0, default = true)]
type TimerMonotonic = Monotonic<Alarm0>;
type TimerMonotonicInstant = <TimerMonotonic as rtic::rtic_monotonic::Monotonic>::Instant;
// type alias for UART pins
type MidiOutUartPin = Pin<Gpio16, FunctionUart>;
type MidiInUartPin = Pin<Gpio17, FunctionUart>;
type MidiUartPins = (MidiOutUartPin, MidiInUartPin);
// type alias for display pins
type DisplaySdaPin = Pin<Gpio26, FunctionI2C>;
type DisplaySclPin = Pin<Gpio27, FunctionI2C>;
type DisplayPins = (DisplaySdaPin, DisplaySclPin);
// type alias for button pins
type ButtonTrackPin = Pin<Gpio0, PullUpInput>;
type ButtonGroovePin = Pin<Gpio1, PullUpInput>;
type ButtonMelodyPin = Pin<Gpio2, PullUpInput>;
// type alias for encoder bound to some pins - pin state not checked
type AnyEncoder = Rotary<DynPin, DynPin>;
// TODO move to UI/input module
pub enum InputMode {
Track,
Groove,
Melody
}
// RTIC shared resources
#[shared]
struct Shared {
// are we playing, or not?
playing: bool,
// current page of the UI
input_mode: InputMode,
// encoder positions
encoder0_pos: i8,
encoder1_pos: i8,
encoder2_pos: i8,
encoder3_pos: i8,
encoder4_pos: i8,
encoder5_pos: i8,
// tracks are where we store our sequence data
tracks: Vec<Option<Track>, 16>,
}
// RTIC local resources
#[local]
struct Local {
// midi ports (2 halves of the split UART)
midi_in: MidiIn<Reader<UART0, MidiUartPins>>,
midi_out: MidiOut<Writer<UART0, MidiUartPins>>,
// display interface
display: Ssd1306<
I2CInterface<I2C<I2C1, DisplayPins>>,
DisplaySize128x64,
BufferedGraphicsMode<DisplaySize128x64>
>,
// pins for buttons
button_track_pin: ButtonTrackPin,
button_groove_pin: ButtonGroovePin,
button_melody_pin: ButtonMelodyPin,
// encoders
encoder0: AnyEncoder,
encoder1: AnyEncoder,
encoder2: AnyEncoder,
encoder3: AnyEncoder,
encoder4: AnyEncoder,
encoder5: AnyEncoder,
// a buffer to track the intervals between MIDI ticks, which we can
// use to estimate the tempo, we can then use our tempo estimate to
// implement note lengths and swing
tick_history: HistoryBuffer::<u64, 24>,
}
// RTIC init
#[init]
fn init(mut ctx: init::Context) -> (Shared, Local, init::Monotonics) {
info!("hello world!");
// release spinlocks to avoid a deadlock after soft-reset
unsafe {
sio::spinlock_reset();
}
// DEVICE SETUP
// clock setup for timers and alarms
let mut watchdog = Watchdog::new(ctx.device.WATCHDOG);
let clocks = clocks::init_clocks_and_plls(
XOSC_CRYSTAL_FREQ,
ctx.device.XOSC,
ctx.device.CLOCKS,
ctx.device.PLL_SYS,
ctx.device.PLL_USB,
&mut ctx.device.RESETS,
&mut watchdog,
)
.ok()
.expect("init_clocks_and_plls(...) should succeed");
// timer for, well, timing
let mut timer = Timer::new(ctx.device.TIMER, &mut ctx.device.RESETS);
// the single-cycle i/o block controls our gpio pins
let sio = Sio::new(ctx.device.SIO);
// set the pins to their default state
let pins = Pins::new(
ctx.device.IO_BANK0,
ctx.device.PADS_BANK0,
sio.gpio_bank0,
&mut ctx.device.RESETS,
);
// BUTTONS
// configure interrupts on button and encoder GPIO pins
let button_track_pin = pins.gpio0.into_pull_up_input();
let button_groove_pin = pins.gpio1.into_pull_up_input();
let button_melody_pin = pins.gpio2.into_pull_up_input();
button_track_pin.set_interrupt_enabled(EdgeLow, true);
button_groove_pin.set_interrupt_enabled(EdgeLow, true);
button_melody_pin.set_interrupt_enabled(EdgeLow, true);
// ENCODERS
let encoder0 = Rotary::new(pins.gpio9.into_pull_up_input().into(), pins.gpio10.into_pull_up_input().into());
let encoder1 = Rotary::new(pins.gpio11.into_pull_up_input().into(), pins.gpio12.into_pull_up_input().into());
let encoder2 = Rotary::new(pins.gpio13.into_pull_up_input().into(), pins.gpio14.into_pull_up_input().into());
let encoder3 = Rotary::new(pins.gpio3.into_pull_up_input().into(), pins.gpio4.into_pull_up_input().into());
let encoder4 = Rotary::new(pins.gpio5.into_pull_up_input().into(), pins.gpio6.into_pull_up_input().into());
let encoder5 = Rotary::new(pins.gpio7.into_pull_up_input().into(), pins.gpio8.into_pull_up_input().into());
// MIDI
// put pins for midi into uart mode
let midi_uart_pins = (
pins.gpio16.into_mode::<FunctionUart>(),
pins.gpio17.into_mode::<FunctionUart>(),
);
// make a uart peripheral on the given pins
let uart_config = UartConfig::new(31_250.Hz(), DataBits::Eight, None, StopBits::One);
let mut midi_uart = UartPeripheral::new(ctx.device.UART0, midi_uart_pins, &mut ctx.device.RESETS)
.enable(uart_config, clocks.peripheral_clock.freq())
.expect("midi_uart.enable(...) should succeed");
// configure uart interrupt to fire on midi input
midi_uart.enable_rx_interrupt();
// split the uart into rx and tx channels and create MidiIn/Out interfaces
let (midi_reader, midi_writer) = midi_uart.split();
let midi_in = MidiIn::new(midi_reader);
let midi_out = MidiOut::new(midi_writer);
// DISPLAY
// configure i2c pins
let sda_pin = pins.gpio26.into_mode::<FunctionI2C>();
let scl_pin = pins.gpio27.into_mode::<FunctionI2C>();
// create i2c driver
let i2c = I2C::i2c1(
ctx.device.I2C1,
sda_pin,
scl_pin,
1.MHz(),
&mut ctx.device.RESETS,
&clocks.peripheral_clock,
);
// create i2c display interface
let mut display = Ssd1306::new(
I2CDisplayInterface::new_alternate_address(i2c),
DisplaySize128x64,
DisplayRotation::Rotate0
).into_buffered_graphics_mode();
// intialise display
display.init().expect("init: display initialisation failed");
// show splash screen
display.clear();
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_8X13_ITALIC)
.text_color(BinaryColor::On)
.build();
Text::with_baseline("MICROGROOVE", Point::new(20, 20), text_style, Baseline::Top)
.draw(&mut display)
.unwrap();
display.flush().unwrap();
info!("init: display initialised");
// RTIC MONOTONIC
// create a monotonic timer for RTIC (1us resolution!)
let monotonic_alarm = timer.alarm_0().unwrap();
let monotonic_timer = Monotonic::new(timer, monotonic_alarm);
// configure source of timestamps for defmt
defmt::timestamp!("{=u64:us}", {
monotonics::now().duration_since_epoch().to_micros()
});
// APP STATE
let playing = false;
// show track page of UI at startup
let input_mode = InputMode::Track;
// initial encoder positions
let encoder0_pos = 0;
let encoder1_pos = 1;
let encoder2_pos = 2;
let encoder3_pos = 3;
let encoder4_pos = 4;
let encoder5_pos = 5;
// create a track
let mut tracks = Vec::new();
tracks.push(Some(Track::new())).unwrap();
// buffer to collect MIDI tick intervals
let tick_history = HistoryBuffer::<u64, 24>::new();
// LET'S GOOOO!!
// start reading encoders
read_encoders::spawn().unwrap();
// start scheduled display updates
display_update::spawn().unwrap();
info!("init: complete 🤘");
(
Shared {
input_mode,
playing,
encoder0_pos,
encoder1_pos,
encoder2_pos,
encoder3_pos,
encoder4_pos,
encoder5_pos,
tracks,
},
Local {
midi_in,
midi_out,
display,
button_track_pin,
button_groove_pin,
button_melody_pin,
encoder0,
encoder1,
encoder2,
encoder3,
encoder4,
encoder5,
tick_history,
},
init::Monotonics(monotonic_timer)
)
}
// handles UART0 interrupts, which is MIDI input
#[task(
binds = UART0_IRQ,
priority = 4,
shared = [playing],
local = [midi_in]
)]
fn uart0_irq(mut ctx: uart0_irq::Context) {
// check midi input for messages
trace!("a wild uart0 interrupt has fired!");
// read those sweet sweet midi bytes!
if let Ok(message) = block!(ctx.local.midi_in.read()) {
// log the message
match message {
MidiMessage::TimingClock => {
trace!("midi: clock");
// if clock, spawn task to tick tracks and potentially generate midi output
ctx.shared.playing.lock(|playing| {
if *playing {
sequencer_advance::spawn().expect("sequencer_advance::spawn() should succeed");
}
});
}
MidiMessage::Start => {
info!("midi: start");
ctx.shared.playing.lock(|playing| {
*playing = true;
});
}
MidiMessage::Stop => {
info!("midi: stop");
ctx.shared.playing.lock(|playing| {
*playing = false;
});
}
MidiMessage::Continue => {
info!("midi: continue");
ctx.shared.playing.lock(|playing| {
*playing = true;
});
}
_ => trace!("midi: UNKNOWN"),
}
// pass received message to midi out ("soft thru")
match midi_send::spawn(message) {
Ok(_) => (),
Err(_) => error!("could not spawn midi_send to pass through message"),
}
}
}
#[task(
priority = 3,
capacity = 64,
local = [midi_out]
)]
fn midi_send(ctx: midi_send::Context, message: MidiMessage) {
trace!("midi_send");
match message {
MidiMessage::TimingClock => trace!("midi_send: clock"),
MidiMessage::Start => trace!("midi_send: start"),
MidiMessage::Stop => trace!("midi_send: stop"),
MidiMessage::Continue => trace!("midi_send: continue"),
MidiMessage::NoteOn(midi_channel, note, velocity) => {
let midi_channel: u8 = midi_channel.into();
let note: u8 = note.into();
let velocity: u8 = velocity.into();
debug!("midi_send: note on midi_channel={} note={} velocity={}", midi_channel, note, velocity);
}
MidiMessage::NoteOff(midi_channel, note, _velocity) => {
let midi_channel: u8 = midi_channel.into();
let note: u8 = note.into();
debug!("midi_send: note off midi_channel={} note={}", midi_channel, note);
}
_ => trace!("midi: UNKNOWN"),
}
ctx.local.midi_out.write(&message).expect("midi_out.write(message) should succeed");
}
#[task(
priority = 2,
shared = [tracks],
local = [ticks: u32 = 0, last_tick_instant: Option<TimerMonotonicInstant> = None, tick_history]
)]
fn sequencer_advance(mut ctx: sequencer_advance::Context) {
trace!("sequencer_advance");
let sequencer_advance::LocalResources { ticks, last_tick_instant, tick_history } = ctx.local;
// calculate average interval between last K ticks
// TODO should move to some impl
let mut tick_duration: MicrosDurationU64 = 20_830.micros(); // time between ticks at 120bpm
if let Some(last_tick_instant) = *last_tick_instant {
let last_tick_duration = monotonics::now().checked_duration_since(last_tick_instant).unwrap().to_micros();
tick_history.write(last_tick_duration);
tick_duration = (tick_history.as_slice().iter().sum::<u64>() / tick_history.len() as u64).micros();
}
*last_tick_instant = Some(monotonics::now());
trace!("sequencer_advance: tick_duration={}", tick_duration.to_micros());
// TODO should all move out of task, e.g. Tracks::advance()?
ctx.shared.tracks.lock(|tracks| {
for track in tracks.as_slice() {
if let Some(track) = track {
if *ticks % (track.time_division as u32) == 0 {
let step_num = (*ticks % track.length as u32) as usize;
if let Some(step) = &track.steps.get(step_num).unwrap() {
let note_on_message = MidiMessage::NoteOn(track.midi_channel, step.note, step.velocity);
let midi_channel: u8 = track.midi_channel.into();
let note: u8 = step.note.into();
let velocity: u8 = step.velocity.into();
trace!("sequencer_advance: note_on channel={} note={} velocity={}", midi_channel, note, velocity);
match midi_send::spawn(note_on_message) {
Ok(_) => (),
Err(_error) => error!("could not spawn midi_send for note on message"),
}
let note_off_message = MidiMessage::NoteOff(track.midi_channel, step.note, 0.into());
let note_off_time = ((tick_duration.to_micros() * (track.time_division as u64) * step.length_step_cents as u64) / 100).micros();
trace!("sequencer_advance: scheduling note off message for {}us", note_off_time.to_micros());
match midi_send::spawn_after(note_off_time, note_off_message) {
Ok(_) => (),
Err(_error) => error!("could not spawn midi_send for note off message"),
}
}
}
}
}
});
*ticks += 1; // will overflow after a few years of continuous play
}
// handle button pin interrupts
#[task(
binds = IO_IRQ_BANK0,
priority = 4,
shared = [input_mode],
local = [button_track_pin, button_groove_pin, button_melody_pin]
)]
fn io_irq_bank0(mut ctx: io_irq_bank0::Context) {
trace!("a wild gpio_bank0 interrupt has fired!");
// for each button, check interrupt status to see if we fired
if ctx.local.button_track_pin.interrupt_status(EdgeLow) {
info!("track button pressed");
ctx.shared.input_mode.lock(|input_mode| {
*input_mode = InputMode::Track;
});
ctx.local.button_track_pin.clear_interrupt(EdgeLow);
}
if ctx.local.button_groove_pin.interrupt_status(EdgeLow) {
info!("groove button pressed");
ctx.shared.input_mode.lock(|input_mode| {
*input_mode = InputMode::Groove;
});
ctx.local.button_groove_pin.clear_interrupt(EdgeLow);
}
if ctx.local.button_melody_pin.interrupt_status(EdgeLow) {
info!("melody button pressed");
ctx.shared.input_mode.lock(|input_mode| {
*input_mode = InputMode::Melody;
});
ctx.local.button_melody_pin.clear_interrupt(EdgeLow);
}
}
/// Check encoders every 1ms to remove some of the noise vs checking on interrupt.
#[task(
priority = 4,
shared = [encoder0_pos, encoder1_pos, encoder2_pos, encoder3_pos, encoder4_pos, encoder5_pos],
local = [encoder0, encoder1, encoder2, encoder3, encoder4, encoder5],
)]
fn read_encoders(ctx: read_encoders::Context) {
let (l, mut s) = (ctx.local, ctx.shared);
update_encoder_pos(l.encoder0, &mut s.encoder0_pos);
update_encoder_pos(l.encoder1, &mut s.encoder1_pos);
update_encoder_pos(l.encoder2, &mut s.encoder2_pos);
update_encoder_pos(l.encoder3, &mut s.encoder3_pos);
update_encoder_pos(l.encoder4, &mut s.encoder4_pos);
update_encoder_pos(l.encoder5, &mut s.encoder5_pos);
// read again in 1ms
read_encoders::spawn_after(ENCODER_READ_INTERVAL).unwrap();
}
fn update_encoder_pos(encoder: &mut AnyEncoder, mut encoder_pos: impl Mutex<T = i8>) {
match encoder.update() {
Ok(Direction::Clockwise) => {
encoder_pos.lock(|pos| {
*pos += 1;
});
}
Ok(Direction::CounterClockwise) => {
encoder_pos.lock(|pos| {
*pos -= 1;
});
}
Ok(Direction::None) => {}
Err(_error) => { error!("could not update encoder"); }
}
}
#[task(
priority = 1,
shared = [playing, input_mode, encoder0_pos],
local = [display, display_ticks: u32 = 0]
)]
fn display_update(mut ctx: display_update::Context) {
*ctx.local.display_ticks += 1;
ctx.local.display.clear();
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_4X6)
.text_color(BinaryColor::On)
.build();
Text::with_baseline("MICROGROOVE", Point::zero(), text_style, Baseline::Top)
.draw(&mut *ctx.local.display)
.unwrap();
ctx.shared.playing.lock(|playing| {
let text: String<30> = String::from(if *playing { "PLAYING" } else { "STOPPED" });
Text::with_baseline(text.as_str(), Point::new(0, 6), text_style, Baseline::Top)
.draw(&mut *ctx.local.display)
.unwrap();
});
ctx.shared.input_mode.lock(|input_mode| {
let mut text: String<30> = String::new();
let mode_text = match *input_mode {
InputMode::Track => "TRACK",
InputMode::Groove => "GROOVE",
InputMode::Melody => "MELODY",
};
let _ = write!(text, "MODE: {}", mode_text);
Text::with_baseline(text.as_str(), Point::new(0, 12), text_style, Baseline::Top)
.draw(&mut *ctx.local.display)
.unwrap();
});
ctx.shared.encoder0_pos.lock(|encoder0_pos| {
let mut text: String<30> = String::new();
let _ = write!(text, "ENC0: {}", *encoder0_pos);
Text::with_baseline(text.as_str(), Point::new(0, 18), text_style, Baseline::Top)
.draw(&mut *ctx.local.display)
.unwrap();
});
ctx.local.display.flush().unwrap();
display_update::spawn_after(DISPLAY_UPDATE_INTERVAL).expect("should be able to spawn_after display_update");
}
// idle task needed in debug mode, default RTIC idle task calls wfi(), which breaks rtt
#[idle]
fn task_main(_: task_main::Context) -> ! {
loop {
cortex_m::asm::nop();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment