Less memory allocations at runtime

This commit is contained in:
2026-02-02 21:55:10 +01:00
parent 194030d953
commit 8024c18bb0
9 changed files with 146 additions and 457 deletions

View File

@@ -1,3 +1,5 @@
use std::sync::Arc;
use super::ops::Op; use super::ops::Op;
use super::types::{Dictionary, SourceSpan}; use super::types::{Dictionary, SourceSpan};
use super::words::compile_word; use super::words::compile_word;
@@ -118,7 +120,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
ops.push(Op::PushFloat(*f, Some(*span))); ops.push(Op::PushFloat(*f, Some(*span)));
} }
} }
Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))), Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
Token::Word(w, span) => { Token::Word(w, span) => {
let word = w.as_str(); let word = w.as_str();
if word == "{" { if word == "{" {
@@ -129,7 +131,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
start: span.start, start: span.start,
end: end_span.end, end: end_span.end,
}; };
ops.push(Op::Quotation(quote_ops, Some(body_span))); ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
} else if word == "}" { } else if word == "}" {
return Err("unexpected }".into()); return Err("unexpected }".into());
} else if word == ":" { } else if word == ":" {

View File

@@ -1,10 +1,12 @@
use std::sync::Arc;
use super::types::SourceSpan; use super::types::SourceSpan;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Op { pub enum Op {
PushInt(i64, Option<SourceSpan>), PushInt(i64, Option<SourceSpan>),
PushFloat(f64, Option<SourceSpan>), PushFloat(f64, Option<SourceSpan>),
PushStr(String, Option<SourceSpan>), PushStr(Arc<str>, Option<SourceSpan>),
Dup, Dup,
Dupn, Dupn,
Drop, Drop,
@@ -71,7 +73,7 @@ pub enum Op {
Ftom, Ftom,
SetTempo, SetTempo,
Every, Every,
Quotation(Vec<Op>, Option<SourceSpan>), Quotation(Arc<[Op]>, Option<SourceSpan>),
When, When,
Unless, Unless,
Adsr, Adsr,

View File

@@ -60,9 +60,9 @@ pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]);
pub enum Value { pub enum Value {
Int(i64, Option<SourceSpan>), Int(i64, Option<SourceSpan>),
Float(f64, Option<SourceSpan>), Float(f64, Option<SourceSpan>),
Str(String, Option<SourceSpan>), Str(Arc<str>, Option<SourceSpan>),
Quotation(Vec<Op>, Option<SourceSpan>), Quotation(Arc<[Op]>, Option<SourceSpan>),
CycleList(Vec<Value>), CycleList(Arc<[Value]>),
} }
impl PartialEq for Value { impl PartialEq for Value {
@@ -116,7 +116,7 @@ impl Value {
match self { match self {
Value::Int(i, _) => i.to_string(), Value::Int(i, _) => i.to_string(),
Value::Float(f, _) => f.to_string(), Value::Float(f, _) => f.to_string(),
Value::Str(s, _) => s.clone(), Value::Str(s, _) => s.to_string(),
Value::Quotation(..) => String::new(), Value::Quotation(..) => String::new(),
Value::CycleList(_) => String::new(), Value::CycleList(_) => String::new(),
} }

View File

@@ -1,6 +1,7 @@
use rand::rngs::StdRng; use rand::rngs::StdRng;
use rand::{Rng as RngTrait, SeedableRng}; use rand::{Rng as RngTrait, SeedableRng};
use std::borrow::Cow; use std::borrow::Cow;
use std::sync::Arc;
use super::compiler::compile_script; use super::compiler::compile_script;
use super::ops::Op; use super::ops::Op;
@@ -423,7 +424,7 @@ impl Forth {
let val = if values.len() == 1 { let val = if values.len() == 1 {
values.into_iter().next().unwrap() values.into_iter().next().unwrap()
} else { } else {
Value::CycleList(values) Value::CycleList(Arc::from(values))
}; };
cmd.set_sound(val); cmd.set_sound(val);
} }
@@ -435,7 +436,7 @@ impl Forth {
let val = if values.len() == 1 { let val = if values.len() == 1 {
values.into_iter().next().unwrap() values.into_iter().next().unwrap()
} else { } else {
Value::CycleList(values) Value::CycleList(Arc::from(values))
}; };
cmd.set_param(param.clone(), val); cmd.set_param(param.clone(), val);
} }
@@ -735,7 +736,7 @@ impl Forth {
} else { } else {
let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern); let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern);
let val = format!("{bank}:{pattern}"); let val = format!("{bank}:{pattern}");
self.vars.lock().unwrap().insert(key, Value::Str(val, None)); self.vars.lock().unwrap().insert(key, Value::Str(Arc::from(val), None));
} }
} }

