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-markdown = { path = "crates/markdown" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
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"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ pub enum Op {
|
|||||||
EuclidRot,
|
EuclidRot,
|
||||||
Times,
|
Times,
|
||||||
Chord(&'static [i64]),
|
Chord(&'static [i64]),
|
||||||
|
// Audio-rate modulation DSL
|
||||||
|
ModLfo(u8),
|
||||||
|
ModSlide(u8),
|
||||||
|
ModRnd(u8),
|
||||||
|
ModEnv,
|
||||||
// MIDI
|
// MIDI
|
||||||
MidiEmit,
|
MidiEmit,
|
||||||
GetMidiCC,
|
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
|
// MIDI operations
|
||||||
Op::MidiEmit => {
|
Op::MidiEmit => {
|
||||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||||
|
|||||||
@@ -104,6 +104,17 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"mstop" => Op::MidiStop,
|
"mstop" => Op::MidiStop,
|
||||||
"mcont" => Op::MidiContinue,
|
"mcont" => Op::MidiContinue,
|
||||||
"forget" => Op::Forget,
|
"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,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -659,4 +659,115 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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"),
|
include_str!("../../docs/engine_distortion.md"),
|
||||||
),
|
),
|
||||||
Topic("Space & Time", include_str!("../../docs/engine_space.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")),
|
Topic("Words & Sounds", include_str!("../../docs/engine_words.md")),
|
||||||
// MIDI
|
// MIDI
|
||||||
Section("MIDI"),
|
Section("MIDI"),
|
||||||
|
|||||||
Reference in New Issue
Block a user