10 Commits

Author SHA1 Message Date
6d71c64a34 Fix: fix two show-stopper bugs 2026-03-16 16:21:02 +01:00
097104a074 chore: Release 2026-03-16 15:13:35 +01:00
c13ddaaf37 Failing to support ASIO with crossbuild 2026-03-16 15:13:08 +01:00
001a42abfc Feat: start updating workflows for asio on windows 2026-03-16 14:58:38 +01:00
0d0c2738f5 Feat: start preparing for release 2026-03-16 14:51:09 +01:00
859629ae34 Feat: adding LPG 2026-03-14 13:02:01 +01:00
82e5f47933 Feat: adapt cagire to doux v0.0.12
Some checks failed
Deploy Website / deploy (push) Failing after 20s
2026-03-14 12:43:18 +01:00
9cc17d14de Feat: add new words for new audio rate modulations 2026-03-12 17:33:50 +01:00
453ba62403 Feat: audio input channel selection 2026-03-12 14:54:34 +01:00
35aa97a93d Feat: rework recording 2026-03-10 18:20:36 +01:00
28 changed files with 694 additions and 653 deletions

View File

@@ -54,22 +54,22 @@ jobs:
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
- name: Build
run: cargo build --release --target x86_64-pc-windows-msvc
run: cargo build --release --features asio --target x86_64-pc-windows-msvc
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
run: cargo build --release --features desktop,asio --bin cagire-desktop --target x86_64-pc-windows-msvc
- name: Test
if: inputs.run-tests
run: cargo test --target x86_64-pc-windows-msvc
run: cargo test --features asio --target x86_64-pc-windows-msvc
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --target x86_64-pc-windows-msvc -- -D warnings
run: cargo clippy --features asio --target x86_64-pc-windows-msvc -- -D warnings
- name: Bundle CLAP plugin
if: inputs.build-packages
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
run: cargo xtask bundle cagire-plugins --release --features asio --target x86_64-pc-windows-msvc
- name: Install NSIS
if: inputs.build-packages

View File

@@ -2,6 +2,56 @@
All notable changes to this project will be documented in this file.
## [0.1.4]
### Breaking
- **Doux v0.0.12**: removed Mutable Instruments Plaits modes (`modal`, `va`, `analog`, `waveshape`, `grain`, `chord`, `swarm`, `pnoise`, etc.). Native percussion models retained; new models added: `tom`, `cowbell`, `cymbal`.
- Simplified effects/filter API: removed per-filter envelope parameters in favor of the universal `env` word.
- Recording commands simplified: removed `/sound/` path segment from `rec`, `overdub`, `orec`, `odub`.
### Forth Language
- New modulation transition words: `islide` (swell), `oslide` (pluck), `pslide` (stair/stepped).
- New `lpg` word (Low Pass Gate): pairs amplitude envelope with lowpass filter modulation.
- New `inchan` word: select audio input channel by index.
- New EQ frequency words: `eqlofreq`, `eqmidfreq`, `eqhifreq`.
### UI / UX
- Redesigned top bar: consolidated transport, tempo, bar:beat display with visual beat segments.
- CPU meter with color-coded fill bar (green/yellow/red).
### Engine
- Audio input channel selection support.
- Audio buffer sizing improved for multi-channel input.
### Packaging
- CI migrated from GitHub Actions to Gitea Actions.
- Removed WIX installer; Windows now distributed via zip and NSIS only.
- Gitea Actions workflow for automatic website deployment.
- Added LICENSE file.
### Documentation
- Extensive documentation updates reflecting doux v0.0.12 API changes across sources, filters, modulation, wavetable, and audio modulation docs.
## [0.1.3]
### Forth Language
- New `stretch` word: pitch-independent time stretching via phase vocoder (e.g., `kick sound 2 stretch .` plays at half speed, same pitch).
- Automatic default release time on sounds when none is explicitly set.
### Engine
- Sample-accurate timing: delta computation switched from float seconds to integer sample ticks, fixing precision issues.
- Lock-free audio input buffer: replaced `Arc<Mutex<VecDeque>>` with `HeapRb` ring buffer.
- Theme access optimized: `Rc<ThemeColors>` replaces deep cloning on every `get()`.
- Dictionary keys cached in `App` to avoid repeated lock acquisitions during rendering.
### Fixed
- Realtime priority diagnostics: dedicated `warn_no_rt()` on Linux, lookahead widened from 20ms to 40ms when RT priority unavailable.
- Float epsilon precision in delta/nudge zero-comparisons.
- Windows build fixes for standalone and plugin targets.
### Documentation
- Time stretching usage guide added to `docs/engine/samples.md`.
## [0.1.2]
### Forth Language

179
Cargo.lock generated
View File

