Init
This commit is contained in:
461
src/engine/sequencer.rs
Normal file
461
src/engine/sequencer.rs
Normal file
@@ -0,0 +1,461 @@
|
||||
use crossbeam_channel::{bounded, Receiver, Sender, TrySendError};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::LinkState;
|
||||
use crate::config::{MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::model::{ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Variables};
|
||||
use crate::state::LiveKeyState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct PatternId {
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum PatternChange {
|
||||
Start { bank: usize, pattern: usize },
|
||||
Stop { bank: usize, pattern: usize },
|
||||
}
|
||||
|
||||
impl PatternChange {
|
||||
pub fn pattern_id(&self) -> PatternId {
|
||||
match self {
|
||||
PatternChange::Start { bank, pattern } => PatternId {
|
||||
bank: *bank,
|
||||
pattern: *pattern,
|
||||
},
|
||||
PatternChange::Stop { bank, pattern } => PatternId {
|
||||
bank: *bank,
|
||||
pattern: *pattern,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AudioCommand {
|
||||
Evaluate(String),
|
||||
Hush,
|
||||
Panic,
|
||||
LoadSamples(Vec<doux::sample::SampleEntry>),
|
||||
#[allow(dead_code)]
|
||||
ResetEngine,
|
||||
}
|
||||
|
||||
pub enum SeqCommand {
|
||||
PatternUpdate {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
data: PatternSnapshot,
|
||||
},
|
||||
PatternStart {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
},
|
||||
PatternStop {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PatternSnapshot {
|
||||
pub speed: crate::model::PatternSpeed,
|
||||
pub length: usize,
|
||||
pub steps: Vec<StepSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StepSnapshot {
|
||||
pub active: bool,
|
||||
pub script: String,
|
||||
pub source: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
pub struct ActivePatternState {
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
pub step_index: usize,
|
||||
pub iter: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SharedSequencerState {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
pub pattern_traces: HashMap<PatternId, Vec<SourceSpan>>,
|
||||
pub event_count: usize,
|
||||
}
|
||||
|
||||
pub struct SequencerSnapshot {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
pub pattern_traces: HashMap<PatternId, Vec<SourceSpan>>,
|
||||
pub event_count: usize,
|
||||
}
|
||||
|
||||
impl SequencerSnapshot {
|
||||
pub fn is_playing(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.active_patterns
|
||||
.iter()
|
||||
.any(|p| p.bank == bank && p.pattern == pattern)
|
||||
}
|
||||
|
||||
pub fn get_step(&self, bank: usize, pattern: usize) -> Option<usize> {
|
||||
self.active_patterns
|
||||
.iter()
|
||||
.find(|p| p.bank == bank && p.pattern == pattern)
|
||||
.map(|p| p.step_index)
|
||||
}
|
||||
|
||||
pub fn get_iter(&self, bank: usize, pattern: usize) -> Option<usize> {
|
||||
self.active_patterns
|
||||
.iter()
|
||||
.find(|p| p.bank == bank && p.pattern == pattern)
|
||||
.map(|p| p.iter)
|
||||
}
|
||||
|
||||
pub fn get_trace(&self, bank: usize, pattern: usize) -> Option<&Vec<SourceSpan>> {
|
||||
self.pattern_traces.get(&PatternId { bank, pattern })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SequencerHandle {
|
||||
pub cmd_tx: Sender<SeqCommand>,
|
||||
pub audio_tx: Sender<AudioCommand>,
|
||||
pub audio_rx: Receiver<AudioCommand>,
|
||||
shared_state: Arc<Mutex<SharedSequencerState>>,
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl SequencerHandle {
|
||||
pub fn snapshot(&self) -> SequencerSnapshot {
|
||||
let state = self.shared_state.lock().unwrap();
|
||||
SequencerSnapshot {
|
||||
active_patterns: state.active_patterns.clone(),
|
||||
pattern_traces: state.pattern_traces.clone(),
|
||||
event_count: state.event_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(self) {
|
||||
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||
let _ = self.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct ActivePattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
step_index: usize,
|
||||
iter: usize,
|
||||
}
|
||||
|
||||
struct AudioState {
|
||||
prev_beat: f64,
|
||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||
pending_starts: Vec<PatternId>,
|
||||
pending_stops: Vec<PatternId>,
|
||||
}
|
||||
|
||||
impl AudioState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prev_beat: -1.0,
|
||||
active_patterns: HashMap::new(),
|
||||
pending_starts: Vec::new(),
|
||||
pending_stops: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_sequencer(
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
variables: Variables,
|
||||
rng: Rng,
|
||||
quantum: f64,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
) -> SequencerHandle {
|
||||
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
|
||||
|
||||
let shared_state = Arc::new(Mutex::new(SharedSequencerState::default()));
|
||||
let shared_state_clone = Arc::clone(&shared_state);
|
||||
let audio_tx_clone = audio_tx.clone();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("sequencer".into())
|
||||
.spawn(move || {
|
||||
sequencer_loop(
|
||||
cmd_rx,
|
||||
audio_tx_clone,
|
||||
link,
|
||||
playing,
|
||||
variables,
|
||||
rng,
|
||||
quantum,
|
||||
shared_state_clone,
|
||||
live_keys,
|
||||
);
|
||||
})
|
||||
.expect("Failed to spawn sequencer thread");
|
||||
|
||||
SequencerHandle {
|
||||
cmd_tx,
|
||||
audio_tx,
|
||||
audio_rx,
|
||||
shared_state,
|
||||
thread,
|
||||
}
|
||||
}
|
||||
|
||||
struct PatternCache {
|
||||
patterns: [[Option<PatternSnapshot>; MAX_PATTERNS]; MAX_BANKS],
|
||||
}
|
||||
|
||||
impl PatternCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
patterns: std::array::from_fn(|_| std::array::from_fn(|_| None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, bank: usize, pattern: usize) -> Option<&PatternSnapshot> {
|
||||
self.patterns
|
||||
.get(bank)
|
||||
.and_then(|b| b.get(pattern))
|
||||
.and_then(|p| p.as_ref())
|
||||
}
|
||||
|
||||
fn set(&mut self, bank: usize, pattern: usize, data: PatternSnapshot) {
|
||||
if bank < MAX_BANKS && pattern < MAX_PATTERNS {
|
||||
self.patterns[bank][pattern] = Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PatternSnapshot {
|
||||
fn resolve_source(&self, index: usize) -> usize {
|
||||
let mut current = index;
|
||||
for _ in 0..self.steps.len() {
|
||||
if let Some(step) = self.steps.get(current) {
|
||||
if let Some(source) = step.source {
|
||||
current = source;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
fn resolve_script(&self, index: usize) -> Option<&str> {
|
||||
let source_idx = self.resolve_source(index);
|
||||
self.steps.get(source_idx).map(|s| s.script.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
type StepKey = (usize, usize, usize);
|
||||
|
||||
struct RunsCounter {
|
||||
counts: HashMap<StepKey, usize>,
|
||||
}
|
||||
|
||||
impl RunsCounter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
counts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_and_increment(&mut self, bank: usize, pattern: usize, step: usize) -> usize {
|
||||
let key = (bank, pattern, step);
|
||||
let count = self.counts.entry(key).or_insert(0);
|
||||
let current = *count;
|
||||
*count += 1;
|
||||
current
|
||||
}
|
||||
}
|
||||
|
||||
fn sequencer_loop(
|
||||
cmd_rx: Receiver<SeqCommand>,
|
||||
audio_tx: Sender<AudioCommand>,
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
variables: Variables,
|
||||
rng: Rng,
|
||||
quantum: f64,
|
||||
shared_state: Arc<Mutex<SharedSequencerState>>,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
) {
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), rng);
|
||||
let mut audio_state = AudioState::new();
|
||||
let mut pattern_cache = PatternCache::new();
|
||||
let mut runs_counter = RunsCounter::new();
|
||||
let mut pattern_traces: HashMap<PatternId, Vec<SourceSpan>> = HashMap::new();
|
||||
let mut event_count: usize = 0;
|
||||
|
||||
loop {
|
||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||
match cmd {
|
||||
SeqCommand::PatternUpdate {
|
||||
bank,
|
||||
pattern,
|
||||
data,
|
||||
} => {
|
||||
pattern_cache.set(bank, pattern, data);
|
||||
}
|
||||
SeqCommand::PatternStart { bank, pattern } => {
|
||||
let id = PatternId { bank, pattern };
|
||||
audio_state.pending_stops.retain(|p| *p != id);
|
||||
if !audio_state.pending_starts.contains(&id) {
|
||||
audio_state.pending_starts.push(id);
|
||||
}
|
||||
}
|
||||
SeqCommand::PatternStop { bank, pattern } => {
|
||||
let id = PatternId { bank, pattern };
|
||||
audio_state.pending_starts.retain(|p| *p != id);
|
||||
if !audio_state.pending_stops.contains(&id) {
|
||||
audio_state.pending_stops.push(id);
|
||||
}
|
||||
}
|
||||
SeqCommand::Shutdown => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !playing.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_micros(500));
|
||||
continue;
|
||||
}
|
||||
|
||||
let state = link.capture_app_state();
|
||||
let time = link.clock_micros();
|
||||
let beat = state.beat_at_time(time, quantum);
|
||||
let tempo = state.tempo();
|
||||
|
||||
let bar = (beat / quantum).floor() as i64;
|
||||
let prev_bar = (audio_state.prev_beat / quantum).floor() as i64;
|
||||
if bar != prev_bar && audio_state.prev_beat >= 0.0 {
|
||||
for id in audio_state.pending_starts.drain(..) {
|
||||
audio_state.active_patterns.insert(
|
||||
id,
|
||||
ActivePattern {
|
||||
bank: id.bank,
|
||||
pattern: id.pattern,
|
||||
step_index: 0,
|
||||
iter: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
for id in audio_state.pending_stops.drain(..) {
|
||||
audio_state.active_patterns.remove(&id);
|
||||
pattern_traces.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
let prev_beat = audio_state.prev_beat;
|
||||
|
||||
for (id, active) in audio_state.active_patterns.iter_mut() {
|
||||
let Some(pattern) = pattern_cache.get(active.bank, active.pattern) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let speed_mult = pattern.speed.multiplier();
|
||||
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
|
||||
let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64;
|
||||
|
||||
if beat_int != prev_beat_int && prev_beat >= 0.0 {
|
||||
let step_idx = active.step_index % pattern.length;
|
||||
|
||||
if let Some(step) = pattern.steps.get(step_idx) {
|
||||
let resolved_script = pattern.resolve_script(step_idx);
|
||||
let has_script = resolved_script
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
|
||||
if step.active && has_script {
|
||||
let source_idx = pattern.resolve_source(step_idx);
|
||||
let runs =
|
||||
runs_counter.get_and_increment(active.bank, active.pattern, source_idx);
|
||||
let ctx = StepContext {
|
||||
step: step_idx,
|
||||
beat,
|
||||
bank: active.bank,
|
||||
pattern: active.pattern,
|
||||
tempo,
|
||||
phase: beat % quantum,
|
||||
slot: 0,
|
||||
runs,
|
||||
iter: active.iter,
|
||||
speed: speed_mult,
|
||||
fill: live_keys.fill(),
|
||||
};
|
||||
if let Some(script) = resolved_script {
|
||||
let mut trace = ExecutionTrace::default();
|
||||
if let Ok(cmds) =
|
||||
script_engine.evaluate_with_trace(script, &ctx, &mut trace)
|
||||
{
|
||||
pattern_traces
|
||||
.insert(*id, std::mem::take(&mut trace.selected_spans));
|
||||
for cmd in cmds {
|
||||
match audio_tx.try_send(AudioCommand::Evaluate(cmd)) {
|
||||
Ok(()) => {
|
||||
event_count += 1;
|
||||
}
|
||||
Err(TrySendError::Full(_)) => {}
|
||||
Err(TrySendError::Disconnected(_)) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(new_tempo) = {
|
||||
let mut vars = variables.lock().unwrap();
|
||||
vars.remove("__tempo__").and_then(|v| v.as_float().ok())
|
||||
} {
|
||||
link.set_tempo(new_tempo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let next_step = active.step_index + 1;
|
||||
if next_step >= pattern.length {
|
||||
active.iter += 1;
|
||||
}
|
||||
active.step_index = next_step % pattern.length;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = shared_state.lock().unwrap();
|
||||
state.active_patterns = audio_state
|
||||
.active_patterns
|
||||
.values()
|
||||
.map(|a| ActivePatternState {
|
||||
bank: a.bank,
|
||||
pattern: a.pattern,
|
||||
step_index: a.step_index,
|
||||
iter: a.iter,
|
||||
})
|
||||
.collect();
|
||||
state.pattern_traces = pattern_traces.clone();
|
||||
state.event_count = event_count;
|
||||
}
|
||||
|
||||
audio_state.prev_beat = beat;
|
||||
|
||||
thread::sleep(Duration::from_micros(500));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user