Compare commits
2 Commits
194030d953
...
8c31ed4196
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c31ed4196 | |||
| 8024c18bb0 |
@@ -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 == ":" {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub struct ExecutionTrace {
|
|||||||
pub selected_spans: Vec<SourceSpan>,
|
pub selected_spans: Vec<SourceSpan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StepContext {
|
pub struct StepContext<'a> {
|
||||||
pub step: usize,
|
pub step: usize,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
pub bank: usize,
|
pub bank: usize,
|
||||||
@@ -36,6 +36,8 @@ pub struct StepContext {
|
|||||||
pub fill: bool,
|
pub fill: bool,
|
||||||
pub nudge_secs: f64,
|
pub nudge_secs: f64,
|
||||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
|
pub speed_key: &'a str,
|
||||||
|
pub chain_key: &'a str,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
pub mouse_x: f64,
|
pub mouse_x: f64,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -44,7 +46,7 @@ pub struct StepContext {
|
|||||||
pub mouse_down: f64,
|
pub mouse_down: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StepContext {
|
impl StepContext<'_> {
|
||||||
pub fn step_duration(&self) -> f64 {
|
pub fn step_duration(&self) -> f64 {
|
||||||
60.0 / self.tempo / 4.0 / self.speed
|
60.0 / self.tempo / 4.0 / self.speed
|
||||||
}
|
}
|
||||||
@@ -60,9 +62,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 +118,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(),
|
||||||
}
|
}
|
||||||
@@ -138,6 +140,14 @@ pub(super) struct CmdRegister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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) {
|
pub(super) fn set_sound(&mut self, val: Value) {
|
||||||
self.sound = Some(val);
|
self.sound = Some(val);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -69,8 +70,8 @@ impl Forth {
|
|||||||
trace: Option<&mut ExecutionTrace>,
|
trace: Option<&mut ExecutionTrace>,
|
||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
let mut stack = self.stack.lock().unwrap();
|
let mut stack = self.stack.lock().unwrap();
|
||||||
let mut outputs: Vec<String> = Vec::new();
|
let mut outputs: Vec<String> = Vec::with_capacity(8);
|
||||||
let mut cmd = CmdRegister::default();
|
let mut cmd = CmdRegister::new();
|
||||||
|
|
||||||
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
||||||
|
|
||||||
@@ -147,8 +148,8 @@ impl Forth {
|
|||||||
return Err("stack underflow".into());
|
return Err("stack underflow".into());
|
||||||
}
|
}
|
||||||
let start = stack.len() - count;
|
let start = stack.len() - count;
|
||||||
let values: Vec<Value> = stack.drain(start..).collect();
|
let selected = stack[start + idx].clone();
|
||||||
let selected = values[idx].clone();
|
stack.truncate(start);
|
||||||
select_and_run(selected, stack, outputs, cmd)
|
select_and_run(selected, stack, outputs, cmd)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -717,11 +718,10 @@ impl Forth {
|
|||||||
Op::SetSpeed => {
|
Op::SetSpeed => {
|
||||||
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
let clamped = speed.clamp(0.125, 8.0);
|
let clamped = speed.clamp(0.125, 8.0);
|
||||||
let key = format!("__speed_{}_{}__", ctx.bank, ctx.pattern);
|
|
||||||
self.vars
|
self.vars
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(key, Value::Float(clamped, None));
|
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Chain => {
|
Op::Chain => {
|
||||||
@@ -733,9 +733,10 @@ impl Forth {
|
|||||||
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
||||||
// chaining to self is a no-op
|
// chaining to self is a no-op
|
||||||
} else {
|
} else {
|
||||||
let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern);
|
use std::fmt::Write;
|
||||||
let val = format!("{bank}:{pattern}");
|
let mut val = String::with_capacity(8);
|
||||||
self.vars.lock().unwrap().insert(key, Value::Str(val, None));
|
let _ = write!(&mut val, "{bank}:{pattern}");
|
||||||
|
self.vars.lock().unwrap().insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,36 +1010,57 @@ fn emit_output(
|
|||||||
nudge_secs: f64,
|
nudge_secs: f64,
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
) {
|
) {
|
||||||
let mut pairs: Vec<(String, String)> = if let Some(s) = sound {
|
use std::fmt::Write;
|
||||||
vec![("sound".into(), s.to_string())]
|
let mut out = String::with_capacity(128);
|
||||||
} else {
|
out.push('/');
|
||||||
vec![]
|
|
||||||
};
|
let has_dur = params.iter().any(|(k, _)| k == "dur");
|
||||||
pairs.extend(params.iter().cloned());
|
let delaytime_idx = params.iter().position(|(k, _)| k == "delaytime");
|
||||||
if nudge_secs > 0.0 {
|
|
||||||
pairs.push(("delta".into(), nudge_secs.to_string()));
|
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") {
|
for (i, (k, v)) in params.iter().enumerate() {
|
||||||
pairs.push(("dur".into(), step_duration.to_string()));
|
if !out.ends_with('/') {
|
||||||
}
|
out.push('/');
|
||||||
// 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()));
|
|
||||||
}
|
}
|
||||||
}
|
if is_tempo_scaled_param(k) {
|
||||||
for pair in &mut pairs {
|
if let Ok(val) = v.parse::<f64>() {
|
||||||
if is_tempo_scaled_param(&pair.0) {
|
let _ = write!(&mut out, "{k}/{}", val * step_duration);
|
||||||
if let Ok(val) = pair.1.parse::<f64>() {
|
continue;
|
||||||
pair.1 = (val * step_duration).to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
fn perlin_grad(hash_input: i64) -> f64 {
|
||||||
@@ -1115,11 +1137,6 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_cmd(pairs: &[(String, String)]) -> String {
|
|
||||||
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
|
||||||
format!("/{}", parts.join("/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
|
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
|
||||||
match val {
|
match val {
|
||||||
Value::CycleList(items) if !items.is_empty() => {
|
Value::CycleList(items) if !items.is_empty() => {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ impl App {
|
|||||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
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 (bank, pattern) = self.current_bank_pattern();
|
||||||
let speed = self
|
let speed = self
|
||||||
.project_state
|
.project_state
|
||||||
@@ -288,6 +288,8 @@ impl App {
|
|||||||
fill: false,
|
fill: false,
|
||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
|
speed_key: "",
|
||||||
|
chain_key: "",
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ pub fn build_stream(
|
|||||||
|
|
||||||
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
||||||
|
|
||||||
|
let mut cmd_buffer = String::with_capacity(256);
|
||||||
|
|
||||||
let stream = device
|
let stream = device
|
||||||
.build_output_stream(
|
.build_output_stream(
|
||||||
&stream_config,
|
&stream_config,
|
||||||
@@ -291,11 +293,16 @@ pub fn build_stream(
|
|||||||
while let Ok(cmd) = audio_rx.try_recv() {
|
while let Ok(cmd) = audio_rx.try_recv() {
|
||||||
match cmd {
|
match cmd {
|
||||||
AudioCommand::Evaluate { cmd, time } => {
|
AudioCommand::Evaluate { cmd, time } => {
|
||||||
let cmd_with_time = match time {
|
let cmd_ref = match time {
|
||||||
Some(t) => format!("{cmd}/time/{t:.6}"),
|
Some(t) => {
|
||||||
None => cmd,
|
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 => {
|
AudioCommand::Hush => {
|
||||||
engine.hush();
|
engine.hush();
|
||||||
|
|||||||
@@ -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>,
|
||||||
}
|
}
|
||||||
@@ -574,14 +555,12 @@ pub(crate) struct SequencerState {
|
|||||||
speed_overrides: HashMap<(usize, usize), f64>,
|
speed_overrides: HashMap<(usize, usize), f64>,
|
||||||
key_cache: KeyCache,
|
key_cache: KeyCache,
|
||||||
buf_audio_commands: Vec<TimestampedCommand>,
|
buf_audio_commands: Vec<TimestampedCommand>,
|
||||||
|
buf_activated: Vec<PatternId>,
|
||||||
|
buf_stopped: Vec<PatternId>,
|
||||||
cc_access: Option<Arc<dyn CcAccess>>,
|
cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
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 {
|
||||||
@@ -603,14 +582,13 @@ impl SequencerState {
|
|||||||
variables,
|
variables,
|
||||||
speed_overrides: HashMap::new(),
|
speed_overrides: HashMap::new(),
|
||||||
key_cache: KeyCache::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,
|
cc_access,
|
||||||
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 +673,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 => {}
|
||||||
}
|
}
|
||||||
@@ -713,15 +689,8 @@ impl SequencerState {
|
|||||||
let beat = input.beat;
|
let beat = input.beat;
|
||||||
let prev_beat = self.audio_state.prev_beat;
|
let prev_beat = self.audio_state.prev_beat;
|
||||||
|
|
||||||
let activated = self.activate_pending(beat, prev_beat, input.quantum);
|
self.activate_pending(beat, prev_beat, input.quantum);
|
||||||
self.audio_state
|
self.deactivate_pending(beat, prev_beat, input.quantum);
|
||||||
.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));
|
|
||||||
|
|
||||||
let steps = self.execute_steps(
|
let steps = self.execute_steps(
|
||||||
beat,
|
beat,
|
||||||
@@ -741,7 +710,7 @@ impl SequencerState {
|
|||||||
input.mouse_down,
|
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.apply_chain_transitions(vars.chain_transitions);
|
||||||
|
|
||||||
self.audio_state.prev_beat = beat;
|
self.audio_state.prev_beat = beat;
|
||||||
@@ -774,8 +743,8 @@ impl SequencerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) -> Vec<PatternId> {
|
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||||
let mut activated = Vec::new();
|
self.buf_activated.clear();
|
||||||
for pending in &self.audio_state.pending_starts {
|
for pending in &self.audio_state.pending_starts {
|
||||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||||
let start_step = match pending.sync_mode {
|
let start_step = match pending.sync_mode {
|
||||||
@@ -801,79 +770,32 @@ impl SequencerState {
|
|||||||
iter: 0,
|
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<PatternId> {
|
fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||||
let mut stopped = Vec::new();
|
self.buf_stopped.clear();
|
||||||
for pending in &self.audio_state.pending_stops {
|
for pending in &self.audio_state.pending_stops {
|
||||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||||
self.audio_state.active_patterns.remove(&pending.id);
|
self.audio_state.active_patterns.remove(&pending.id);
|
||||||
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
|
self.buf_stopped.push(pending.id);
|
||||||
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
|
let stopped = &self.buf_stopped;
|
||||||
|
self.audio_state
|
||||||
|
.pending_stops
|
||||||
|
.retain(|p| !stopped.contains(&p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 +818,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 +829,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 +839,78 @@ 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")]
|
speed_key: self.key_cache.speed_key(active.bank, active.pattern),
|
||||||
mouse_x,
|
chain_key: self.key_cache.chain_key(active.bank, active.pattern),
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_y,
|
mouse_x,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_down,
|
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 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 +918,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,153 +928,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_variables(
|
fn read_variables(
|
||||||
&self,
|
&self,
|
||||||
completed: &[PatternId],
|
completed: &[PatternId],
|
||||||
stopped: &[PatternId],
|
|
||||||
any_step_fired: bool,
|
any_step_fired: bool,
|
||||||
) -> VariableReads {
|
) -> VariableReads {
|
||||||
|
let stopped = &self.buf_stopped;
|
||||||
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
|
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
|
||||||
if !needs_access {
|
if !needs_access {
|
||||||
return VariableReads {
|
return VariableReads {
|
||||||
@@ -1451,10 +1213,18 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
if !cmd.starts_with("/midi/") {
|
if !cmd.starts_with("/midi/") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let parts: Vec<&str> = cmd.split('/').filter(|s| !s.is_empty()).collect();
|
let mut parts: [&str; 16] = [""; 16];
|
||||||
if parts.len() < 2 {
|
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;
|
return None;
|
||||||
}
|
}
|
||||||
|
let parts = &parts[..count];
|
||||||
|
|
||||||
let find_param = |key: &str| -> Option<&str> {
|
let find_param = |key: &str| -> Option<&str> {
|
||||||
parts
|
parts
|
||||||
@@ -1730,7 +1500,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 +1739,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 +1987,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 +2004,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 +2026,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ fn compute_stack_display(
|
|||||||
fill: false,
|
fill: false,
|
||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
|
speed_key: "",
|
||||||
|
chain_key: "",
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rand::SeedableRng;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
pub fn default_ctx() -> StepContext {
|
pub fn default_ctx() -> StepContext<'static> {
|
||||||
StepContext {
|
StepContext {
|
||||||
step: 0,
|
step: 0,
|
||||||
beat: 0.0,
|
beat: 0.0,
|
||||||
@@ -19,10 +19,12 @@ pub fn default_ctx() -> StepContext {
|
|||||||
fill: false,
|
fill: false,
|
||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
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();
|
let mut ctx = default_ctx();
|
||||||
f(&mut ctx);
|
f(&mut ctx);
|
||||||
ctx
|
ctx
|
||||||
@@ -90,7 +92,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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user