View File

@@ -1,5 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::LazyLock; use std::sync::{Arc, LazyLock};
use super::ops::Op; use super::ops::Op;
use super::theory; use super::theory;
@@ -3031,7 +3031,7 @@ pub(super) fn compile_word(
// @varname - fetch variable // @varname - fetch variable
if let Some(var_name) = name.strip_prefix('@') { if let Some(var_name) = name.strip_prefix('@') {
if !var_name.is_empty() { if !var_name.is_empty() {
ops.push(Op::PushStr(var_name.to_string(), span)); ops.push(Op::PushStr(Arc::from(var_name), span));
ops.push(Op::Get); ops.push(Op::Get);
return true; return true;
} }
@@ -3040,7 +3040,7 @@ pub(super) fn compile_word(
// !varname - store into variable // !varname - store into variable
if let Some(var_name) = name.strip_prefix('!') { if let Some(var_name) = name.strip_prefix('!') {
if !var_name.is_empty() { if !var_name.is_empty() {
ops.push(Op::PushStr(var_name.to_string(), span)); ops.push(Op::PushStr(Arc::from(var_name), span));
ops.push(Op::Set); ops.push(Op::Set);
return true; return true;
} }
@@ -3073,6 +3073,6 @@ pub(super) fn compile_word(
} }
// Unrecognized token becomes a string // Unrecognized token becomes a string
ops.push(Op::PushStr(name.to_string(), span)); ops.push(Op::PushStr(Arc::from(name), span));
true true
} }

View File

@@ -438,25 +438,6 @@ fn check_quantization_boundary(
type StepKey = (usize, usize, usize); type StepKey = (usize, usize, usize);
/// Tracks a step that has been pre-evaluated via lookahead scheduling.
/// Used to prevent duplicate evaluation when the step's actual fire time arrives.
struct ScheduledStep {
target_beat: f64,
tempo_at_schedule: f64,
}
/// An audio command scheduled for future emission.
/// Commands are held here until their target_beat passes, then emitted to the audio engine.
struct PendingCommand {
cmd: TimestampedCommand,
target_beat: f64,
bank: usize,
pattern: usize,
}
/// Key for tracking scheduled steps: (bank, pattern, step_index, beat_int)
type ScheduledStepKey = (usize, usize, usize, i64);
struct RunsCounter { struct RunsCounter {
counts: HashMap<StepKey, usize>, counts: HashMap<StepKey, usize>,
} }
@@ -578,10 +559,6 @@ pub(crate) struct SequencerState {
active_notes: HashMap<(u8, u8, u8), ActiveNote>, active_notes: HashMap<(u8, u8, u8), ActiveNote>,
muted: std::collections::HashSet<(usize, usize)>, muted: std::collections::HashSet<(usize, usize)>,
soloed: std::collections::HashSet<(usize, usize)>, soloed: std::collections::HashSet<(usize, usize)>,
// Lookahead scheduling state
scheduled_steps: HashMap<(usize, usize, usize, i64), ScheduledStep>,
pending_commands: Vec<PendingCommand>,
last_tempo: f64,
} }
impl SequencerState { impl SequencerState {
@@ -608,9 +585,6 @@ impl SequencerState {
active_notes: HashMap::new(), active_notes: HashMap::new(),
muted: std::collections::HashSet::new(), muted: std::collections::HashSet::new(),
soloed: std::collections::HashSet::new(), soloed: std::collections::HashSet::new(),
scheduled_steps: HashMap::new(),
pending_commands: Vec::new(),
last_tempo: 120.0,
} }
} }
@@ -695,8 +669,6 @@ impl SequencerState {
Arc::make_mut(&mut self.step_traces).clear(); Arc::make_mut(&mut self.step_traces).clear();
self.runs_counter.counts.clear(); self.runs_counter.counts.clear();
self.audio_state.flush_midi_notes = true; self.audio_state.flush_midi_notes = true;
self.scheduled_steps.clear();
self.pending_commands.clear();
} }
SeqCommand::Shutdown => {} SeqCommand::Shutdown => {}
} }
@@ -815,65 +787,12 @@ impl SequencerState {
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
bank != pending.id.bank || pattern != pending.id.pattern bank != pending.id.bank || pattern != pending.id.pattern
}); });
// Clear scheduled steps and pending commands for this pattern
let (b, p) = (pending.id.bank, pending.id.pattern);
self.scheduled_steps
.retain(|&(bank, pattern, _, _), _| bank != b || pattern != p);
self.pending_commands
.retain(|cmd| cmd.bank != b || cmd.pattern != p);
stopped.push(pending.id); stopped.push(pending.id);
} }
} }
stopped stopped
} }
/// Convert a logical beat position to engine time for audio scheduling.
fn beat_to_engine_time(
target_beat: f64,
current_beat: f64,
engine_time: f64,
tempo: f64,
) -> f64 {
let beats_ahead = target_beat - current_beat;
let secs_ahead = beats_ahead * 60.0 / tempo;
engine_time + secs_ahead
}
/// Reschedule all pending commands when tempo changes.
fn reschedule_for_tempo_change(
&mut self,
new_tempo: f64,
current_beat: f64,
engine_time: f64,
) {
for pending in &mut self.pending_commands {
if pending.cmd.time.is_some() {
pending.cmd.time = Some(Self::beat_to_engine_time(
pending.target_beat,
current_beat,
engine_time,
new_tempo,
));
}
}
for step in self.scheduled_steps.values_mut() {
step.tempo_at_schedule = new_tempo;
}
self.last_tempo = new_tempo;
}
/// Main step execution with lookahead scheduling support.
///
/// This function handles two timing modes:
/// 1. **Immediate firing**: When a beat boundary is crossed (`beat_int != prev_beat_int`),
/// the current step fires. If already pre-evaluated via lookahead, we skip evaluation.
/// 2. **Lookahead pre-evaluation**: When `lookahead_secs > 0`, we pre-evaluate future steps
/// and queue their commands with precise timestamps for later emission.
///
/// The lookahead scheduling improves timing accuracy by:
/// - Evaluating scripts BEFORE their logical fire time
/// - Scheduling audio commands at exact beat positions using engine time
/// - Allowing the audio engine to play sounds at the precise moment
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn execute_steps( fn execute_steps(
&mut self, &mut self,
@@ -896,12 +815,6 @@ impl SequencerState {
any_step_fired: false, any_step_fired: false,
}; };
// Reschedule pending commands if tempo changed
if (tempo - self.last_tempo).abs() > 0.001 {
self.reschedule_for_tempo_change(tempo, beat, engine_time);
}
// Load speed overrides from variables
self.speed_overrides.clear(); self.speed_overrides.clear();
{ {
let vars = self.variables.lock().unwrap(); let vars = self.variables.lock().unwrap();
@@ -913,15 +826,6 @@ impl SequencerState {
} }
} }
let muted_snapshot = self.muted.clone();
let soloed_snapshot = self.soloed.clone();
let lookahead_beats = if tempo > 0.0 {
lookahead_secs * tempo / 60.0
} else {
0.0
};
// Process each active pattern
for (_id, active) in self.audio_state.active_patterns.iter_mut() { for (_id, active) in self.audio_state.active_patterns.iter_mut() {
let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else { let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
continue; continue;
@@ -932,86 +836,76 @@ impl SequencerState {
.get(&(active.bank, active.pattern)) .get(&(active.bank, active.pattern))
.copied() .copied()
.unwrap_or_else(|| pattern.speed.multiplier()); .unwrap_or_else(|| pattern.speed.multiplier());
let beat_int = (beat * 4.0 * speed_mult).floor() as i64; let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64; let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64;
let step_fires = beat_int != prev_beat_int && prev_beat >= 0.0;
// === IMMEDIATE STEP EXECUTION === if beat_int != prev_beat_int && prev_beat >= 0.0 {
// Fire the current step if a beat boundary was crossed
if step_fires {
result.any_step_fired = true; result.any_step_fired = true;
let step_idx = active.step_index % pattern.length; let step_idx = active.step_index % pattern.length;
let sched_key: ScheduledStepKey =
(active.bank, active.pattern, step_idx, beat_int);
// Skip evaluation if already done via lookahead if let Some(step) = pattern.steps.get(step_idx) {
if !self.scheduled_steps.contains_key(&sched_key) { let resolved_script = pattern.resolve_script(step_idx);
if let Some(step) = pattern.steps.get(step_idx) { let has_script = resolved_script
let resolved_script = pattern.resolve_script(step_idx); .map(|s| !s.trim().is_empty())
let has_script = resolved_script .unwrap_or(false);
.map(|s| !s.trim().is_empty())
.unwrap_or(false);
if step.active && has_script { if step.active && has_script {
let pattern_key = (active.bank, active.pattern); let pattern_key = (active.bank, active.pattern);
let is_muted = muted_snapshot.contains(&pattern_key) let is_muted = self.muted.contains(&pattern_key)
|| (!soloed_snapshot.is_empty() || (!self.soloed.is_empty()
&& !soloed_snapshot.contains(&pattern_key)); && !self.soloed.contains(&pattern_key));
if !is_muted { if !is_muted {
let source_idx = pattern.resolve_source(step_idx); let source_idx = pattern.resolve_source(step_idx);
let runs = self.runs_counter.get_and_increment( let runs = self.runs_counter.get_and_increment(
active.bank, active.bank,
active.pattern, active.pattern,
source_idx, source_idx,
); );
let ctx = StepContext { let ctx = StepContext {
step: step_idx, step: step_idx,
beat, beat,
bank: active.bank, bank: active.bank,
pattern: active.pattern, pattern: active.pattern,
tempo, tempo,
phase: beat % quantum, phase: beat % quantum,
slot: 0, slot: 0,
runs, runs,
iter: active.iter, iter: active.iter,
speed: speed_mult, speed: speed_mult,
fill, fill,
nudge_secs, nudge_secs,
cc_access: self.cc_access.clone(), cc_access: self.cc_access.clone(),
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
mouse_x, mouse_x,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
mouse_y, mouse_y,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
mouse_down, mouse_down,
}; };
if let Some(script) = resolved_script {
let mut trace = ExecutionTrace::default();
if let Ok(cmds) = self
.script_engine
.evaluate_with_trace(script, &ctx, &mut trace)
{
Arc::make_mut(&mut self.step_traces).insert(
(active.bank, active.pattern, source_idx),
std::mem::take(&mut trace),
);
if let Some(script) = resolved_script { let event_time = if lookahead_secs > 0.0 {
let mut trace = ExecutionTrace::default(); Some(engine_time + lookahead_secs)
if let Ok(cmds) = self } else {
.script_engine None
.evaluate_with_trace(script, &ctx, &mut trace) };
{
Arc::make_mut(&mut self.step_traces).insert(
(active.bank, active.pattern, source_idx),
std::mem::take(&mut trace),
);
let event_time = if lookahead_secs > 0.0 { for cmd in cmds {
Some(engine_time + lookahead_secs) self.event_count += 1;
} else { self.buf_audio_commands.push(TimestampedCommand {
None cmd,
}; time: event_time,
});
for cmd in cmds {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
}
} }
} }
} }
@@ -1019,7 +913,6 @@ impl SequencerState {
} }
} }
// Advance step index
let next_step = active.step_index + 1; let next_step = active.step_index + 1;
if next_step >= pattern.length { if next_step >= pattern.length {
active.iter += 1; active.iter += 1;
@@ -1030,144 +923,8 @@ impl SequencerState {
} }
active.step_index = next_step % pattern.length; active.step_index = next_step % pattern.length;
} }
// === LOOKAHEAD PRE-EVALUATION ===
// Pre-evaluate future steps within the lookahead window
if lookahead_secs > 0.0 {
let future_beat = beat + lookahead_beats;
let future_beat_int = (future_beat * 4.0 * speed_mult).floor() as i64;
let start_beat_int = beat_int + 1;
let mut lookahead_step = active.step_index;
let mut lookahead_iter = active.iter;
for target_beat_int in start_beat_int..=future_beat_int {
let step_idx = lookahead_step % pattern.length;
let sched_key: ScheduledStepKey =
(active.bank, active.pattern, step_idx, target_beat_int);
// Skip if already scheduled
if self.scheduled_steps.contains_key(&sched_key) {
let next = lookahead_step + 1;
if next >= pattern.length {
lookahead_iter += 1;
}
lookahead_step = next % pattern.length;
continue;
}
// Calculate the logical beat time for this step
let target_beat = target_beat_int as f64 / (4.0 * speed_mult);
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 pattern_key = (active.bank, active.pattern);
let is_muted = muted_snapshot.contains(&pattern_key)
|| (!soloed_snapshot.is_empty()
&& !soloed_snapshot.contains(&pattern_key));
if !is_muted {
let source_idx = pattern.resolve_source(step_idx);
let runs = self.runs_counter.get_and_increment(
active.bank,
active.pattern,
source_idx,
);
let ctx = StepContext {
step: step_idx,
beat: target_beat,
bank: active.bank,
pattern: active.pattern,
tempo,
phase: target_beat % quantum,
slot: 0,
runs,
iter: lookahead_iter,
speed: speed_mult,
fill,
nudge_secs,
cc_access: self.cc_access.clone(),
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
mouse_y,
#[cfg(feature = "desktop")]
mouse_down,
};
if let Some(script) = resolved_script {
let mut trace = ExecutionTrace::default();
if let Ok(cmds) = self
.script_engine
.evaluate_with_trace(script, &ctx, &mut trace)
{
Arc::make_mut(&mut self.step_traces).insert(
(active.bank, active.pattern, source_idx),
std::mem::take(&mut trace),
);
let event_time = Some(Self::beat_to_engine_time(
target_beat,
beat,
engine_time,
tempo,
));
for cmd in cmds {
self.event_count += 1;
self.pending_commands.push(PendingCommand {
cmd: TimestampedCommand { cmd, time: event_time },
target_beat,
bank: active.bank,
pattern: active.pattern,
});
}
}
}
}
}
}
// Mark step as scheduled
self.scheduled_steps.insert(
sched_key,
ScheduledStep {
target_beat,
tempo_at_schedule: tempo,
},
);
// Advance for next iteration
let next = lookahead_step + 1;
if next >= pattern.length {
lookahead_iter += 1;
}
lookahead_step = next % pattern.length;
}
}
} }
// === EMIT READY COMMANDS ===
// Move commands whose target_beat has passed from pending to output
let (ready, still_pending): (Vec<_>, Vec<_>) = std::mem::take(&mut self.pending_commands)
.into_iter()
.partition(|p| p.target_beat <= beat);
self.pending_commands = still_pending;
for pending in ready {
self.buf_audio_commands.push(pending.cmd);
}
// Cleanup stale scheduled_steps (more than 1 beat in the past)
self.scheduled_steps
.retain(|_, s| s.target_beat > beat - 1.0);
result result
} }
@@ -1730,7 +1487,7 @@ mod tests {
let mut vars = state.variables.lock().unwrap(); let mut vars = state.variables.lock().unwrap();
vars.insert( vars.insert(
"__chain_0_0__".to_string(), "__chain_0_0__".to_string(),
Value::Str("0:1".to_string(), None), Value::Str(std::sync::Arc::from("0:1"), None),
); );
} }
@@ -1969,7 +1726,7 @@ mod tests {
let mut vars = state.variables.lock().unwrap(); let mut vars = state.variables.lock().unwrap();
vars.insert( vars.insert(
"__chain_0_0__".to_string(), "__chain_0_0__".to_string(),
Value::Str("0:1".to_string(), None), Value::Str(std::sync::Arc::from("0:1"), None),
); );
} }
@@ -2217,27 +1974,6 @@ mod tests {
assert_eq!(output.new_tempo, Some(140.0)); assert_eq!(output.new_tempo, Some(140.0));
} }
fn tick_with_lookahead(beat: f64, lookahead_secs: f64) -> TickInput {
TickInput {
commands: Vec::new(),
playing: true,
beat,
tempo: 120.0,
quantum: 4.0,
fill: false,
nudge_secs: 0.0,
current_time_us: 0,
engine_time: beat * 0.5, // At 120 BPM, 1 beat = 0.5 seconds
lookahead_secs,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
}
}
fn pattern_with_sound(length: usize) -> PatternSnapshot { fn pattern_with_sound(length: usize) -> PatternSnapshot {
PatternSnapshot { PatternSnapshot {
speed: Default::default(), speed: Default::default(),
@@ -2255,14 +1991,14 @@ mod tests {
} }
#[test] #[test]
fn test_lookahead_pre_evaluates_future_steps() { fn test_continuous_step_firing() {
let mut state = make_state(); let mut state = make_state();
state.tick(tick_with( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, bank: 0,
pattern: 0, pattern: 0,
data: pattern_with_sound(4), data: pattern_with_sound(16),
}], }],
0.0, 0.0,
)); ));
@@ -2277,117 +2013,65 @@ mod tests {
0.5, 0.5,
)); ));
// With 100ms lookahead at 120 BPM = 0.2 beats lookahead // Tick through many bars, counting steps
// At beat 0.75, future_beat = 0.95 let mut step_count = 0;
// beat_int = 3, future_beat_int = 3 for i in 1..400 {
// next_beat_int = 4 > future_beat_int, so no lookahead yet let beat = 0.5 + (i as f64) * 0.25;
let output = state.tick(tick_with_lookahead(0.75, 0.1)); let output = state.tick(tick_at(beat, true));
// Step fired (step 1), commands emitted immediately if !output.audio_commands.is_empty() {
assert!(output.shared_state.active_patterns.iter().any(|p| p.step_index == 2)); step_count += 1;
}
// With 500ms lookahead = 1 beat lookahead
// At beat 1.0, future_beat = 2.0
// beat_int = 4, future_beat_int = 8
// Should pre-evaluate steps at beat_ints 5, 6, 7, 8
let _output = state.tick(tick_with_lookahead(1.0, 0.5));
// Check that scheduled_steps contains the pre-evaluated steps
// At beat 1.0, step_index is 3 (step 2 just fired)
// Lookahead will schedule steps: 3@5, 0@6, 1@7, 2@8
assert!(state.scheduled_steps.contains_key(&(0, 0, 3, 5)));
assert!(state.scheduled_steps.contains_key(&(0, 0, 0, 6)));
assert!(state.scheduled_steps.contains_key(&(0, 0, 1, 7)));
assert!(state.scheduled_steps.contains_key(&(0, 0, 2, 8)));
// Pending commands should exist for future steps
assert!(!state.pending_commands.is_empty());
}
#[test]
fn test_lookahead_commands_emit_at_correct_time() {
let mut state = make_state();
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: pattern_with_sound(4),
}],
0.0,
));
state.tick(tick_with(
vec![SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}],
0.5,
));
// Pre-evaluate with 1 beat lookahead
state.tick(tick_with_lookahead(0.75, 0.5));
// Commands for step 2 (at beat 1.0) should be in pending_commands
let pending_for_step2: Vec<_> = state
.pending_commands
.iter()
.filter(|p| (p.target_beat - 1.0).abs() < 0.01)
.collect();
assert!(!pending_for_step2.is_empty());
// Advance to beat 1.0 - pending commands should be emitted
let output = state.tick(tick_with_lookahead(1.0, 0.5));
// The commands should have been moved to buf_audio_commands
assert!(!output.audio_commands.is_empty());
}
#[test]
fn test_lookahead_tempo_change_reschedules() {
let mut state = make_state();
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: pattern_with_sound(4),
}],
0.0,
));
state.tick(tick_with(
vec![SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}],
0.5,
));
// Pre-evaluate with lookahead
state.tick(tick_with_lookahead(0.75, 0.5));
// Record original event times
let original_times: Vec<_> = state
.pending_commands
.iter()
.map(|p| p.cmd.time)
.collect();
assert!(!original_times.is_empty());
// Simulate tempo change by ticking with different tempo
let mut input = tick_with_lookahead(0.8, 0.5);
input.tempo = 140.0; // Changed from 120
state.tick(input);
// Event times should have been rescheduled
// (The exact times depend on the reschedule algorithm)
// At minimum, scheduled_steps should have updated tempo
for step in state.scheduled_steps.values() {
// Tempo should be updated for all scheduled steps
assert!((step.tempo_at_schedule - 140.0).abs() < 0.01);
} }
// Should fire steps continuously without gaps
assert!(step_count > 350, "Expected continuous steps, got {step_count}");
}
#[test]
fn test_multiple_patterns_fire_together() {
let mut state = make_state();
state.tick(tick_with(
vec![
SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: pattern_with_sound(4),
},
SeqCommand::PatternUpdate {
bank: 0,
pattern: 1,
data: pattern_with_sound(4),
},
],
0.0,
));
state.tick(tick_with(
vec![
SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
},
SeqCommand::PatternStart {
bank: 0,
pattern: 1,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
},
],
0.5,
));
// Both patterns should be active
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 1)));
// Tick and verify both produce commands
let output = state.tick(tick_at(1.0, true));
// Should have commands from both patterns (2 patterns * 1 command each)
assert!(output.audio_commands.len() >= 2);
} }
} }

