diff --git a/Cargo.toml b/Cargo.toml index 52ffa5f..94c94ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ thread-priority = "1" ringbuf = "0.4" arc-swap = "1" midir = "0.10" +parking_lot = "0.12" # Desktop-only dependencies (behind feature flag) egui = { version = "0.33", optional = true } diff --git a/crates/forth/Cargo.toml b/crates/forth/Cargo.toml index cfa0a06..d3d7a82 100644 --- a/crates/forth/Cargo.toml +++ b/crates/forth/Cargo.toml @@ -13,3 +13,4 @@ desktop = [] [dependencies] rand = "0.8" +parking_lot = "0.12" diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index b367147..be98335 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -106,7 +106,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { match &tokens[i] { Token::Int(n, span) => { let key = n.to_string(); - if let Some(body) = dict.lock().unwrap().get(&key).cloned() { + if let Some(body) = dict.lock().get(&key).cloned() { ops.extend(body); } else { ops.push(Op::PushInt(*n, Some(*span))); @@ -114,7 +114,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { } Token::Float(f, span) => { let key = f.to_string(); - if let Some(body) = dict.lock().unwrap().get(&key).cloned() { + if let Some(body) = dict.lock().get(&key).cloned() { ops.extend(body); } else { ops.push(Op::PushFloat(*f, Some(*span))); @@ -137,7 +137,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { } else if word == ":" { let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?; i += consumed; - dict.lock().unwrap().insert(name, body); + dict.lock().insert(name, body); } else if word == ";" { return Err("unexpected ;".into()); } else if word == "if" { diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index dc77ea6..57ae2c2 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -1,6 +1,7 @@ +use parking_lot::Mutex; use rand::rngs::StdRng; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use super::ops::Op; diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 647930f..27f0b5e 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -1,3 +1,4 @@ +use parking_lot::Mutex; use rand::rngs::StdRng; use rand::{Rng as RngTrait, SeedableRng}; use std::borrow::Cow; @@ -19,7 +20,7 @@ pub struct Forth { impl Forth { pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self { Self { - stack: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + stack: Arc::new(Mutex::new(Vec::new())), vars, dict, rng, @@ -28,12 +29,12 @@ impl Forth { #[allow(dead_code)] pub fn stack(&self) -> Vec { - self.stack.lock().unwrap().clone() + self.stack.lock().clone() } #[allow(dead_code)] pub fn clear_stack(&self) { - self.stack.lock().unwrap().clear(); + self.stack.lock().clear(); } pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result, String> { @@ -69,7 +70,7 @@ impl Forth { ctx: &StepContext, trace: Option<&mut ExecutionTrace>, ) -> Result, String> { - let mut stack = self.stack.lock().unwrap(); + let mut stack = self.stack.lock(); let mut outputs: Vec = Vec::with_capacity(8); let mut cmd = CmdRegister::new(); @@ -474,7 +475,7 @@ impl Forth { Op::Get => { let name = stack.pop().ok_or("stack underflow")?; let name = name.as_str()?; - let vars = self.vars.lock().unwrap(); + let vars = self.vars.lock(); let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None)); stack.push(val); } @@ -482,7 +483,7 @@ impl Forth { let name = stack.pop().ok_or("stack underflow")?; let name = name.as_str()?.to_string(); let val = stack.pop().ok_or("stack underflow")?; - self.vars.lock().unwrap().insert(name, val); + self.vars.lock().insert(name, val); } Op::GetContext(name) => { @@ -520,7 +521,7 @@ impl Forth { } else { (*b_i, *a_i) }; - let val = self.rng.lock().unwrap().gen_range(lo..=hi); + let val = self.rng.lock().gen_range(lo..=hi); stack.push(Value::Int(val, None)); } _ => { @@ -530,7 +531,7 @@ impl Forth { let val = if (hi - lo).abs() < f64::EPSILON { lo } else { - self.rng.lock().unwrap().gen_range(lo..hi) + self.rng.lock().gen_range(lo..hi) }; stack.push(Value::Float(val, None)); } @@ -543,7 +544,7 @@ impl Forth { return Err("exprand requires positive values".into()); } let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) }; - let u: f64 = self.rng.lock().unwrap().gen(); + let u: f64 = self.rng.lock().gen(); let val = lo * (hi / lo).powf(u); stack.push(Value::Float(val, None)); } @@ -554,13 +555,13 @@ impl Forth { return Err("logrand requires positive values".into()); } let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) }; - let u: f64 = self.rng.lock().unwrap().gen(); + let u: f64 = self.rng.lock().gen(); let val = hi * (lo / hi).powf(u); stack.push(Value::Float(val, None)); } Op::Seed => { let s = stack.pop().ok_or("stack underflow")?.as_int()?; - *self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64); + *self.rng.lock() = StdRng::seed_from_u64(s as u64); } Op::Cycle | Op::PCycle => { @@ -580,14 +581,14 @@ impl Forth { if count == 0 { return Err("choose count must be > 0".into()); } - let idx = self.rng.lock().unwrap().gen_range(0..count); + let idx = self.rng.lock().gen_range(0..count); drain_select_run(count, idx, stack, outputs, cmd)?; } Op::ChanceExec | Op::ProbExec => { let threshold = stack.pop().ok_or("stack underflow")?.as_float()?; let quot = stack.pop().ok_or("stack underflow")?; - let val: f64 = self.rng.lock().unwrap().gen(); + let val: f64 = self.rng.lock().gen(); let limit = match &ops[pc] { Op::ChanceExec => threshold, _ => threshold / 100.0, @@ -598,7 +599,7 @@ impl Forth { } Op::Coin => { - let val: f64 = self.rng.lock().unwrap().gen(); + let val: f64 = self.rng.lock().gen(); stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None)); } @@ -711,7 +712,6 @@ impl Forth { let clamped = tempo.clamp(20.0, 300.0); self.vars .lock() - .unwrap() .insert("__tempo__".to_string(), Value::Float(clamped, None)); } @@ -720,7 +720,6 @@ impl Forth { let clamped = speed.clamp(0.125, 8.0); self.vars .lock() - .unwrap() .insert(ctx.speed_key.to_string(), Value::Float(clamped, None)); } @@ -736,7 +735,7 @@ impl Forth { 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)); + self.vars.lock().insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None)); } } @@ -852,7 +851,6 @@ impl Forth { for i in 0..count { self.vars .lock() - .unwrap() .insert("i".to_string(), Value::Int(i, None)); run_quotation(quot.clone(), stack, outputs, cmd)?; } @@ -954,7 +952,7 @@ impl Forth { } Op::Forget => { let name = stack.pop().ok_or("stack underflow")?.as_str()?.to_string(); - self.dict.lock().unwrap().remove(&name); + self.dict.lock().remove(&name); } } pc += 1; diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 52067a5..2e105fd 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -3067,7 +3067,7 @@ pub(super) fn compile_word( } // User-defined words from dictionary - if let Some(body) = dict.lock().unwrap().get(name) { + if let Some(body) = dict.lock().get(name) { ops.extend(body.iter().cloned()); return true; } diff --git a/src/app.rs b/src/app.rs index f71d88f..8ca19a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,9 @@ +use parking_lot::Mutex; use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use crossbeam_channel::Sender; @@ -605,8 +606,8 @@ impl App { link.set_tempo(tempo); self.playback.clear_queues(); - self.variables.lock().unwrap().clear(); - self.dict.lock().unwrap().clear(); + self.variables.lock().clear(); + self.dict.lock().clear(); for (bank, pattern) in playing { self.playback.queued_changes.push(StagedChange { diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 9d34f42..0d7cd40 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -494,7 +494,6 @@ pub(crate) struct TickOutput { } struct StepResult { - completed_iterations: Vec, any_step_fired: bool, } @@ -557,6 +556,7 @@ pub(crate) struct SequencerState { buf_audio_commands: Vec, buf_activated: Vec, buf_stopped: Vec, + buf_completed_iterations: Vec, cc_access: Option>, active_notes: HashMap<(u8, u8, u8), ActiveNote>, muted: std::collections::HashSet<(usize, usize)>, @@ -580,11 +580,12 @@ impl SequencerState { dropped_events: 0, script_engine, variables, - speed_overrides: HashMap::new(), + speed_overrides: HashMap::with_capacity(MAX_PATTERNS), key_cache: KeyCache::new(), buf_audio_commands: Vec::with_capacity(32), buf_activated: Vec::with_capacity(16), buf_stopped: Vec::with_capacity(16), + buf_completed_iterations: Vec::with_capacity(16), cc_access, active_notes: HashMap::new(), muted: std::collections::HashSet::new(), @@ -710,7 +711,7 @@ impl SequencerState { input.mouse_down, ); - let vars = self.read_variables(&steps.completed_iterations, steps.any_step_fired); + let vars = self.read_variables(&self.buf_completed_iterations, steps.any_step_fired); self.apply_chain_transitions(vars.chain_transitions); self.audio_state.prev_beat = beat; @@ -813,14 +814,14 @@ impl SequencerState { #[cfg(feature = "desktop")] mouse_down: f64, ) -> StepResult { self.buf_audio_commands.clear(); + self.buf_completed_iterations.clear(); let mut result = StepResult { - completed_iterations: Vec::new(), any_step_fired: false, }; self.speed_overrides.clear(); { - let vars = self.variables.lock().unwrap(); + let vars = self.variables.lock(); for id in self.audio_state.active_patterns.keys() { let key = self.key_cache.speed_key(id.bank, id.pattern); if let Some(v) = vars.get(key).and_then(|v| v.as_float().ok()) { @@ -921,7 +922,7 @@ impl SequencerState { let next_step = active.step_index + 1; if next_step >= pattern.length { active.iter += 1; - result.completed_iterations.push(PatternId { + self.buf_completed_iterations.push(PatternId { bank: active.bank, pattern: active.pattern, }); @@ -947,7 +948,7 @@ impl SequencerState { }; } - let mut vars = self.variables.lock().unwrap(); + let mut vars = self.variables.lock(); let new_tempo = vars.remove("__tempo__").and_then(|v| v.as_float().ok()); let mut chain_transitions = Vec::new(); @@ -1073,7 +1074,7 @@ fn sequencer_loop( let mut seq_state = SequencerState::new(variables, dict, rng, cc_access); loop { - let mut commands = Vec::new(); + let mut commands = Vec::with_capacity(8); while let Ok(cmd) = cmd_rx.try_recv() { if matches!(cmd, SeqCommand::Shutdown) { return; @@ -1205,7 +1206,16 @@ fn sequencer_loop( shared_state.store(Arc::new(output.shared_state)); - thread::sleep(Duration::from_micros(200)); + // Adaptive sleep: calculate time until next substep boundary + // At max speed (8x), substeps occur every beat/32 + // Sleep for most of that time, leaving 500μs margin for processing + let beats_per_sec = tempo / 60.0; + let max_speed = 8.0; // Maximum speed multiplier from speed.clamp() + let secs_per_substep = 1.0 / (beats_per_sec * 4.0 * max_speed); + let substep_us = (secs_per_substep * 1_000_000.0) as u64; + // Sleep for most of the substep duration, clamped to reasonable bounds + let sleep_us = substep_us.saturating_sub(500).clamp(50, 2000); + thread::sleep(Duration::from_micros(sleep_us)); } } @@ -1317,7 +1327,7 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option)> { #[cfg(test)] mod tests { use super::*; - use std::sync::Mutex; + use parking_lot::Mutex; fn make_state() -> SequencerState { let variables: Variables = Arc::new(Mutex::new(HashMap::new())); @@ -1497,7 +1507,7 @@ mod tests { // Set chain variable { - let mut vars = state.variables.lock().unwrap(); + let mut vars = state.variables.lock(); vars.insert( "__chain_0_0__".to_string(), Value::Str(std::sync::Arc::from("0:1"), None), @@ -1736,7 +1746,7 @@ mod tests { // Set chain: 0:0 -> 0:1 { - let mut vars = state.variables.lock().unwrap(); + let mut vars = state.variables.lock(); vars.insert( "__chain_0_0__".to_string(), Value::Str(std::sync::Arc::from("0:1"), None), @@ -1979,7 +1989,7 @@ mod tests { // Script fires at beat 1.0 (step 0). Set __tempo__ as if the script did. { - let mut vars = state.variables.lock().unwrap(); + let mut vars = state.variables.lock(); vars.insert("__tempo__".to_string(), Value::Float(140.0, None)); } diff --git a/src/midi.rs b/src/midi.rs index d29217c..a00ac4a 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -1,4 +1,5 @@ -use std::sync::{Arc, Mutex}; +use parking_lot::Mutex; +use std::sync::Arc; use midir::{MidiInput, MidiOutput}; @@ -28,9 +29,8 @@ impl CcMemory { /// Set a CC value (for testing) #[allow(dead_code)] pub fn set_cc(&self, device: usize, channel: usize, cc: usize, value: u8) { - if let Ok(mut mem) = self.0.lock() { - mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value; - } + let mut mem = self.0.lock(); + mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value; } } @@ -42,11 +42,8 @@ impl Default for CcMemory { impl CcAccess for CcMemory { fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8 { - self.0 - .lock() - .ok() - .map(|mem| mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)]) - .unwrap_or(0) + let mem = self.0.lock(); + mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] } } @@ -154,9 +151,8 @@ impl MidiState { let data2 = message[2]; if (status & 0xF0) == 0xB0 && data1 < 128 { let channel = (status & 0x0F) as usize; - if let Ok(mut mem) = cc_mem.lock() { - mem[*slot][channel][data1] = data2; - } + let mut mem = cc_mem.lock(); + mem[*slot][channel][data1] = data2; } } }, diff --git a/src/views/render.rs b/src/views/render.rs index 60516b3..97c2ec2 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -1,7 +1,8 @@ +use parking_lot::Mutex; use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; use std::hash::{Hash, Hasher}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Instant; use rand::rngs::StdRng; diff --git a/tests/forth/harness.rs b/tests/forth/harness.rs index bdeb0b4..8be9b6a 100644 --- a/tests/forth/harness.rs +++ b/tests/forth/harness.rs @@ -1,8 +1,9 @@ use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables}; +use parking_lot::Mutex; use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; pub fn default_ctx() -> StepContext<'static> { StepContext {