This commit is contained in:
2026-01-21 17:05:30 +01:00
commit 67322381c3
59 changed files with 10421 additions and 0 deletions

141
src/engine/audio.rs Normal file
View File

@@ -0,0 +1,141 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::Stream;
use crossbeam_channel::Receiver;
use doux::{Engine, EngineMetrics};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use super::AudioCommand;
pub struct ScopeBuffer {
pub samples: [AtomicU32; 64],
peak_left: AtomicU32,
peak_right: AtomicU32,
}
impl ScopeBuffer {
pub fn new() -> Self {
Self {
samples: std::array::from_fn(|_| AtomicU32::new(0)),
peak_left: AtomicU32::new(0),
peak_right: AtomicU32::new(0),
}
}
pub fn write(&self, data: &[f32]) {
let mut peak_l: f32 = 0.0;
let mut peak_r: f32 = 0.0;
for (i, atom) in self.samples.iter().enumerate() {
let idx = i * 2;
let left = data.get(idx).copied().unwrap_or(0.0);
let right = data.get(idx + 1).copied().unwrap_or(0.0);
peak_l = peak_l.max(left.abs());
peak_r = peak_r.max(right.abs());
atom.store(left.to_bits(), Ordering::Relaxed);
}
self.peak_left.store(peak_l.to_bits(), Ordering::Relaxed);
self.peak_right.store(peak_r.to_bits(), Ordering::Relaxed);
}
pub fn read(&self) -> [f32; 64] {
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
}
pub fn peaks(&self) -> (f32, f32) {
let left = f32::from_bits(self.peak_left.load(Ordering::Relaxed));
let right = f32::from_bits(self.peak_right.load(Ordering::Relaxed));
(left, right)
}
}
pub struct AudioStreamConfig {
pub output_device: Option<String>,
pub channels: u16,
pub buffer_size: u32,
}
pub fn build_stream(
config: &AudioStreamConfig,
audio_rx: Receiver<AudioCommand>,
scope_buffer: Arc<ScopeBuffer>,
metrics: Arc<EngineMetrics>,
initial_samples: Vec<doux::sample::SampleEntry>,
) -> Result<(Stream, f32), String> {
let host = cpal::default_host();
let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name)
.ok_or_else(|| format!("Device not found: {name}"))?,
None => host
.default_output_device()
.ok_or("No default output device")?,
};
let default_config = device.default_output_config().map_err(|e| e.to_string())?;
let sample_rate = default_config.sample_rate().0 as f32;
let buffer_size = if config.buffer_size > 0 {
cpal::BufferSize::Fixed(config.buffer_size)
} else {
cpal::BufferSize::Default
};
let stream_config = cpal::StreamConfig {
channels: config.channels,
sample_rate: default_config.sample_rate(),
buffer_size,
};
let sr = sample_rate;
let channels = config.channels as usize;
let metrics_clone = Arc::clone(&metrics);
let mut engine = Engine::new_with_metrics(sample_rate, channels, Arc::clone(&metrics));
engine.sample_index = initial_samples;
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _| {
let buffer_samples = data.len() / channels;
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
while let Ok(cmd) = audio_rx.try_recv() {
match cmd {
AudioCommand::Evaluate(s) => {
engine.evaluate(&s);
}
AudioCommand::Hush => {
engine.hush();
}
AudioCommand::Panic => {
engine.panic();
}
AudioCommand::LoadSamples(samples) => {
engine.sample_index.extend(samples);
}
AudioCommand::ResetEngine => {
let old_samples = std::mem::take(&mut engine.sample_index);
engine =
Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
engine.sample_index = old_samples;
}
}
}
engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(data, &[], &[]);
scope_buffer.write(&engine.output);
},
|err| eprintln!("stream error: {err}"),
None,
)
.map_err(|e| format!("Failed to build stream: {e}"))?;
stream
.play()
.map_err(|e| format!("Failed to play stream: {e}"))?;
Ok((stream, sample_rate))
}

89
src/engine/link.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::sync::atomic::{AtomicU64, Ordering};
use rusty_link::{AblLink, SessionState};
pub struct LinkState {
link: AblLink,
quantum: AtomicU64,
}
impl LinkState {
pub fn new(tempo: f64, quantum: f64) -> Self {
let link = AblLink::new(tempo);
Self {
link,
quantum: AtomicU64::new(quantum.to_bits()),
}
}
pub fn is_enabled(&self) -> bool {
self.link.is_enabled()
}
pub fn set_enabled(&self, enabled: bool) {
self.link.enable(enabled);
}
pub fn enable(&self) {
self.link.enable(true);
}
pub fn is_start_stop_sync_enabled(&self) -> bool {
self.link.is_start_stop_sync_enabled()
}
pub fn set_start_stop_sync_enabled(&self, enabled: bool) {
self.link.enable_start_stop_sync(enabled);
}
pub fn quantum(&self) -> f64 {
f64::from_bits(self.quantum.load(Ordering::Relaxed))
}
pub fn set_quantum(&self, quantum: f64) {
let clamped = quantum.clamp(1.0, 16.0);
self.quantum.store(clamped.to_bits(), Ordering::Relaxed);
}
pub fn clock_micros(&self) -> i64 {
self.link.clock_micros()
}
pub fn tempo(&self) -> f64 {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
state.tempo()
}
pub fn beat(&self) -> f64 {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
let time = self.link.clock_micros();
state.beat_at_time(time, self.quantum())
}
pub fn phase(&self) -> f64 {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
let time = self.link.clock_micros();
state.phase_at_time(time, self.quantum())
}
pub fn peers(&self) -> u64 {
self.link.num_peers()
}
pub fn set_tempo(&self, tempo: f64) {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
let time = self.link.clock_micros();
state.set_tempo(tempo, time);
self.link.commit_app_session_state(&state);
}
pub fn capture_app_state(&self) -> SessionState {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
state
}
}

10
src/engine/mod.rs Normal file
View File

@@ -0,0 +1,10 @@
mod audio;
mod link;
mod sequencer;
pub use audio::{build_stream, AudioStreamConfig, ScopeBuffer};
pub use link::LinkState;
pub use sequencer::{
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand, SequencerSnapshot,
StepSnapshot,
};

461
src/engine/sequencer.rs Normal file
View 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));
}
}