diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index 524fd4a..b367147 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use super::ops::Op; use super::types::{Dictionary, SourceSpan}; use super::words::compile_word; @@ -118,7 +120,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { 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) => { let word = w.as_str(); if word == "{" { @@ -129,7 +131,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { start: span.start, 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 == "}" { return Err("unexpected }".into()); } else if word == ":" { diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index c5f2d68..47cdba3 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use super::types::SourceSpan; #[derive(Clone, Debug, PartialEq)] pub enum Op { PushInt(i64, Option), PushFloat(f64, Option), - PushStr(String, Option), + PushStr(Arc, Option), Dup, Dupn, Drop, @@ -71,7 +73,7 @@ pub enum Op { Ftom, SetTempo, Every, - Quotation(Vec, Option), + Quotation(Arc<[Op]>, Option), When, Unless, Adsr, diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 8687c0d..5fd4ad6 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -60,9 +60,9 @@ pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]); pub enum Value { Int(i64, Option), Float(f64, Option), - Str(String, Option), - Quotation(Vec, Option), - CycleList(Vec), + Str(Arc, Option), + Quotation(Arc<[Op]>, Option), + CycleList(Arc<[Value]>), } impl PartialEq for Value { @@ -116,7 +116,7 @@ impl Value { match self { Value::Int(i, _) => i.to_string(), Value::Float(f, _) => f.to_string(), - Value::Str(s, _) => s.clone(), + Value::Str(s, _) => s.to_string(), Value::Quotation(..) => String::new(), Value::CycleList(_) => String::new(), } diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 0b4f5f1..881904e 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -1,6 +1,7 @@ use rand::rngs::StdRng; use rand::{Rng as RngTrait, SeedableRng}; use std::borrow::Cow; +use std::sync::Arc; use super::compiler::compile_script; use super::ops::Op; @@ -423,7 +424,7 @@ impl Forth { let val = if values.len() == 1 { values.into_iter().next().unwrap() } else { - Value::CycleList(values) + Value::CycleList(Arc::from(values)) }; cmd.set_sound(val); } @@ -435,7 +436,7 @@ impl Forth { let val = if values.len() == 1 { values.into_iter().next().unwrap() } else { - Value::CycleList(values) + Value::CycleList(Arc::from(values)) }; cmd.set_param(param.clone(), val); } @@ -735,7 +736,7 @@ impl Forth { } else { let key = format!("__chain_{}_{}__", ctx.bank, ctx.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)); } } diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 403cdd2..52067a5 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use super::ops::Op; use super::theory; @@ -3031,7 +3031,7 @@ pub(super) fn compile_word( // @varname - fetch variable if let Some(var_name) = name.strip_prefix('@') { 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); return true; } @@ -3040,7 +3040,7 @@ pub(super) fn compile_word( // !varname - store into variable if let Some(var_name) = name.strip_prefix('!') { 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); return true; } @@ -3073,6 +3073,6 @@ pub(super) fn compile_word( } // Unrecognized token becomes a string - ops.push(Op::PushStr(name.to_string(), span)); + ops.push(Op::PushStr(Arc::from(name), span)); true } diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index a88baf6..10da1e4 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -438,25 +438,6 @@ fn check_quantization_boundary( 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 { counts: HashMap, } @@ -578,10 +559,6 @@ pub(crate) struct SequencerState { active_notes: HashMap<(u8, u8, u8), ActiveNote>, muted: 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, - last_tempo: f64, } impl SequencerState { @@ -608,9 +585,6 @@ impl SequencerState { active_notes: HashMap::new(), muted: 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(); self.runs_counter.counts.clear(); self.audio_state.flush_midi_notes = true; - self.scheduled_steps.clear(); - self.pending_commands.clear(); } SeqCommand::Shutdown => {} } @@ -815,65 +787,12 @@ impl SequencerState { Arc::make_mut(&mut self.step_traces).retain(|&(bank, 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 } - /// 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)] fn execute_steps( &mut self, @@ -896,12 +815,6 @@ impl SequencerState { 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(); { 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() { let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else { continue; @@ -932,86 +836,76 @@ impl SequencerState { .get(&(active.bank, active.pattern)) .copied() .unwrap_or_else(|| 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; - let step_fires = beat_int != prev_beat_int && prev_beat >= 0.0; - // === IMMEDIATE STEP EXECUTION === - // Fire the current step if a beat boundary was crossed - if step_fires { + if beat_int != prev_beat_int && prev_beat >= 0.0 { result.any_step_fired = true; 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 !self.scheduled_steps.contains_key(&sched_key) { - 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 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 step.active && has_script { + let pattern_key = (active.bank, active.pattern); + let is_muted = self.muted.contains(&pattern_key) + || (!self.soloed.is_empty() + && !self.soloed.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, - bank: active.bank, - pattern: active.pattern, - tempo, - phase: beat % quantum, - slot: 0, - runs, - iter: active.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 !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, + bank: active.bank, + pattern: active.pattern, + tempo, + phase: beat % quantum, + slot: 0, + runs, + iter: active.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), + ); - 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 = if lookahead_secs > 0.0 { + Some(engine_time + lookahead_secs) + } else { + None + }; - let event_time = if lookahead_secs > 0.0 { - Some(engine_time + lookahead_secs) - } else { - None - }; - - for cmd in cmds { - self.event_count += 1; - self.buf_audio_commands.push(TimestampedCommand { - 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; if next_step >= pattern.length { active.iter += 1; @@ -1030,144 +923,8 @@ impl SequencerState { } 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 } @@ -1730,7 +1487,7 @@ mod tests { let mut vars = state.variables.lock().unwrap(); vars.insert( "__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(); vars.insert( "__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)); } - 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 { PatternSnapshot { speed: Default::default(), @@ -2255,14 +1991,14 @@ mod tests { } #[test] - fn test_lookahead_pre_evaluates_future_steps() { + fn test_continuous_step_firing() { let mut state = make_state(); state.tick(tick_with( vec![SeqCommand::PatternUpdate { bank: 0, pattern: 0, - data: pattern_with_sound(4), + data: pattern_with_sound(16), }], 0.0, )); @@ -2277,117 +2013,65 @@ mod tests { 0.5, )); - // With 100ms lookahead at 120 BPM = 0.2 beats lookahead - // At beat 0.75, future_beat = 0.95 - // beat_int = 3, future_beat_int = 3 - // next_beat_int = 4 > future_beat_int, so no lookahead yet - let output = state.tick(tick_with_lookahead(0.75, 0.1)); - // Step fired (step 1), commands emitted immediately - assert!(output.shared_state.active_patterns.iter().any(|p| p.step_index == 2)); - - // 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); + // Tick through many bars, counting steps + let mut step_count = 0; + for i in 1..400 { + let beat = 0.5 + (i as f64) * 0.25; + let output = state.tick(tick_at(beat, true)); + if !output.audio_commands.is_empty() { + step_count += 1; + } } + + // 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); } } diff --git a/tests/forth/definitions.rs b/tests/forth/definitions.rs index f5d8e14..26eb5f4 100644 --- a/tests/forth/definitions.rs +++ b/tests/forth/definitions.rs @@ -128,7 +128,7 @@ fn forget_removes_word() { let stack = f.stack(); assert_eq!(stack.len(), 1); 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), } } diff --git a/tests/forth/errors.rs b/tests/forth/errors.rs index 27f69cc..6d44e32 100644 --- a/tests/forth/errors.rs +++ b/tests/forth/errors.rs @@ -46,7 +46,7 @@ fn float_literal() { fn string_with_spaces() { let f = run(r#""hello world" !x @x"#); 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), } } diff --git a/tests/forth/harness.rs b/tests/forth/harness.rs index 2214b8e..5e72171 100644 --- a/tests/forth/harness.rs +++ b/tests/forth/harness.rs @@ -90,7 +90,7 @@ pub fn expect_int(script: &str, expected: i64) { } 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) {