diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 5fd4ad6..dc77ea6 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -22,7 +22,7 @@ pub struct ExecutionTrace { pub selected_spans: Vec, } -pub struct StepContext { +pub struct StepContext<'a> { pub step: usize, pub beat: f64, pub bank: usize, @@ -36,6 +36,8 @@ pub struct StepContext { pub fill: bool, pub nudge_secs: f64, pub cc_access: Option>, + pub speed_key: &'a str, + pub chain_key: &'a str, #[cfg(feature = "desktop")] pub mouse_x: f64, #[cfg(feature = "desktop")] @@ -44,7 +46,7 @@ pub struct StepContext { pub mouse_down: f64, } -impl StepContext { +impl StepContext<'_> { pub fn step_duration(&self) -> f64 { 60.0 / self.tempo / 4.0 / self.speed } @@ -138,6 +140,14 @@ pub(super) struct CmdRegister { } impl CmdRegister { + pub(super) fn new() -> Self { + Self { + sound: None, + params: Vec::with_capacity(16), + deltas: Vec::with_capacity(4), + } + } + pub(super) fn set_sound(&mut self, val: Value) { self.sound = Some(val); } diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 881904e..647930f 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -70,8 +70,8 @@ impl Forth { trace: Option<&mut ExecutionTrace>, ) -> Result, String> { let mut stack = self.stack.lock().unwrap(); - let mut outputs: Vec = Vec::new(); - let mut cmd = CmdRegister::default(); + let mut outputs: Vec = Vec::with_capacity(8); + let mut cmd = CmdRegister::new(); self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?; @@ -148,8 +148,8 @@ impl Forth { return Err("stack underflow".into()); } let start = stack.len() - count; - let values: Vec = stack.drain(start..).collect(); - let selected = values[idx].clone(); + let selected = stack[start + idx].clone(); + stack.truncate(start); select_and_run(selected, stack, outputs, cmd) }; @@ -718,11 +718,10 @@ impl Forth { Op::SetSpeed => { let speed = stack.pop().ok_or("stack underflow")?.as_float()?; let clamped = speed.clamp(0.125, 8.0); - let key = format!("__speed_{}_{}__", ctx.bank, ctx.pattern); self.vars .lock() .unwrap() - .insert(key, Value::Float(clamped, None)); + .insert(ctx.speed_key.to_string(), Value::Float(clamped, None)); } Op::Chain => { @@ -734,9 +733,10 @@ impl Forth { if bank as usize == ctx.bank && pattern as usize == ctx.pattern { // chaining to self is a no-op } else { - let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern); - let val = format!("{bank}:{pattern}"); - self.vars.lock().unwrap().insert(key, Value::Str(Arc::from(val), None)); + use std::fmt::Write; + let mut val = String::with_capacity(8); + let _ = write!(&mut val, "{bank}:{pattern}"); + self.vars.lock().unwrap().insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None)); } } @@ -1010,36 +1010,57 @@ fn emit_output( nudge_secs: f64, outputs: &mut Vec, ) { - let mut pairs: Vec<(String, String)> = if let Some(s) = sound { - vec![("sound".into(), s.to_string())] - } else { - vec![] - }; - pairs.extend(params.iter().cloned()); - if nudge_secs > 0.0 { - pairs.push(("delta".into(), nudge_secs.to_string())); + use std::fmt::Write; + let mut out = String::with_capacity(128); + out.push('/'); + + let has_dur = params.iter().any(|(k, _)| k == "dur"); + let delaytime_idx = params.iter().position(|(k, _)| k == "delaytime"); + + if let Some(s) = sound { + let _ = write!(&mut out, "sound/{s}"); } - // Only add default dur if there's a sound (new voice) - if sound.is_some() && !pairs.iter().any(|(k, _)| k == "dur") { - pairs.push(("dur".into(), step_duration.to_string())); - } - // Only add default delaytime if there's a sound (new voice) - if sound.is_some() { - if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { - let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); - pairs[idx].1 = (ratio * step_duration).to_string(); - } else { - pairs.push(("delaytime".into(), step_duration.to_string())); + + for (i, (k, v)) in params.iter().enumerate() { + if !out.ends_with('/') { + out.push('/'); } - } - for pair in &mut pairs { - if is_tempo_scaled_param(&pair.0) { - if let Ok(val) = pair.1.parse::() { - pair.1 = (val * step_duration).to_string(); + if is_tempo_scaled_param(k) { + if let Ok(val) = v.parse::() { + let _ = write!(&mut out, "{k}/{}", val * step_duration); + continue; } } + if Some(i) == delaytime_idx && sound.is_some() { + let ratio: f64 = v.parse().unwrap_or(1.0); + let _ = write!(&mut out, "{k}/{}", ratio * step_duration); + } else { + let _ = write!(&mut out, "{k}/{v}"); + } } - outputs.push(format_cmd(&pairs)); + + if nudge_secs > 0.0 { + if !out.ends_with('/') { + out.push('/'); + } + let _ = write!(&mut out, "delta/{nudge_secs}"); + } + + if sound.is_some() && !has_dur { + if !out.ends_with('/') { + out.push('/'); + } + let _ = write!(&mut out, "dur/{step_duration}"); + } + + if sound.is_some() && delaytime_idx.is_none() { + if !out.ends_with('/') { + out.push('/'); + } + let _ = write!(&mut out, "delaytime/{step_duration}"); + } + + outputs.push(out); } fn perlin_grad(hash_input: i64) -> f64 { @@ -1116,11 +1137,6 @@ where Ok(()) } -fn format_cmd(pairs: &[(String, String)]) -> String { - let parts: Vec = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect(); - format!("/{}", parts.join("/")) -} - fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> { match val { Value::CycleList(items) if !items.is_empty() => { diff --git a/src/app.rs b/src/app.rs index 0fd8110..f71d88f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -266,7 +266,7 @@ impl App { self.project_state.mark_dirty(change.bank, change.pattern); } - fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext { + fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext<'static> { let (bank, pattern) = self.current_bank_pattern(); let speed = self .project_state @@ -288,6 +288,8 @@ impl App { fill: false, nudge_secs: 0.0, cc_access: None, + speed_key: "", + chain_key: "", #[cfg(feature = "desktop")] mouse_x: 0.5, #[cfg(feature = "desktop")] diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 98221cd..2b42ad1 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -281,6 +281,8 @@ pub fn build_stream( let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer); + let mut cmd_buffer = String::with_capacity(256); + let stream = device .build_output_stream( &stream_config, @@ -291,11 +293,16 @@ pub fn build_stream( while let Ok(cmd) = audio_rx.try_recv() { match cmd { AudioCommand::Evaluate { cmd, time } => { - let cmd_with_time = match time { - Some(t) => format!("{cmd}/time/{t:.6}"), - None => cmd, + let cmd_ref = match time { + Some(t) => { + cmd_buffer.clear(); + use std::fmt::Write; + let _ = write!(&mut cmd_buffer, "{cmd}/time/{t:.6}"); + cmd_buffer.as_str() + } + None => &cmd, }; - engine.evaluate(&cmd_with_time); + engine.evaluate(cmd_ref); } AudioCommand::Hush => { engine.hush(); diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 10da1e4..9d34f42 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -555,6 +555,8 @@ pub(crate) struct SequencerState { speed_overrides: HashMap<(usize, usize), f64>, key_cache: KeyCache, buf_audio_commands: Vec, + buf_activated: Vec, + buf_stopped: Vec, cc_access: Option>, active_notes: HashMap<(u8, u8, u8), ActiveNote>, muted: std::collections::HashSet<(usize, usize)>, @@ -580,7 +582,9 @@ impl SequencerState { variables, speed_overrides: HashMap::new(), key_cache: KeyCache::new(), - buf_audio_commands: Vec::new(), + buf_audio_commands: Vec::with_capacity(32), + buf_activated: Vec::with_capacity(16), + buf_stopped: Vec::with_capacity(16), cc_access, active_notes: HashMap::new(), muted: std::collections::HashSet::new(), @@ -685,15 +689,8 @@ impl SequencerState { let beat = input.beat; let prev_beat = self.audio_state.prev_beat; - let activated = self.activate_pending(beat, prev_beat, input.quantum); - self.audio_state - .pending_starts - .retain(|p| !activated.contains(&p.id)); - - let stopped = self.deactivate_pending(beat, prev_beat, input.quantum); - self.audio_state - .pending_stops - .retain(|p| !stopped.contains(&p.id)); + self.activate_pending(beat, prev_beat, input.quantum); + self.deactivate_pending(beat, prev_beat, input.quantum); let steps = self.execute_steps( beat, @@ -713,7 +710,7 @@ impl SequencerState { input.mouse_down, ); - let vars = self.read_variables(&steps.completed_iterations, &stopped, steps.any_step_fired); + let vars = self.read_variables(&steps.completed_iterations, steps.any_step_fired); self.apply_chain_transitions(vars.chain_transitions); self.audio_state.prev_beat = beat; @@ -746,8 +743,8 @@ impl SequencerState { } } - fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) -> Vec { - let mut activated = Vec::new(); + fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) { + self.buf_activated.clear(); for pending in &self.audio_state.pending_starts { if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { let start_step = match pending.sync_mode { @@ -773,24 +770,30 @@ impl SequencerState { iter: 0, }, ); - activated.push(pending.id); + self.buf_activated.push(pending.id); } } - activated + let activated = &self.buf_activated; + self.audio_state + .pending_starts + .retain(|p| !activated.contains(&p.id)); } - fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) -> Vec { - let mut stopped = Vec::new(); + fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) { + self.buf_stopped.clear(); for pending in &self.audio_state.pending_stops { if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) { self.audio_state.active_patterns.remove(&pending.id); Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| { bank != pending.id.bank || pattern != pending.id.pattern }); - stopped.push(pending.id); + self.buf_stopped.push(pending.id); } } - stopped + let stopped = &self.buf_stopped; + self.audio_state + .pending_stops + .retain(|p| !stopped.contains(&p.id)); } #[allow(clippy::too_many_arguments)] @@ -876,6 +879,8 @@ impl SequencerState { fill, nudge_secs, cc_access: self.cc_access.clone(), + speed_key: self.key_cache.speed_key(active.bank, active.pattern), + chain_key: self.key_cache.chain_key(active.bank, active.pattern), #[cfg(feature = "desktop")] mouse_x, #[cfg(feature = "desktop")] @@ -931,9 +936,9 @@ impl SequencerState { fn read_variables( &self, completed: &[PatternId], - stopped: &[PatternId], any_step_fired: bool, ) -> VariableReads { + let stopped = &self.buf_stopped; let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired; if !needs_access { return VariableReads { @@ -1208,10 +1213,18 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option)> { if !cmd.starts_with("/midi/") { return None; } - let parts: Vec<&str> = cmd.split('/').filter(|s| !s.is_empty()).collect(); - if parts.len() < 2 { + let mut parts: [&str; 16] = [""; 16]; + let mut count = 0; + for part in cmd.split('/').filter(|s| !s.is_empty()) { + if count < 16 { + parts[count] = part; + count += 1; + } + } + if count < 2 { return None; } + let parts = &parts[..count]; let find_param = |key: &str| -> Option<&str> { parts diff --git a/src/views/render.rs b/src/views/render.rs index 6471923..60516b3 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -81,6 +81,8 @@ fn compute_stack_display( fill: false, nudge_secs: 0.0, cc_access: None, + speed_key: "", + chain_key: "", #[cfg(feature = "desktop")] mouse_x: 0.5, #[cfg(feature = "desktop")] diff --git a/tests/forth/harness.rs b/tests/forth/harness.rs index 5e72171..bdeb0b4 100644 --- a/tests/forth/harness.rs +++ b/tests/forth/harness.rs @@ -4,7 +4,7 @@ use rand::SeedableRng; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -pub fn default_ctx() -> StepContext { +pub fn default_ctx() -> StepContext<'static> { StepContext { step: 0, beat: 0.0, @@ -19,10 +19,12 @@ pub fn default_ctx() -> StepContext { fill: false, nudge_secs: 0.0, cc_access: None, + speed_key: "__speed_0_0__", + chain_key: "__chain_0_0__", } } -pub fn ctx_with(f: impl FnOnce(&mut StepContext)) -> StepContext { +pub fn ctx_with(f: impl FnOnce(&mut StepContext<'static>)) -> StepContext<'static> { let mut ctx = default_ctx(); f(&mut ctx); ctx