Init
This commit is contained in:
141
src/engine/audio.rs
Normal file
141
src/engine/audio.rs
Normal 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
89
src/engine/link.rs
Normal 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
10
src/engine/mod.rs
Normal 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
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