@@ -372,6 +372,20 @@ dependencies = [
"libloading 0.8.9",
]
[[package]]
name = "asio-sys"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826194e1612938c9be09b78b58323fbb2e326de3d491b4230186cf6e832d8ded"
dependencies = [
"bindgen",
"cc",
"num-derive",
"num-traits",
"parse_cfg",
"walkdir",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -846,7 +860,7 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21"
[[package]]
name = "cagire"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"arboard",
"arc-swap",
@@ -859,7 +873,7 @@ dependencies = [
"cpal 0.17.1",
"crossbeam-channel",
"crossterm",
"doux",
"doux 0.0.14",
"eframe",
"egui",
"egui_ratatui",
@@ -885,7 +899,7 @@ dependencies = [
[[package]]
name = "cagire-forth"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"arc-swap",
"parking_lot",
@@ -894,7 +908,7 @@ dependencies = [
[[package]]
name = "cagire-markdown"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"minimad",
"ratatui",
@@ -902,7 +916,7 @@ dependencies = [
[[package]]
name = "cagire-plugins"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"arc-swap",
"cagire",
@@ -911,7 +925,7 @@ dependencies = [
"cagire-ratatui",
"crossbeam-channel",
"crossterm",
"doux",
"doux 0.0.13",
"egui_ratatui",
"nih_plug",
"nih_plug_egui",
@@ -926,7 +940,7 @@ dependencies = [
[[package]]
name = "cagire-project"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"base64",
"brotli",
@@ -938,7 +952,7 @@ dependencies = [
[[package]]
name = "cagire-ratatui"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"rand",
"ratatui",
@@ -1452,6 +1466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
dependencies = [
"alsa 0.10.0",
"asio-sys",
"coreaudio-rs 0.13.0",
"dasp_sample",
"jack 0.13.5",
@@ -1473,7 +1488,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.62.2",
"windows 0.61.3",
]
[[package]]
@@ -1809,14 +1824,30 @@ dependencies = [
[[package]]
name = "doux"
version = "0.0.8"
source = "git+https://github.com/sova-org/doux#a916717fa54b1cc0be093c131e03668d14ee1e3b"
version = "0.0.13"
source = "git+https://github.com/sova-org/doux?tag=v0.0.13#b8150d907e4cc2764e82fdaa424df41ceef9b0d2"
dependencies = [
"arc-swap",
"clap",
"cpal 0.17.1",
"crossbeam-channel",
"mi-plaits-dsp",
"ringbuf",
"rosc",
"rustyline",
"soundfont",
"symphonia",
]
[[package]]
name = "doux"
version = "0.0.14"
source = "git+https://github.com/sova-org/doux?tag=v0.0.14#f0de4f4047adfced8fb2116edd3b33d260ba75c8"
dependencies = [
"arc-swap",
"clap",
"cpal 0.17.1",
"crossbeam-channel",
"ringbuf",
"rosc",
"rustyline",
"soundfont",
@@ -1835,12 +1866,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecolor"
version = "0.33.3"
@@ -3238,16 +3263,6 @@ dependencies = [
"paste",
]
[[package]]
name = "mi-plaits-dsp"
version = "0.1.0"
source = "git+https://github.com/sourcebox/mi-plaits-dsp-rs?rev=dc55bd55e73bd6f86fbbb4f8adc3b598d659fdb4#dc55bd55e73bd6f86fbbb4f8adc3b598d659fdb4"
dependencies = [
"dyn-clone",
"num-traits",
"spin",
]
[[package]]
name = "micromath"
version = "2.1.0"
@@ -4118,6 +4133,15 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "parse_cfg"
version = "4.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49"
dependencies = [
"nom",
]
[[package]]
name = "paste"
version = "1.0.15"
@@ -5128,15 +5152,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a7f4cb358863e55f8f1a3882f68601360cf6c42fc53ff2fe9aea41c33e24489"
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@@ -6567,23 +6582,11 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections 0.2.0",
"windows-collections",
"windows-core 0.61.2",
"windows-future 0.2.1",
"windows-future",
"windows-link 0.1.3",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
"windows-numerics",
]
[[package]]
@@ -6595,15 +6598,6 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.54.0"
@@ -6652,19 +6646,6 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-future"
version = "0.2.1"
@@ -6673,18 +6654,7 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
"windows-threading 0.1.0",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
"windows-threading",
]
[[package]]
@@ -6775,16 +6745,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -6812,15 +6772,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
@@ -6840,15 +6791,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -6951,15 +6893,6 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"

View File

@@ -2,7 +2,7 @@
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
[workspace.package]
version = "0.1.3"
version = "0.1.4"
edition = "2021"
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
license = "AGPL-3.0"
@@ -45,13 +45,14 @@ desktop = [
"dep:egui_ratatui",
"dep:image",
]
asio = ["doux/asio", "cpal/asio"]
[dependencies]
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", "soundfont"] }
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
rusty_link = "0.4"
ratatui = "0.30"
crossterm = "0.29"

View File

@@ -45,13 +45,13 @@ sine sound 2 fm 0.5 fmh
- User-defined words: extend (or redefine) the language on the fly with `:name ... ;` definitions.
- Interactive documentation: built-in tutorials with runnable examples.
- **Audio engine** (powered by [Doux](https://doux.livecoding.fr)):
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive, FM (2-op, 3 algorithms), additive synthesis, wavetables, 7-voice spread, Mutable Instruments Plaits models: modal, granular, waveshaping, chord, swarm, etc.
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive (up to 32 partials), FM (2-op, 3 algorithms), wavetables, 7-voice spread.
- Drum models: seven drum models with timbral morphing.
- Sampling: disk-loaded samples with slicing, looping, pitch tracking, wavetable mode, and live recording from engine output or line input.
- Filters: biquad LP/HP/BP and ladder filters, each with independent envelope. Filters can be modulated, stacked, etc.
- Filters: biquad LP/HP/BP and ladder filters. Filters can be modulated, stacked, etc.
- Effects: phaser, flanger, chorus, smear, distortion, wavefolder, wavewrapper, bitcrusher, sample-rate reduction, 3-band EQ, tilt EQ, Haas stereo.
- Bus effects: delay (standard, ping-pong, tape, multitap), two reverb engines (Dattorro plate, Vital Space), comb filter, feedback delay with LFO, sidechain compressor.
- Modulation: vibrato, AM, ring mod, pitch envelope, FM envelope, glide — all with selectable LFO shapes (sine, tri, saw, square, sample & hold).
- Modulation: vibrato, AM, ring mod, audio-rate LFO, transitions, DAHDSR envelope modulation — all applicable to any parameter.
- **Sequencing**: probabilities, patterns, euclidean structures, sub-step timing, pattern chaining and a lot more.
- **MIDI**: receive or send MIDI messages across up to 4 inputs and 4 outputs.
- **Ableton Link**: tempo and phase sync with any Link-enabled software or hardware.
@@ -77,7 +77,6 @@ Cagire includes interactive documentation with runnable code examples. Press **F
Cagire is developed by [BuboBubo](https://raphaelforment.fr) (Raphael Forment).
- **[Doux](https://doux.livecoding.fr)** (audio engine) — Rust port of Dough, originally written in C by Felix Roos
- **mi-plaits-dsp-rs** — Rust port of Mutable Instruments Plaits DSP by Oliver Rockstedt, original code by Emilie Gillet
### License

View File

@@ -133,6 +133,9 @@ pub enum Op {
ModSlide(u8),
ModRnd(u8),
ModEnv,
ModEnvAd,
ModEnvAdr,
Lpg,
// Global params
EmitAll,
ClearGlobal,

View File

@@ -1433,7 +1433,7 @@ impl Forth {
let dur = pop_float(stack)? * ctx.step_duration();
let end = pop_float(stack)?;
let start = pop_float(stack)?;
let suffix = match curve { 1 => "e", 2 => "s", _ => "" };
let suffix = match curve { 1 => "e", 2 => "s", 3 => "i", 4 => "o", 5 => "p", _ => "" };
let s = format!("{start}>{end}:{dur}{suffix}");
stack.push(Value::Str(s.into(), None));
}
@@ -1446,25 +1446,57 @@ impl Forth {
stack.push(Value::Str(s.into(), None));
}
Op::ModEnv => {
ensure(stack, 1)?;
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();
let release = pop_float(stack)? * ctx.step_duration();
let sustain = pop_float(stack)?;
let decay = pop_float(stack)? * ctx.step_duration();
let attack = pop_float(stack)? * ctx.step_duration();
let max = pop_float(stack)?;
let min = pop_float(stack)?;
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);
}
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:{sustain}:{release}");
stack.push(Value::Str(s.into(), None));
}
Op::ModEnvAd => {
let decay = pop_float(stack)? * ctx.step_duration();
let attack = pop_float(stack)? * ctx.step_duration();
let max = pop_float(stack)?;
let min = pop_float(stack)?;
use std::fmt::Write;
let mut s = String::new();
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:0:0");
stack.push(Value::Str(s.into(), None));
}
Op::ModEnvAdr => {
let release = pop_float(stack)? * ctx.step_duration();
let decay = pop_float(stack)? * ctx.step_duration();
let attack = pop_float(stack)? * ctx.step_duration();
let max = pop_float(stack)?;
let min = pop_float(stack)?;
use std::fmt::Write;
let mut s = String::new();
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:0:{release}");
stack.push(Value::Str(s.into(), None));
}
Op::Lpg => {
let depth = pop_float(stack)?.clamp(0.0, 1.0);
let max = pop_float(stack)?;
let min = pop_float(stack)?;
let effective_max = min + (max - min) * depth;
let sd = ctx.step_duration();
let a = cmd_param_float(cmd, "attack").unwrap_or(0.0) * sd;
let d = cmd_param_float(cmd, "decay").unwrap_or(1.0) * sd;
let s = cmd_param_float(cmd, "sustain").unwrap_or(0.0);
let r = cmd_param_float(cmd, "release").unwrap_or(0.0) * sd;
use std::fmt::Write;
let mut mod_str = String::new();
let _ = write!(&mut mod_str, "{min}^{effective_max}:{a}:{d}:{s}:{r}");
cmd.set_param("lpf", Value::Str(mod_str.into(), None));
}
// MIDI operations
Op::MidiEmit => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
@@ -1642,21 +1674,21 @@ impl Forth {
}
Op::Rec => {
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}", name.as_str()?));
outputs.push(format!("/doux/rec/{}", name.as_str()?));
}
Op::Overdub => {
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?));
outputs.push(format!("/doux/rec/{}/overdub/1", name.as_str()?));
}
Op::Orec => {
let orbit = pop(stack)?.as_int()?;
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/orbit/{}", name.as_str()?, orbit));
outputs.push(format!("/doux/rec/{}/orbit/{}", name.as_str()?, orbit));
}
Op::Odub => {
let orbit = pop(stack)?.as_int()?;
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
outputs.push(format!("/doux/rec/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
}
Op::Forget => {
let name = pop(stack)?;
@@ -1710,30 +1742,18 @@ fn extract_dev_param(params: &[(&str, Value)]) -> u8 {
.unwrap_or(0)
}
fn cmd_param_float(cmd: &CmdRegister, name: &str) -> Option<f64> {
cmd.params()
.iter()
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| v.as_float().ok())
}
fn is_tempo_scaled_param(name: &str) -> bool {
matches!(
name,
"attack"
| "decay"
| "release"
| "lpa"
| "lpd"
| "lpr"
| "hpa"
| "hpd"
| "hpr"
| "bpa"
| "bpd"
| "bpr"
| "patt"
| "pdec"
| "prel"
| "fma"
| "fmd"
| "fmr"
| "glide"
| "chorusdelay"
| "duration"
"attack" | "decay" | "release" | "envdelay" | "hold" | "chorusdelay"
)
}
@@ -1758,6 +1778,9 @@ fn emit_output(
}
for (i, (k, v)) in params.iter().enumerate() {
if v.is_empty() {
continue;
}
if !out.ends_with('/') {
out.push('/');
}

View File

@@ -136,10 +136,16 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"slide" => Op::ModSlide(0),
"expslide" => Op::ModSlide(1),
"sslide" => Op::ModSlide(2),
"islide" => Op::ModSlide(3),
"oslide" => Op::ModSlide(4),
"pslide" => Op::ModSlide(5),
"jit" => Op::ModRnd(0),
"sjit" => Op::ModRnd(1),
"drunk" => Op::ModRnd(2),
"env" => Op::ModEnv,
"ead" => Op::ModEnvAd,
"eadr" => Op::ModEnvAdr,
"eadsr" | "env" => Op::ModEnv,
"lpg" => Op::Lpg,
_ => return None,
})
}

View File

@@ -73,6 +73,26 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "envdelay",
aliases: &["envdly"],
category: "Envelope",
stack: "(v.. --)",
desc: "Set envelope delay time",
example: "0.1 envdelay",
compile: Param,
varargs: true,
},
Word {
name: "hold",
aliases: &["hld"],
category: "Envelope",
stack: "(v.. --)",
desc: "Set envelope hold time",
example: "0.05 hold",
compile: Param,
varargs: true,
},
Word {
name: "adsr",
aliases: &[],
@@ -93,56 +113,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "penv",
aliases: &[],
category: "Envelope",
stack: "(v.. --)",
desc: "Set pitch envelope",
example: "0.5 penv",
compile: Param,
varargs: true,
},
Word {
name: "patt",
aliases: &[],
category: "Envelope",
stack: "(v.. --)",
desc: "Set pitch attack",
example: "0.01 patt",
compile: Param,
varargs: true,
},
Word {
name: "pdec",
aliases: &[],
category: "Envelope",
stack: "(v.. --)",
desc: "Set pitch decay",
example: "0.1 pdec",
compile: Param,
varargs: true,
},
Word {
name: "psus",
aliases: &[],
category: "Envelope",
stack: "(v.. --)",
desc: "Set pitch sustain",
example: "0 psus",
compile: Param,
varargs: true,
},
Word {
name: "prel",
aliases: &[],
category: "Envelope",
stack: "(v.. --)",
desc: "Set pitch release",
example: "0.1 prel",
compile: Param,
varargs: true,
},
// Filter
Word {
name: "lpf",
@@ -164,56 +134,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "lpe",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set lowpass envelope",
example: "0.5 lpe",
compile: Param,
varargs: true,
},
Word {
name: "lpa",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set lowpass attack",
example: "0.01 lpa",
compile: Param,
varargs: true,
},
Word {
name: "lpd",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set lowpass decay",
example: "0.1 lpd",
compile: Param,
varargs: true,
},
Word {
name: "lps",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set lowpass sustain",
example: "0.5 lps",
compile: Param,
varargs: true,
},
Word {
name: "lpr",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set lowpass release",
example: "0.3 lpr",
compile: Param,
varargs: true,
},
Word {
name: "hpf",
aliases: &[],
@@ -234,56 +154,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "hpe",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set highpass envelope",
example: "0.5 hpe",
compile: Param,
varargs: true,
},
Word {
name: "hpa",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set highpass attack",
example: "0.01 hpa",
compile: Param,
varargs: true,
},
Word {
name: "hpd",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set highpass decay",
example: "0.1 hpd",
compile: Param,
varargs: true,
},
Word {
name: "hps",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set highpass sustain",
example: "0.5 hps",
compile: Param,
varargs: true,
},
Word {
name: "hpr",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set highpass release",
example: "0.3 hpr",
compile: Param,
varargs: true,
},
Word {
name: "bpf",
aliases: &[],
@@ -304,56 +174,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "bpe",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set bandpass envelope",
example: "0.5 bpe",
compile: Param,
varargs: true,
},
Word {
name: "bpa",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set bandpass attack",
example: "0.01 bpa",
compile: Param,
varargs: true,
},
Word {
name: "bpd",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set bandpass decay",
example: "0.1 bpd",
compile: Param,
varargs: true,
},
Word {
name: "bps",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set bandpass sustain",
example: "0.5 bps",
compile: Param,
varargs: true,
},
Word {
name: "bpr",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set bandpass release",
example: "0.3 bpr",
compile: Param,
varargs: true,
},
Word {
name: "llpf",
aliases: &[],
@@ -454,6 +274,36 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "eqlofreq",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set low shelf frequency (Hz)",
example: "400 eqlofreq",
compile: Param,
varargs: true,
},
Word {
name: "eqmidfreq",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set mid peak frequency (Hz)",
example: "2000 eqmidfreq",
compile: Param,
varargs: true,
},
Word {
name: "eqhifreq",
aliases: &[],
category: "Filter",
stack: "(v.. --)",
desc: "Set high shelf frequency (Hz)",
example: "8000 eqhifreq",
compile: Param,
varargs: true,
},
Word {
name: "tilt",
aliases: &[],

View File

@@ -126,16 +126,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "repeat",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Set repeat count",
example: "4 repeat",
compile: Param,
varargs: true,
},
Word {
name: "dur",
aliases: &[],
@@ -151,7 +141,7 @@ pub(super) const WORDS: &[Word] = &[
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Set gate time",
desc: "Set gate duration (total note length, 0 = infinite sustain)",
example: "0.8 gate",
compile: Param,
varargs: true,
@@ -246,6 +236,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "inchan",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Select input channel for live input (0-indexed)",
example: "0 inchan",
compile: Param,
varargs: true,
},
Word {
name: "cut",
aliases: &[],
@@ -287,16 +287,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "glide",
aliases: &[],
category: "Oscillator",
stack: "(v.. --)",
desc: "Set glide/portamento",
example: "0.1 glide",
compile: Param,
varargs: true,
},
Word {
name: "pw",
aliases: &[],
@@ -362,7 +352,7 @@ pub(super) const WORDS: &[Word] = &[
aliases: &[],
category: "Oscillator",
stack: "(v.. --)",
desc: "Set harmonics (mutable only)",
desc: "Set harmonics (add source)",
example: "4 harmonics",
compile: Param,
varargs: true,
@@ -372,7 +362,7 @@ pub(super) const WORDS: &[Word] = &[
aliases: &[],
category: "Oscillator",
stack: "(v.. --)",
desc: "Set timbre (mutable only)",
desc: "Set timbre (add source)",
example: "0.5 timbre",
compile: Param,
varargs: true,
@@ -382,7 +372,7 @@ pub(super) const WORDS: &[Word] = &[
aliases: &[],
category: "Oscillator",
stack: "(v.. --)",
desc: "Set morph (mutable only)",
desc: "Set morph (add source)",
example: "0.5 morph",
compile: Param,
varargs: true,
@@ -468,36 +458,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "scanlfo",
aliases: &[],
category: "Wavetable",
stack: "(v.. --)",
desc: "Set scan LFO rate (Hz)",
example: "0.2 scanlfo",
compile: Param,
varargs: true,
},
Word {
name: "scandepth",
aliases: &[],
category: "Wavetable",
stack: "(v.. --)",
desc: "Set scan LFO depth (0-1)",
example: "0.4 scandepth",
compile: Param,
varargs: true,
},
Word {
name: "scanshape",
aliases: &[],
category: "Wavetable",
stack: "(v.. --)",
desc: "Set scan LFO shape (sine/tri/saw/square/sh)",
example: "\"tri\" scanshape",
compile: Param,
varargs: true,
},
// FM
Word {
name: "fm",
@@ -529,56 +489,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "fme",
aliases: &[],
category: "FM",
stack: "(v.. --)",
desc: "Set FM envelope",
example: "0.5 fme",
compile: Param,
varargs: true,
},
Word {
name: "fma",
aliases: &[],
category: "FM",
stack: "(v.. --)",
desc: "Set FM attack",
example: "0.01 fma",
compile: Param,
varargs: true,
},
Word {
name: "fmd",
aliases: &[],
category: "FM",
stack: "(v.. --)",
desc: "Set FM decay",
example: "0.1 fmd",
compile: Param,
varargs: true,
},
Word {
name: "fms",
aliases: &[],
category: "FM",
stack: "(v.. --)",
desc: "Set FM sustain",
example: "0.5 fms",
compile: Param,
varargs: true,
},
Word {
name: "fmr",
aliases: &[],
category: "FM",
stack: "(v.. --)",
desc: "Set FM release",
example: "0.1 fmr",
compile: Param,
varargs: true,
},
Word {
name: "fm2",
aliases: &[],
@@ -852,6 +762,36 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "islide",
aliases: &[],
category: "Audio Modulation",
stack: "(start end dur -- str)",
desc: "Swell transition (slow start, fast finish): start>end:duri",
example: "200 4000 1 islide lpf",
compile: Simple,
varargs: false,
},
Word {
name: "oslide",
aliases: &[],
category: "Audio Modulation",
stack: "(start end dur -- str)",
desc: "Pluck transition (fast attack, slow settle): start>end:duro",
example: "0 1 0.5 oslide gain",
compile: Simple,
varargs: false,
},
Word {
name: "pslide",
aliases: &[],
category: "Audio Modulation",
stack: "(start end dur -- str)",
desc: "Stair transition (8 discrete steps): start>end:durp",
example: "0 1 2 pslide gain",
compile: Simple,
varargs: false,
},
Word {
name: "jit",
aliases: &[],
@@ -882,13 +822,53 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "ead",
aliases: &[],
category: "Audio Modulation",
stack: "(min max a d -- str)",
desc: "Percussive envelope mod: min^max:a:d:0:0",
example: "200 8000 0.01 0.1 ead lpf",
compile: Simple,
varargs: false,
},
Word {
name: "eadr",
aliases: &[],
category: "Audio Modulation",
stack: "(min max a d r -- str)",
desc: "Percussive envelope mod with release: min^max:a:d:0:r",
example: "200 8000 0.01 0.1 0.3 eadr lpf",
compile: Simple,
varargs: false,
},
Word {
name: "eadsr",
aliases: &[],
category: "Audio Modulation",
stack: "(min max a d s r -- str)",
desc: "ADSR envelope mod: min^max:a:d:s:r",
example: "200 8000 0.01 0.1 0.5 0.3 eadsr 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",
stack: "(min max a d s r -- str)",
desc: "DAHDSR envelope modulation: min^max:a:d:s:r",
example: "200 8000 0.01 0.1 0.5 0.3 env lpf",
compile: Simple,
varargs: false,
},
Word {
name: "lpg",
aliases: &[],
category: "Audio Modulation",
stack: "(min max depth --)",
desc: "Low pass gate: pairs amp envelope with lpf modulation",
example: "0.01 0.1 ad 200 8000 1 lpg .",
compile: Simple,
varargs: false,
},

View File

@@ -57,17 +57,53 @@ saw snd 200 4000 1 drunk lpf . ( random walk, each step )
Stack effect: `( min max period -- str )`
## Envelopes
## Envelope Modulation
Define a multi-segment envelope for a parameter. Provide a start value, then pairs of target and duration.
Apply an envelope to any parameter. The `env` word is the complete form: it sweeps from `min` to `max` following a full attack, decay, sustain, release shape. All times are in steps.
```forth
saw snd 0 1 0.1 0.7 0.5 0 8 env gain .
saw snd 200 8000 0.01 0.1 0.5 0.3 env lpf .
```
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: `( min max attack decay sustain release -- str )`
Stack effect: `( start target1 dur1 [target2 dur2 ...] -- str )`
This is the building block. From it, three shorthands drop the parameters you don't need:
| Word | Stack | What it does |
|------|-------|-------------|
| `env` | `( min max a d s r -- str )` | Full envelope (attack, decay, sustain, release) |
| `eadr` | `( min max a d r -- str )` | No sustain (sustain = 0) |
| `ead` | `( min max a d -- str )` | Percussive (sustain = 0, release = 0) |
`eadsr` is an alias for `env`.
```forth
saw snd 200 8000 0.01 0.3 ead lpf . ( percussive filter pluck )
saw snd 0 5 0.01 0.1 0.3 eadr fm . ( FM depth with release tail )
saw snd 200 8000 0.01 0.1 0.5 0.3 env lpf . ( full ADSR on filter )
```
These work on any parameter — `lpf`, `fm`, `gain`, `pan`, `freq`, anything that accepts a value.
## Low Pass Gate
The `lpg` word couples the amplitude envelope with a lowpass filter. Set your amp envelope first with `ad` or `adsr`, then `lpg` mirrors it to `lpf`.
```forth
saw snd 0.01 0.1 ad 200 8000 1 lpg . ( percussive LPG )
saw snd 0.01 0.1 0.5 0.3 adsr 200 4000 1 lpg . ( sustained LPG )
```
Stack effect: `( min max depth -- )`
- `min`/`max` — filter frequency range in Hz
- `depth` — 0 to 1, scales the filter range (1 = full, 0.5 = halfway)
```forth
saw snd 0.01 0.5 ad 200 8000 0.3 lpg . ( subtle LPG, filter barely opens )
```
`lpg` reads `attack`, `decay`, `sustain`, and `release` from the current sound. If none are set, it defaults to a short percussive shape.
## Combining
@@ -77,6 +113,6 @@ Modulation words return strings, so they compose naturally with the rest of the
saw snd
200 4000 4 lfo lpf
0.3 0.7 8 tlfo pan
0 1 0.1 0.7 0.5 0 8 env gain
0 1 0.01 0.1 ead gain
.
```

View File

@@ -57,29 +57,15 @@ The `ftype` parameter sets the filter slope (rolloff steepness).
saw 800 lpf 3 ftype . ( 48 dB/oct lowpass )
```
## Filter Envelope
## Filter Envelope Modulation
Filters can be modulated by an ADSR envelope. The envelope multiplies the base cutoff:
```
final_cutoff = lpf + (lpe × envelope × lpf)
```
When the envelope is at 1.0 and `lpe` is 1.0, the cutoff doubles. When the envelope is at 0, the cutoff equals `lpf`.
Use the `env` word to apply a DAHDSR envelope to any filter cutoff:
```forth
saw 200 lpf 2 lpe 0.01 lpa 0.3 lpd . ( cutoff sweeps from 600 Hz down to 200 Hz )
saw 200 8000 0.01 0.3 0.5 0.3 env lpf . ( cutoff sweeps from 200 to 8000 Hz )
```
| Parameter | Description |
|-----------|-------------|
| `lpe` | Envelope depth (multiplier, 1.0 = double cutoff at peak) |
| `lpa` | Attack time in seconds |
| `lpd` | Decay time in seconds |
| `lps` | Sustain level (0-1) |
| `lpr` | Release time in seconds |
The same pattern works for highpass (`hpe`, `hpa`, etc.) and bandpass (`bpe`, `bpa`, etc.).
The same works for highpass and bandpass: `env hpf`, `env bpf`.
## Ladder Filters
@@ -100,7 +86,7 @@ saw 1000 lbpf 0.8 lbpq . ( ladder bandpass )
| `lbpf` | Hz | Ladder bandpass cutoff |
| `lbpq` | 0-1 | Ladder bandpass resonance |
Ladder filters share the lowpass envelope parameters (`lpe`, `lpa`, etc.).
Ladder filter cutoffs can also be modulated with `env`, `lfo`, `slide`, etc.
## EQ

View File

@@ -16,34 +16,6 @@ saw 5 vib 0.5 vibmod . ( 5 Hz, 0.5 semitone depth )
| `vibmod` | semitones | Modulation depth |
| `vibshape` | shape | LFO waveform (sine, tri, saw, square) |
## Pitch Envelope
The pitch envelope applies an ADSR to the oscillator frequency.
```forth
sine 100 freq 24 penv 0.001 patt 0.1 pdec .
```
| Parameter | Description |
|-----------|-------------|
| `penv` | Envelope depth in semitones |
| `patt` | Attack time in seconds |
| `pdec` | Decay time in seconds |
| `psus` | Sustain level (0-1) |
| `prel` | Release time in seconds |
## Glide
Glide interpolates between pitch changes over time.
```forth
saw c4 0.1 glide . ( 100ms glide )
```
| Parameter | Range | Description |
|-----------|-------|-------------|
| `glide` | seconds | Glide time |
## FM Synthesis
FM modulates the carrier frequency with a modulator oscillator.
@@ -58,7 +30,7 @@ sine 440 freq 2 fm 2 fmh . ( modulator at 2× carrier frequency )
| `fmh` | ratio | Harmonic ratio (modulator / carrier) |
| `fmshape` | shape | Modulator waveform |
FM has its own envelope (`fme`, `fma`, `fmd`, `fms`, `fmr`).
Use `env` to apply a DAHDSR envelope to FM depth: `0 5 0.01 0.1 0.3 0.5 env fm`.
## Amplitude Modulation

View File

@@ -56,37 +56,29 @@ Noise sources ignore pitch. Use filters to shape the spectrum.
All filter and effect parameters apply to the input signal.
## Plaits Engines
## Additive
The Plaits engines come from Mutable Instruments and provide a range of synthesis methods. Beware, these sources can be quite CPU hungry. All share three control parameters (`0.0`-`1.0`):
| Name | Description |
|------|-------------|
| `add` | Stacks 1-32 sine partials with spectral tilt, even/odd morph, harmonic stretching, phase shaping. |
| Parameter | Controls |
|-----------|----------|
| `harmonics` | Harmonic content, structure, detuning. |
| `timbre` | Brightness, tonal color. |
| `harmonics` | Harmonic content / structure. |
| `timbre` | Brightness / tonal color. |
| `morph` | Smooth transitions between variations. |
| `partials` | Number of active harmonics (1-32). |
### Pitched
## Percussion
Native drum synthesis with timbral morphing. All share `wave` (waveform: 0=sine, 0.5=tri, 1=saw), `morph`, `harmonics`, and `timbre` parameters.
| Name | Description |
|------|-------------|
| `modal` | Struck/plucked resonant bodies (strings, plates, tubes). |
| `va`, `analog` | Virtual analog with waveform sync and crossfading. |
| `ws`, `waveshape` | Waveshaper and wavefolder. |
| `fm2` | Two-operator FM synthesis with feedback. |
| `grain` | Granular formant oscillator (vowel-like). |
| `additive` | Harmonic additive synthesis. |
| `wavetable` | Built-in Plaits wavetables (four 8x8 banks). |
| `chord` | Four-note chord generator. |
| `swarm` | Granular cloud of enveloped sawtooths. |
| `pnoise` | Clocked noise through multimode filter. |
### Percussion
| Name | Description |
|------|-------------|
| `kick`, `bass` | 808-style bass drum. |
| `snare` | Analog snare drum with tone/noise balance. |
| `hihat`, `hat` | Metallic 808-style hi-hat. |
Percussions are super hard to use correctly, because you need to tweak their envelope correctly.
| `kick` | Bass drum. |
| `snare` | Snare drum with tone/noise balance. |
| `hat` | Hi-hat. |
| `tom` | Tom drum. |
| `rim` | Rimshot. |
| `cowbell` | Cowbell. |
| `cymbal` | Cymbal. |

View File

@@ -32,9 +32,6 @@ Without `scan`, the sample plays normally. With `scan`, it becomes a looping wav
|-----------|-------|-------------|
| `scan` | 0-1 | Position in wavetable (0 = first cycle, 1 = last) |
| `wtlen` | samples | Cycle length in samples (0 = entire sample) |
| `scanlfo` | Hz | LFO rate for scan modulation |
| `scandepth` | 0-1 | LFO modulation depth |
| `scanshape` | shape | LFO waveform |
## Cycle Length
@@ -57,24 +54,16 @@ pad 0.5 scan . ( blend between middle cycles )
pad 1 scan . ( last cycle only )
```
## LFO Modulation
## Scan Modulation
Automate the scan position with a built-in LFO:
Use audio-rate modulation words to automate the scan position:
```forth
pad 0 scan 2 scanlfo 0.3 scandepth . ( 2 Hz modulation, 30% depth )
pad 0 1 2 lfo scan . ( sine LFO, full range, 2 Hz )
pad 0 0.5 1 tlfo scan . ( triangle LFO, half range, 1 Hz )
pad 0 1 0.5 jit scan . ( random scan every 0.5 steps )
```
Available LFO shapes:
| Shape | Description |
|-------|-------------|
| `sine` | Smooth oscillation (default) |
| `tri` | Triangle wave |
| `saw` | Sawtooth, ramps up |
| `square` | Alternates between extremes |
| `sh` | Sample and hold, random steps |
## Creating Wavetables
A proper wavetable file:

View File

@@ -9,7 +9,7 @@ Each bank can carry its own prelude script. Press `p` to open the current bank's
Bank preludes make banks self-contained. When you share a bank, its prelude travels with it — recipients get all the definitions they need without merging anything into their own project.
```forth
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
: pad sine sound 0.5 gain 2 spread 1.5 attack 0.4 verb . ;
```
@@ -48,13 +48,13 @@ Only non-empty bank preludes are evaluated. Last-evaluated wins for name collisi
The most common use of a bank prelude is to define words for your instruments. Without a prelude, every step that plays a bass has to spell out the full sound design:
```forth
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
pulse sound c2 note 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width .
```
In the bank prelude, define it once:
```forth
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
```
Now every step just writes `c2 note bass`. Change the sound in one place, every step follows.
@@ -63,7 +63,7 @@ Now every step just writes `c2 note bass`. Change the sound in one place, every
```forth
;; instruments
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
: pad sine sound 0.5 gain 2 spread 1.5 attack 0.4 verb . ;
: lead tri sound 0.6 gain 5000 lpf 2 decay . ;

View File

@@ -51,7 +51,7 @@ Cagire includes a complete synthesis and sampling engine. No external software i
```forth
;; sawtooth wave + lowpass filter with envelope + chorus + reverb
100 199 freq saw sound 250 lpf 8 lpe 1 lpd 0.2 chorus 0.8 verb 2 dur .
100 199 freq saw sound 250 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 dur .
```
```forth
@@ -61,12 +61,12 @@ Cagire includes a complete synthesis and sampling engine. No external software i
```forth
;; white noise + sine wave + envelope = percussion
white sine sound 100 freq 0.5 decay 24 penv 0.5 pdec 2 dur .
white sine sound 100 freq 0.5 decay 2 dur .
```
```forth
;; random robot noises: sine + randomized freq + ring modulation
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmddepth .
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmdepth .
```
By _creating words_, registering synth definitions and effects, you will form a vocabulary that can be used to create complex sounds and music. The audio engine is quite capable, and you won't ever run out of new things to try!

View File

@@ -35,7 +35,7 @@ c4 M3 P5 note 1.5 decay sine snd .
That builds a C major triad from scratch: C4 (60), then a major third above (64), then a perfect fifth above the root (67). Three notes on the stack, all played together.
```forth
a3 m3 P5 note 1.2 decay va snd .
a3 m3 P5 note 1.2 decay saw snd .
```
A minor triad: A3, C4, E4.
@@ -94,7 +94,7 @@ c4 maj note 1.5 decay sine snd .
That's the same C major triad, but in one word instead of `M3 P5`. A few more:
```forth
d3 min7 note 1.5 decay va snd .
d3 min7 note 1.5 decay saw snd .
```
```forth
@@ -160,13 +160,13 @@ G3 C4 E4. The fifth drops below the root.
`drop2` and `drop3` are jazz voicing techniques for four-note chords. `drop2` takes the second-from-top note and drops it an octave:
```forth
c4 maj7 drop2 note 1.2 decay va snd .
c4 maj7 drop2 note 1.2 decay saw snd .
```
From C4 E4 G4 B4, the G drops to G3: G3 C4 E4 B4. `drop3` drops the third-from-top:
```forth
c4 maj7 drop3 note 1.2 decay va snd .
c4 maj7 drop3 note 1.2 decay saw snd .
```
E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jazz guitar and piano.
@@ -182,7 +182,7 @@ c4 maj 3 tp note 1.5 decay sine snd .
C major transposed up 3 semitones becomes Eb major. Works with any number of notes:
```forth
c4 min7 -2 tp note 1.5 decay va snd .
c4 min7 -2 tp note 1.5 decay saw snd .
```
Shifts the whole chord down 2 semitones (Bb minor 7).
@@ -219,7 +219,7 @@ Walk through a scale with `cycle`:
Random notes from a scale:
```forth
0 7 rand pentatonic note 0.8 decay va snd .
0 7 rand pentatonic note 0.8 decay saw snd .
```
### Setting the key
@@ -273,7 +273,7 @@ Degree 0 of the major scale, stacked in thirds: C E G — a major triad. The sca
`seventh` adds a fourth note:
```forth
0 major seventh note 1.2 decay va snd .
0 major seventh note 1.2 decay saw snd .
```
C E G B — Cmaj7. Degree 1 gives Dm7, degree 4 gives G7 (dominant). The diatonic context determines everything.
@@ -291,7 +291,7 @@ A I-vi-IV-V chord progression using `pcycle`:
```forth
( 0 major seventh ) ( 5 major seventh )
( 3 major seventh ) ( 4 major seventh ) 4 pcycle
note 1.2 decay va snd .
note 1.2 decay saw snd .
```
Combine with voicings for smoother voice leading:
@@ -299,7 +299,7 @@ Combine with voicings for smoother voice leading:
```forth
( 0 major seventh ) ( 5 major seventh inv )
( 3 major seventh ) ( 4 major seventh drop2 ) 4 pcycle
note 1.5 decay va snd .
note 1.5 decay saw snd .
```
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):

View File

@@ -188,7 +188,7 @@ A melodic step with weighted note selection and random timbre:
c4 0.4 e4 0.3 g4 0.2 b4 0.1 4 wchoose note
0.3 0.7 rand decay
1.0 4.0 exprand harmonics
modal snd .
add snd .
```
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.

View File

@@ -22,4 +22,3 @@ Cagire is mainly developed by BuboBubo (Raphaël Maurice Forment, [raphaelformen
### Credits
* **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
* **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits (Emilie Gillet). Rust port by Oliver Rockstedt.

View File

@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
cagire-forth = { path = "../../crates/forth" }
cagire-project = { path = "../../crates/project" }
cagire-ratatui = { path = "../../crates/ratatui" }
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.13", features = ["native", "soundfont"] }
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
egui_ratatui = "2.1"

View File

@@ -234,6 +234,7 @@ pub fn create_editor(
// Read live snapshot from the audio thread
let shared = editor.bridge.shared_state.load();
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
editor.app.playback.playing = editor.snapshot.playing;
// Sync host tempo into LinkState so title bar shows real tempo
if shared.tempo > 0.0 {
@@ -298,6 +299,11 @@ pub fn create_editor(
let elapsed = editor.last_frame.elapsed();
editor.last_frame = Instant::now();
if editor.app.playback.has_armed() {
let rate = std::f32::consts::TAU;
editor.app.ui.pulse_phase = (editor.app.ui.pulse_phase + elapsed.as_secs_f32() * rate) % std::f32::consts::TAU;
}
let link = &editor.link;
let app = &editor.app;
let snapshot = &editor.snapshot;

View File

@@ -496,6 +496,11 @@ impl eframe::App for CagireDesktop {
let elapsed = self.last_frame.elapsed();
self.last_frame = std::time::Instant::now();
if self.app.playback.has_armed() {
let rate = std::f32::consts::TAU;
self.app.ui.pulse_phase = (self.app.ui.pulse_phase + elapsed.as_secs_f32() * rate) % std::f32::consts::TAU;
}
let link = &self.link;
let app = &self.app;
self.terminal

View File

@@ -339,8 +339,9 @@ pub fn build_stream(
let channels = channels as usize;
let max_voices = config.max_voices;
let block_size = if config.buffer_size > 0 { config.buffer_size as usize } else { 512 };
let mut engine =
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics));
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics), block_size);
engine.sample_index = initial_samples;
for path in sample_paths {
@@ -355,9 +356,6 @@ pub fn build_stream(
let registry = Arc::clone(&engine.sample_registry);
const INPUT_BUFFER_SIZE: usize = 8192;
let (input_producer, input_consumer) = HeapRb::<f32>::new(INPUT_BUFFER_SIZE).split();
let input_device = config
.input_device
.as_ref()
@@ -374,6 +372,12 @@ pub fn build_stream(
.and_then(|dev| dev.default_input_config().ok())
.map_or(0, |cfg| cfg.channels() as usize);
engine.input_channels = input_channels;
const INPUT_BUFFER_BASE: usize = 8192;
let input_buffer_size = INPUT_BUFFER_BASE * (input_channels.max(2) / 2);
let (input_producer, input_consumer) = HeapRb::<f32>::new(input_buffer_size).split();
let input_stream = input_device.and_then(|dev| {
let input_cfg = match dev.default_input_config() {
Ok(cfg) => cfg,
@@ -476,47 +480,16 @@ pub fn build_stream(
}
}
// doux expects stereo interleaved live_input (CHANNELS=2)
let stereo_len = buffer_samples * 2;
if live_scratch.len() < stereo_len {
live_scratch.resize(stereo_len, 0.0);
}
match input_channels {
0 => {
live_scratch[..stereo_len].fill(0.0);
}
1 => {
for i in 0..buffer_samples {
let s = input_consumer.try_pop().unwrap_or(0.0);
live_scratch[i * 2] = s;
live_scratch[i * 2 + 1] = s;
}
}
2 => {
for sample in &mut live_scratch[..stereo_len] {
*sample = input_consumer.try_pop().unwrap_or(0.0);
}
}
_ => {
for i in 0..buffer_samples {
let l = input_consumer.try_pop().unwrap_or(0.0);
let r = input_consumer.try_pop().unwrap_or(0.0);
for _ in 2..input_channels {
input_consumer.try_pop();
}
live_scratch[i * 2] = l;
live_scratch[i * 2 + 1] = r;
}
}
}
// Discard excess if input produced more than we consumed
let excess = input_consumer.occupied_len().saturating_sub(INPUT_BUFFER_SIZE / 2);
for _ in 0..excess {
input_consumer.try_pop();
let nch_in = input_channels.max(1);
let raw_len = buffer_samples * nch_in;
if live_scratch.len() < raw_len {
live_scratch.resize(raw_len, 0.0);
}
live_scratch[..raw_len].fill(0.0);
input_consumer.pop_slice(&mut live_scratch[..raw_len]);
engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(data, &[], &live_scratch[..stereo_len]);
engine.process_block(data, &[], &live_scratch[..raw_len]);
scope_buffer.write(data);
// Feed mono mix to analysis thread via ring buffer (non-blocking)

View File

@@ -170,6 +170,7 @@ pub struct SharedSequencerState {
pub event_count: usize,
pub tempo: f64,
pub beat: f64,
pub playing: bool,
pub script_trace: Option<ExecutionTrace>,
pub print_output: Option<String>,
}
@@ -180,6 +181,7 @@ pub struct SequencerSnapshot {
pub event_count: usize,
pub tempo: f64,
pub beat: f64,
pub playing: bool,
script_trace: Option<ExecutionTrace>,
pub print_output: Option<String>,
}
@@ -192,6 +194,7 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
event_count: s.event_count,
tempo: s.tempo,
beat: s.beat,
playing: s.playing,
script_trace: s.script_trace.clone(),
print_output: s.print_output.clone(),
}
@@ -207,6 +210,7 @@ impl SequencerSnapshot {
event_count: 0,
tempo: 0.0,
beat: 0.0,
playing: false,
script_trace: None,
print_output: None,
}
@@ -306,6 +310,7 @@ struct PendingPattern {
struct AudioState {
prev_beat: f64,
pause_beat: Option<f64>,
active_patterns: HashMap<PatternId, ActivePattern>,
pending_starts: Vec<PendingPattern>,
pending_stops: Vec<PendingPattern>,
@@ -316,6 +321,7 @@ impl AudioState {
fn new() -> Self {
Self {
prev_beat: -1.0,
pause_beat: None,
active_patterns: HashMap::new(),
pending_starts: Vec::new(),
pending_stops: Vec::new(),
@@ -572,6 +578,7 @@ pub struct SequencerState {
soloed: std::collections::HashSet<(usize, usize)>,
last_tempo: f64,
last_beat: f64,
last_playing: bool,
script_text: String,
script_speed: crate::model::PatternSpeed,
script_length: usize,
@@ -610,6 +617,7 @@ impl SequencerState {
soloed: std::collections::HashSet::new(),
last_tempo: 0.0,
last_beat: 0.0,
last_playing: false,
script_text: String::new(),
script_speed: crate::model::PatternSpeed::default(),
script_length: 16,
@@ -714,6 +722,7 @@ impl SequencerState {
self.audio_state.active_patterns.clear();
self.audio_state.pending_starts.clear();
self.audio_state.pending_stops.clear();
self.audio_state.pause_beat = None;
self.step_traces = Arc::new(HashMap::new());
self.runs_counter.counts.clear();
self.audio_state.flush_midi_notes = true;
@@ -724,6 +733,7 @@ impl SequencerState {
active.iter = 0;
}
self.audio_state.prev_beat = -1.0;
self.audio_state.pause_beat = None;
self.script_frontier = -1.0;
self.script_step = 0;
self.script_trace = None;
@@ -751,6 +761,7 @@ impl SequencerState {
self.process_commands(input.commands);
self.last_tempo = input.tempo;
self.last_beat = input.beat;
self.last_playing = input.playing;
if !input.playing {
return self.tick_paused();
@@ -758,14 +769,21 @@ impl SequencerState {
let frontier = self.audio_state.prev_beat;
let lookahead_end = input.lookahead_end;
let resuming = frontier < 0.0;
if frontier < 0.0 {
let boundary_frontier = if resuming {
self.audio_state.pause_beat.take().unwrap_or(input.beat)
} else {
frontier
};
self.activate_pending(lookahead_end, boundary_frontier, input.quantum);
self.deactivate_pending(lookahead_end, boundary_frontier, input.quantum);
if resuming {
self.realign_phaselock_patterns(lookahead_end);
}
self.activate_pending(lookahead_end, frontier, input.quantum);
self.deactivate_pending(lookahead_end, frontier, input.quantum);
let steps = self.execute_steps(
input.beat,
frontier,
@@ -822,6 +840,9 @@ impl SequencerState {
self.pattern_cache.set(key.0, key.1, snapshot);
}
}
if self.audio_state.prev_beat >= 0.0 {
self.audio_state.pause_beat = Some(self.audio_state.prev_beat);
}
self.audio_state.prev_beat = -1.0;
self.script_frontier = -1.0;
self.script_step = 0;
@@ -1240,6 +1261,7 @@ impl SequencerState {
event_count: self.event_count,
tempo: self.last_tempo,
beat: self.last_beat,
playing: self.last_playing,
script_trace: self.script_trace.clone(),
print_output: self.print_output.clone(),
}
@@ -1975,10 +1997,8 @@ mod tests {
assert!(!state.audio_state.pending_starts.is_empty());
// Resume playing — first tick resets prev_beat from -1 to 2.0
// Resume playing — Immediate fires on first tick
state.tick(tick_at(2.0, true));
// Second tick: prev_beat is now >= 0, so Immediate fires
state.tick(tick_at(2.25, true));
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
}
@@ -2187,4 +2207,222 @@ mod tests {
// Should have commands from both patterns (2 patterns * 1 command each)
assert!(output.audio_commands.len() >= 2);
}
#[test]
fn test_bar_boundary_crossed_during_pause() {
let mut state = make_state();
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: simple_pattern(4),
}],
0.0,
));
// Queue Bar-quantized start at beat 3.5 (before bar at beat 4.0)
state.tick(tick_with(
vec![SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}],
3.5,
));
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
// Pause — saves prev_beat=3.5 as pause_beat
state.tick(tick_at(3.75, false));
// Resume after bar boundary (beat 4.0 crossed)
state.tick(tick_at(4.5, true));
assert!(
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
"Bar-quantized pattern should activate on resume after bar boundary"
);
}
#[test]
fn test_immediate_activates_on_first_resume_tick() {
let mut state = make_state();
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: simple_pattern(4),
}],
0.0,
));
// Pause
state.tick(tick_at(1.0, false));
// Queue Immediate start while paused
state.tick(TickInput {
commands: vec![SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
}],
..tick_at(2.0, false)
});
// Resume — Immediate should fire on this single tick
state.tick(tick_at(3.0, true));
assert!(
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
"Immediate should activate on first resume tick"
);
}
#[test]
fn test_multiple_patterns_sync_after_pause() {
let mut state = make_state();
state.tick(tick_with(
vec![
SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: simple_pattern(4),
},
SeqCommand::PatternUpdate {
bank: 0,
pattern: 1,
data: simple_pattern(8),
},
],
0.0,
));
// Queue two Bar-quantized starts
state.tick(tick_with(
vec![
SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
},
SeqCommand::PatternStart {
bank: 0,
pattern: 1,
quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
},
],
3.5,
));
// Pause before bar
state.tick(tick_at(3.75, false));
// Resume after bar boundary
state.tick(tick_at(4.5, true));
assert!(
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
"First pattern should activate"
);
assert!(
state.audio_state.active_patterns.contains_key(&pid(0, 1)),
"Second pattern should activate together"
);
}
fn phaselock_pattern(length: usize) -> PatternSnapshot {
PatternSnapshot {
speed: Default::default(),
length,
steps: (0..length)
.map(|_| StepSnapshot {
active: true,
script: "test".into(),
source: None,
})
.collect(),
sync_mode: SyncMode::PhaseLock,
follow_up: FollowUp::default(),
}
}
#[test]
fn test_phaselock_position_correct_after_resume() {
let mut state = make_state();
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: phaselock_pattern(16),
}],
0.0,
));
// Queue PhaseLock pattern
state.tick(tick_with(
vec![SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::PhaseLock,
}],
3.5,
));
// Pause before bar
state.tick(tick_at(3.75, false));
// Resume at beat 5.0 (after bar boundary at 4.0)
let resume_beat = 5.0;
state.tick(tick_at(resume_beat, true));
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
// realign_phaselock_patterns uses: (beat * 4.0).floor() + 1 % length
// At beat 5.0: (5.0 * 4.0).floor() = 20, +1 = 21, 21 % 16 = 5
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
let expected = ((resume_beat * 4.0).floor() as usize + 1) % 16;
assert_eq!(
ap.step_index, expected,
"PhaseLock step should be based on resume beat, not pause beat"
);
}
#[test]
fn test_no_false_boundary_after_pause_within_same_bar() {
let mut state = make_state();
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
pattern: 0,
data: simple_pattern(4),
}],
0.0,
));
// Queue Bar-quantized start at beat 1.0
state.tick(tick_with(
vec![SeqCommand::PatternStart {
bank: 0,
pattern: 0,
quantization: LaunchQuantization::Bar,
sync_mode: SyncMode::Reset,
}],
1.0,
));
// Pause at beat 1.5 (within same bar)
state.tick(tick_at(1.5, false));
// Resume at beat 2.5 (still within same bar, quantum=4)
state.tick(tick_at(2.5, true));
assert!(
!state.audio_state.active_patterns.contains_key(&pid(0, 0)),
"Bar-quantized pattern should NOT activate when no bar boundary was crossed"
);
}
}

View File

@@ -230,19 +230,19 @@ fn noall_clears_across_evaluations() {
#[test]
fn rec() {
let outputs = expect_outputs(r#""loop1" rec"#, 1);
assert_eq!(outputs[0], "/doux/rec/sound/loop1");
assert_eq!(outputs[0], "/doux/rec/loop1");
}
#[test]
fn overdub() {
let outputs = expect_outputs(r#""loop1" overdub"#, 1);
assert_eq!(outputs[0], "/doux/rec/sound/loop1/overdub/1");
assert_eq!(outputs[0], "/doux/rec/loop1/overdub/1");
}
#[test]
fn overdub_alias_dub() {
let outputs = expect_outputs(r#""loop1" dub"#, 1);
assert_eq!(outputs[0], "/doux/rec/sound/loop1/overdub/1");
assert_eq!(outputs[0], "/doux/rec/loop1/overdub/1");
}
#[test]

View File

@@ -5,9 +5,9 @@ import fs from 'node:fs';
const EMIT = new Set(['.', '.!']);
const SOUNDS = new Set([
'sound', 's', 'saw', 'sine', 'kick', 'hat', 'snare', 'modal', 'noise',
'square', 'tri', 'pulse', 'clap', 'rim', 'crash', 'fm', 'sample', 'plaits',
'analog', 'waveshaping', 'granular', 'string', 'chord', 'speech', 'sub',
'sound', 's', 'saw', 'sine', 'kick', 'hat', 'snare', 'noise', 'add',
'square', 'tri', 'pulse', 'clap', 'rim', 'crash', 'fm', 'sample',
'tom', 'cowbell', 'cymbal', 'white', 'pink', 'brown', 'live', 'sub',
'super', 'wt', 'input', 'hh',
]);
const PARAMS = new Set([

View File

@@ -103,9 +103,9 @@ const DL = 'https://dlcagire.raphaelforment.fr';
<h2>Features (click to learn more!)</h2>
<div class="features">
<div class="feature-tags">
<button data-desc="Powered by an audio engine crafted specifically for live coding. Classic waveforms (aliased and non-aliased). Noise generators. Mutable Instruments Plaits modes (modal, virtual analog, waveshaping, FM, granular, additive, etc). Small but fun FM synth with 2 operators, multiple algorithms and feedback control. Wavetable scanning with LFO control: bring your own wavetables. Sub-oscillators with independent waveform and octave. Sample playback with slicing, time-stretching and gating. Live microphone input. Configurable polyphony (default 32 voices). 8 independent effect buses (orbits).">Synthesis</button>
<button data-desc="Powered by an audio engine crafted specifically for live coding. Classic waveforms (aliased and non-aliased). Noise generators. Additive synthesis (up to 32 partials). 7 native drum models with timbral morphing. FM synth with 2 operators, multiple algorithms and feedback control. Wavetable scanning: bring your own wavetables. Sub-oscillators with independent waveform and octave. Sample playback with slicing, time-stretching and gating. Live microphone input. Configurable polyphony (default 32 voices). 8 independent effect buses (orbits).">Synthesis</button>
<button data-desc="Two reverb algorithms, Four delay types: standard, ping-pong, tape and multitap. Feedback, chorus, phaser and flanger with configurable depth, sweep and feedback. Distortion, bitcrusher, wave folding and wave wrapping. Comb filter with tunable frequency, feedback and damping. Audio-rate modulation for the final touch. ">Effects</button>
<button data-desc="Multimode filter with lowpass, highpass and bandpass modes, each with its own ADSR envelope, frequency and resonance. Selectable filter slope (12, 24 or 48 dB/oct). Ladder filter variants (lowpass, highpass, bandpass) if you like that! 3-band parametric EQ (lo/mid/hi) and a tilt EQ for broad tonal shaping.">Filters</button>
<button data-desc="Multimode filter with lowpass, highpass and bandpass modes, modulatable via universal envelope modulation, frequency and resonance. Selectable filter slope (12, 24 or 48 dB/oct). Ladder filter variants (lowpass, highpass, bandpass) if you like that! 3-band parametric EQ (lo/mid/hi) and a tilt EQ for broad tonal shaping.">Filters</button>
<button data-desc="4 MIDI inputs and 4 MIDI outputs. Send and receive CC messages, pitch bend, channel aftertouch and program changes. MIDI clock output (clock, start, stop, continue). Read incoming CC values in real-time from any device. Full channel selection per voice.">MIDI</button>
<button data-desc="Write notes by name (c4, d#5), build chords and scales from a rich built-in library. Convert between MIDI and frequency on the fly. Express musical ideas directly in code.">Theory</button>
<button data-desc="Conditional execution and probability branching built into the language. Weighted random choices, coin flips, euclidean rhythms with rotation. Multiple random distributions, Perlin noise, seeded randomness. Cagire is designed to be generative and fun.">Probability</button>