Lots + MIDI implementation
This commit is contained in:
@@ -5,6 +5,6 @@ pub mod sequencer;
|
||||
pub use audio::{build_stream, AnalysisHandle, AudioStreamConfig, ScopeBuffer, SpectrumBuffer};
|
||||
pub use link::LinkState;
|
||||
pub use sequencer::{
|
||||
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand, SequencerConfig,
|
||||
SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||
spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand,
|
||||
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::time::Duration;
|
||||
use thread_priority::{set_current_thread_priority, ThreadPriority};
|
||||
|
||||
use super::LinkState;
|
||||
use crate::model::{Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables};
|
||||
use crate::model::{CcMemory, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables};
|
||||
use crate::model::{LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::state::LiveKeyState;
|
||||
|
||||
@@ -51,6 +51,13 @@ pub enum AudioCommand {
|
||||
ResetEngine,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MidiCommand {
|
||||
NoteOn { channel: u8, note: u8, velocity: u8 },
|
||||
NoteOff { channel: u8, note: u8 },
|
||||
CC { channel: u8, cc: u8, value: u8 },
|
||||
}
|
||||
|
||||
pub enum SeqCommand {
|
||||
PatternUpdate {
|
||||
bank: usize,
|
||||
@@ -142,6 +149,7 @@ impl SequencerSnapshot {
|
||||
pub struct SequencerHandle {
|
||||
pub cmd_tx: Sender<SeqCommand>,
|
||||
pub audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
pub midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
@@ -163,6 +171,12 @@ impl SequencerHandle {
|
||||
new_rx
|
||||
}
|
||||
|
||||
pub fn swap_midi_channel(&self) -> Receiver<MidiCommand> {
|
||||
let (new_tx, new_rx) = bounded::<MidiCommand>(256);
|
||||
self.midi_tx.store(Arc::new(new_tx));
|
||||
new_rx
|
||||
}
|
||||
|
||||
pub fn shutdown(self) {
|
||||
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||
if let Err(e) = self.thread.join() {
|
||||
@@ -191,6 +205,7 @@ struct AudioState {
|
||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||
pending_starts: Vec<PendingPattern>,
|
||||
pending_stops: Vec<PendingPattern>,
|
||||
flush_midi_notes: bool,
|
||||
}
|
||||
|
||||
impl AudioState {
|
||||
@@ -200,6 +215,7 @@ impl AudioState {
|
||||
active_patterns: HashMap::new(),
|
||||
pending_starts: Vec::new(),
|
||||
pending_stops: Vec::new(),
|
||||
flush_midi_notes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +224,7 @@ pub struct SequencerConfig {
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
pub lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
|
||||
pub cc_memory: Option<CcMemory>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -221,14 +238,17 @@ pub fn spawn_sequencer(
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
config: SequencerConfig,
|
||||
) -> (SequencerHandle, Receiver<AudioCommand>) {
|
||||
) -> (SequencerHandle, Receiver<AudioCommand>, Receiver<MidiCommand>) {
|
||||
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
|
||||
let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
|
||||
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
|
||||
let midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));
|
||||
|
||||
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
|
||||
let shared_state_clone = Arc::clone(&shared_state);
|
||||
let audio_tx_for_thread = Arc::clone(&audio_tx);
|
||||
let midi_tx_for_thread = Arc::clone(&midi_tx);
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("sequencer".into())
|
||||
@@ -236,6 +256,7 @@ pub fn spawn_sequencer(
|
||||
sequencer_loop(
|
||||
cmd_rx,
|
||||
audio_tx_for_thread,
|
||||
midi_tx_for_thread,
|
||||
link,
|
||||
playing,
|
||||
variables,
|
||||
@@ -248,6 +269,7 @@ pub fn spawn_sequencer(
|
||||
config.audio_sample_pos,
|
||||
config.sample_rate,
|
||||
config.lookahead_ms,
|
||||
config.cc_memory,
|
||||
);
|
||||
})
|
||||
.expect("Failed to spawn sequencer thread");
|
||||
@@ -255,10 +277,11 @@ pub fn spawn_sequencer(
|
||||
let handle = SequencerHandle {
|
||||
cmd_tx,
|
||||
audio_tx,
|
||||
midi_tx,
|
||||
shared_state,
|
||||
thread,
|
||||
};
|
||||
(handle, audio_rx)
|
||||
(handle, audio_rx, midi_rx)
|
||||
}
|
||||
|
||||
struct PatternCache {
|
||||
@@ -388,6 +411,7 @@ pub(crate) struct TickOutput {
|
||||
pub audio_commands: Vec<TimestampedCommand>,
|
||||
pub new_tempo: Option<f64>,
|
||||
pub shared_state: SharedSequencerState,
|
||||
pub flush_midi_notes: bool,
|
||||
}
|
||||
|
||||
struct StepResult {
|
||||
@@ -434,6 +458,12 @@ impl KeyCache {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ActiveNote {
|
||||
off_time_us: i64,
|
||||
start_time_us: i64,
|
||||
}
|
||||
|
||||
pub(crate) struct SequencerState {
|
||||
audio_state: AudioState,
|
||||
pattern_cache: PatternCache,
|
||||
@@ -446,10 +476,12 @@ pub(crate) struct SequencerState {
|
||||
speed_overrides: HashMap<(usize, usize), f64>,
|
||||
key_cache: KeyCache,
|
||||
buf_audio_commands: Vec<TimestampedCommand>,
|
||||
cc_memory: Option<CcMemory>,
|
||||
active_notes: HashMap<(u8, u8), ActiveNote>,
|
||||
}
|
||||
|
||||
impl SequencerState {
|
||||
pub fn new(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||
pub fn new(variables: Variables, dict: Dictionary, rng: Rng, cc_memory: Option<CcMemory>) -> Self {
|
||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
||||
Self {
|
||||
audio_state: AudioState::new(),
|
||||
@@ -463,6 +495,8 @@ impl SequencerState {
|
||||
speed_overrides: HashMap::new(),
|
||||
key_cache: KeyCache::new(),
|
||||
buf_audio_commands: Vec::new(),
|
||||
cc_memory,
|
||||
active_notes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,6 +547,7 @@ impl SequencerState {
|
||||
self.audio_state.pending_stops.clear();
|
||||
Arc::make_mut(&mut self.step_traces).clear();
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
SeqCommand::Shutdown => {}
|
||||
}
|
||||
@@ -556,10 +591,12 @@ impl SequencerState {
|
||||
|
||||
self.audio_state.prev_beat = beat;
|
||||
|
||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||
TickOutput {
|
||||
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||
new_tempo: vars.new_tempo,
|
||||
shared_state: self.build_shared_state(),
|
||||
flush_midi_notes: flush,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,10 +610,12 @@ impl SequencerState {
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.buf_audio_commands.clear();
|
||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||
TickOutput {
|
||||
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||
new_tempo: None,
|
||||
shared_state: self.build_shared_state(),
|
||||
flush_midi_notes: flush,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,6 +738,7 @@ impl SequencerState {
|
||||
speed: speed_mult,
|
||||
fill,
|
||||
nudge_secs,
|
||||
cc_memory: self.cc_memory.clone(),
|
||||
};
|
||||
if let Some(script) = resolved_script {
|
||||
let mut trace = ExecutionTrace::default();
|
||||
@@ -841,6 +881,7 @@ impl SequencerState {
|
||||
fn sequencer_loop(
|
||||
cmd_rx: Receiver<SeqCommand>,
|
||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
variables: Variables,
|
||||
@@ -853,12 +894,13 @@ fn sequencer_loop(
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
|
||||
cc_memory: Option<CcMemory>,
|
||||
) {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
let _ = set_current_thread_priority(ThreadPriority::Max);
|
||||
|
||||
let mut seq_state = SequencerState::new(variables, dict, rng);
|
||||
let mut seq_state = SequencerState::new(variables, dict, rng, cc_memory);
|
||||
|
||||
loop {
|
||||
let mut commands = Vec::new();
|
||||
@@ -899,18 +941,66 @@ fn sequencer_loop(
|
||||
let output = seq_state.tick(input);
|
||||
|
||||
for tsc in output.audio_commands {
|
||||
let cmd = AudioCommand::Evaluate {
|
||||
cmd: tsc.cmd,
|
||||
time: tsc.time,
|
||||
};
|
||||
match audio_tx.load().try_send(cmd) {
|
||||
Ok(()) => {}
|
||||
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
|
||||
seq_state.dropped_events += 1;
|
||||
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
|
||||
match midi_tx.load().try_send(midi_cmd.clone()) {
|
||||
Ok(()) => {
|
||||
if let (MidiCommand::NoteOn { channel, note, .. }, Some(dur_secs)) =
|
||||
(&midi_cmd, dur)
|
||||
{
|
||||
let dur_us = (dur_secs * 1_000_000.0) as i64;
|
||||
seq_state.active_notes.insert(
|
||||
(*channel, *note),
|
||||
ActiveNote {
|
||||
off_time_us: current_time_us + dur_us,
|
||||
start_time_us: current_time_us,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
|
||||
seq_state.dropped_events += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cmd = AudioCommand::Evaluate {
|
||||
cmd: tsc.cmd,
|
||||
time: tsc.time,
|
||||
};
|
||||
match audio_tx.load().try_send(cmd) {
|
||||
Ok(()) => {}
|
||||
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => {
|
||||
seq_state.dropped_events += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_NOTE_DURATION_US: i64 = 30_000_000; // 30 second safety timeout
|
||||
|
||||
if output.flush_midi_notes {
|
||||
for ((channel, note), _) in seq_state.active_notes.drain() {
|
||||
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { channel, note });
|
||||
}
|
||||
// Send MIDI panic (CC 123 = All Notes Off) on all 16 channels
|
||||
for chan in 0..16u8 {
|
||||
let _ = midi_tx
|
||||
.load()
|
||||
.try_send(MidiCommand::CC { channel: chan, cc: 123, value: 0 });
|
||||
}
|
||||
} else {
|
||||
seq_state.active_notes.retain(|&(channel, note), active| {
|
||||
let should_release = current_time_us >= active.off_time_us;
|
||||
let timed_out = (current_time_us - active.start_time_us) > MAX_NOTE_DURATION_US;
|
||||
|
||||
if should_release || timed_out {
|
||||
let _ = midi_tx.load().try_send(MidiCommand::NoteOff { channel, note });
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(t) = output.new_tempo {
|
||||
link.set_tempo(t);
|
||||
}
|
||||
@@ -921,6 +1011,48 @@ fn sequencer_loop(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
||||
if !cmd.starts_with("/midi/") {
|
||||
return None;
|
||||
}
|
||||
let parts: Vec<&str> = cmd.split('/').filter(|s| !s.is_empty()).collect();
|
||||
if parts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
match parts[1] {
|
||||
"note" => {
|
||||
// /midi/note/<note>/vel/<vel>/chan/<chan>/dur/<dur>
|
||||
let note: u8 = parts.get(2)?.parse().ok()?;
|
||||
let vel: u8 = parts.get(4)?.parse().ok()?;
|
||||
let chan: u8 = parts.get(6)?.parse().ok()?;
|
||||
let dur: Option<f64> = parts.get(8).and_then(|s| s.parse().ok());
|
||||
Some((
|
||||
MidiCommand::NoteOn {
|
||||
channel: chan,
|
||||
note,
|
||||
velocity: vel,
|
||||
},
|
||||
dur,
|
||||
))
|
||||
}
|
||||
"cc" => {
|
||||
// /midi/cc/<cc>/<val>/chan/<chan>
|
||||
let cc: u8 = parts.get(2)?.parse().ok()?;
|
||||
let val: u8 = parts.get(3)?.parse().ok()?;
|
||||
let chan: u8 = parts.get(5)?.parse().ok()?;
|
||||
Some((
|
||||
MidiCommand::CC {
|
||||
channel: chan,
|
||||
cc,
|
||||
value: val,
|
||||
},
|
||||
None,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -932,7 +1064,7 @@ mod tests {
|
||||
let rng: Rng = Arc::new(Mutex::new(
|
||||
<rand::rngs::StdRng as rand::SeedableRng>::seed_from_u64(0),
|
||||
));
|
||||
SequencerState::new(variables, dict, rng)
|
||||
SequencerState::new(variables, dict, rng, None)
|
||||
}
|
||||
|
||||
fn simple_pattern(length: usize) -> PatternSnapshot {
|
||||
|
||||
Reference in New Issue
Block a user