View File

@@ -128,7 +128,7 @@ fn forget_removes_word() {
let stack = f.stack(); let stack = f.stack();
assert_eq!(stack.len(), 1); assert_eq!(stack.len(), 1);
match &stack[0] { match &stack[0] {
Value::Str(s, _) => assert_eq!(s, "double"), Value::Str(s, _) => assert_eq!(s.as_ref(), "double"),
other => panic!("expected Str, got {:?}", other), other => panic!("expected Str, got {:?}", other),
} }
} }

View File

@@ -46,7 +46,7 @@ fn float_literal() {
fn string_with_spaces() { fn string_with_spaces() {
let f = run(r#""hello world" !x @x"#); let f = run(r#""hello world" !x @x"#);
match stack_top(&f) { match stack_top(&f) {
cagire::forth::Value::Str(s, _) => assert_eq!(s, "hello world"), cagire::forth::Value::Str(s, _) => assert_eq!(s.as_ref(), "hello world"),
other => panic!("expected string, got {:?}", other), other => panic!("expected string, got {:?}", other),
} }
} }

View File

@@ -90,7 +90,7 @@ pub fn expect_int(script: &str, expected: i64) {
} }
pub fn expect_str(script: &str, expected: &str) { pub fn expect_str(script: &str, expected: &str) {
expect_stack(script, &[Value::Str(expected.to_string(), None)]); expect_stack(script, &[Value::Str(Arc::from(expected), None)]);
} }
pub fn expect_float(script: &str, expected: f64) { pub fn expect_float(script: &str, expected: f64) {