Feat: parameter duration scaling

This commit is contained in:
2026-01-27 12:17:23 +01:00
parent 324d1feda1
commit 5fa2c5b6b0
3 changed files with 149 additions and 27 deletions

View File

@@ -86,7 +86,7 @@ impl Forth {
// Resolve root scope at end of script // Resolve root scope at end of script
if let Some(scope) = scope_stack.pop() { if let Some(scope) = scope_stack.pop() {
resolve_scope(&scope, &mut outputs); resolve_scope(&scope, ctx.step_duration(), &mut outputs);
} }
Ok(outputs) Ok(outputs)
@@ -536,7 +536,11 @@ impl Forth {
let cond = stack.pop().ok_or("stack underflow")?; let cond = stack.pop().ok_or("stack underflow")?;
let false_quot = stack.pop().ok_or("stack underflow")?; let false_quot = stack.pop().ok_or("stack underflow")?;
let true_quot = stack.pop().ok_or("stack underflow")?; let true_quot = stack.pop().ok_or("stack underflow")?;
let quot = if cond.is_truthy() { true_quot } else { false_quot }; let quot = if cond.is_truthy() {
true_quot
} else {
false_quot
};
run_quotation(quot, stack, outputs, scope_stack, cmd)?; run_quotation(quot, stack, outputs, scope_stack, cmd)?;
} }
@@ -613,10 +617,7 @@ impl Forth {
} else { } else {
let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern); let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern);
let val = format!("{bank}:{pattern}"); let val = format!("{bank}:{pattern}");
self.vars self.vars.lock().unwrap().insert(key, Value::Str(val, None));
.lock()
.unwrap()
.insert(key, Value::Str(val, None));
} }
} }
@@ -725,7 +726,7 @@ impl Forth {
let child = scope_stack.pop().unwrap(); let child = scope_stack.pop().unwrap();
if child.stacked { if child.stacked {
resolve_scope(&child, outputs); resolve_scope(&child, ctx.step_duration(), outputs);
} else { } else {
let parent = scope_stack.last_mut().ok_or("scope stack underflow")?; let parent = scope_stack.last_mut().ok_or("scope stack underflow")?;
let parent_slot = parent.claim_slot(); let parent_slot = parent.claim_slot();
@@ -750,7 +751,6 @@ impl Forth {
emit_once(cmd, scope_stack)?; emit_once(cmd, scope_stack)?;
} }
} }
} }
pc += 1; pc += 1;
} }
@@ -759,7 +759,7 @@ impl Forth {
} }
} }
fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec<String>) { fn resolve_scope(scope: &ScopeContext, step_duration: f64, outputs: &mut Vec<String>) {
let slot_dur = if scope.slot_count == 0 { let slot_dur = if scope.slot_count == 0 {
scope.duration * scope.weight scope.duration * scope.weight
} else { } else {
@@ -797,10 +797,21 @@ fn resolve_scope(scope: &ScopeContext, outputs: &mut Vec<String>) {
}); });
} }
emissions.sort_by(|a, b| a.delta.partial_cmp(&b.delta).unwrap_or(std::cmp::Ordering::Equal)); emissions.sort_by(|a, b| {
a.delta
.partial_cmp(&b.delta)
.unwrap_or(std::cmp::Ordering::Equal)
});
for em in emissions { for em in emissions {
emit_output(&em.sound, &em.params, em.delta, em.dur, outputs); emit_output(
&em.sound,
&em.params,
em.delta,
em.dur,
step_duration,
outputs,
);
} }
} }
@@ -842,7 +853,40 @@ fn resolve_scope_to_parent(child: &ScopeContext, parent_slot: usize, parent: &mu
} }
} }
fn emit_output(sound: &str, params: &[(String, String)], delta: f64, dur: f64, outputs: &mut Vec<String>) { const TEMPO_SCALED_PARAMS: &[&str] = &[
"attack",
"decay",
"release",
"lpa",
"lpd",
"lpr",
"hpa",
"hpd",
"hpr",
"bpa",
"bpd",
"bpr",
"patt",
"pdec",
"prel",
"fma",
"fmd",
"fmr",
"glide",
"verbdecay",
"verbpredelay",
"chorusdelay",
"duration",
];
fn emit_output(
sound: &str,
params: &[(String, String)],
delta: f64,
dur: f64,
step_duration: f64,
outputs: &mut Vec<String>,
) {
let mut pairs = vec![("sound".into(), sound.to_string())]; let mut pairs = vec![("sound".into(), sound.to_string())];
pairs.extend(params.iter().cloned()); pairs.extend(params.iter().cloned());
if delta > 0.0 { if delta > 0.0 {
@@ -857,12 +901,20 @@ fn emit_output(sound: &str, params: &[(String, String)], delta: f64, dur: f64, o
} else { } else {
pairs.push(("delaytime".into(), dur.to_string())); pairs.push(("delaytime".into(), dur.to_string()));
} }
for pair in &mut pairs {
if TEMPO_SCALED_PARAMS.contains(&pair.0.as_str()) {
if let Ok(val) = pair.1.parse::<f64>() {
pair.1 = (val * step_duration).to_string();
}
}
}
outputs.push(format_cmd(&pairs)); outputs.push(format_cmd(&pairs));
} }
fn perlin_grad(hash_input: i64) -> f64 { fn perlin_grad(hash_input: i64) -> f64 {
let mut h = let mut h = (hash_input as u64)
(hash_input as u64).wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); .wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
h ^= h >> 33; h ^= h >> 33;
h = h.wrapping_mul(0xff51afd7ed558ccd); h = h.wrapping_mul(0xff51afd7ed558ccd);
h ^= h >> 33; h ^= h >> 33;
@@ -924,7 +976,11 @@ where
{ {
let b = stack.pop().ok_or("stack underflow")?; let b = stack.pop().ok_or("stack underflow")?;
let a = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?;
let result = if f(a.as_float()?, b.as_float()?) { 1 } else { 0 }; let result = if f(a.as_float()?, b.as_float()?) {
1
} else {
0
};
stack.push(Value::Int(result, None)); stack.push(Value::Int(result, None));
Ok(()) Ok(())
} }

View File

@@ -1222,6 +1222,54 @@ pub const WORDS: &[Word] = &[
example: "0.3 bpr", example: "0.3 bpr",
compile: Param, compile: Param,
}, },
Word {
name: "llpf",
category: "Ladder Filter",
stack: "(f --)",
desc: "Set ladder lowpass frequency",
example: "2000 llpf",
compile: Param,
},
Word {
name: "llpq",
category: "Ladder Filter",
stack: "(f --)",
desc: "Set ladder lowpass resonance",
example: "0.5 llpq",
compile: Param,
},
Word {
name: "lhpf",
category: "Ladder Filter",
stack: "(f --)",
desc: "Set ladder highpass frequency",
example: "100 lhpf",
compile: Param,
},
Word {
name: "lhpq",
category: "Ladder Filter",
stack: "(f --)",
desc: "Set ladder highpass resonance",
example: "0.5 lhpq",
compile: Param,
},
Word {
name: "lbpf",
category: "Ladder Filter",
stack: "(f --)",
desc: "Set ladder bandpass frequency",
example: "1000 lbpf",
compile: Param,
},
Word {
name: "lbpq",
category: "Ladder Filter",
stack: "(f --)",
desc: "Set ladder bandpass resonance",
example: "0.5 lbpq",
compile: Param,
},
Word { Word {
name: "ftype", name: "ftype",
category: "Filter", category: "Filter",
@@ -1902,11 +1950,28 @@ fn parse_interval(name: &str) -> Option<i64> {
Some(simple) Some(simple)
} }
pub(super) fn compile_word(name: &str, span: Option<SourceSpan>, ops: &mut Vec<Op>, dict: &Dictionary) -> bool { pub(super) fn compile_word(
name: &str,
span: Option<SourceSpan>,
ops: &mut Vec<Op>,
dict: &Dictionary,
) -> bool {
match name { match name {
"linramp" => { ops.push(Op::PushFloat(1.0, span)); ops.push(Op::Ramp); return true; } "linramp" => {
"expramp" => { ops.push(Op::PushFloat(3.0, span)); ops.push(Op::Ramp); return true; } ops.push(Op::PushFloat(1.0, span));
"logramp" => { ops.push(Op::PushFloat(0.3, span)); ops.push(Op::Ramp); return true; } ops.push(Op::Ramp);
return true;
}
"expramp" => {
ops.push(Op::PushFloat(3.0, span));
ops.push(Op::Ramp);
return true;
}
"logramp" => {
ops.push(Op::PushFloat(0.3, span));
ops.push(Op::Ramp);
return true;
}
_ => {} _ => {}
} }

View File

@@ -46,14 +46,15 @@ fn multiple_emits() {
#[test] #[test]
fn envelope_params() { fn envelope_params() {
// Values are tempo-scaled: 0.01 * step_duration(0.125) = 0.00125, etc.
let outputs = expect_outputs( let outputs = expect_outputs(
r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#, r#""synth" s 0.01 attack 0.1 decay 0.7 sustain 0.3 release ."#,
1, 1,
); );
assert!(outputs[0].contains("attack/0.01")); assert!(outputs[0].contains("attack/0.00125"));
assert!(outputs[0].contains("decay/0.1")); assert!(outputs[0].contains("decay/0.0125"));
assert!(outputs[0].contains("sustain/0.7")); assert!(outputs[0].contains("sustain/0.7"));
assert!(outputs[0].contains("release/0.3")); assert!(outputs[0].contains("release/0.0375"));
} }
#[test] #[test]
@@ -66,17 +67,17 @@ fn filter_params() {
#[test] #[test]
fn adsr_sets_all_envelope_params() { fn adsr_sets_all_envelope_params() {
let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1); let outputs = expect_outputs(r#""synth" s 0.01 0.1 0.5 0.3 adsr ."#, 1);
assert!(outputs[0].contains("attack/0.01")); assert!(outputs[0].contains("attack/0.00125"));
assert!(outputs[0].contains("decay/0.1")); assert!(outputs[0].contains("decay/0.0125"));
assert!(outputs[0].contains("sustain/0.5")); assert!(outputs[0].contains("sustain/0.5"));
assert!(outputs[0].contains("release/0.3")); assert!(outputs[0].contains("release/0.0375"));
} }
#[test] #[test]
fn ad_sets_attack_decay_sustain_zero() { fn ad_sets_attack_decay_sustain_zero() {
let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1); let outputs = expect_outputs(r#""synth" s 0.01 0.1 ad ."#, 1);
assert!(outputs[0].contains("attack/0.01")); assert!(outputs[0].contains("attack/0.00125"));
assert!(outputs[0].contains("decay/0.1")); assert!(outputs[0].contains("decay/0.0125"));
assert!(outputs[0].contains("sustain/0")); assert!(outputs[0].contains("sustain/0"));
} }