17 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
25866f66d4 Feat: UI / UX improvements (top bar) 2026-03-07 19:31:31 +01:00
8b058f2bb9 Feat: CPU meter in top bar 2026-03-07 19:08:54 +01:00
cb82337d24 Feat: add missing LICENSE file 2026-03-07 15:32:23 +01:00
539aa6a9f7 Feat: move CI (GitHub - Gitea) 2026-03-07 14:23:28 +01:00
b7d9436cee Feat: move out of GitHub, remove GitHub references
All checks were successful
Deploy Website / deploy (push) Successful in 25s
2026-03-07 14:17:58 +01:00
3d345d57f5 Merge branch 'main' of https://git.raphaelforment.fr/BuboBubo/cagire
All checks were successful
Deploy Website / deploy (push) Successful in 25s
2026-03-07 14:15:21 +01:00
c6b14bf508 Feat: remove wix 2026-03-07 14:15:13 +01:00
50 changed files with 889 additions and 1009 deletions

View File

@@ -81,7 +81,7 @@ jobs:
- name: Build .pkg installer
run: |
VERSION="${GITHUB_REF_NAME#v}"
VERSION="${GITEA_REF_NAME#v}"
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
cp -R Cagire.app pkg-root/Applications/
cp cagire pkg-root/usr/local/bin/

View File

@@ -36,9 +36,7 @@ jobs:
- name: Prepare plugin artifacts
run: |
mkdir -p target/bundled
# CLAP: single .so renamed to .clap
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so target/bundled/cagire-plugins.clap
# VST3: correct directory structure
mkdir -p "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux"
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux/cagire-plugins.so"

View File

@@ -0,0 +1,17 @@
name: Build Plugins
on:
workflow_dispatch:
jobs:
linux:
uses: ./.gitea/workflows/build-plugins-linux.yml
macos:
uses: ./.gitea/workflows/build-plugins-macos.yml
windows:
uses: ./.gitea/workflows/build-plugins-windows.yml
rpi:
uses: ./.gitea/workflows/build-plugins-rpi.yml

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

23
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,23 @@
name: CI
on:
workflow_dispatch:
jobs:
linux:
uses: ./.gitea/workflows/build-linux.yml
with:
run-tests: true
run-clippy: true
macos:
uses: ./.gitea/workflows/build-macos.yml
with:
run-tests: true
run-clippy: true
windows:
uses: ./.gitea/workflows/build-windows.yml
with:
run-tests: true
run-clippy: true

View File

