WIP: prepare the ground for audio rate modulation

This commit is contained in:
2026-02-07 12:08:11 +01:00
parent 5758b18d58
commit c72733bac8
7 changed files with 260 additions and 1 deletions

View File

@@ -106,6 +106,11 @@ pub enum Op {
EuclidRot,
Times,
Chord(&'static [i64]),
// Audio-rate modulation DSL
ModLfo(u8),
ModSlide(u8),
ModRnd(u8),
ModEnv,
// MIDI
MidiEmit,
GetMidiCC,

View File

@@ -1103,6 +1103,52 @@ impl Forth {
}
}
Op::ModLfo(shape) => {
let period = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration();
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
let suffix = match shape { 1 => "t", 2 => "w", 3 => "q", _ => "" };
let s = format!("{min}~{max}:{period}{suffix}");
stack.push(Value::Str(s.into(), None));
}
Op::ModSlide(curve) => {
let dur = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration();
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
let suffix = match curve { 1 => "e", 2 => "s", _ => "" };
let s = format!("{start}>{end}:{dur}{suffix}");
stack.push(Value::Str(s.into(), None));
}
Op::ModRnd(dist) => {
let period = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration();
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
let suffix = match dist { 1 => "s", 2 => "d", _ => "" };
let s = format!("{min}?{max}:{period}{suffix}");
stack.push(Value::Str(s.into(), None));
}
Op::ModEnv => {
if stack.is_empty() {
return Err("stack underflow".into());
}
let values = std::mem::take(stack);
let mut floats = Vec::with_capacity(values.len());
for v in &values {
floats.push(v.as_float()?);
}
if floats.len() < 3 || (floats.len() - 1) % 2 != 0 {
return Err("env expects: start target1 dur1 [target2 dur2 ...]".into());
}
let step_dur = ctx.step_duration();
use std::fmt::Write;
let mut s = String::new();
let _ = write!(&mut s, "{}", floats[0]);
for pair in floats[1..].chunks(2) {
let _ = write!(&mut s, ">{}:{}", pair[0], pair[1] * step_dur);
}
stack.push(Value::Str(s.into(), None));
}
// MIDI operations
Op::MidiEmit => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));

View File

@@ -104,6 +104,17 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"mstop" => Op::MidiStop,
"mcont" => Op::MidiContinue,
"forget" => Op::Forget,
"lfo" => Op::ModLfo(0),
"tlfo" => Op::ModLfo(1),
"wlfo" => Op::ModLfo(2),
"qlfo" => Op::ModLfo(3),
"slide" => Op::ModSlide(0),
"expslide" => Op::ModSlide(1),
"sslide" => Op::ModSlide(2),
"jit" => Op::ModRnd(0),
"sjit" => Op::ModRnd(1),
"drunk" => Op::ModRnd(2),
"env" => Op::ModEnv,
_ => return None,
})
}

View File

@@ -659,4 +659,115 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
// Audio-rate Modulation DSL
Word {
name: "lfo",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Sine oscillation: min~max:period",
example: "200 4000 2 lfo lpf",
compile: Simple,
varargs: false,
},
Word {
name: "tlfo",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Triangle oscillation: min~max:periodt",
example: "0.3 0.7 0.5 tlfo pan",
compile: Simple,
varargs: false,
},
Word {
name: "wlfo",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Sawtooth oscillation: min~max:periodw",
example: "200 4000 1 wlfo lpf",
compile: Simple,
varargs: false,
},
Word {
name: "qlfo",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Square oscillation: min~max:periodq",
example: "0.0 1.0 0.25 qlfo gain",
compile: Simple,
varargs: false,
},
Word {
name: "slide",
aliases: &[],
category: "Audio Modulation",
stack: "(start end dur -- str)",
desc: "Linear transition: start>end:dur",
example: "0 1 0.01 slide gain",
compile: Simple,
varargs: false,
},
Word {
name: "expslide",
aliases: &[],
category: "Audio Modulation",
stack: "(start end dur -- str)",
desc: "Exponential transition: start>end:dure",
example: "0 1 0.5 expslide gain",
compile: Simple,
varargs: false,
},
Word {
name: "sslide",
aliases: &[],
category: "Audio Modulation",
stack: "(start end dur -- str)",
desc: "Smooth transition: start>end:durs",
example: "200 800 1 sslide lpf",
compile: Simple,
varargs: false,
},
Word {
name: "jit",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Random hold: min?max:period",
example: "200 4000 0.5 jit lpf",
compile: Simple,
varargs: false,
},
Word {
name: "sjit",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Smooth random: min?max:periods",
example: "200 4000 0.5 sjit lpf",
compile: Simple,
varargs: false,
},
Word {
name: "drunk",
aliases: &[],
category: "Audio Modulation",
stack: "(min max period -- str)",
desc: "Drunk walk: min?max:periodd",
example: "200 4000 0.5 drunk lpf",
compile: Simple,
varargs: false,
},
Word {
name: "env",
aliases: &[],
category: "Audio Modulation",
stack: "(start t1 d1 ... -- str)",
desc: "Multi-segment envelope: start>t1:d1>...",
example: "0 1 0.01 0.7 0.1 0 2 env gain",
compile: Simple,
varargs: false,
},
];