WIP: prepare the ground for audio rate modulation
This commit is contained in:
@@ -49,7 +49,7 @@ cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { git = "https://github.com/sova-org/doux", features = ["native"] }
|
||||
doux = { path = "/Users/bubo/doux", features = ["native"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, &[]));
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
82
docs/engine_audio_modulation.md
Normal file
82
docs/engine_audio_modulation.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Audio-Rate Modulation
|
||||
|
||||
Any parameter can be modulated continuously using modulation words. Instead of a fixed value, these words produce a modulation string that the engine interprets as a moving signal.
|
||||
|
||||
All time values are in **steps**, just like `attack`, `decay`, and `release`. At 120 BPM with speed 1, one step is 0.125 seconds. Writing `4 lfo` means a 4-step period.
|
||||
|
||||
## LFOs
|
||||
|
||||
Oscillate a parameter between two values.
|
||||
|
||||
```forth
|
||||
saw s 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
|
||||
saw s 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
|
||||
```
|
||||
|
||||
| Word | Shape | Output |
|
||||
|------|-------|--------|
|
||||
| `lfo` | Sine | `min~max:period` |
|
||||
| `tlfo` | Triangle | `min~max:periodt` |
|
||||
| `wlfo` | Sawtooth | `min~max:periodw` |
|
||||
| `qlfo` | Square | `min~max:periodq` |
|
||||
|
||||
Stack effect: `( min max period -- str )`
|
||||
|
||||
## Slides
|
||||
|
||||
Transition from one value to another over a duration.
|
||||
|
||||
```forth
|
||||
saw s 0 1 0.5 slide gain . ( fade in over half a step )
|
||||
saw s 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
|
||||
```
|
||||
|
||||
| Word | Curve | Output |
|
||||
|------|-------|--------|
|
||||
| `slide` | Linear | `start>end:dur` |
|
||||
| `expslide` | Exponential | `start>end:dure` |
|
||||
| `sslide` | Smooth (S-curve) | `start>end:durs` |
|
||||
|
||||
Stack effect: `( start end dur -- str )`
|
||||
|
||||
## Random
|
||||
|
||||
Randomize a parameter within a range, retriggering at a given period.
|
||||
|
||||
```forth
|
||||
saw s 200 4000 2 jit lpf . ( new random value every 2 steps )
|
||||
saw s 200 4000 2 sjit lpf . ( same but smoothly interpolated )
|
||||
saw s 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
```
|
||||
|
||||
| Word | Behavior | Output |
|
||||
|------|----------|--------|
|
||||
| `jit` | Sample & hold | `min?max:period` |
|
||||
| `sjit` | Smooth interpolation | `min?max:periods` |
|
||||
| `drunk` | Random walk | `min?max:periodd` |
|
||||
|
||||
Stack effect: `( min max period -- str )`
|
||||
|
||||
## Envelopes
|
||||
|
||||
Define a multi-segment envelope for a parameter. Provide a start value, then pairs of target and duration.
|
||||
|
||||
```forth
|
||||
saw s 0 1 0.1 0.7 0.5 0 8 env gain .
|
||||
```
|
||||
|
||||
This creates: start at `0`, rise to `1` in `0.1` steps, drop to `0.7` in `0.5` steps, fall to `0` in `8` steps.
|
||||
|
||||
Stack effect: `( start target1 dur1 [target2 dur2 ...] -- str )`
|
||||
|
||||
## Combining
|
||||
|
||||
Modulation words return strings, so they compose naturally with the rest of the language. Use them anywhere a parameter value is expected.
|
||||
|
||||
```forth
|
||||
saw s
|
||||
200 4000 4 lfo lpf
|
||||
0.3 0.7 8 tlfo pan
|
||||
0 1 0.1 0.7 0.5 0 8 env gain
|
||||
.
|
||||
```
|
||||
@@ -46,6 +46,10 @@ pub const DOCS: &[DocEntry] = &[
|
||||
include_str!("../../docs/engine_distortion.md"),
|
||||
),
|
||||
Topic("Space & Time", include_str!("../../docs/engine_space.md")),
|
||||
Topic(
|
||||
"Audio-Rate Mod",
|
||||
include_str!("../../docs/engine_audio_modulation.md"),
|
||||
),
|
||||
Topic("Words & Sounds", include_str!("../../docs/engine_words.md")),
|
||||
// MIDI
|
||||
Section("MIDI"),
|
||||
|
||||
Reference in New Issue
Block a user