@@ -2,23 +2,15 @@ name: Release
on:
workflow_dispatch:
push:
tags: ['v*']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
linux:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-linux.yml
uses: ./.gitea/workflows/build-linux.yml
with:
build-packages: true
macos:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-macos.yml
uses: ./.gitea/workflows/build-macos.yml
with:
build-packages: true
matrix: >-
@@ -28,26 +20,21 @@ jobs:
]
windows:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-windows.yml
uses: ./.gitea/workflows/build-windows.yml
with:
build-packages: true
cross:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-cross.yml
uses: ./.gitea/workflows/build-cross.yml
assemble-macos:
needs: macos
uses: ./.github/workflows/assemble-macos.yml
uses: ./.gitea/workflows/assemble-macos.yml
release:
needs: [linux, macos, windows, cross, assemble-macos]
if: startsWith(github.ref, 'refs/tags/v') && github.server_url == 'https://github.com'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download all artifacts
@@ -100,8 +87,25 @@ jobs:
fi
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true
- name: Create Gitea release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
TAG="${GITEA_REF_NAME:-manual-$(date +%Y%m%d-%H%M%S)}"
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases"
RELEASE_ID=$(curl -s -X POST "$API_URL" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"draft\": true}" \
| jq -r '.id')
for file in release/*; do
filename=$(basename "$file")
curl -s -X POST "$API_URL/$RELEASE_ID/assets?name=$filename" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$file"
done
echo "Release $TAG created as draft with $(ls release | wc -l) assets"

View File

@@ -1,18 +0,0 @@
name: Build Plugins
on:
workflow_call:
workflow_dispatch:
jobs:
linux:
uses: ./.github/workflows/build-plugins-linux.yml
macos:
uses: ./.github/workflows/build-plugins-macos.yml
windows:
uses: ./.github/workflows/build-plugins-windows.yml
rpi:
uses: ./.github/workflows/build-plugins-rpi.yml

View File

@@ -1,28 +0,0 @@
name: CI
on:
push:
tags: ['v*']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
linux:
uses: ./.github/workflows/build-linux.yml
with:
run-tests: true
run-clippy: true
macos:
uses: ./.github/workflows/build-macos.yml
with:
run-tests: true
run-clippy: true
windows:
uses: ./.github/workflows/build-windows.yml
with:
run-tests: true
run-clippy: true

View File

@@ -1,59 +0,0 @@
name: Deploy Website
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
deploy:
if: github.server_url == 'https://github.com'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: website/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
working-directory: website
- name: Build
run: pnpm build
working-directory: website
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: website/dist
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -3,7 +3,7 @@
## Quick Start
```bash
git clone https://github.com/Bubobubobubobubo/cagire
git clone https://git.raphaelforment.fr/BuboBubo/cagire
cd cagire
cargo build --release
```

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,11 +2,11 @@
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"
repository = "https://github.com/Bubobubobubobubo/cagire"
repository = "https://git.raphaelforment.fr/BuboBubo/cagire"
homepage = "https://cagire.raphaelforment.fr"
description = "Forth-based live coding music sequencer"
@@ -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

@@ -8,7 +8,7 @@
<p align="center">
<a href="https://cagire.raphaelforment.fr">Website</a> &middot;
<a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> &middot;
<a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a> &middot;
AGPL-3.0
</p>
@@ -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

@@ -58,6 +58,7 @@ pub fn build(p: &Palette) -> ThemeColors {
header: HeaderColors {
tempo_bg: rgb(tint(p.bg, p.tempo_color, 0.30)),
tempo_fg: rgb(p.tempo_color),
beat_bg: rgb(tint(p.bg, p.tempo_color, 0.45)),
bank_bg: rgb(tint(p.bg, p.bank_color, 0.25)),
bank_fg: rgb(p.bank_color),
pattern_bg: rgb(tint(p.bg, p.pattern_color, 0.25)),

View File

@@ -175,6 +175,7 @@ pub struct TileColors {
pub struct HeaderColors {
pub tempo_bg: Color,
pub tempo_fg: Color,
pub beat_bg: Color,
pub bank_bg: Color,
pub bank_fg: Color,
pub pattern_bg: Color,

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

@@ -51,7 +51,7 @@ Section "Cagire (required)" SecCore
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "Publisher" "Raphael Forment"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayIcon" '"$INSTDIR\cagire-desktop.exe"'
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://github.com/Bubobubobubobubo/cagire"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://git.raphaelforment.fr/BuboBubo/cagire"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "HelpLink" "https://cagire.raphaelforment.fr"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoRepair" 1

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

@@ -0,0 +1,15 @@
ISC License
Copyright (c) Robbert van der Helm <mail@robbertvanderhelm.nl>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

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

@@ -293,15 +293,14 @@ fn render_header(
let pad = Padding::vertical(1);
let [logo_area, transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] =
let [logo_area, transport_area, tempo_area, bank_area, pattern_area, stats_area] =
Layout::horizontal([
Constraint::Length(5),
Constraint::Min(12),
Constraint::Length(9),
Constraint::Min(14),
Constraint::Min(20),
Constraint::Fill(1),
Constraint::Fill(2),
Constraint::Min(20),
Constraint::Min(24),
])
.areas(area);
@@ -317,43 +316,76 @@ fn render_header(
logo_area,
);
// Transport block
let (transport_bg, transport_text) = if app.playback.playing {
// Transport block (with fill indicator)
let fill = app.live_keys.fill();
let (transport_bg, transport_label) = if app.playback.playing {
(theme.status.playing_bg, " ▶ PLAYING ")
} else {
(theme.status.stopped_bg, " ■ STOPPED ")
};
let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary);
let fill_span = if fill {
Span::styled("F", Style::new().fg(theme.status.fill_on).bg(transport_bg))
} else {
Span::styled(" ", Style::new().bg(transport_bg))
};
let transport_line = Line::from(vec![
Span::styled(transport_label, Style::new().fg(theme.ui.text_primary).bg(transport_bg)),
fill_span,
Span::styled(" ", Style::new().bg(transport_bg)),
]);
frame.render_widget(
Paragraph::new(transport_text)
.block(Block::default().padding(pad).style(transport_style))
Paragraph::new(transport_line)
.block(Block::default().padding(pad).style(Style::new().bg(transport_bg)))
.alignment(Alignment::Center),
transport_area,
);
// Fill indicator
let fill = app.live_keys.fill();
let fill_fg = if fill {
theme.status.fill_on
} else {
theme.status.fill_off
};
let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
// Tempo + bar:beat position block (beat segments as background fills)
let tempo_bg = theme.header.tempo_bg;
let tempo_fg = theme.ui.text_primary;
let quantum = link.quantum();
let quantum_int = quantum.max(1.0) as usize;
// Base background
frame.render_widget(
Paragraph::new(if fill { "F" } else { "·" })
.block(Block::default().padding(pad).style(fill_style))
.alignment(Alignment::Center),
live_area,
Block::default().style(Style::new().bg(tempo_bg)),
tempo_area,
);
// Tempo block
let tempo_style = Style::new()
.bg(theme.header.tempo_bg)
.fg(theme.ui.text_primary)
.add_modifier(Modifier::BOLD);
// Beat segment highlight (like CPU meter but divided into quantum segments)
if app.playback.playing && quantum_int <= 16 {
let phase = link.phase();
let beat_in_bar = phase.floor() as usize;
let seg_w = tempo_area.width / quantum_int as u16;
let seg_x = tempo_area.x + seg_w * beat_in_bar as u16;
let seg_width = if beat_in_bar == quantum_int - 1 {
tempo_area.width - seg_w * beat_in_bar as u16
} else {
seg_w
};
frame.render_widget(
Block::default().style(Style::new().bg(theme.header.beat_bg)),
Rect {
x: seg_x,
width: seg_width,
..tempo_area
},
);
}
// Text overlay
let tempo_text = if app.playback.playing {
let phase = link.phase();
let beat_in_bar = phase.floor() as usize + 1;
let bar = (link.beat() / quantum).floor() as usize + 1;
format!(" {:.1} BPM {bar}:{beat_in_bar} ", link.tempo())
} else {
format!(" {:.1} BPM ─:─ ", link.tempo())
};
frame.render_widget(
Paragraph::new(format!(" {:.1} BPM ", link.tempo()))
.block(Block::default().padding(pad).style(tempo_style))
Paragraph::new(tempo_text)
.block(Block::default().padding(pad))
.style(Style::new().fg(tempo_fg).add_modifier(Modifier::BOLD))
.alignment(Alignment::Center),
tempo_area,
);
@@ -393,42 +425,61 @@ fn render_header(
.get_iter(app.editor_ctx.bank, app.editor_ctx.pattern)
.map(|iter| format!(" · #{}", iter + 1))
.unwrap_or_default();
let pattern_text = format!(
" {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info
);
let pattern_style = Style::new()
.bg(theme.header.pattern_bg)
.fg(theme.ui.text_primary);
let pattern_bg = theme.header.pattern_bg;
let active_count = snapshot.active_patterns.len();
let active_info = format!(" · ▶{active_count}");
let active_style = if active_count > 0 {
Style::new().bg(pattern_bg).fg(theme.ui.text_primary)
} else {
Style::new().bg(pattern_bg).fg(theme.ui.text_muted)
};
let pattern_line = Line::from(vec![
Span::styled(
format!(
" {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info
),
Style::new().bg(pattern_bg).fg(theme.ui.text_primary),
),
Span::styled(active_info, active_style),
]);
frame.render_widget(
Paragraph::new(pattern_text)
.block(Block::default().padding(pad).style(pattern_style))
Paragraph::new(pattern_line)
.block(Block::default().padding(pad).style(Style::new().bg(pattern_bg)))
.alignment(Alignment::Center),
pattern_area,
);
// Stats block
// Stats block — CPU bar filling the area, text overlaid
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
let peers = link.peers();
let voices = app.metrics.active_voices;
let cpu_color = if cpu_pct >= 80.0 {
let cpu_bar_color = if cpu_pct >= 80.0 {
theme.flash.error_fg
} else if cpu_pct >= 50.0 {
theme.ui.accent
} else {
theme.header.stats_fg
theme.meter.low
};
let dim = Style::new()
.bg(theme.header.stats_bg)
.fg(theme.header.stats_fg);
let stats_line = Line::from(vec![
Span::styled(format!(" CPU {cpu_pct:.0}%"), dim.fg(cpu_color)),
Span::styled(format!(" V:{voices} L:{peers} "), dim),
]);
let block_style = Style::new().bg(theme.header.stats_bg);
frame.render_widget(
Paragraph::new(stats_line)
.block(Block::default().padding(pad).style(block_style))
Block::default().style(Style::new().bg(theme.header.stats_bg)),
stats_area,
);
let filled_w = (cpu_pct / 100.0 * stats_area.width as f32).round() as u16;
if filled_w > 0 {
frame.render_widget(
Block::default().style(Style::new().bg(cpu_bar_color)),
Rect {
width: filled_w.min(stats_area.width),
..stats_area
},
);
}
let stats_text = format!("CPU {cpu_pct:.0}% V:{voices}");
frame.render_widget(
Paragraph::new(stats_text)
.block(Block::default().padding(pad))
.style(Style::new().fg(theme.ui.text_primary))
.alignment(Alignment::Center),
stats_area,
);

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

@@ -87,11 +87,11 @@ const DL = 'https://dlcagire.raphaelforment.fr';
<tr>
<td>Windows (x86_64)</td>
<td><a href={`${DL}/cagire-windows-x86_64.zip`}>zip</a></td>
<td><a href={`${DL}/cagire-desktop-windows-x86_64.zip`}>zip</a> · <s>.msi</s></td>
<td><a href={`${DL}/plugins-windows-x86_64-clap.zip`}>CLAP</a> · <a href={`${DL}/plugins-windows-x86_64-vst3.zip`}>VST3</a></td>
<td><a href={`${DL}/cagire-windows-x86_64-desktop.zip`}>zip</a> · <a href={`${DL}/cagire-windows-x86_64-installer.zip`}>installer</a></td>
<td><a href={`${DL}/cagire-windows-x86_64-clap.zip`}>CLAP</a> · <a href={`${DL}/cagire-windows-x86_64-vst3.zip`}>VST3</a></td>
</tr>
</table>
<p class="note">Source code and issue tracker on <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a>. You can also compile the software yourself from source!</p>
<p class="note">Source code and issue tracker on <a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a>. You can also compile the software yourself from source!</p>
<h2>Documentation</h2>
@@ -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>
@@ -128,7 +128,7 @@ const DL = 'https://dlcagire.raphaelforment.fr';
<video src="/mono_cagire.mp4" autoplay muted loop playsinline></video>
<p class="colophon">
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · <a href="/docs">Docs</a> · AGPL-3.0 </p>
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a> · <a href="/docs">Docs</a> · AGPL-3.0 </p>
<script is:inline src="/script.js"></script>
</body>

View File

@@ -1,20 +0,0 @@
{\rtf1\ansi\deff0\nouicompat{\fonttbl{\f0\fswiss\fcharset0 Helvetica;}}
{\*\generator Msftedit 5.41.21.2510;}\viewkind4\uc1
\pard\sa200\sl276\slmult1\f0\fs20\lang9
CAGIRE - Forth-based Music Sequencer\par
Copyright (c) 2025 Rapha\"el Forment\par
\par
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.\par
\par
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.\par
\par
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see https://www.gnu.org/licenses/.\par
}

View File

@@ -1,146 +0,0 @@
<?xml version='1.0' encoding='windows-1252'?>
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = intel64 ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif ?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<Product
Id='*'
Name='Cagire'
UpgradeCode='F2A3D4E5-6B7C-8D9E-0F1A-2B3C4D5E6F7A'
Manufacturer='Raphael Forment'
Language='1033'
Codepage='1252'
Version='$(var.Version)'>
<Package Id='*'
Keywords='Installer'
Description='Cagire - Forth-based music sequencer'
Manufacturer='Raphael Forment'
InstallerVersion='450'
Languages='1033'
Compressed='yes'
InstallScope='perMachine'
SummaryCodepage='1252'
/>
<MajorUpgrade
Schedule='afterInstallInitialize'
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
<Property Id='DiskPrompt' Value='Cagire Installation'/>
<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
<Directory Id='APPLICATIONFOLDER' Name='Cagire'>
<Component Id='CagireCLI' Guid='A1B2C3D4-E5F6-7890-ABCD-EF1234567890' Win64='yes'>
<File
Id='CagireEXE'
Name='cagire.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\cagire.exe'
KeyPath='yes'/>
</Component>
<Component Id='CagireDesktop' Guid='B2C3D4E5-F6A7-8901-BCDE-F12345678901' Win64='yes'>
<File
Id='CagireDesktopEXE'
Name='cagire-desktop.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\cagire-desktop.exe'
KeyPath='yes'/>
</Component>
<Component Id='PathEntry' Guid='C3D4E5F6-A7B8-9012-CDEF-123456789012' Win64='yes' KeyPath='yes'>
<Environment
Id='PATH'
Name='PATH'
Value='[APPLICATIONFOLDER]'
Permanent='no'
Part='last'
Action='set'
System='yes'/>
</Component>
</Directory>
</Directory>
<Directory Id='ProgramMenuFolder'>
<Directory Id='ApplicationProgramsFolder' Name='Cagire'>
<Component Id='StartMenuShortcut' Guid='D4E5F6A7-B8C9-0123-DEFA-234567890123' Win64='yes'>
<Shortcut
Id='CagireDesktopShortcut'
Name='Cagire'
Description='Forth-based music sequencer'
Target='[APPLICATIONFOLDER]cagire-desktop.exe'
WorkingDirectory='APPLICATIONFOLDER'
Icon='CagireIcon.exe'/>
<RemoveFolder Id='CleanUpShortcutFolder' On='uninstall'/>
<RegistryValue
Root='HKCU'
Key='Software\Cagire'
Name='installed'
Type='integer'
Value='1'
KeyPath='yes'/>
</Component>
</Directory>
</Directory>
</Directory>
<Feature
Id='Binaries'
Title='Application'
Description='Installs Cagire CLI and Desktop binaries.'
Level='1'
ConfigurableDirectory='APPLICATIONFOLDER'
AllowAdvertise='no'
Display='expand'
Absent='disallow'>
<ComponentRef Id='CagireCLI'/>
<ComponentRef Id='CagireDesktop'/>
<Feature
Id='Environment'
Title='PATH Environment Variable'
Description='Add the install location to the PATH system environment variable. This allows the cagire CLI to be called from any location.'
Level='1'
Absent='allow'>
<ComponentRef Id='PathEntry'/>
</Feature>
</Feature>
<Feature
Id='StartMenu'
Title='Start Menu Shortcut'
Description='Add a Cagire shortcut to the Start Menu.'
Level='1'
Absent='allow'>
<ComponentRef Id='StartMenuShortcut'/>
</Feature>
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
<Icon Id='CagireIcon.exe' SourceFile='assets\Cagire.ico'/>
<Property Id='ARPPRODUCTICON' Value='CagireIcon.exe'/>
<Property Id='ARPHELPLINK' Value='https://cagire.raphaelforment.fr'/>
<Property Id='ARPURLINFOABOUT' Value='https://github.com/Bubobubobubobubo/cagire'/>
<UI>
<UIRef Id='WixUI_FeatureTree'/>
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
</UI>
<WixVariable Id='WixUILicenseRtf' Value='wix\License.rtf'/>
</Product>
</Wix>