72 Commits

Author SHA1 Message Date
362cdd498b Feat: update CHANGELOG.md 2026-03-20 23:59:34 +01:00
e26d5e2958 Fix: highlighting was a bit too intense with new 'at' 2026-03-20 23:43:54 +01:00
f020b5a172 Feat: improve 'at' in cagire grammar 2026-03-20 23:29:47 +01:00
609fe108bc Fix: device input name matching 2026-03-20 15:50:50 +01:00
f4a3e26d51 Feat: small fix for build 2026-03-20 13:36:35 +01:00
b6daa81304 Feat: big movement for ASIO 2026-03-20 13:03:32 +01:00
5c5488a9f0 ok 2026-03-20 00:17:18 +01:00
4043a67d38 ok 2026-03-20 00:15:30 +01:00
af3c5c0985 Redo lost work 2026-03-20 00:08:57 +01:00
44fe435770 Feat: wrong path 2026-03-18 16:09:48 +01:00
ef7ee019f1 Feat: adapt workflows for my runner 2026-03-18 16:04:01 +01:00
5dffdd4a8d Feat: adapt for v0.1.4 release
Some checks failed
Deploy Website / deploy (push) Failing after 3s
2026-03-18 15:59:49 +01:00
e1cf72542c Feat: import / export fix 2026-03-18 15:09:49 +01:00
97a1a997f6 Feat: better engine output device switching 2026-03-18 14:27:05 +01:00
005155e486 Feat: fix windows build script 2026-03-18 13:39:45 +01:00
712bd4e74e Feat: fix windows build script 2026-03-18 13:35:17 +01:00
144c2487c2 Feat: fix windows build script 2026-03-18 13:30:41 +01:00
260bc9dbdf Feat: fix sequencer precision regression
Some checks failed
Deploy Website / deploy (push) Failing after 4s
2026-03-18 03:41:51 +01:00
68bd62f57f Fix: clean ratatui > egui interaction 2026-03-18 02:28:57 +01:00
f1c83c66a0 Fix: MIDI precision 2026-03-18 02:18:21 +01:00
30dfe7372d Fix: MIDI precision 2026-03-18 02:16:05 +01:00
faf541e536 Feat: try again 2026-03-17 13:51:47 +01:00
85cacfe53e Feat: build script UI/UX 2026-03-17 13:26:48 +01:00
c507552b7c Feat: build script UI/UX 2026-03-17 13:21:46 +01:00
d0b2076bf6 Feat: build script UI/UX 2026-03-17 12:58:52 +01:00
ab93acd17f Feat: build script UI/UX 2026-03-17 12:54:57 +01:00
d72b36b8f1 Feat: build script UI/UX 2026-03-17 12:49:01 +01:00
3d9d2ad759 Feat: improve script 2026-03-17 12:41:56 +01:00
5b1353f7e7 Feat: improve script 2026-03-17 12:39:13 +01:00
f78b4374b6 Feat: improve local build script 2026-03-17 12:31:50 +01:00
dacc9bd6be Fix: update documentation with sync mode removal 2026-03-17 02:49:23 +01:00
bfd52c0053 Fix: sync mode is not required 2026-03-17 02:45:41 +01:00
12172ce1e8 Revert "Fix: try to fix the non working sync"
This reverts commit 1513d80a8d.
2026-03-16 22:10:14 +01:00
1513d80a8d Fix: try to fix the non working sync 2026-03-16 22:07:15 +01:00
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
Debian
5d755594cb Add Gitea Actions workflow for website deployment
Deploys the Astro website to the VPS nginx container via
the runner's mounted host volume on pushes to main.
2026-03-07 13:05:27 +00:00
6b60b3761b chore: Release
Some checks failed
Deploy Website / deploy (push) Has been skipped
CI / linux (push) Failing after 11m18s
CI / macos (push) Failing after 44s
CI / windows (push) Failing after 44s
Release / linux (push) Has been skipped
Release / macos (push) Has been skipped
Release / assemble-macos (push) Has been skipped
Release / windows (push) Has been skipped
Release / cross (push) Has been skipped
Release / release (push) Has been skipped
2026-03-07 11:53:01 +01:00
63fd2419d3 Update lock file 2026-03-07 11:52:09 +01:00
da92fa6622 Update cargo 2026-03-07 11:49:27 +01:00
8e43e1bb3c Feat: document time stretching
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-07 11:47:54 +01:00
3104a61490 Feat: optimizations 2026-03-07 11:38:49 +01:00
20d72c9b21 Feat: words and default release 2026-03-07 00:10:09 +01:00
09cfa82809 Fix: lots of various fixes
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-06 20:48:50 +01:00
bc1396d61d Fix: Windows BUILD fails again
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-06 10:14:14 +01:00
82d51a9add chore: Release
Some checks failed
Deploy Website / deploy (push) Has been skipped
CI / linux (push) Failing after 11m28s
CI / macos (push) Failing after 48s
CI / windows (push) Failing after 48s
Release / linux (push) Has been skipped
Release / macos (push) Has been skipped
Release / assemble-macos (push) Has been skipped
Release / windows (push) Has been skipped
Release / cross (push) Has been skipped
Release / release (push) Has been skipped
2026-03-06 09:35:21 +01:00
fed7781bae Feat: update doux
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-06 09:10:02 +01:00
d055d2bfc6 Fix: update docs about snd 2026-03-06 08:40:41 +01:00
f273470eaf Fix: audio engine fixes 2026-03-06 08:27:54 +01:00
b2a089fb0c ok 2026-03-05 22:35:25 +01:00
04b68850d0 Wip 2026-03-05 22:14:28 +01:00
77364dddae Fix: refresh devices while arriving on engine page 2026-03-05 21:19:17 +01:00
5a72e4cef4 Small fixes and additions
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-05 19:20:52 +01:00
0097777449 Fixes 2026-03-05 18:24:09 +01:00
4743c33916 Feat: begin sample explorer overhaul
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-05 00:42:39 +01:00
2c8a6794a3 Feat: UI/UX fixes
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-05 00:28:30 +01:00
60fb62829f Feat: UI/UX fixes + removing clones from places 2026-03-05 00:15:51 +01:00
147 changed files with 6584 additions and 13461 deletions

View File

@@ -3,8 +3,3 @@ MACOSX_DEPLOYMENT_TARGET = "12.0"
[alias]
xtask = "run --package xtask --release --"
[target.x86_64-pc-windows-gnu]
rustflags = [
"-C", "link-args=-Wl,-Bstatic -lstdc++ -lgcc -lgcc_eh -lpthread -Wl,-Bdynamic -lmingwex -lmsvcrt -lws2_32 -liphlpapi -lwinmm -lole32 -loleaut32 -luuid -lkernel32",
]

View File

@@ -0,0 +1,39 @@
name: Deploy Website
on:
push:
branches: [main]
paths:
- 'website/**'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: 22
- name: Install dependencies
working-directory: website
run: pnpm install
- name: Build
working-directory: website
run: pnpm build
- name: Deploy to host volume
run: |
rm -rf /home/debian/my-services/data/cagire-website/*
cp -r website/dist/* /home/debian/my-services/data/cagire-website/

View File

@@ -1,135 +0,0 @@
name: Assemble macOS Universal
on:
workflow_call:
jobs:
assemble:
runs-on: macos-14
timeout-minutes: 10
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
pattern: cagire-macos-*
path: artifacts
- name: Create universal CLI binary
run: |
lipo -create \
artifacts/cagire-macos-x86_64/cagire \
artifacts/cagire-macos-aarch64/cagire \
-output cagire
chmod +x cagire
lipo -info cagire
- name: Create universal app bundle
run: |
cd artifacts/cagire-macos-aarch64-desktop
unzip Cagire.app.zip
cd ../cagire-macos-x86_64-desktop
unzip Cagire.app.zip
cd ../..
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
lipo -create \
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
-output Cagire.app/Contents/MacOS/cagire-desktop
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
zip -r Cagire.app.zip Cagire.app
- name: Create universal CLAP plugin
run: |
mkdir -p cagire-plugins.clap/Contents/MacOS
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
cagire-plugins.clap/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
cagire-plugins.clap/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
- name: Create universal VST3 plugin
run: |
mkdir -p cagire-plugins.vst3/Contents/MacOS
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
- uses: actions/checkout@v4
with:
sparse-checkout: |
assets/DMG-README.txt
scripts/make-dmg.sh
clean: false
- name: Create DMG
run: |
chmod +x scripts/make-dmg.sh
scripts/make-dmg.sh Cagire.app .
- name: Build .pkg installer
run: |
VERSION="${GITHUB_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/
pkgbuild --analyze --root pkg-root component.plist
plutil -replace BundleIsRelocatable -bool NO component.plist
pkgbuild --root pkg-root --identifier com.sova.cagire \
--version "$VERSION" --install-location / \
--component-plist component.plist \
"Cagire-${VERSION}-universal.pkg"
- name: Upload universal CLI
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal
path: cagire
- name: Upload universal app bundle
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-desktop
path: Cagire.app.zip
- name: Prepare universal plugin staging
run: |
mkdir -p staging/clap staging/vst3
cp -R cagire-plugins.clap staging/clap/
cp -R cagire-plugins.vst3 staging/vst3/
- name: Upload universal CLAP plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-clap
path: staging/clap/
- name: Upload universal VST3 plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-vst3
path: staging/vst3/
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-dmg
path: Cagire-*.dmg
- name: Upload .pkg installer
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-pkg
path: Cagire-*-universal.pkg

View File

@@ -1,49 +0,0 @@
name: Build Cross (Linux ARM64)
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: aarch64-unknown-linux-gnu
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build
run: cross build --release --target aarch64-unknown-linux-gnu
- name: Build desktop
run: cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
- name: Upload CLI artifact
uses: actions/upload-artifact@v4
with:
name: cagire-linux-aarch64
path: target/aarch64-unknown-linux-gnu/release/cagire
- name: Upload desktop artifact
uses: actions/upload-artifact@v4
with:
name: cagire-linux-aarch64-desktop
path: target/aarch64-unknown-linux-gnu/release/cagire-desktop

View File

@@ -1,131 +0,0 @@
name: Build Linux
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: false
run-clippy:
type: boolean
default: false
build-packages:
type: boolean
default: false
workflow_dispatch:
inputs:
run-tests:
type: boolean
default: true
run-clippy:
type: boolean
default: true
build-packages:
type: boolean
default: false
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-unknown-linux-gnu
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev \
libx11-dev libx11-xcb-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
- name: Build
run: cargo build --release --target x86_64-unknown-linux-gnu
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
- name: Test
if: inputs.run-tests
run: cargo test --target x86_64-unknown-linux-gnu
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --target x86_64-unknown-linux-gnu -- -D warnings
- name: Install cargo-bundle
if: inputs.build-packages
run: cargo install cargo-bundle
- name: Bundle desktop app
if: inputs.build-packages
run: cargo bundle --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
- name: Build AppImages
if: inputs.build-packages
run: |
mkdir -p target/releases
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 target/releases
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire-desktop x86_64 target/releases
- name: Bundle CLAP plugin
if: inputs.build-packages
run: cargo xtask bundle cagire-plugins --release --target x86_64-unknown-linux-gnu
- name: Upload CLI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64
path: target/x86_64-unknown-linux-gnu/release/cagire
- name: Upload desktop artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-desktop
path: target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
- name: Upload AppImage artifacts
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-appimage
path: target/releases/*.AppImage
- name: Prepare plugin artifacts
if: inputs.build-packages
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-vst3
path: staging/vst3/

View File

@@ -1,127 +0,0 @@
name: Build macOS
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: false
run-clippy:
type: boolean
default: false
build-packages:
type: boolean
default: false
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"}]'
workflow_dispatch:
inputs:
run-tests:
type: boolean
default: true
run-clippy:
type: boolean
default: true
build-packages:
type: boolean
default: false
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"}]'
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "12.0"
jobs:
build:
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(inputs.matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install dependencies
run: brew list cmake &>/dev/null || brew install cmake
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Test
if: inputs.run-tests
run: cargo test --target ${{ matrix.target }}
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --target ${{ matrix.target }} -- -D warnings
- name: Bundle desktop app
if: inputs.build-packages
run: scripts/make-app-bundle.sh ${{ matrix.target }}
- name: Bundle CLAP plugin
if: inputs.build-packages
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Zip macOS app bundle
if: inputs.build-packages
run: |
cd target/${{ matrix.target }}/release/bundle/osx
zip -r Cagire.app.zip Cagire.app
- name: Upload CLI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire
- name: Upload desktop artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
- name: Prepare plugin artifacts
if: inputs.build-packages
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: staging/clap/
- name: Upload VST3 artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: staging/vst3/

View File

@@ -1,56 +0,0 @@
name: Build Plugins Linux
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-unknown-linux-gnu-plugins
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev \
libx11-dev libx11-xcb-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
- name: Build plugins
run: cargo xtask bundle cagire-plugins --release --target x86_64-unknown-linux-gnu
- name: Prepare plugin artifacts
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-x86_64-vst3
path: staging/vst3/

View File

@@ -1,66 +0,0 @@
name: Build Plugins macOS
on:
workflow_call:
inputs:
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"plugins-macos-aarch64"},{"os":"macos-15-intel","target":"x86_64-apple-darwin","artifact":"plugins-macos-x86_64"}]'
workflow_dispatch:
inputs:
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"plugins-macos-aarch64"}]'
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "12.0"
jobs:
build:
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(inputs.matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}-plugins
- name: Install dependencies
run: brew list cmake &>/dev/null || brew install cmake
- name: Build plugins
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Prepare plugin artifacts
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: staging/vst3/

View File

@@ -1,59 +0,0 @@
name: Build Plugins RPi
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: aarch64-unknown-linux-gnu-plugins
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build plugins
run: cross build --release -p cagire-plugins --target aarch64-unknown-linux-gnu
- 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"
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-aarch64-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-aarch64-vst3
path: staging/vst3/

View File

@@ -1,59 +0,0 @@
name: Build Plugins Windows
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
timeout-minutes: 60
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-pc-windows-msvc-plugins
- name: Install dependencies
shell: pwsh
run: |
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
- name: Build plugins
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
- name: Prepare plugin artifacts
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: plugins-windows-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: plugins-windows-x86_64-vst3
path: staging/vst3/

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,122 +0,0 @@
name: Build Windows
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: false
run-clippy:
type: boolean
default: false
build-packages:
type: boolean
default: false
workflow_dispatch:
inputs:
run-tests:
type: boolean
default: true
run-clippy:
type: boolean
default: true
build-packages:
type: boolean
default: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-pc-windows-msvc
- name: Install dependencies
run: |
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
- name: Build
run: cargo build --release --target x86_64-pc-windows-msvc
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
- name: Test
if: inputs.run-tests
run: cargo test --target x86_64-pc-windows-msvc
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --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
- name: Install cargo-wix
if: inputs.build-packages
run: cargo install cargo-wix
- name: Build MSI installer
if: inputs.build-packages
run: cargo wix --no-build --nocapture --package cagire -C -arch -C x64
- name: Upload CLI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64
path: target/x86_64-pc-windows-msvc/release/cagire.exe
- name: Upload desktop artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-desktop
path: target/x86_64-pc-windows-msvc/release/cagire-desktop.exe
- name: Upload MSI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-msi
path: target/wix/*.msi
- name: Prepare plugin artifacts
if: inputs.build-packages
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-vst3
path: staging/vst3/

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

@@ -1,107 +0,0 @@
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
with:
build-packages: true
macos:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-macos.yml
with:
build-packages: true
matrix: >-
[
{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"},
{"os":"macos-15-intel","target":"x86_64-apple-darwin","artifact":"cagire-macos-x86_64"}
]
windows:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-windows.yml
with:
build-packages: true
cross:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-cross.yml
assemble-macos:
needs: macos
uses: ./.github/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
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
for dir in artifacts/*/; do
name=$(basename "$dir")
if [[ "$name" == "cagire-macos-universal-dmg" ]]; then
cp "$dir"/*.dmg release/
elif [[ "$name" == "cagire-macos-universal-pkg" ]]; then
cp "$dir"/*.pkg release/
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
elif [[ "$name" == "cagire-macos-universal" ]]; then
cp "$dir/cagire" "release/cagire-macos-universal"
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-clap ]]; then
base="${name%-clap}"
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == *-vst3 ]]; then
base="${name%-vst3}"
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-msi ]]; then
cp "$dir"/*.msi release/
elif [[ "$name" == *-appimage ]]; then
cp "$dir"/*.AppImage release/
elif [[ "$name" == *-desktop ]]; then
base="${name%-desktop}"
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
cp "$dir"/*.deb "release/${base}-desktop.deb"
elif [ -f "$dir/Cagire.app.zip" ]; then
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
elif [ -f "$dir/cagire-desktop.exe" ]; then
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
fi
else
if [ -f "$dir/cagire.exe" ]; then
cp "$dir/cagire.exe" "release/${name}.exe"
elif [ -f "$dir/cagire" ]; then
cp "$dir/cagire" "release/${name}"
fi
fi
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true

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
```
@@ -110,57 +110,49 @@ cargo run --release --features desktop --bin cagire-desktop
## Cross-Compilation
[cross](https://github.com/cross-rs/cross) uses Docker to build for other platforms without installing their toolchains locally. It works on any OS that runs Docker.
### Targets
| Target | Method | Binaries |
|--------|--------|----------|
| aarch64-apple-darwin | Native (macOS ARM only) | `cagire`, `cagire-desktop` |
| x86_64-apple-darwin | Native (macOS only) | `cagire`, `cagire-desktop` |
| x86_64-unknown-linux-gnu | `cross build` | `cagire`, `cagire-desktop` |
| aarch64-unknown-linux-gnu (RPi 64-bit) | `cross build` | `cagire`, `cagire-desktop` |
| x86_64-pc-windows-gnu | `cross build` | `cagire`, `cagire-desktop` |
| x86_64-unknown-linux-gnu | `cross build` (Docker) | `cagire`, `cagire-desktop` |
| aarch64-unknown-linux-gnu (RPi 64-bit) | `cross build` (Docker) | `cagire`, `cagire-desktop` |
| x86_64-pc-windows-msvc | `cargo xwin build` (native) | `cagire`, `cagire-desktop` |
macOS targets can only be built on macOS — Apple does not support cross-compilation to macOS from other platforms. Linux and Windows targets can be cross-compiled from any OS. The aarch64-unknown-linux-gnu target covers Raspberry Pi (64-bit OS).
### Windows ABI
CI produces `x86_64-pc-windows-msvc` binaries (native Windows build, better compatibility). Local cross-compilation from non-Windows hosts produces `x86_64-pc-windows-gnu` binaries (MinGW via Docker). Both work; MSVC is preferred for releases.
macOS targets can only be built on macOS. Linux targets are cross-compiled via Docker (`cross`). Windows targets are cross-compiled natively via `cargo-xwin` (downloads Windows SDK + MSVC CRT headers, no Docker needed).
### Prerequisites
1. **Docker**: https://docs.docker.com/get-docker/
2. **cross**: `cargo install cross --git https://github.com/cross-rs/cross`
1. **Docker** + **cross** (Linux targets only): `cargo install cross --git https://github.com/cross-rs/cross`
2. **cargo-xwin** (Windows target): `cargo install cargo-xwin` and `rustup target add x86_64-pc-windows-msvc`
3. On macOS, add the Intel target: `rustup target add x86_64-apple-darwin`
Docker must be running before invoking `cross` or `scripts/build-all.sh`.
### Building Individual Targets
```bash
# Linux x86_64
# Linux x86_64 (Docker)
cross build --release --target x86_64-unknown-linux-gnu
cross build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
# Linux aarch64
# Linux aarch64 (Docker)
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
# Windows x86_64
cross build --release --target x86_64-pc-windows-gnu
cross build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-gnu
# Windows x86_64 (native, no Docker)
cargo xwin build --release --target x86_64-pc-windows-msvc
cargo xwin build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
```
### Building All Targets (macOS only)
### Building All Targets
```bash
# Interactive (prompts for platform/target selection):
scripts/build-all.sh
uv run scripts/build.py
# Non-interactive:
scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop --yes
scripts/build-all.sh --all --yes
uv run scripts/build.py --platforms macos-arm64,linux-x86_64 --targets cli,desktop
uv run scripts/build.py --all
```
Builds selected targets, producing binaries in `releases/`.
@@ -170,18 +162,11 @@ Target aliases: `cli`, `desktop`, `plugins`.
### Linux AppImage Packaging
Linux releases ship as AppImages — self-contained executables that bundle all shared library dependencies (ALSA, JACK, X11, OpenGL). No runtime dependencies required.
After building a Linux target, produce an AppImage with:
```bash
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 releases
```
`scripts/build-all.sh` does this automatically for every Linux target selected. The CI pipeline produces AppImages for the x86_64 Linux build. Cross-arch AppImage building (e.g. aarch64 on x86_64) is not supported — run on a matching host or in CI.
Linux releases ship as AppImages — self-contained executables that bundle all shared library dependencies (ALSA, JACK, X11, OpenGL). No runtime dependencies required. `build.py` handles AppImage creation automatically for Linux targets.
### Notes
- Custom Dockerfiles in `cross/` install the native libraries Cagire depends on (ALSA, JACK, X11, cmake, libclang, etc.). `Cross.toml` maps each target to its Dockerfile.
- The first build per target downloads Docker base images and installs packages. Subsequent builds use cached layers.
- Cross-architecture Docker builds (e.g. aarch64 on x86_64 or vice versa) run under QEMU emulation and are significantly slower.
- Custom Dockerfiles in `scripts/cross/` install the native libraries for Linux cross-compilation (ALSA, JACK, X11, cmake, libclang, etc.). `Cross.toml` maps each Linux target to its Dockerfile.
- The first Linux cross-build per target downloads Docker base images and installs packages. Subsequent builds use cached layers.
- Cross-architecture Docker builds (e.g. aarch64 on x86_64) run under QEMU emulation and are significantly slower.
- Windows cross-compilation via `cargo-xwin` runs natively on the host (no Docker) and uses real Windows SDK headers, ensuring correct ABI and struct layouts.

View File

@@ -2,6 +2,99 @@
All notable changes to this project will be documented in this file.
## [0.1.5]
### Forth Language
- **`at` reworked as a looping block**: `at` now captures all stack values as deltas, then re-executes its body once per delta. Closed by `.` (audio emit), `m.` (MIDI emit), or `done` (no emit). Each iteration gets independent nondeterministic rolls (e.g., `0 0.5 at kick snd 1 2 rand freq .` re-evaluates `kick snd 1 2 rand freq` at delta 0 and 0.5).
- Removed `ArpList` type and `arp` word — arpeggio spreading is now handled by at-loops directly.
### Added
- Support i32/i16 sample formats at cpal boundary for ASIO compatibility
### Fixed
- Resolved value annotations deduplicated: nondeterministic ops inside at-loops now show only the last resolved value per span, instead of one annotation per iteration.
- Audio input device name matching.
## [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.
- MIDI output sends directly from dispatcher thread, bypassing UI-thread polling (~30x less jitter).
### 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
- Single-letter envelope aliases: `a` (attack), `d` (decay), `s` (sustain), `r` (release).
- `sound` alias changed from `s` to `snd` (frees `s` for sustain).
- New `partials` word: set number of active harmonics for additive oscillator.
- Velocity parameter normalized to 01 float range (was 0127 integer).
### UI / UX
- **Sample Explorer as dedicated page**: the side panel is now a full page (Tab key), with keyboard navigation (j/k, search with `/`, preview with Enter), replacing the old collapsible side panel.
- **Pulsing armed-changes bar** on patterns page: staged play/stop/mute/solo changes shown in a launch bar with animated feedback ("c to launch").
- Pulsing highlight on banks and patterns with staged changes.
- Sample browser shows child count on collapsed folders and uses `+`/`-` tree icons.
- File browser modal: shows audio file counts per directory, colored path segments, and hint bar.
- Audio devices refreshed automatically when entering the Engine page.
- Bank prelude field added to data model (foundation for bank-level Forth scripts).
### Engine
- Audio timing switched from float seconds to integer tick-based scheduling, improving timing precision.
- Stream error handling refined: only `DeviceNotAvailable` and `StreamInvalidated` trigger device-lost recovery (non-fatal errors no longer restart the stream).
- Step traces use `Arc` for cheaper cloning between threads.
### Packaging
- **Windows: NSIS installer** replaces cargo-wix MSI. Includes optional PATH registration, Start Menu shortcut, and proper Add/Remove Programs entry with uninstaller.
- Improved Windows cross-compilation from Unix hosts (MinGW toolchain detection).
- CI build timeouts increased to 60 minutes across all platforms.
- Website download matrix updated.
## [0.1.1]
### Forth Language

985
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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.1"
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"
@@ -36,22 +36,22 @@ required-features = ["desktop"]
[features]
default = ["cli"]
cli = ["dep:cpal", "dep:midir", "dep:confy", "dep:clap", "dep:thread-priority"]
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash", "dep:egui"]
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash", "dep:egui", "dep:egui_ratatui"]
desktop = [
"cli",
"block-renderer",
"cagire-forth/desktop",
"dep:eframe",
"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.19", features = ["native", "soundfont"] }
rusty_link = "0.4"
ratatui = "0.30"
crossterm = "0.29"
@@ -86,12 +86,12 @@ image = { version = "0.25", default-features = false, features = ["png"], option
[target.'cfg(target_os = "linux")'.dependencies]
cpal = { version = "0.17", optional = true, features = ["jack"] }
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
[build-dependencies]
winresource = "0.1"
[profile.release]
opt-level = 3
lto = "fat"
lto = "thin"
codegen-units = 1
panic = "abort"
strip = true

View File

@@ -3,6 +3,3 @@ dockerfile = "./scripts/cross/aarch64-linux.Dockerfile"
[target.x86_64-unknown-linux-gnu]
dockerfile = "./scripts/cross/x86_64-linux.Dockerfile"
[target.x86_64-pc-windows-gnu]
dockerfile = "./scripts/cross/x86_64-windows.Dockerfile"

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>
@@ -31,8 +31,8 @@ A generative pattern using randomness, scales, and effects:
```forth
sine sound 2 fm 0.5 fmh
0 7 rand minor 50 + note
.1 .8 rrand cutoff
1 4 irand 10 * delay .5 delayfb
.1 .8 rand lpf
1 4 rand 10 * delay .5 delayfeedback
.
```
@@ -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.
@@ -66,7 +66,7 @@ To build from source instead, see [BUILDING.md](BUILDING.md).
### Documentation
Cagire includes interactive documentation with runnable code examples. Press **F1** in the application to open it.
Cagire includes interactive documentation with runnable code examples. Press **F4** in the application to open it.
- [Website](https://cagire.raphaelforment.fr)
- [BUILDING.md](BUILDING.md) — build instructions and CLI flags
@@ -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

@@ -4,8 +4,6 @@ fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "windows" {
// C++ runtime (stdc++, gcc, gcc_eh, pthread) linked statically via .cargo/config.toml
// using -Wl,-Bstatic. Only Windows system DLLs go here.
println!("cargo:rustc-link-lib=ws2_32");
println!("cargo:rustc-link-lib=iphlpapi");
println!("cargo:rustc-link-lib=winmm");
@@ -13,13 +11,15 @@ fn main() {
println!("cargo:rustc-link-lib=oleaut32");
}
#[cfg(windows)]
{
let mut res = winres::WindowsResource::new();
res.set_icon("assets/Cagire.ico")
if target_os == "windows" {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let icon = format!("{manifest_dir}/assets/Cagire.ico");
winresource::WindowsResource::new()
.set_icon(&icon)
.set("ProductName", "Cagire")
.set("FileDescription", "Forth-based music sequencer")
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment");
res.compile().expect("Failed to compile Windows resources");
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment")
.compile()
.expect("Failed to compile Windows resources");
}
}

View File

@@ -176,6 +176,13 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
ops.push(Op::Branch(else_ops.len()));
ops.extend(else_ops);
}
} else if word == "at" {
if let Some((body_ops, consumed)) = compile_at(&tokens[i + 1..], dict)? {
i += consumed;
ops.push(Op::AtLoop(Arc::from(body_ops)));
} else if !compile_word(word, Some(*span), &mut ops, dict) {
return Err(format!("unknown word: {word}"));
}
} else if word == "case" {
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
i += consumed;
@@ -355,6 +362,37 @@ fn compile_if(
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
}
fn compile_at(tokens: &[Token], dict: &Dictionary) -> Result<Option<(Vec<Op>, usize)>, String> {
let mut depth = 1;
enum AtCloser { Dot, MidiDot, Done }
let mut found: Option<(usize, AtCloser)> = None;
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"at" => depth += 1,
"." if depth == 1 => { found = Some((i, AtCloser::Dot)); break; }
"m." if depth == 1 => { found = Some((i, AtCloser::MidiDot)); break; }
"done" if depth == 1 => { found = Some((i, AtCloser::Done)); break; }
"." | "m." | "done" => depth -= 1,
_ => {}
}
}
}
let Some((pos, closer)) = found else {
return Ok(None);
};
let mut body_ops = compile(&tokens[..pos], dict)?;
match closer {
AtCloser::Dot => body_ops.push(Op::Emit),
AtCloser::MidiDot => body_ops.push(Op::MidiEmit),
AtCloser::Done => {}
}
Ok(Some((body_ops, pos + 1)))
}
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
let mut depth = 1;
let mut endcase_pos = None;

View File

@@ -110,7 +110,8 @@ pub enum Op {
ClearCmd,
SetSpeed,
At,
Arp,
AtLoop(Arc<[Op]>),
IntRange,
StepRange,
Generate,
@@ -133,6 +134,9 @@ pub enum Op {
ModSlide(u8),
ModRnd(u8),
ModEnv,
ModEnvAd,
ModEnvAdr,
Lpg,
// Global params
EmitAll,
ClearGlobal,

View File

@@ -63,6 +63,7 @@ pub struct StepContext<'a> {
pub speed: f64,
pub fill: bool,
pub nudge_secs: f64,
pub sr: f64,
pub cc_access: Option<&'a dyn CcAccess>,
pub speed_key: &'a str,
pub mouse_x: f64,
@@ -95,7 +96,7 @@ pub enum Value {
Str(Arc<str>, Option<SourceSpan>),
Quotation(Arc<[Op]>, Option<SourceSpan>),
CycleList(Arc<[Value]>),
ArpList(Arc<[Value]>),
}
impl PartialEq for Value {
@@ -106,7 +107,7 @@ impl PartialEq for Value {
(Value::Str(a, _), Value::Str(b, _)) => a == b,
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
(Value::CycleList(a), Value::CycleList(b)) => a == b,
(Value::ArpList(a), Value::ArpList(b)) => a == b,
_ => false,
}
}
@@ -142,7 +143,7 @@ impl Value {
Value::Float(f, _) => *f != 0.0,
Value::Str(s, _) => !s.is_empty(),
Value::Quotation(..) => true,
Value::CycleList(items) | Value::ArpList(items) => !items.is_empty(),
Value::CycleList(items) => !items.is_empty(),
}
}
@@ -152,14 +153,14 @@ impl Value {
Value::Float(f, _) => f.to_string(),
Value::Str(s, _) => s.to_string(),
Value::Quotation(..) => String::new(),
Value::CycleList(_) | Value::ArpList(_) => String::new(),
Value::CycleList(_) => String::new(),
}
}
pub(super) fn span(&self) -> Option<SourceSpan> {
match self {
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
Value::CycleList(_) | Value::ArpList(_) => None,
Value::CycleList(_) => None,
}
}
}
@@ -170,6 +171,7 @@ pub(super) struct CmdRegister {
params: Vec<(&'static str, Value)>,
deltas: Vec<Value>,
global_params: Vec<(&'static str, Value)>,
delta_secs: Option<f64>,
}
impl CmdRegister {
@@ -179,6 +181,7 @@ impl CmdRegister {
params: Vec::with_capacity(16),
deltas: Vec::with_capacity(4),
global_params: Vec::new(),
delta_secs: None,
}
}
@@ -236,9 +239,26 @@ impl CmdRegister {
std::mem::take(&mut self.global_params)
}
pub(super) fn set_delta_secs(&mut self, secs: f64) {
self.delta_secs = Some(secs);
}
pub(super) fn take_delta_secs(&mut self) -> Option<f64> {
self.delta_secs.take()
}
pub(super) fn clear_sound(&mut self) {
self.sound = None;
}
pub(super) fn clear_params(&mut self) {
self.params.clear();
}
pub(super) fn clear(&mut self) {
self.sound = None;
self.params.clear();
self.deltas.clear();
self.delta_secs = None;
}
}

View File

@@ -112,7 +112,7 @@ impl Forth {
let vars_snapshot = self.vars.load_full();
let mut var_writes: HashMap<String, Value> = HashMap::new();
cmd.set_global(self.global_params.lock().clone());
cmd.set_global(std::mem::take(&mut *self.global_params.lock()));
self.execute_ops(
ops,
@@ -241,31 +241,7 @@ impl Forth {
sound_len.max(param_max)
};
let has_arp_list = |cmd: &CmdRegister| -> bool {
matches!(cmd.sound(), Some(Value::ArpList(_)))
|| cmd.global_params().iter().chain(cmd.params().iter())
.any(|(_, v)| matches!(v, Value::ArpList(_)))
};
let compute_arp_count = |cmd: &CmdRegister| -> usize {
let sound_len = match cmd.sound() {
Some(Value::ArpList(items)) => items.len(),
_ => 0,
};
let param_max = cmd
.params()
.iter()
.map(|(_, v)| match v {
Value::ArpList(items) => items.len(),
_ => 0,
})
.max()
.unwrap_or(0);
sound_len.max(param_max).max(1)
};
let emit_with_cycling = |cmd: &CmdRegister,
arp_idx: usize,
poly_idx: usize,
delta_secs: f64,
outputs: &mut Vec<String>|
@@ -277,7 +253,7 @@ impl Forth {
return Err("nothing to emit".into());
}
let resolved_sound_val =
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
cmd.sound().map(|sv| resolve_value(sv, poly_idx));
let sound_str = match &resolved_sound_val {
Some(v) => Some(v.as_str()?.to_string()),
None => None,
@@ -286,8 +262,8 @@ impl Forth {
.iter()
.chain(cmd.params().iter())
.map(|(k, v)| {
let resolved = resolve_value(v, arp_idx, poly_idx);
if let Value::CycleList(_) | Value::ArpList(_) = v {
let resolved = resolve_value(v, poly_idx);
if let Value::CycleList(_) = v {
if let Some(span) = resolved.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
@@ -302,6 +278,7 @@ impl Forth {
&resolved_params,
ctx.step_duration(),
delta_secs,
ctx.sr,
outputs,
);
Ok(resolved_sound_val.map(|v| v.into_owned()))
@@ -459,7 +436,7 @@ impl Forth {
if b.as_float().map_or(true, |v| v == 0.0) {
return Err("division by zero".into());
}
stack.push(lift_binary(a, b, |x, y| x / y)?);
stack.push(lift_binary(&a, &b, |x, y| x / y)?);
}
Op::Mod => {
let b = pop(stack)?;
@@ -467,47 +444,47 @@ impl Forth {
if b.as_float().map_or(true, |v| v == 0.0) {
return Err("modulo by zero".into());
}
let result = lift_binary(a, b, |x, y| (x as i64 % y as i64) as f64)?;
let result = lift_binary(&a, &b, |x, y| (x as i64 % y as i64) as f64)?;
stack.push(result);
}
Op::Neg => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| -x)?);
stack.push(lift_unary(&v, |x| -x)?);
}
Op::Abs => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.abs())?);
stack.push(lift_unary(&v, |x| x.abs())?);
}
Op::Floor => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.floor())?);
stack.push(lift_unary(&v, |x| x.floor())?);
}
Op::Ceil => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.ceil())?);
stack.push(lift_unary(&v, |x| x.ceil())?);
}
Op::Round => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.round())?);
stack.push(lift_unary(&v, |x| x.round())?);
}
Op::Min => binary_op(stack, |a, b| a.min(b))?,
Op::Max => binary_op(stack, |a, b| a.max(b))?,
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
Op::Sqrt => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.sqrt())?);
stack.push(lift_unary(&v, |x| x.sqrt())?);
}
Op::Sin => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.sin())?);
stack.push(lift_unary(&v, |x| x.sin())?);
}
Op::Cos => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.cos())?);
stack.push(lift_unary(&v, |x| x.cos())?);
}
Op::Log => {
let v = pop(stack)?;
stack.push(lift_unary(v, |x| x.ln())?);
stack.push(lift_unary(&v, |x| x.ln())?);
}
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
@@ -594,47 +571,17 @@ impl Forth {
}
Op::Emit => {
if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
if let Some(dsecs) = cmd.take_delta_secs() {
let poly_count = compute_poly_count(cmd);
let explicit_deltas = !cmd.deltas().is_empty();
let delta_list: Vec<Value> = if explicit_deltas {
cmd.deltas().to_vec()
} else {
Vec::new()
};
let count = if explicit_deltas {
arp_count.max(delta_list.len())
} else {
arp_count
};
for i in 0..count {
let delta_secs = if explicit_deltas {
let dv = &delta_list[i % delta_list.len()];
let frac = dv.as_float()?;
if let Some(span) = dv.span() {
for poly_idx in 0..poly_count {
if let Some(sound_val) =
emit_with_cycling(cmd, poly_idx, dsecs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
for poly_i in 0..poly_count {
if let Some(sound_val) =
emit_with_cycling(cmd, i, poly_i, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) =
trace_cell.borrow_mut().as_mut()
{
trace.selected_spans.push(span);
}
}
}
}
}
} else {
@@ -656,7 +603,7 @@ impl Forth {
}
}
if let Some(sound_val) =
emit_with_cycling(cmd, 0, poly_idx, delta_secs, outputs)?
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) =
@@ -1055,7 +1002,7 @@ impl Forth {
let key = read_key(&var_writes_cell, vars_snapshot);
let values = std::mem::take(stack);
for val in values {
let result = lift_unary_int(val, |degree| {
let result = lift_unary_int(&val, |degree| {
let octave_offset = degree.div_euclid(len);
let idx = degree.rem_euclid(len) as usize;
key + octave_offset * 12 + pattern[idx]
@@ -1155,7 +1102,7 @@ impl Forth {
Op::Oct => {
let shift = pop(stack)?;
let note = pop(stack)?;
let result = lift_binary(note, shift, |n, s| n + s * 12.0)?;
let result = lift_binary(&note, &shift, |n, s| n + s * 12.0)?;
stack.push(result);
}
@@ -1186,7 +1133,7 @@ impl Forth {
}
let dur = steps * ctx.step_duration();
cmd.set_param("fit", Value::Float(dur, None));
cmd.set_param("dur", Value::Float(dur, None));
cmd.set_param("gate", Value::Float(steps, None));
}
Op::At => {
@@ -1195,12 +1142,60 @@ impl Forth {
cmd.set_deltas(deltas);
}
Op::Arp => {
Op::AtLoop(body_ops) => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
stack.push(Value::ArpList(Arc::from(values)));
let deltas = std::mem::take(stack);
let n = deltas.len();
for (i, delta_val) in deltas.iter().enumerate() {
let frac = delta_val.as_float()?;
let delta_secs = ctx.nudge_secs + frac * ctx.step_duration();
let iter_ctx = StepContext {
step: ctx.step,
beat: ctx.beat,
bank: ctx.bank,
pattern: ctx.pattern,
tempo: ctx.tempo,
phase: ctx.phase,
slot: ctx.slot,
runs: ctx.runs * n + i,
iter: ctx.iter,
speed: ctx.speed,
fill: ctx.fill,
nudge_secs: ctx.nudge_secs,
sr: ctx.sr,
cc_access: ctx.cc_access,
speed_key: ctx.speed_key,
mouse_x: ctx.mouse_x,
mouse_y: ctx.mouse_y,
mouse_down: ctx.mouse_down,
};
cmd.set_delta_secs(delta_secs);
let mut trace_opt = trace_cell.borrow_mut().take();
let mut var_writes_guard = var_writes_cell.borrow_mut();
let vw = var_writes_guard.as_mut().expect("var_writes taken");
self.execute_ops(
body_ops,
&iter_ctx,
stack,
outputs,
cmd,
trace_opt.as_deref_mut(),
vars_snapshot,
vw,
)?;
drop(var_writes_guard);
*trace_cell.borrow_mut() = trace_opt;
cmd.clear_params();
cmd.clear_sound();
}
}
Op::Adsr => {
let r = pop(stack)?;
let s = pop(stack)?;
@@ -1432,7 +1427,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));
}
@@ -1445,56 +1440,66 @@ 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 at_loop_delta = cmd.take_delta_secs();
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
// Build schedule: (arp_idx, poly_idx, delta_secs)
let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
// Build schedule: (poly_idx, delta_secs)
let schedule: Vec<(usize, f64)> = if let Some(dsecs) = at_loop_delta {
let poly_count = compute_poly_count(cmd);
let explicit = !cmd.deltas().is_empty();
let delta_list = cmd.deltas();
let count = if explicit {
arp_count.max(delta_list.len())
} else {
arp_count
};
let mut sched = Vec::with_capacity(count * poly_count);
for i in 0..count {
let delta_secs = if explicit {
let frac = delta_list[i % delta_list.len()]
.as_float()
.unwrap_or(0.0);
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
for poly_i in 0..poly_count {
sched.push((i, poly_i, delta_secs));
}
}
sched
(0..poly_count).map(|pi| (pi, dsecs)).collect()
} else {
let poly_count = compute_poly_count(cmd);
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
@@ -1509,7 +1514,6 @@ impl Forth {
for poly_idx in 0..poly_count {
for &frac in &deltas {
sched.push((
0,
poly_idx,
ctx.nudge_secs + frac * ctx.step_duration(),
));
@@ -1518,14 +1522,14 @@ impl Forth {
sched
};
for (arp_idx, poly_idx, delta_secs) in schedule {
for (poly_idx, delta_secs) in schedule {
let get_int = |name: &str| -> Option<i64> {
params
.iter()
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| {
resolve_value(v, arp_idx, poly_idx).as_int().ok()
resolve_value(v, poly_idx).as_int().ok()
})
};
let get_float = |name: &str| -> Option<f64> {
@@ -1534,7 +1538,7 @@ impl Forth {
.rev()
.find(|(k, _)| *k == name)
.and_then(|(_, v)| {
resolve_value(v, arp_idx, poly_idx).as_float().ok()
resolve_value(v, poly_idx).as_float().ok()
})
};
let chan = get_int("chan")
@@ -1542,7 +1546,7 @@ impl Forth {
.unwrap_or(0);
let dev =
get_int("dev").map(|d| d.clamp(0, 3) as u8).unwrap_or(0);
let delta_suffix = if delta_secs > 0.0 {
let delta_suffix = if delta_secs.abs() > 1e-9 {
format!("/delta/{delta_secs}")
} else {
String::new()
@@ -1573,7 +1577,7 @@ impl Forth {
} else {
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
let velocity =
get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
(get_float("velocity").unwrap_or(0.8) * 127.0).clamp(0.0, 127.0) as u8;
let dur = get_float("dur").unwrap_or(1.0);
let dur_secs = dur * ctx.step_duration();
outputs.push(format!(
@@ -1641,21 +1645,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)?;
@@ -1709,30 +1713,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" | "gate"
)
}
@@ -1741,13 +1733,15 @@ fn emit_output(
params: &[(&str, String)],
step_duration: f64,
nudge_secs: f64,
sr: f64,
outputs: &mut Vec<String>,
) {
use std::fmt::Write;
let mut out = String::with_capacity(128);
out.push('/');
let has_dur = params.iter().any(|(k, _)| *k == "dur");
let has_gate = params.iter().any(|(k, _)| *k == "gate");
let has_release = params.iter().any(|(k, _)| *k == "release");
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
if let Some(s) = sound {
@@ -1755,6 +1749,9 @@ fn emit_output(
}
for (i, (k, v)) in params.iter().enumerate() {
if v.is_empty() {
continue;
}
if !out.ends_with('/') {
out.push('/');
}
@@ -1772,18 +1769,26 @@ fn emit_output(
}
}
if nudge_secs > 0.0 {
if nudge_secs.abs() > 1e-9 {
if !out.ends_with('/') {
out.push('/');
}
let _ = write!(&mut out, "delta/{nudge_secs}");
let delta_ticks = (nudge_secs * sr).round() as i64;
let _ = write!(&mut out, "delta/{delta_ticks}");
}
if !has_dur {
if !has_gate {
if !out.ends_with('/') {
out.push('/');
}
let _ = write!(&mut out, "dur/{}", step_duration * 4.0);
let _ = write!(&mut out, "gate/{}", step_duration * 4.0);
}
if !has_release {
if !out.ends_with('/') {
out.push('/');
}
let _ = write!(&mut out, "release/{}", 12.0 * step_duration);
}
if sound.is_some() && delaytime_idx.is_none() {
@@ -1921,65 +1926,46 @@ fn float_to_value(result: f64) -> Value {
}
}
fn lift_unary<F>(val: Value, f: F) -> Result<Value, String>
fn lift_unary<F>(val: &Value, f: F) -> Result<Value, String>
where
F: Fn(f64) -> f64 + Copy,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect();
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
v => Ok(float_to_value(f(v.as_float()?))),
}
}
fn lift_unary_int<F>(val: Value, f: F) -> Result<Value, String>
fn lift_unary_int<F>(val: &Value, f: F) -> Result<Value, String>
where
F: Fn(i64) -> i64 + Copy,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x.clone(), f)).collect();
items.iter().map(|x| lift_unary_int(x, f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
v => Ok(Value::Int(f(v.as_int()?), None)),
}
}
fn lift_binary<F>(a: Value, b: Value, f: F) -> Result<Value, String>
fn lift_binary<F>(a: &Value, b: &Value, f: F) -> Result<Value, String>
where
F: Fn(f64, f64) -> f64 + Copy,
{
match (a, b) {
(Value::ArpList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(a, Value::ArpList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(Value::CycleList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
items.iter().map(|x| lift_binary(x, b, f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
(a, Value::CycleList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
items.iter().map(|x| lift_binary(a, x, f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
(a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))),
@@ -1992,7 +1978,7 @@ where
{
let b = pop(stack)?;
let a = pop(stack)?;
stack.push(lift_binary(a, b, f)?);
stack.push(lift_binary(&a, &b, f)?);
Ok(())
}
@@ -2011,11 +1997,8 @@ where
Ok(())
}
fn resolve_value(val: &Value, arp_idx: usize, poly_idx: usize) -> Cow<'_, Value> {
fn resolve_value(val: &Value, poly_idx: usize) -> Cow<'_, Value> {
match val {
Value::ArpList(items) if !items.is_empty() => {
Cow::Owned(items[arp_idx % items.len()].clone())
}
Value::CycleList(items) if !items.is_empty() => {
Cow::Owned(items[poly_idx % items.len()].clone())
}

View File

@@ -88,7 +88,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"tempo!" => Op::SetTempo,
"speed!" => Op::SetSpeed,
"at" => Op::At,
"arp" => Op::Arp,
"adsr" => Op::Adsr,
"ad" => Op::Ad,
"apply" => Op::Apply,
@@ -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

@@ -28,14 +28,14 @@ pub(super) const WORDS: &[Word] = &[
aliases: &[],
category: "Envelope",
stack: "(v.. --)",
desc: "Set velocity",
example: "100 velocity",
desc: "Set velocity (0-1)",
example: "0.8 velocity",
compile: Param,
varargs: true,
},
Word {
name: "attack",
aliases: &["att"],
aliases: &["att", "a"],
category: "Envelope",
stack: "(v.. --)",
desc: "Set attack time",
@@ -45,7 +45,7 @@ pub(super) const WORDS: &[Word] = &[
},
Word {
name: "decay",
aliases: &["dec"],
aliases: &["dec", "d"],
category: "Envelope",
stack: "(v.. --)",
desc: "Set decay time",
@@ -55,7 +55,7 @@ pub(super) const WORDS: &[Word] = &[
},
Word {
name: "sustain",
aliases: &["sus"],
aliases: &["sus", "s"],
category: "Envelope",
stack: "(v.. --)",
desc: "Set sustain level",
@@ -65,7 +65,7 @@ pub(super) const WORDS: &[Word] = &[
},
Word {
name: "release",
aliases: &["rel"],
aliases: &["rel", "r"],
category: "Envelope",
stack: "(v.. --)",
desc: "Set release time",
@@ -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

@@ -309,9 +309,9 @@ pub(super) const WORDS: &[Word] = &[
name: "at",
aliases: &[],
category: "Time",
stack: "(v1..vn --)",
desc: "Set delta context for emit timing",
example: "0 0.5 at kick s . => emits at 0 and 0.5 of step",
stack: "(v1..vn -- )",
desc: "Looping block: re-executes body per delta. Close with . (audio), m. (MIDI), or done (no emit)",
example: "0 0.5 at kick snd 1 2 rand freq . | 0 0.5 at 60 note m. | 0 0.5 at !x done",
compile: Simple,
varargs: true,
},

View File

@@ -6,7 +6,7 @@ pub(super) const WORDS: &[Word] = &[
// Sound
Word {
name: "sound",
aliases: &["s"],
aliases: &["snd"],
category: "Sound",
stack: "(name --)",
desc: "Begin sound command",
@@ -24,16 +24,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "arp",
aliases: &[],
category: "Sound",
stack: "(v1..vn -- arplist)",
desc: "Wrap stack values as arpeggio list for spreading across deltas",
example: "c4 e4 g4 b4 arp note => arpeggio",
compile: Simple,
varargs: true,
},
Word {
name: "clear",
aliases: &[],
@@ -126,22 +116,12 @@ 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: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Set duration",
desc: "Set MIDI note duration (for audio, use gate)",
example: "0.5 dur",
compile: Param,
varargs: true,
@@ -151,7 +131,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,
@@ -166,6 +146,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "stretch",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Time stretch factor (pitch-independent)",
example: "2 stretch",
compile: Param,
varargs: true,
},
Word {
name: "begin",
aliases: &[],
@@ -236,6 +226,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: &[],
@@ -277,16 +277,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: &[],
@@ -352,7 +342,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,
@@ -362,7 +352,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,
@@ -372,11 +362,21 @@ 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,
},
Word {
name: "partials",
aliases: &[],
category: "Oscillator",
stack: "(v.. --)",
desc: "Set number of active harmonics (add source only)",
example: "16 partials",
compile: Param,
varargs: true,
},
Word {
name: "coarse",
aliases: &[],
@@ -448,36 +448,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",
@@ -509,56 +479,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: &[],
@@ -832,6 +752,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: &[],
@@ -862,13 +812,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

@@ -14,4 +14,4 @@ pub const MAX_STEPS: usize = 1024;
pub const DEFAULT_LENGTH: usize = 16;
pub use file::{load, load_str, save, FileError};
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step};

View File

@@ -170,6 +170,17 @@ impl LaunchQuantization {
}
}
pub fn short_label(&self) -> &'static str {
match self {
Self::Immediate => "Imm",
Self::Beat => "Bt",
Self::Bar => "1B",
Self::Bars2 => "2B",
Self::Bars4 => "4B",
Self::Bars8 => "8B",
}
}
/// Cycle to the next longer quantization, clamped at `Bars8`.
pub fn next(&self) -> Self {
match self {
@@ -195,32 +206,6 @@ impl LaunchQuantization {
}
}
/// How a pattern synchronizes when launched: restart or phase-lock.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum SyncMode {
#[default]
Reset,
PhaseLock,
}
impl SyncMode {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Reset => "Reset",
Self::PhaseLock => "Phase-Lock",
}
}
/// Toggle between Reset and PhaseLock.
pub fn toggle(&self) -> Self {
match self {
Self::Reset => Self::PhaseLock,
Self::PhaseLock => Self::Reset,
}
}
}
/// What happens when a pattern finishes: loop, stop, or chain to another.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum FollowUp {
@@ -297,7 +282,7 @@ impl Default for Step {
}
}
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
/// Sequence of steps with playback settings (speed, quantization, follow-up).
#[derive(Clone)]
pub struct Pattern {
pub steps: Vec<Step>,
@@ -306,7 +291,6 @@ pub struct Pattern {
pub name: Option<String>,
pub description: Option<String>,
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp,
}
@@ -343,8 +327,6 @@ struct SparsePattern {
description: Option<String>,
#[serde(default, skip_serializing_if = "is_default_quantization")]
quantization: LaunchQuantization,
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
sync_mode: SyncMode,
#[serde(default, skip_serializing_if = "is_default_follow_up")]
follow_up: FollowUp,
}
@@ -353,10 +335,6 @@ fn is_default_quantization(q: &LaunchQuantization) -> bool {
*q == LaunchQuantization::default()
}
fn is_default_sync_mode(s: &SyncMode) -> bool {
*s == SyncMode::default()
}
#[derive(Deserialize)]
struct LegacyPattern {
steps: Vec<Step>,
@@ -370,8 +348,6 @@ struct LegacyPattern {
#[serde(default)]
quantization: LaunchQuantization,
#[serde(default)]
sync_mode: SyncMode,
#[serde(default)]
follow_up: FollowUp,
}
@@ -398,7 +374,6 @@ impl Serialize for Pattern {
name: self.name.clone(),
description: self.description.clone(),
quantization: self.quantization,
sync_mode: self.sync_mode,
follow_up: self.follow_up,
};
sparse.serialize(serializer)
@@ -434,7 +409,6 @@ impl<'de> Deserialize<'de> for Pattern {
name: sparse.name,
description: sparse.description,
quantization: sparse.quantization,
sync_mode: sparse.sync_mode,
follow_up: sparse.follow_up,
})
}
@@ -445,7 +419,6 @@ impl<'de> Deserialize<'de> for Pattern {
name: legacy.name,
description: legacy.description,
quantization: legacy.quantization,
sync_mode: legacy.sync_mode,
follow_up: legacy.follow_up,
}),
}
@@ -461,7 +434,6 @@ impl Default for Pattern {
name: None,
description: None,
quantization: LaunchQuantization::default(),
sync_mode: SyncMode::default(),
follow_up: FollowUp::default(),
}
}
@@ -525,6 +497,8 @@ pub struct Bank {
pub patterns: Vec<Pattern>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub prelude: String,
}
impl Bank {
@@ -542,6 +516,7 @@ impl Default for Bank {
Self {
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
name: None,
prelude: String::new(),
}
}
}

View File

@@ -11,6 +11,24 @@ use crate::{Bank, Pattern};
const PATTERN_PREFIX: &str = "cgr:";
const BANK_PREFIX: &str = "cgrb:";
pub enum ImportResult {
Pattern(Pattern),
Bank(Bank),
}
/// Auto-detect format from the prefix and decode.
pub fn import_auto(text: &str) -> Result<ImportResult, ShareError> {
// Strip everything non-ASCII — valid share strings are pure ASCII
let clean: String = text.chars().filter(|c| c.is_ascii_graphic()).collect();
if clean.starts_with(BANK_PREFIX) {
Ok(ImportResult::Bank(decode(&clean, BANK_PREFIX)?))
} else if clean.starts_with(PATTERN_PREFIX) {
Ok(ImportResult::Pattern(decode(&clean, PATTERN_PREFIX)?))
} else {
Err(ShareError::InvalidPrefix)
}
}
/// Error during pattern or bank import/export.
#[derive(Debug)]
pub enum ShareError {
@@ -63,7 +81,12 @@ fn encode<T: serde::Serialize>(value: &T, prefix: &str) -> Result<String, ShareE
fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T, ShareError> {
let text = text.trim();
let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?;
let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?;
// Strip invisible characters that clipboard managers / web copies can inject
let clean: String = payload
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect();
let compressed = URL_SAFE_NO_PAD.decode(&clean).map_err(ShareError::Base64)?;
let packed = decompress(&compressed)?;
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
}
@@ -146,7 +169,7 @@ mod tests {
#[test]
fn bad_base64() {
assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_))));
assert!(import("cgr:not-valid-data").is_err());
}
#[test]
@@ -164,7 +187,7 @@ mod tests {
for i in 0..16 {
pattern.steps[i] = Step {
active: true,
script: format!("kick {i} note 0.5 dur"),
script: format!("kick {i} note 0.5 gate"),
source: None,
name: Some(format!("step_{i}")),
};

View File

@@ -1,6 +1,7 @@
//! Script editor widget with completion, search, and sample finder popups.
use std::cell::Cell;
use std::sync::Arc;
use crate::theme;
use ratatui::{
@@ -25,7 +26,7 @@ pub struct CompletionCandidate {
}
struct CompletionState {
candidates: Vec<CompletionCandidate>,
candidates: Arc<[CompletionCandidate]>,
matches: Vec<usize>,
cursor: usize,
prefix: String,
@@ -37,7 +38,7 @@ struct CompletionState {
impl CompletionState {
fn new() -> Self {
Self {
candidates: Vec::new(),
candidates: Arc::from([]),
matches: Vec::new(),
cursor: 0,
prefix: String::new(),
@@ -171,7 +172,7 @@ impl Editor {
self.scroll_offset.set(0);
}
pub fn set_candidates(&mut self, candidates: Vec<CompletionCandidate>) {
pub fn set_candidates(&mut self, candidates: Arc<[CompletionCandidate]>) {
self.completion.candidates = candidates;
}

View File

@@ -14,11 +14,14 @@ pub struct FileBrowserModal<'a> {
title: &'a str,
input: &'a str,
entries: &'a [(String, bool, bool)],
audio_counts: &'a [Option<usize>],
selected: usize,
scroll_offset: usize,
border_color: Option<Color>,
width: u16,
height: u16,
hints: Option<Line<'a>>,
color_path: bool,
}
impl<'a> FileBrowserModal<'a> {
@@ -27,11 +30,14 @@ impl<'a> FileBrowserModal<'a> {
title,
input,
entries,
audio_counts: &[],
selected: 0,
scroll_offset: 0,
border_color: None,
width: 60,
height: 16,
hints: None,
color_path: false,
}
}
@@ -60,6 +66,21 @@ impl<'a> FileBrowserModal<'a> {
self
}
pub fn hints(mut self, hints: Line<'a>) -> Self {
self.hints = Some(hints);
self
}
pub fn audio_counts(mut self, counts: &'a [Option<usize>]) -> Self {
self.audio_counts = counts;
self
}
pub fn color_path(mut self) -> Self {
self.color_path = true;
self
}
pub fn render_centered(self, frame: &mut Frame, term: Rect) -> Rect {
let colors = theme::get();
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
@@ -70,37 +91,61 @@ impl<'a> FileBrowserModal<'a> {
.border_color(border_color)
.render_centered(frame, term);
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
let has_hints = self.hints.is_some();
let constraints = if has_hints {
vec![
Constraint::Length(1),
Constraint::Min(1),
Constraint::Length(1),
]
} else {
vec![Constraint::Length(1), Constraint::Min(1)]
};
let rows = Layout::vertical(constraints).split(inner);
// Input line
frame.render_widget(
Paragraph::new(Line::from(vec![
let input_spans = if self.color_path {
let (path_part, filter_part) = match self.input.rfind('/') {
Some(pos) => (&self.input[..=pos], &self.input[pos + 1..]),
None => ("", self.input),
};
vec![
Span::raw("> "),
Span::styled(path_part.to_string(), Style::new().fg(colors.browser.directory)),
Span::styled(filter_part.to_string(), Style::new().fg(colors.input.text)),
Span::styled("", Style::new().fg(colors.input.cursor)),
]
} else {
vec![
Span::raw("> "),
Span::styled(self.input, Style::new().fg(colors.input.text)),
Span::styled("", Style::new().fg(colors.input.cursor)),
])),
rows[0],
);
]
};
frame.render_widget(Paragraph::new(Line::from(input_spans)), rows[0]);
// Hints bar
if let Some(hints) = self.hints {
let hint_row = rows[2];
frame.render_widget(
Paragraph::new(hints).alignment(ratatui::layout::Alignment::Right),
hint_row,
);
}
// Entries list
let visible_height = rows[1].height as usize;
let visible_entries = self
.entries
.iter()
.enumerate()
.skip(self.scroll_offset)
.take(visible_height);
let lines: Vec<Line> = visible_entries
.enumerate()
.map(|(i, (name, is_dir, is_cagire))| {
let abs_idx = i + self.scroll_offset;
.map(|(abs_idx, (name, is_dir, is_cagire))| {
let is_selected = abs_idx == self.selected;
let prefix = if is_selected { "> " } else { " " };
let display = if *is_dir {
format!("{prefix}{name}/")
} else {
format!("{prefix}{name}")
};
let color = if is_selected {
colors.browser.selected
} else if *is_dir {
@@ -110,7 +155,21 @@ impl<'a> FileBrowserModal<'a> {
} else {
colors.browser.file
};
Line::from(Span::styled(display, Style::new().fg(color)))
let display = if *is_dir {
format!("{prefix}{name}/")
} else {
format!("{prefix}{name}")
};
let mut spans = vec![Span::styled(display, Style::new().fg(color))];
if *is_dir && name != ".." {
if let Some(Some(count)) = self.audio_counts.get(abs_idx) {
spans.push(Span::styled(
format!(" ({count})"),
Style::new().fg(colors.browser.file),
));
}
}
Line::from(spans)
})
.collect();

View File

@@ -23,6 +23,7 @@ pub struct TreeLine {
pub label: String,
pub folder: String,
pub index: usize,
pub child_count: usize,
}
/// Tree-view browser for navigating sample folders.
@@ -136,10 +137,10 @@ impl<'a> SampleBrowser<'a> {
let (icon, icon_color) = match entry.kind {
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
("\u{25BC} ", colors.browser.folder_icon)
("\u{2212} ", colors.browser.folder_icon)
}
TreeLineKind::Root { expanded: false }
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
| TreeLineKind::Folder { expanded: false } => ("+ ", colors.browser.folder_icon),
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
};
@@ -163,15 +164,43 @@ impl<'a> SampleBrowser<'a> {
Style::new().fg(icon_color)
};
let prefix_width = indent.len() + 2; // indent + icon
let suffix = match entry.kind {
TreeLineKind::File => format!(" {}", entry.index),
TreeLineKind::Root { expanded: false }
| TreeLineKind::Folder { expanded: false }
if entry.child_count > 0 =>
{
format!(" ({})", entry.child_count)
}
_ => String::new(),
};
let max_label = (area.width as usize)
.saturating_sub(prefix_width)
.saturating_sub(suffix.len());
let label: std::borrow::Cow<str> = if entry.label.len() > max_label && max_label > 1 {
let truncated: String = entry.label.chars().take(max_label - 1).collect();
format!("{}\u{2026}", truncated).into()
} else {
(&entry.label).into()
};
let mut spans = vec![
Span::raw(indent),
Span::styled(icon, icon_style),
Span::styled(&entry.label, label_style),
Span::styled(label, label_style),
];
if matches!(entry.kind, TreeLineKind::File) {
let idx_style = Style::new().fg(colors.browser.empty_text);
spans.push(Span::styled(format!(" {}", entry.index), idx_style));
match entry.kind {
TreeLineKind::File => {
let idx_style = Style::new().fg(colors.browser.empty_text);
spans.push(Span::styled(suffix, idx_style));
}
_ if !suffix.is_empty() => {
let dim_style = Style::new().fg(colors.browser.empty_text);
spans.push(Span::styled(suffix, dim_style));
}
_ => {}
}
lines.push(Line::from(spans));

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

@@ -30,6 +30,7 @@ pub mod transform;
use ratatui::style::Color;
use std::cell::RefCell;
use std::rc::Rc;
/// Entry in the theme registry: id, display label, and palette constructor.
pub struct ThemeEntry {
@@ -66,17 +67,17 @@ pub const THEMES: &[ThemeEntry] = &[
];
thread_local! {
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new(build::build(&(THEMES[0].palette)()));
static CURRENT_THEME: RefCell<Rc<ThemeColors>> = RefCell::new(Rc::new(build::build(&(THEMES[0].palette)())));
}
/// Return the current thread-local theme.
pub fn get() -> ThemeColors {
CURRENT_THEME.with(|t| t.borrow().clone())
/// Return the current thread-local theme (cheap Rc clone, not a deep copy).
pub fn get() -> Rc<ThemeColors> {
CURRENT_THEME.with(|t| Rc::clone(&t.borrow()))
}
/// Set the current thread-local theme.
pub fn set(theme: ThemeColors) {
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
CURRENT_THEME.with(|t| *t.borrow_mut() = Rc::new(theme));
}
/// Complete set of resolved colors for all UI components.
@@ -174,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

@@ -7,7 +7,7 @@
"steps": [
{
"i": 0,
"script": "0 7 .. at\n c2 maj9 arp note\n wide bigverb mysynth \n 2000 1000 0.4 0.8 rand expslide llpf\n 0.4 0.8 rand llpq\n ."
"script": "0 7 .. at\n mysynth [ c2 maj9 ] cycle note\n wide bigverb\n 2000 1000 0.4 0.8 rand expslide llpf\n 0.4 0.8 rand llpq\n ."
},
{
"i": 8,

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@ All time values are in **steps**, just like `attack`, `decay`, and `release`. At
Oscillate a parameter between two values.
```forth
saw s 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
saw s 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
saw snd 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
saw snd 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
```
| Word | Shape | Output |
@@ -27,8 +27,8 @@ Stack effect: `( min max period -- str )`
Transition from one value to another over a duration.
```forth
saw s 0 1 0.5 slide gain . ( fade in over half a step )
saw s 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
saw snd 0 1 0.5 slide gain . ( fade in over half a step )
saw snd 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
```
| Word | Curve | Output |
@@ -44,9 +44,9 @@ Stack effect: `( start end dur -- str )`
Randomize a parameter within a range, retriggering at a given period.
```forth
saw s 200 4000 2 jit lpf . ( new random value every 2 steps )
saw s 200 4000 2 sjit lpf . ( same but smoothly interpolated )
saw s 200 4000 1 drunk lpf . ( random walk, each step )
saw snd 200 4000 2 jit lpf . ( new random value every 2 steps )
saw snd 200 4000 2 sjit lpf . ( same but smoothly interpolated )
saw snd 200 4000 1 drunk lpf . ( random walk, each step )
```
| Word | Behavior | Output |
@@ -57,26 +57,62 @@ saw s 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 s 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
Modulation words return strings, so they compose naturally with the rest of the language. Use them anywhere a parameter value is expected.
```forth
saw s
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

@@ -7,7 +7,7 @@ Cagire includes an audio engine called `Doux`. No external software is needed to
When you write a Forth script and emit (`.`), the script produces a command string. This command travels to the audio engine, which interprets it and creates a voice. The voice plays until its envelope finishes or until it is killed by another voice. You can also spawn infinite voices, but you will need to manage their lifecycle manually, otherwise they will never stop.
```forth
saw s c4 note 0.8 gain 0.3 verb .
saw snd c4 note 0.8 gain 0.3 verb .
```
## Voices
@@ -24,7 +24,7 @@ Press `r` on the Engine page to reset the peak counter.
After selecting a sound source, you add parameters. Each parameter word takes a value from the stack and stores it in the command register:
```forth
saw s
saw snd
c4 note ;; pitch
0.5 gain ;; volume
0.1 attack ;; envelope attack time
@@ -42,14 +42,14 @@ Use `all` to apply parameters globally. Global parameters persist across all pat
```forth
;; Prospective: set params before emitting
500 lpf 0.5 verb all
kick s 60 note . ;; gets lpf=500 verb=0.5
hat s 70 note . ;; gets lpf=500 verb=0.5
kick snd 60 note . ;; gets lpf=500 verb=0.5
hat snd 70 note . ;; gets lpf=500 verb=0.5
```
```forth
;; Retroactive: patch already-emitted sounds
kick s 60 note .
hat s 70 note .
kick snd 60 note .
hat snd 70 note .
500 lpf 0.5 verb all ;; both outputs get lpf and verb
```
@@ -57,17 +57,17 @@ Per-sound parameters override global ones:
```forth
500 lpf all
kick s 2000 lpf . ;; lpf=2000 (per-sound wins)
hat s . ;; lpf=500 (global)
kick snd 2000 lpf . ;; lpf=2000 (per-sound wins)
hat snd . ;; lpf=500 (global)
```
Use `noall` to clear global parameters:
```forth
500 lpf all
kick s . ;; gets lpf
kick snd . ;; gets lpf
noall
hat s . ;; no lpf
hat snd . ;; no lpf
```
## Controlling Existing Voices

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

@@ -50,6 +50,7 @@ snare sound 0.5 speed . ( play snare at half speed )
| `slice` | 1+ | Divide sample into N equal slices |
| `pick` | 0+ | Select which slice to play (0-indexed, wraps) |
| `speed` | any | Playback speed multiplier |
| `stretch` | 0+ | Time-stretch factor (pitch-independent) |
| `freq` | Hz | Base frequency for pitch tracking |
| `fit` | seconds | Stretch/compress sample to fit duration |
| `cut` | 0+ | Choke group |
@@ -105,6 +106,24 @@ crow sound -1 speed . ( play backwards at nominal speed )
crow sound -4 speed . ( play backwards, 4 times faster )
```
## Time Stretching
The `stretch` parameter changes sample duration without affecting pitch, using a phase vocoder algorithm. This contrasts with `speed`, which changes both tempo and pitch together.
```forth
kick sound 2 stretch . ( twice as long, same pitch )
kick sound 0.5 stretch . ( half as long, same pitch )
kick sound 0 stretch . ( freeze — holds at current position )
```
Combine with `slice` and `pick` for pitch-locked breakbeat manipulation:
```forth
break sound 8 slice step pick 2 stretch . ( sliced break, stretched x2, original pitch )
```
Reverse playback is not available with `stretch` — use `speed` for that.
## Fitting to Duration
The `fit` parameter stretches or compresses a sample to match a target duration in seconds. This adjusts speed automatically.

View File

@@ -79,16 +79,16 @@ Top-level files are named by their filename (without extension). Files inside fo
Reference samples by name:
```forth
kick s . ;; play kick.wav
snare s 0.5 gain . ;; play snare at half volume
kick snd . ;; play kick.wav
snare snd 0.5 gain . ;; play snare at half volume
```
For samples in folders, use `n` to select which one:
```forth
hats s 0 n . ;; play hats/closed.wav (index 0)
hats s 1 n . ;; play hats/open.wav (index 1)
hats s 2 n . ;; play hats/pedal.wav (index 2)
hats snd 0 n . ;; play hats/closed.wav (index 0)
hats snd 1 n . ;; play hats/open.wav (index 1)
hats snd 2 n . ;; play hats/pedal.wav (index 2)
```
The index wraps around. If you have 3 samples and request `5 n`, you get index 2 (because 5 % 3 = 2).
@@ -106,9 +106,9 @@ samples/
```
```forth
kick s . ;; plays kick.wav
kick s a bank . ;; plays kick_a.wav
kick s hard bank . ;; plays kick_hard.wav
kick snd . ;; plays kick.wav
kick snd a bank . ;; plays kick_a.wav
kick snd hard bank . ;; plays kick_hard.wav
```
If the banked version does not exist, it falls back to the default.

View File

@@ -1,6 +1,6 @@
# Sources
The audio engine provides a variety of sound sources. Use the `sound` word (or `s` for short) to select one.
The audio engine provides a variety of sound sources. Use the `sound` word (or `snd` for short) to select one.
## Basic Oscillators
@@ -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

@@ -5,7 +5,7 @@ Word definitions let you abstract sound design into reusable units.
## Defining Sounds
```forth
: lead "saw" s 0.3 gain 1200 lpf ;
: lead "saw" snd 0.3 gain 1200 lpf ;
```
Use it with different notes:
@@ -20,8 +20,8 @@ e4 note lead .
Include the emit to make the word play directly:
```forth
: kk "kick" s 1 decay . ;
: hh "hihat" s 0.5 gain 0.5 decay . ;
: kk "kick" snd 1 decay . ;
: hh "hihat" snd 0.5 gain 0.5 decay . ;
```
Steps become simple:
@@ -39,5 +39,5 @@ kk
```
```forth
c4 note saw s dark wet .
c4 note saw snd dark wet .
```

View File

@@ -78,7 +78,7 @@ Because parentheses defer execution, wrapping code in `( ... )` without a consum
.
```
Any word that is not recognized as a built-in or a user definition becomes a string on the stack. This means `kick s` and `"kick" s` are equivalent. You only need quotes when the string contains spaces or when it conflicts with an existing word name.
Any word that is not recognized as a built-in or a user definition becomes a string on the stack. This means `kick snd` and `"kick" snd` are equivalent. You only need quotes when the string contains spaces or when it conflicts with an existing word name.
## The Command Register
@@ -94,7 +94,7 @@ kick sound ;; sets the sound name
. ;; emits the command and clears the register
```
The word `sound` (or its shorthand `s`) sets what sound to play. Parameter words like `gain`, `freq`, `decay`, or `verb` add key-value pairs to the register. Nothing happens until you emit with `.` (dot). At that moment, the register is packaged into a command and sent to the audio engine.
The word `sound` (or its shorthand `snd`) sets what sound to play. Parameter words like `gain`, `freq`, `decay`, or `verb` add key-value pairs to the register. Nothing happens until you emit with `.` (dot). At that moment, the register is packaged into a command and sent to the audio engine.
This design lets you build sounds incrementally:
@@ -110,14 +110,14 @@ c4 note
Each line adds something to the register. The final `.` triggers the sound. You can also write it all on one line:
```forth
"sine" s c4 note 0.5 gain 0.3 decay 0.4 verb .
"sine" snd c4 note 0.5 gain 0.3 decay 0.4 verb .
```
The order of parameters does not matter. You can even emit multiple times in a single step. If you need to discard the register without emitting, use `clear`:
```forth
"kick" s 0.5 gain clear ;; nothing plays, register is emptied
"hat" s . ;; only the hat plays
"kick" snd 0.5 gain clear ;; nothing plays, register is emptied
"hat" snd . ;; only the hat plays
```
This is useful when conditionals might cancel a sound before it emits.

View File

@@ -47,7 +47,7 @@ The outer quotation runs every 4th iteration. Inside, a coin flip picks the note
Wrapping code in a quotation without consuming it is a quick way to disable it:
```forth
( kick s . )
( kick snd . )
```
Nothing will execute this quotation — it just sits on the stack and gets discarded. Useful for temporarily silencing a line while editing.
@@ -63,7 +63,7 @@ Square brackets execute their contents immediately, then push a count of how man
After this runs, the stack holds `60 64 67 3` — three values plus the count `3`. This is useful with words that need to know how many items precede them:
```forth
[ 60 64 67 ] cycle note sine s .
[ 60 64 67 ] cycle note sine snd .
```
The `cycle` word reads the count to know how many values to rotate through. Without brackets you would write `60 64 67 3 cycle` — the brackets save you from counting manually.
@@ -71,8 +71,8 @@ The `cycle` word reads the count to know how many values to rotate through. With
Square brackets work with any word that takes a count:
```forth
[ c4 e4 g4 ] choose note saw s . ;; random note from the list
[ 60 64 67 ] note sine s . ;; 3-note chord (note consumes all)
[ c4 e4 g4 ] choose note saw snd . ;; random note from the list
[ 60 64 67 ] note sine snd . ;; 3-note chord (note consumes all)
```
### Nesting
@@ -88,7 +88,7 @@ Square brackets can nest. Each pair produces its own count:
The contents are compiled and executed normally, so you can use any Forth code:
```forth
[ c4 c4 3 + c4 7 + ] note sine s . ;; root, minor third, fifth
[ c4 c4 3 + c4 7 + ] note sine snd . ;; root, minor third, fifth
```
## { ... } — Curly Braces
@@ -96,13 +96,13 @@ The contents are compiled and executed normally, so you can use any Forth code:
Curly braces are ignored by the compiler. They do nothing. Use them as a visual aid to group related code:
```forth
{ kick s } { 0.5 gain } { 0.3 verb } .
{ kick snd } { 0.5 gain } { 0.3 verb } .
```
This compiles to exactly the same thing as:
```forth
kick s 0.5 gain 0.3 verb .
kick snd 0.5 gain 0.3 verb .
```
They can help readability in dense one-liners but have no semantic meaning.

View File

@@ -112,7 +112,7 @@ Reads naturally: "c3 or c5, depending on the coin."
```forth
( 0.8 gain ) ( 0.3 gain ) fill ifelse
tri s c4 note 0.2 decay .
tri snd c4 note 0.2 decay .
```
Loud during fills, quiet otherwise.
@@ -123,7 +123,7 @@ Choose the nth quotation from a list. The index is 0-based:
```forth
( c4 ) ( e4 ) ( g4 ) ( b4 ) 0 3 rand select
note sine s 0.5 decay .
note sine snd 0.5 decay .
```
Four notes of a major seventh chord picked randomly. Note that this is unnecessarily complex :)

View File

@@ -9,13 +9,13 @@ Sequential rotation through values.
`cycle` advances based on `runs` — how many times this particular step has played:
```forth
60 64 67 3 cycle note sine s . ;; 60, 64, 67, 60, 64, 67, ...
60 64 67 3 cycle note sine snd . ;; 60, 64, 67, 60, 64, 67, ...
```
`pcycle` advances based on `iter` — the pattern iteration count:
```forth
kick snare 2 pcycle s . ;; kick on even iterations, snare on odd
kick snare 2 pcycle snd . ;; kick on even iterations, snare on odd
```
The distinction matters when patterns have different lengths or when multiple steps share the same script. `cycle` gives each step its own independent counter. `pcycle` ties all steps to the same global pattern position.
@@ -25,8 +25,8 @@ The distinction matters when patterns have different lengths or when multiple st
Ping-pong instead of wrapping. With 4 values the sequence is 0, 1, 2, 3, 2, 1, 0, 1, 2, ...
```forth
60 64 67 72 4 bounce note sine s . ;; ping-pong by step runs
60 64 67 72 4 pbounce note sine s . ;; ping-pong by pattern iteration
60 64 67 72 4 bounce note sine snd . ;; ping-pong by step runs
60 64 67 72 4 pbounce note sine snd . ;; ping-pong by pattern iteration
```
Same `runs` vs `iter` split as `cycle` / `pcycle`.
@@ -36,7 +36,7 @@ Same `runs` vs `iter` split as `cycle` / `pcycle`.
Uniform random selection:
```forth
kick snare hat 3 choose s . ;; random drum hit each time
kick snare hat 3 choose snd . ;; random drum hit each time
```
Unlike the cycling words, `choose` is nondeterministic — every evaluation picks independently.
@@ -46,7 +46,7 @@ Unlike the cycling words, `choose` is nondeterministic — every evaluation pick
Weighted random. Push value/weight pairs, then the count:
```forth
kick 0.5 snare 0.3 hat 0.2 3 wchoose s .
kick 0.5 snare 0.3 hat 0.2 3 wchoose snd .
```
Kick plays 50% of the time, snare 30%, hat 20%. Weights are normalized automatically — they don't need to sum to 1.
@@ -56,8 +56,8 @@ Kick plays 50% of the time, snare 30%, hat 20%. Weights are normalized automatic
Direct lookup by an explicit index. The index wraps with modulo, so it never goes out of bounds. Negative indices count from the end:
```forth
[ c4 e4 g4 ] step index note sine s . ;; step number picks the note
[ c4 e4 g4 ] iter index note sine s . ;; pattern iteration picks the note
[ c4 e4 g4 ] step index note sine snd . ;; step number picks the note
[ c4 e4 g4 ] iter index note sine snd . ;; pattern iteration picks the note
```
This is useful when you want full control over which value is selected, driven by any expression you like.
@@ -67,9 +67,9 @@ This is useful when you want full control over which value is selected, driven b
All these words take a count argument `n`. Square brackets compute that count for you:
```forth
[ 60 64 67 ] cycle note sine s . ;; no need to write "3"
[ kick snare hat ] choose s .
[ c4 e4 g4 b4 ] bounce note sine s .
[ 60 64 67 ] cycle note sine snd . ;; no need to write "3"
[ kick snare hat ] choose snd .
[ c4 e4 g4 b4 ] bounce note sine snd .
```
Without brackets: `60 64 67 3 cycle`. With brackets: `[ 60 64 67 ] cycle`. Same result, less counting.
@@ -80,7 +80,7 @@ When any of these words selects a quotation, it executes it instead of pushing i
```forth
[ ( c4 note ) ( e4 note ) ( g4 note ) ] cycle
sine s .
sine snd .
```
On the first run the quotation `( c4 note )` executes, setting the note to C4. Next run, E4. Then G4. Then back to C4.
@@ -88,5 +88,5 @@ On the first run the quotation `( c4 note )` executes, setting the note to C4. N
This works with all selection words. Mix plain values and quotations freely:
```forth
[ ( hat s 0.3 gain . ) ( snare s . ) ( kick s . ) ] choose
[ ( hat snd 0.3 gain . ) ( snare snd . ) ( kick snd . ) ] choose
```

View File

@@ -24,7 +24,7 @@ When you define a word in one step, it becomes available to all other steps. Thi
Step 0:
```forth
: bass "saw" s 0.8 gain 800 lpf ;
: bass "saw" snd 0.8 gain 800 lpf ;
```
Step 4:
@@ -75,7 +75,7 @@ This only affects words you defined with `:` ... `;`. Built-in words cannot be f
**Synth definitions** save you from repeating sound design:
```forth
: pad "sine" s 0.3 gain 2 attack 0.5 verb ;
: pad "sine" snd 0.3 gain 2 attack 0.5 verb ;
```
**Transpositions** and musical helpers:
@@ -90,8 +90,8 @@ This only affects words you defined with `:` ... `;`. Built-in words cannot be f
A word can contain `.` to emit sounds directly:
```forth
: kick "kick" s . ;
: hat "hat" s 0.4 gain . ;
: kick "kick" snd . ;
: hat "hat" snd 0.4 gain . ;
```
Then a step becomes trivial:

View File

@@ -33,4 +33,4 @@ Each word entry shows:
- **Description**: What the word does
- **Example**: How to use it
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing. Use the dictionary while writing scripts to check stack effects and study their behavior. Some words also come with shorter aliases (e.g., `sound``s`). You will learn aliases quite naturally, because aliases are usually reserved for very common words.
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing. Use the dictionary while writing scripts to check stack effects and study their behavior. Some words also come with shorter aliases (e.g., `sound``snd`). You will learn aliases quite naturally, because aliases are usually reserved for very common words.

View File

@@ -35,8 +35,8 @@ Cagire supports this syntax but also provides quotation-based conditionals:
The words `?` and `!?` execute a quotation based on a condition:
```forth
( "kick" s . ) coin ? ;; execute if coin is 1
( "snare" s . ) coin !? ;; execute if coin is 0
( "kick" snd . ) coin ? ;; execute if coin is 1
( "snare" snd . ) coin !? ;; execute if coin is 0
```
## Strings
@@ -56,7 +56,7 @@ Cagire has first-class strings:
This pushes a string value onto the stack. Strings are used for sound names, sample names, and variable keys. You often do not need quotes at all. Any unrecognized word becomes a string automatically:
```forth
kick s . ;; "kick" is not a word, so it becomes the string "kick"
kick snd . ;; "kick" is not a word, so it becomes the string "kick"
myweirdname ;; pushes "myweirdname" onto the stack
```
@@ -110,8 +110,8 @@ Cagire uses a quotation-based loop with `times`:
The loop counter is stored in the variable `i`, accessed with `@i`. This fits Cagire's style where control flow uses quotations.
```forth
4 ( @i 4 / at hat s . ) times ;; hat at 0, 0.25, 0.5, 0.75
4 ( c4 @i + note sine s . ) times ;; ascending notes
4 ( @i 4 / at hat snd . ) times ;; hat at 0, 0.25, 0.5, 0.75
4 ( c4 @i + note sine snd . ) times ;; ascending notes
```
For generating sequences without side effects, use `..` or `gen`:
@@ -155,11 +155,11 @@ These have no equivalent in classic Forth. They connect your script to the seque
Classic Forth is deterministic. Cagire has built-in randomness:
```forth
( "snare" s . ) 50 prob ;; 50% chance
( "clap" s . ) 0.25 chance ;; 25% chance
( "hat" s . ) often ;; 75% chance
( "rim" s . ) sometimes ;; 50% chance
( "tom" s . ) rarely ;; 25% chance
( "snare" snd . ) 50 prob ;; 50% chance
( "clap" snd . ) 0.25 chance ;; 25% chance
( "hat" snd . ) often ;; 75% chance
( "rim" snd . ) sometimes ;; 50% chance
( "tom" snd . ) rarely ;; 25% chance
```
These words take a quotation and execute it probabilistically.
@@ -169,9 +169,9 @@ These words take a quotation and execute it probabilistically.
Execute a quotation on specific iterations:
```forth
( "snare" s . ) 4 every ;; every 4th pattern iteration
( "hat" s . ) 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
( "hat" s . ) 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations
( "snare" snd . ) 4 every ;; every 4th pattern iteration
( "hat" snd . ) 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
( "hat" snd . ) 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations
```
`every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped.
@@ -183,13 +183,13 @@ Execute a quotation on specific iterations:
Parameter words like `note`, `freq`, and `gain` consume the entire stack. If you push multiple values before a param word, you get polyphony:
```forth
60 64 67 note sine s . ;; emits 3 voices with notes 60, 64, 67
60 64 67 note sine snd . ;; emits 3 voices with notes 60, 64, 67
```
This works for any param and for the sound word itself:
```forth
440 880 freq sine tri s . ;; 2 voices: sine at 440, tri at 880
440 880 freq sine tri snd . ;; 2 voices: sine at 440, tri at 880
```
When params have different lengths, shorter lists cycle:
@@ -197,7 +197,7 @@ When params have different lengths, shorter lists cycle:
```forth
60 64 67 note ;; 3 notes
0.5 1.0 gain ;; 2 gains (cycles: 0.5, 1.0, 0.5)
sine s . ;; emits 3 voices
sine snd . ;; emits 3 voices
```
Polyphony multiplies with `at` deltas:
@@ -205,7 +205,7 @@ Polyphony multiplies with `at` deltas:
```forth
0 0.5 at ;; 2 time points
60 64 note ;; 2 notes
sine s . ;; emits 4 voices (2 notes × 2 times)
sine snd . ;; emits 4 voices (2 notes × 2 times)
```
## Summary

View File

@@ -1,44 +1,69 @@
# The Prelude
# Preludes
You can define words in any step and they become available to all other steps. But as a project grows, definitions get scattered across steps and become hard to find and maintain. The **prelude** is a dedicated place for this. It is a project-wide Forth script that runs once before the first step plays. Definitions, variables, settings — everything in one place. Press `d` to open the prelude editor. Press `Esc` to save and close. Press `D` (Shift+d) to re-evaluate it without opening the editor.
Cagire has two levels of prelude: a **project prelude** shared by all banks, and **bank preludes** that travel with each bank.
## Bank Prelude
Each bank can carry its own prelude script. Press `p` to open the current bank's prelude editor. Press `Esc` to save, evaluate, and close.
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 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 . ;
```
Every step in that bank can now use `bass` and `pad`. Share the bank and the recipient gets these definitions automatically.
## Project Prelude
The project prelude is a global script shared across all banks. Press `P` (Shift+p) to open it. Use it for truly project-wide definitions, variables, and settings that every bank should see.
```forth
c2 !root
0 !mode
42 seed
```
## Evaluation Order
When preludes are evaluated (on playback start, project load, or pressing `d`):
1. **Project prelude** runs first
2. **Bank 0 prelude** runs next (if non-empty)
3. **Bank 1 prelude**, then **Bank 2**, ... up to **Bank 31**
Only non-empty bank preludes are evaluated. Last-evaluated wins for name collisions — a bank prelude can override a project-level definition.
## Keybindings
| Key | Action |
|-----|--------|
| `p` | Open current bank's prelude editor |
| `P` | Open project prelude editor |
| `d` | Re-evaluate all preludes (project + all banks) |
## Naming Your Sounds
The most common use of the 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 or to create a new word before using it:
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 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 .
```
Repeat this across eight steps without making a new word and you have eight copies of the same thing. Change the filter? Change it eight times.
In the prelude, define it once:
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.
A step that used to read:
```forth
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
```
Becomes:
```forth
c2 note bass
```
## Building a Vocabulary
The prelude is where you build the vocabulary for your music. Not just instruments but any combination of code / words you want to reuse:
```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 . ;
@@ -49,11 +74,11 @@ The prelude is where you build the vocabulary for your music. Not just instrumen
: loud 0.9 gain ;
```
By using the prelude and predefined words, steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
Steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
## Setting Initial State
The prelude also runs plain Forth, not just definitions. You can use it to set variables and seed the random generator:
The project prelude is the right place for global state:
```forth
c2 !root
@@ -61,18 +86,18 @@ c2 !root
42 seed
```
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible — same seed, same sequence every time you hit play.
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible.
## When It Runs
## When Preludes Run
The prelude evaluates at three moments:
Preludes evaluate at three moments:
1. When you press **Space** to start playback
2. When you **load** a project
3. When you press **D** manually
3. When you press **d** manually
It runs once at these moments, not on every step. This makes it the right place for definitions and initial values. If you edit the prelude while playing, press `D` to push changes into the running session. New definitions take effect immediately; the next time a step runs, it sees the updated words.
They run once at these moments, not on every step. If you edit a prelude while playing, press `d` to push changes into the running session.
## What Not to Put Here
The prelude has no access to sequencer state. Words like `step`, `beat`, `iter`, and `phase` are meaningless here because no step is playing yet. Use the prelude for definitions and setup, not for logic that depends on timing. The prelude also should not emit sounds. It runs silently — any `.` calls here would fire before the sequencer clock is running and produce nothing useful.
Preludes have no access to sequencer state. Words like `step`, `beat`, `iter`, and `phase` are meaningless here because no step is playing yet. Use preludes for definitions and setup, not for logic that depends on timing. Preludes also should not emit sounds — any `.` calls here would fire before the sequencer clock is running.

View File

@@ -28,7 +28,6 @@ Each pattern is an independent sequence of steps with its own properties:
| Length | Steps before the pattern loops (`1`-`1024`) | `16` |
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
| Quantization | When the pattern launches | `Bar` |
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
| Follow Up | What happens when the pattern finishes an iteration | `Loop` |
Press `e` in the patterns view to edit these settings. After editing properties, you will have to hit the `c` key to _launch_ these changes. More about that later!

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 gate .
```
```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 gate .
```
```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

@@ -32,12 +32,13 @@ To create mirrors: copy a step with `Ctrl+C`, then paste with `Ctrl+B` instead o
- `Ctrl+D` — Duplicate selection
- `Ctrl+H` — Harden mirrors (convert to independent copies)
## Prelude
## Preludes
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
Each bank has its own prelude a Forth script for definitions and setup that travels with the bank when shared. There is also a project-wide prelude for global configuration.
- `p` — Open the prelude editor
- `d`Evaluate the prelude
- `p` — Open current bank's prelude editor
- `P`Open project prelude editor
- `d` — Evaluate all preludes (project + all banks)
## Pattern Controls

View File

@@ -33,7 +33,7 @@ You can also arm mute/solo changes:
- Press `Shift+m` to clear all mutes
- Press `Shift+x` to clear all solos
A pattern might not start immediately depending on the sync mode you have chosen.
A pattern might not start immediately depending on its quantization setting.
It might wait for the next beat/bar boundary.
## Status Indicators
@@ -63,9 +63,4 @@ Launched changes don't execute immediately. They wait for a quantization boundar
Edit quantization in pattern properties (press `e` on a pattern).
## Sync Mode
When a pattern starts, its playback position depends on sync mode:
- **Reset**: Always start at step 0
- **Phase-Lock**: Start at the current beat-aligned position (stays in sync with other patterns)
Patterns always start at a beat-aligned position (phase-lock), staying in sync with other running patterns.

View File

@@ -42,8 +42,8 @@ Crossfade between two sounds:
```forth
1 1 ccval 127 / ;; normalize to 0.0-1.0
dup saw s swap gain .
1 swap - tri s swap gain .
dup saw snd swap gain .
1 swap - tri snd swap gain .
```
## Scaling Values

View File

@@ -15,7 +15,7 @@ Configure your MIDI devices in the **Options** view. Select input and output dev
The audio engine (`Doux`) and MIDI are independent systems. Use `.` to emit audio commands, use `m.` to emit MIDI messages. You can use both in the same script:
```forth
saw s c4 note 0.5 gain . ;; audio
saw snd c4 note 0.5 gain . ;; audio
60 note 100 velocity m. ;; MIDI
```

View File

@@ -7,19 +7,19 @@ Every step has a duration. By default, sounds emit at the very start of that dur
`at` drains the entire stack and stores the values as timing offsets. Each value is a fraction of the step duration: 0 = start, 0.5 = halfway, 1.0 = next step boundary.
```forth
0.5 at kick s . ;; kick at the midpoint
0.5 at kick snd . ;; kick at the midpoint
```
Push multiple values before calling `at` to get multiple emits from a single `.`:
```forth
0 0.5 at kick s .
0 0.5 at kick snd .
```
Two kicks: one at start, one at midpoint.
```forth
0 0.25 0.5 0.75 at hat s .
0 0.25 0.5 0.75 at hat snd .
```
Four hats, evenly spaced.
@@ -28,44 +28,42 @@ The deltas persist across multiple `.` calls until `clear` or a new `at`:
```forth
0 0.5 at
kick s . ;; 2 kicks
hat s . ;; 2 hats (same timing)
kick snd . ;; 2 kicks
hat snd . ;; 2 hats (same timing)
clear
snare s . ;; 1 snare (deltas cleared)
snare snd . ;; 1 snare (deltas cleared)
```
## Cross-product: at Without arp
## Polyphonic at
Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2 deltas, you get 6 emits -- every note at every delta:
Deltas multiply with polyphonic voices. If you have 3 notes and 2 deltas, you get 6 emits every note at every delta:
```forth
0 0.5 at
c4 e4 g4 note 1.5 decay sine s .
c4 e4 g4 note 1.5 decay sine snd .
```
6 emits: 3 notes x 2 deltas. A chord played twice per step.
## 1:1 Pairing: at With arp
## Arpeggios with at + cycle
`arp` changes the behavior. Instead of cross-product, deltas and arp values pair up 1:1. Each delta gets one note from the arpeggio:
Use `cycle` inside an `at` block to pick one note per subdivision:
```forth
0 0.33 0.66 at
c4 e4 g4 arp note 0.5 decay sine s .
sine snd [ c4 e4 g4 ] cycle note 0.5 decay .
```
C4 at 0, E4 at 0.33, G4 at 0.66.
C4 at 0, E4 at 0.33, G4 at 0.66. `cycle` advances per iteration of the at-loop.
If the lists differ in length, the shorter one wraps around:
If the list is shorter than the number of deltas, it wraps:
```forth
0 0.25 0.5 0.75 at
c4 e4 arp note 0.3 decay sine s .
sine snd [ c4 e4 ] cycle note 0.3 decay .
```
C4, E4, C4, E4 — the shorter list wraps to fill 4 time points.
This is THE key distinction. Without `arp`: every note at every time. With `arp`: one note per time slot.
C4, E4, C4, E4 — wraps to fill 4 time points.
## Generating Deltas
@@ -74,25 +72,25 @@ You rarely type deltas by hand. Use generators:
Evenly spaced via `.,`:
```forth
0 1 0.25 ., at hat s . ;; 0 0.25 0.5 0.75 1.0
0 1 0.25 ., at hat snd . ;; 0 0.25 0.5 0.75 1.0
```
Euclidean distribution via `euclid`:
```forth
3 8 euclid at hat s . ;; 3 hats at positions 0, 3, 5
3 8 euclid at hat snd . ;; 3 hats at positions 0, 3, 5
```
Random timing via `gen`:
```forth
( 0.0 1.0 rand ) 4 gen at hat s . ;; 4 hats at random positions
( 0.0 1.0 rand ) 4 gen at hat snd . ;; 4 hats at random positions
```
Geometric spacing via `geom..`:
```forth
0.0 2.0 4 geom.. at hat s . ;; exponentially spaced
0.0 2.0 4 geom.. at hat snd . ;; exponentially spaced
```
## Gating at
@@ -101,14 +99,14 @@ Wrap `at` expressions in quotations for conditional timing:
```forth
( 0 0.25 0.5 0.75 at ) 2 every
hat s .
hat snd .
```
16th-note hats every other bar.
```forth
( 0 0.5 at ) 0.5 chance
kick s .
kick snd .
```
50% chance of double-hit.

View File

@@ -1,6 +1,6 @@
# Generators & Sequences
Sequences of values drive music: arpeggios, parameter sweeps, rhythmic patterns. Cagire has dedicated words for building sequences on the stack, transforming them, and collapsing them to single values.
Sequences of values drive music: melodic patterns, parameter sweeps, rhythmic patterns. Cagire has dedicated words for building sequences on the stack, transforming them, and collapsing them to single values.
## Ranges
@@ -48,7 +48,7 @@ Contrast with `times`, which executes for side effects and does not collect. `ti
```forth
4 ( @i ) times ;; 0 1 2 3 (pushes @i each iteration)
4 ( @i 60 + note sine s . ) times ;; plays 4 notes, collects nothing
4 ( @i 60 + note sine snd . ) times ;; plays 4 notes, collects nothing
```
The distinction: `gen` is for building data. `times` is for doing things.
@@ -81,7 +81,7 @@ Four words reshape values already on the stack. All take n (the count of items t
```forth
1 2 3 4 4 rev ;; 4 3 2 1
c4 e4 g4 3 rev ;; g4 e4 c4 (descending arpeggio)
c4 e4 g4 3 rev ;; g4 e4 c4 (descending)
```
`shuffle` randomizes order:
@@ -124,9 +124,9 @@ c4 4 dupn ;; c4 c4 c4 c4
Build a drone chord -- same note, different octaves:
```forth
c3 note 0.5 gain sine s .
c3 note 12 + 0.5 gain sine s .
c3 note 24 + 0.3 gain sine s .
c3 note 0.5 gain sine snd .
c3 note 12 + 0.5 gain sine snd .
c3 note 24 + 0.3 gain sine snd .
```
Or replicate a value for batch processing:

View File

@@ -7,17 +7,17 @@ This tutorial covers everything pitch-related: notes, intervals, chords, voicing
A note name followed by an octave number compiles to a MIDI integer:
```forth
c4 note sine s .
c4 note sine snd .
```
That plays middle C (MIDI 60). `a4` is concert A (69), `e3` is 52. Sharps use `s` or `#`, flats use `b`:
```forth
fs4 note 0.5 decay saw s .
fs4 note 0.5 decay saw snd .
```
```forth
eb4 note 0.8 decay tri s .
eb4 note 0.8 decay tri snd .
```
`fs4` and `f#4` both mean F sharp 4 (MIDI 66). `bb3` is B flat 3 (58). Octave range is -1 to 9.
@@ -29,13 +29,13 @@ Notes are just integers. They work anywhere an integer works — you can do arit
An interval duplicates the top of the stack and adds semitones. Stack two intervals to build a chord by hand:
```forth
c4 M3 P5 note 1.5 decay sine s .
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 s .
a3 m3 P5 note 1.2 decay saw snd .
```
A minor triad: A3, C4, E4.
@@ -78,7 +78,7 @@ A minor triad: A3, C4, E4.
Custom voicings with wide intervals:
```forth
c3 P5 P8 M10 note 1.5 decay sine s .
c3 P5 P8 M10 note 1.5 decay sine snd .
```
C3, G3, C4, E4 — an open-voiced C major spread across two octaves.
@@ -88,21 +88,21 @@ C3, G3, C4, E4 — an open-voiced C major spread across two octaves.
Chord words replace a root note with all the chord tones. They're shortcuts for what intervals do manually:
```forth
c4 maj note 1.5 decay sine s .
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 s .
d3 min7 note 1.5 decay saw snd .
```
```forth
e3 dom9 note 1.2 decay saw s .
e3 dom9 note 1.2 decay saw snd .
```
```forth
a3 sus2 note 1.5 decay tri s .
a3 sus2 note 1.5 decay tri snd .
```
Common triads:
@@ -140,19 +140,19 @@ Four words reshape chord voicings without changing the harmony.
`inv` moves the bottom note up an octave (inversion):
```forth
c4 maj inv note 1.5 decay sine s .
c4 maj inv note 1.5 decay sine snd .
```
The root C goes up, giving E4 G4 C5 — first inversion. Apply it twice for second inversion:
```forth
c4 maj inv inv note 1.5 decay sine s .
c4 maj inv inv note 1.5 decay sine snd .
```
G4 C5 E5. `dinv` does the opposite — moves the top note down an octave:
```forth
c4 maj dinv note 1.5 decay sine s .
c4 maj dinv note 1.5 decay sine snd .
```
G3 C4 E4. The fifth drops below the root.
@@ -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 s .
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 s .
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.
@@ -176,13 +176,13 @@ E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jaz
`tp` shifts every note on the stack by N semitones:
```forth
c4 maj 3 tp note 1.5 decay sine s .
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 s .
c4 min7 -2 tp note 1.5 decay saw snd .
```
Shifts the whole chord down 2 semitones (Bb minor 7).
@@ -190,14 +190,14 @@ Shifts the whole chord down 2 semitones (Bb minor 7).
`oct` shifts a single note by octaves:
```forth
c4 1 oct note 0.3 decay sine s .
c4 1 oct note 0.3 decay sine snd .
```
C5 (one octave up). Useful for bass lines:
```forth
0 2 4 5 7 5 4 2 8 cycle minor note
-2 oct 0.8 gain sine s .
-2 oct 0.8 gain sine snd .
```
## Scales
@@ -205,7 +205,7 @@ C5 (one octave up). Useful for bass lines:
Scale words convert a degree index into a MIDI note. By default the root is C4 (MIDI 60):
```forth
0 major note 0.5 decay sine s .
0 major note 0.5 decay sine snd .
```
Degree 0 of the major scale: C4. Degrees wrap with octave transposition — degree 7 gives C5 (72), degree -1 gives B3 (59).
@@ -213,13 +213,13 @@ Degree 0 of the major scale: C4. Degrees wrap with octave transposition — degr
Walk through a scale with `cycle`:
```forth
0 1 2 3 4 5 6 7 8 cycle minor note 0.5 decay sine s .
0 1 2 3 4 5 6 7 8 cycle minor note 0.5 decay sine snd .
```
Random notes from a scale:
```forth
0 7 rand pentatonic note 0.8 decay va s .
0 7 rand pentatonic note 0.8 decay saw snd .
```
### Setting the key
@@ -227,13 +227,13 @@ Random notes from a scale:
By default scales are rooted at C4. Use `key!` to change the tonal center:
```forth
g3 key! 0 major note 0.5 decay sine s .
g3 key! 0 major note 0.5 decay sine snd .
```
Now degree 0 is G3 (55) instead of C4. The key persists across steps until changed again:
```forth
a3 key! 0 3 5 7 3 cycle minor note 0.8 decay tri s .
a3 key! 0 3 5 7 3 cycle minor note 0.8 decay tri snd .
```
A minor melody starting from A3.
@@ -261,19 +261,19 @@ Jazz, symmetric, and modal variant scales are listed in the Reference section.
`triad` and `seventh` build chords from scale degrees. Instead of specifying a chord type, you get whatever chord the scale produces at that degree:
```forth
0 major triad note 1.5 decay sine s .
0 major triad note 1.5 decay sine snd .
```
Degree 0 of the major scale, stacked in thirds: C E G — a major triad. The scale determines the chord quality automatically. Degree 1 gives D F A (minor), degree 4 gives G B D (major):
```forth
4 major triad note 1.5 decay sine s .
4 major triad note 1.5 decay sine snd .
```
`seventh` adds a fourth note:
```forth
0 major seventh note 1.2 decay va s .
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.
@@ -281,7 +281,7 @@ C E G B — Cmaj7. Degree 1 gives Dm7, degree 4 gives G7 (dominant). The diatoni
Combine with `key!` to play diatonic chords in any key:
```forth
g3 key! 0 major triad note 1.5 decay sine s .
g3 key! 0 major triad note 1.5 decay sine snd .
```
G major triad rooted at G3.
@@ -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 s .
note 1.2 decay saw snd .
```
Combine with voicings for smoother voice leading:
@@ -299,13 +299,13 @@ 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 s .
note 1.5 decay saw snd .
```
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):
Arpeggiate diatonic chords using `at` + `cycle` (see the *Timing with at* tutorial):
```forth
0 major seventh arp note 0.5 decay sine s .
0 0.25 0.5 0.75 at sine snd [ 0 major seventh ] cycle note 0.5 decay .
```
## Frequency Conversion
@@ -313,7 +313,7 @@ Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for de
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
```forth
c4 mtof freq sine s .
c4 mtof freq sine snd .
```
Useful when a synth parameter expects Hz rather than MIDI.

View File

@@ -21,7 +21,7 @@ Press `Enter` to focus the editor. Write Forth code as you would in any step. Pr
```forth
;; a simple drone
saw s c2 note 0.3 gain 0.4 verb .
saw snd c2 note 0.3 gain 0.4 verb .
```
## Speed and Length

View File

@@ -17,15 +17,15 @@ sine sound
`rand` takes a range and returns a random value. If both bounds are integers, the result is an integer. If either is a float, you get a float:
```forth
60 72 rand note sine s .5 decay . ;; random MIDI note from 60 to 72
0.3 0.9 rand gain sine s .5 decay . ;; random gain between 0.3 and 0.9
60 72 rand note sine snd 0.5 decay . ;; random MIDI note from 60 to 72
0.3 0.9 rand gain sine snd 0.5 decay . ;; random gain between 0.3 and 0.9
```
`exprand` and `logrand` give you weighted distributions. `exprand` is biased toward the low end, `logrand` toward the high end:
```forth
200.0 8000.0 exprand freq sine s .5 decay . ;; mostly low frequencies
200.0 8000.0 logrand freq sine s .5 decay . ;; mostly high frequencies
200.0 8000.0 exprand freq sine snd 0.5 decay . ;; mostly low frequencies
200.0 8000.0 logrand freq sine snd 0.5 decay . ;; mostly high frequencies
```
These are useful for parameters where perception is logarithmic, like frequency and duration.
@@ -35,8 +35,8 @@ These are useful for parameters where perception is logarithmic, like frequency
The probability words take a quotation and execute it with some chance. `chance` takes a float from 0.0 to 1.0, `prob` takes a percentage from 0 to 100:
```forth
( hat s . ) 0.25 chance ;; 25% chance
( kick s . ) 75 prob ;; 75% chance
( hat snd . ) 0.25 chance ;; 25% chance
( kick snd . ) 75 prob ;; 75% chance
```
Named probability words save you from remembering numbers:
@@ -52,9 +52,9 @@ Named probability words save you from remembering numbers:
| `never` | 0% |
```forth
( hat s . ) often ;; 75%
( snare s . ) sometimes ;; 50%
( clap s . ) rarely ;; 25%
( hat snd . ) often ;; 75%
( snare snd . ) sometimes ;; 50%
( clap snd . ) rarely ;; 25%
```
`always` and `never` are useful when you want to temporarily mute or unmute a voice without deleting code. Change `sometimes` to `never` to silence it, `always` to bring it back.
@@ -62,8 +62,8 @@ Named probability words save you from remembering numbers:
Use `?` and `!?` with `coin` for quick coin-flip decisions:
```forth
( hat s . ) coin ? ;; execute if coin is 1
( rim s . ) coin !? ;; execute if coin is 0
( hat snd . ) coin ? ;; execute if coin is 1
( rim snd . ) coin !? ;; execute if coin is 0
```
## Selection
@@ -71,21 +71,21 @@ Use `?` and `!?` with `coin` for quick coin-flip decisions:
`choose` picks randomly from n items on the stack:
```forth
kick snare hat 3 choose s . ;; random drum hit
60 64 67 72 4 choose note sine s .5 decay . ;; random note from a set
kick snare hat 3 choose snd . ;; random drum hit
60 64 67 72 4 choose note sine snd 0.5 decay . ;; random note from a set
```
When a chosen item is a quotation, it gets executed:
```forth
( 0.1 decay ) ( 0.5 decay ) ( 0.9 decay ) 3 choose
sine s .
sine snd .
```
`wchoose` lets you assign weights to each option. Push value/weight pairs:
```forth
kick 0.5 snare 0.3 hat 0.2 3 wchoose s .
kick 0.5 snare 0.3 hat 0.2 3 wchoose snd .
```
Kick plays 50% of the time, snare 30%, hat 20%. Weights don't need to sum to 1 -- they're normalized automatically.
@@ -103,7 +103,7 @@ Combined with `note`, this gives you a random permutation of a chord every time
`every` runs a quotation once every n pattern iterations:
```forth
( crash s . ) 4 every ;; crash cymbal every 4th iteration
( crash snd . ) 4 every ;; crash cymbal every 4th iteration
```
`except` is the inverse -- it runs a quotation on all iterations *except* every nth:
@@ -115,22 +115,22 @@ Combined with `note`, this gives you a random permutation of a chord every time
`every+` and `except+` take an extra offset argument to shift the phase:
```forth
( snare s . ) 4 2 every+ ;; fires at iter 2, 6, 10, 14...
( snare s . ) 4 2 except+ ;; skips at iter 2, 6, 10, 14...
( snare snd . ) 4 2 every+ ;; fires at iter 2, 6, 10, 14...
( snare snd . ) 4 2 except+ ;; skips at iter 2, 6, 10, 14...
```
Without the offset, `every` fires at 0, 4, 8... The offset shifts that by 2, so it fires at 2, 6, 10... This lets you interleave patterns that share the same period:
```forth
( kick s . ) 4 every ;; kick at 0, 4, 8...
( snare s . ) 4 2 every+ ;; snare at 2, 6, 10...
( kick snd . ) 4 every ;; kick at 0, 4, 8...
( snare snd . ) 4 2 every+ ;; snare at 2, 6, 10...
```
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms:
```forth
( hat s . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
( hat s . ) 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
( hat snd . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
( hat snd . ) 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
```
`bjork` counts by step runs (how many times this particular step has played). `pbjork` counts by pattern iterations. Some classic patterns:
@@ -148,7 +148,7 @@ By default, every run produces different random values. Use `seed` to make rando
```forth
42 seed
60 72 rand note sine s . ;; always the same "random" note
60 72 rand note sine snd . ;; always the same "random" note
```
The seed is set at the start of the script. Same seed, same sequence. Useful when you want a specific random pattern to repeat.
@@ -158,7 +158,7 @@ The seed is set at the start of the script. Same seed, same sequence. Useful whe
The real power comes from mixing techniques. A hi-hat pattern with ghost notes:
```forth
hat s
hat snd
( 0.3 0.6 rand gain ) ( 0.8 gain ) 2 cycle
.
```
@@ -170,16 +170,16 @@ A bass line that changes every 4 bars:
```forth
( c2 note ) ( e2 note ) ( g2 note ) ( a2 note ) 4 pcycle
( 0.5 decay ) often
sine s .
sine snd .
```
Layered percussion with different densities:
```forth
( kick s . ) always
( snare s . ) 2 every
( hat s . ) 5 8 bjork
( rim s . ) rarely
( kick snd . ) always
( snare snd . ) 2 every
( hat snd . ) 5 8 bjork
( rim snd . ) rarely
```
A melodic step with weighted note selection and random timbre:
@@ -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 s .
add snd .
```
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.

View File

@@ -19,7 +19,7 @@ Play something -- a pattern, a live input, anything that makes sound. When you'r
The recording is now available as a sample:
```forth
drums s .
drums snd .
```
## Playback
@@ -27,10 +27,10 @@ drums s .
Recorded samples are ordinary samples. Everything you can do with a loaded sample works here:
```forth
drums s 0.5 speed . ;; half speed
drums s 0.25 begin 0.5 end . ;; slice the middle quarter
drums s 800 lpf 0.3 verb . ;; filter and reverb
drums s -1 speed . ;; reverse
drums snd 0.5 speed . ;; half speed
drums snd 0.25 begin 0.5 end . ;; slice the middle quarter
drums snd 800 lpf 0.3 verb . ;; filter and reverb
drums snd -1 speed . ;; reverse
```
## Overdub
@@ -70,7 +70,7 @@ Record a foundation, then overdub to build up:
"loop" dub
;; 4. play the result
loop s .
loop snd .
```
Each overdub pass adds to what's already there. The buffer wraps, so longer passes layer cyclically over the original length.
@@ -80,16 +80,16 @@ Each overdub pass adds to what's already there. The buffer wraps, so longer pass
Once you have a recording, carve it up:
```forth
loop s 0.0 begin 0.25 end . ;; first quarter
loop s 0.25 begin 0.5 end . ;; second quarter
loop s 0.5 begin 0.75 end . ;; third quarter
loop s 0.75 begin 1.0 end . ;; last quarter
loop snd 0.0 begin 0.25 end . ;; first quarter
loop snd 0.25 begin 0.5 end . ;; second quarter
loop snd 0.5 begin 0.75 end . ;; third quarter
loop snd 0.75 begin 1.0 end . ;; last quarter
```
Combine with randomness for variation:
```forth
loop s
loop snd
0.0 0.25 0.5 0.75 4 choose begin
0.5 speed
.

View File

@@ -11,9 +11,9 @@ Drop an `.sf2` file into one of your samples directories. The engine finds and l
Use `gm` as the sound source. The `n` parameter selects a program by name or number (0-127):
```forth
gm s piano n . ;; acoustic piano
gm s strings n c4 note . ;; strings playing middle C
gm s 0 n e4 note . ;; program 0 (piano) playing E4
gm snd piano n . ;; acoustic piano
gm snd strings n c4 note . ;; strings playing middle C
gm snd 0 n e4 note . ;; program 0 (piano) playing E4
```
## Drums
@@ -21,10 +21,10 @@ gm s 0 n e4 note . ;; program 0 (piano) playing E4
Drums live on a separate bank. Use `drums` or `percussion` as the `n` value. Each MIDI note triggers a different instrument:
```forth
gm s drums n 36 note . ;; kick
gm s drums n 38 note . ;; snare
gm s drums n 42 note . ;; closed hi-hat
gm s percussion n 49 note . ;; crash cymbal
gm snd drums n 36 note . ;; kick
gm snd drums n 38 note . ;; snare
gm snd drums n 42 note . ;; closed hi-hat
gm snd percussion n 49 note . ;; crash cymbal
```
## Envelope
@@ -32,8 +32,8 @@ gm s percussion n 49 note . ;; crash cymbal
The soundfont embeds ADSR envelope data per preset. The engine applies it automatically. Override any parameter explicitly:
```forth
gm s piano n 0.01 attack 0.3 decay .
gm s strings n 0.5 attack 2.0 release .
gm snd piano n 0.01 attack 0.3 decay .
gm snd strings n 0.5 attack 2.0 release .
```
If you set `attack`, `decay`, `sustain`, or `release`, your value wins. Unspecified parameters keep the soundfont default.
@@ -43,9 +43,9 @@ If you set `attack`, `decay`, `sustain`, or `release`, your value wins. Unspecif
All standard engine parameters work on GM voices. Filter, distort, spatialize:
```forth
gm s bass n 800 lpf 0.3 verb .
gm s epiano n 0.5 delay 1.5 distort .
gm s choir n 0.8 pan 2000 hpf .
gm snd bass n 800 lpf 0.3 verb .
gm snd epiano n 0.5 delay 1.5 distort .
gm snd choir n 0.8 pan 2000 hpf .
```
## Preset Names
@@ -79,22 +79,22 @@ A simple GM drum pattern across four steps:
```forth
;; step 1: kick
gm s drums n 36 note .
gm snd drums n 36 note .
;; step 2: closed hat
gm s drums n 42 note 0.6 gain .
gm snd drums n 42 note 0.6 gain .
;; step 3: snare
gm s drums n 38 note .
gm snd drums n 38 note .
;; step 4: closed hat
gm s drums n 42 note 0.6 gain .
gm snd drums n 42 note 0.6 gain .
```
Layer piano chords with randomness:
```forth
gm s piano n
gm snd piano n
c4 e4 g4 3 choose note
0.3 0.8 rand gain
0.1 0.4 rand verb
@@ -104,7 +104,7 @@ c4 e4 g4 3 choose note
A bass line with envelope override:
```forth
gm s bass n
gm snd bass n
c2 e2 g2 a2 4 cycle note
0.01 attack 0.2 decay 0.0 sustain
.

View File

@@ -17,13 +17,13 @@ Variables let you name values and share data between steps. They are global -- a
`,name` stores just like `!name` but keeps the value on the stack. Useful when you want to name something and keep using it:
```forth
440 ,freq sine s . ;; stores 440 in freq AND passes it to the pipeline
440 ,freq sine snd . ;; stores 440 in freq AND passes it to the pipeline
```
Without `,`, you'd need `dup`:
```forth
440 dup !freq sine s . ;; equivalent, but noisier
440 dup !freq sine snd . ;; equivalent, but noisier
```
## Sharing Between Steps
@@ -35,7 +35,7 @@ Variables are shared across all steps. One step can store a value that another r
c4 iter 7 mod + !root
;; step 4: read it
@root 7 + note sine s .
@root 7 + note sine snd .
```
Every time the pattern loops, step 0 picks a new root. Step 4 always harmonizes with it.
@@ -46,7 +46,7 @@ Fetch, modify, store back. A classic pattern for evolving values:
```forth
@n 1 + !n ;; increment n each time this step runs
@n 12 mod note sine s . ;; cycle through 12 notes
@n 12 mod note sine snd . ;; cycle through 12 notes
```
Reset on some condition:
@@ -69,7 +69,7 @@ Store a sound name in a variable, reuse it across steps:
"sine" !synth
;; step 1, 2, 3...
c4 note @synth s .
c4 note @synth snd .
```
Change one step, all steps follow.

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.19", 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

@@ -6,128 +6,22 @@ use std::time::Instant;
use arc_swap::ArcSwap;
use crossbeam_channel::Sender;
use egui_ratatui::RataguiBackend;
use nih_plug::prelude::*;
use nih_plug_egui::egui;
use nih_plug_egui::{create_egui_editor, EguiState};
use ratatui::Terminal;
use soft_ratatui::embedded_graphics_unicodefonts::{
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
mono_9x18_atlas, mono_9x18_bold_atlas,
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
use cagire::block_renderer::BlockCharBackend;
use cagire::app::App;
use cagire::engine::{AudioCommand, LinkState, SequencerSnapshot};
use cagire::input::{handle_key, handle_mouse, InputContext};
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
use cagire::model::{Dictionary, Rng, Variables};
use cagire::terminal::{create_terminal, FontChoice, TerminalType};
use cagire::theme;
use cagire::views;
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
use crate::params::CagireParams;
use crate::PluginBridge;
type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;
#[derive(Clone, Copy, PartialEq)]
enum FontChoice {
Size6x13,
Size7x13,
Size8x13,
Size9x15,
Size9x18,
Size10x20,
}
impl FontChoice {
fn from_setting(s: &str) -> Self {
match s {
"6x13" => Self::Size6x13,
"7x13" => Self::Size7x13,
"9x15" => Self::Size9x15,
"9x18" => Self::Size9x18,
"10x20" => Self::Size10x20,
_ => Self::Size8x13,
}
}
fn to_setting(self) -> &'static str {
match self {
Self::Size6x13 => "6x13",
Self::Size7x13 => "7x13",
Self::Size8x13 => "8x13",
Self::Size9x15 => "9x15",
Self::Size9x18 => "9x18",
Self::Size10x20 => "10x20",
}
}
fn label(self) -> &'static str {
match self {
Self::Size6x13 => "6x13 (Compact)",
Self::Size7x13 => "7x13",
Self::Size8x13 => "8x13 (Default)",
Self::Size9x15 => "9x15",
Self::Size9x18 => "9x18",
Self::Size10x20 => "10x20 (Large)",
}
}
const ALL: [Self; 6] = [
Self::Size6x13,
Self::Size7x13,
Self::Size8x13,
Self::Size9x15,
Self::Size9x18,
Self::Size10x20,
];
}
fn create_terminal(font: FontChoice) -> TerminalType {
let (regular, bold, italic) = match font {
FontChoice::Size6x13 => (
mono_6x13_atlas(),
Some(mono_6x13_bold_atlas()),
Some(mono_6x13_italic_atlas()),
),
FontChoice::Size7x13 => (
mono_7x13_atlas(),
Some(mono_7x13_bold_atlas()),
Some(mono_7x13_italic_atlas()),
),
FontChoice::Size8x13 => (
mono_8x13_atlas(),
Some(mono_8x13_bold_atlas()),
Some(mono_8x13_italic_atlas()),
),
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
};
let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
let soft = SoftBackend {
buffer: eg.buffer,
cursor: eg.cursor,
cursor_pos: eg.cursor_pos,
char_width: eg.char_width,
char_height: eg.char_height,
blink_counter: eg.blink_counter,
blinking_fast: eg.blinking_fast,
blinking_slow: eg.blinking_slow,
rgb_pixmap: eg.rgb_pixmap,
always_redraw_list: eg.always_redraw_list,
raster_backend: BlockCharBackend {
inner: eg.raster_backend,
},
};
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
}
struct EditorState {
app: App,
terminal: TerminalType,
@@ -234,6 +128,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.playing.load(std::sync::atomic::Ordering::Relaxed);
// Sync host tempo into LinkState so title bar shows real tempo
if shared.tempo > 0.0 {
@@ -298,6 +193,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

@@ -185,6 +185,7 @@ impl Plugin for CagirePlugin {
self.sample_rate,
self.output_channels,
64,
buffer_config.max_buffer_size as usize,
);
self.bridge
.sample_registry
@@ -219,7 +220,6 @@ impl Plugin for CagirePlugin {
source: s.source,
})
.collect(),
sync_mode: pat.sync_mode,
follow_up: pat.follow_up,
};
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
@@ -279,8 +279,6 @@ impl Plugin for CagirePlugin {
};
let lookahead_end = beat + lookahead_beats;
let engine_time = self.sample_pos as f64 / self.sample_rate as f64;
// Drain commands from the editor
let commands: Vec<SeqCommand> = self.bridge.cmd_rx.try_iter().collect();
@@ -294,7 +292,8 @@ impl Plugin for CagirePlugin {
fill: false,
nudge_secs: 0.0,
current_time_us: 0,
engine_time,
corrected_audio_pos: self.sample_pos as f64,
sr: self.sample_rate as f64,
mouse_x: 0.5,
mouse_y: 0.5,
mouse_down: 0.0,
@@ -310,12 +309,12 @@ impl Plugin for CagirePlugin {
// Drain audio commands from the editor (preview, hush, load samples, etc.)
for audio_cmd in self.bridge.audio_cmd_rx.try_iter() {
match audio_cmd {
AudioCommand::Evaluate { ref cmd, time } => {
let cmd_ref = match time {
AudioCommand::Evaluate { ref cmd, tick } => {
let cmd_ref = match tick {
Some(t) => {
self.cmd_buffer.clear();
use std::fmt::Write;
let _ = write!(&mut self.cmd_buffer, "{cmd}/time/{t:.6}");
let _ = write!(&mut self.cmd_buffer, "{cmd}/tick/{t}");
self.cmd_buffer.as_str()
}
None => cmd.as_str(),
@@ -419,11 +418,11 @@ impl Plugin for CagirePlugin {
}
continue;
}
let cmd_ref = match tsc.time {
let cmd_ref = match tsc.tick {
Some(t) => {
self.cmd_buffer.clear();
use std::fmt::Write;
let _ = write!(&mut self.cmd_buffer, "{}/time/{t:.6}", tsc.cmd);
let _ = write!(&mut self.cmd_buffer, "{}/tick/{t}", tsc.cmd);
self.cmd_buffer.as_str()
}
None => &tsc.cmd,

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.

Binary file not shown.

Binary file not shown.

View File

@@ -1,464 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export MACOSX_DEPLOYMENT_TARGET="12.0"
cd "$(git rev-parse --show-toplevel)"
PLUGIN_NAME="cagire-plugins"
LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores
OUT="releases"
PLATFORMS=(
"aarch64-apple-darwin"
"x86_64-apple-darwin"
"x86_64-unknown-linux-gnu"
"aarch64-unknown-linux-gnu"
"x86_64-pc-windows-gnu"
)
PLATFORM_LABELS=(
"macOS aarch64 (native)"
"macOS x86_64 (native)"
"Linux x86_64 (cross)"
"Linux aarch64 (cross)"
"Windows x86_64 (cross)"
)
PLATFORM_ALIASES=(
"macos-arm64"
"macos-x86_64"
"linux-x86_64"
"linux-aarch64"
"windows-x86_64"
)
# --- CLI argument parsing ---
cli_platforms=""
cli_targets=""
cli_yes=false
cli_all=false
while [[ $# -gt 0 ]]; do
case "$1" in
--platforms) cli_platforms="$2"; shift 2 ;;
--targets) cli_targets="$2"; shift 2 ;;
--yes) cli_yes=true; shift ;;
--all) cli_all=true; shift ;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --platforms <list> Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64"
echo " --targets <list> Comma-separated: cli,desktop,plugins"
echo " --all Build all platforms and targets"
echo " --yes Skip confirmation prompt"
echo ""
echo "Without options, runs interactively."
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
resolve_platform_alias() {
local alias="$1"
for i in "${!PLATFORM_ALIASES[@]}"; do
if [[ "${PLATFORM_ALIASES[$i]}" == "$alias" ]]; then
echo "$i"
return
fi
done
echo "Unknown platform: $alias" >&2
exit 1
}
# --- Helpers ---
prompt_platforms() {
echo "Select platform (0=all, comma-separated):"
echo " 0) All"
for i in "${!PLATFORMS[@]}"; do
echo " $((i+1))) ${PLATFORM_LABELS[$i]}"
done
read -rp "> " choice
if [[ "$choice" == "0" || -z "$choice" ]]; then
selected_platforms=("${PLATFORMS[@]}")
selected_labels=("${PLATFORM_LABELS[@]}")
else
IFS=',' read -ra indices <<< "$choice"
selected_platforms=()
selected_labels=()
for idx in "${indices[@]}"; do
idx="${idx// /}"
idx=$((idx - 1))
if (( idx < 0 || idx >= ${#PLATFORMS[@]} )); then
echo "Invalid platform index: $((idx+1))"
exit 1
fi
selected_platforms+=("${PLATFORMS[$idx]}")
selected_labels+=("${PLATFORM_LABELS[$idx]}")
done
fi
}
prompt_targets() {
echo ""
echo "Select targets (0=all, comma-separated):"
echo " 0) All"
echo " 1) cagire"
echo " 2) cagire-desktop"
echo " 3) cagire-plugins (CLAP/VST3)"
read -rp "> " choice
build_cagire=false
build_desktop=false
build_plugins=false
if [[ "$choice" == "0" || -z "$choice" ]]; then
build_cagire=true
build_desktop=true
build_plugins=true
else
IFS=',' read -ra targets <<< "$choice"
for t in "${targets[@]}"; do
t="${t// /}"
case "$t" in
1) build_cagire=true ;;
2) build_desktop=true ;;
3) build_plugins=true ;;
*) echo "Invalid target: $t"; exit 1 ;;
esac
done
fi
}
confirm_summary() {
echo ""
echo "=== Build Summary ==="
echo ""
echo "Platforms:"
for label in "${selected_labels[@]}"; do
echo " - $label"
done
echo ""
echo "Targets:"
$build_cagire && echo " - cagire"
$build_desktop && echo " - cagire-desktop"
$build_plugins && echo " - cagire-plugins (CLAP/VST3)"
echo ""
read -rp "Proceed? [Y/n] " yn
case "${yn,,}" in
n|no) echo "Aborted."; exit 0 ;;
esac
}
platform_os() {
case "$1" in
*windows*) echo "windows" ;;
*linux*) echo "linux" ;;
*apple*) echo "macos" ;;
esac
}
platform_arch() {
case "$1" in
aarch64*) echo "aarch64" ;;
x86_64*) echo "x86_64" ;;
esac
}
platform_suffix() {
case "$1" in
*windows*) echo ".exe" ;;
*) echo "" ;;
esac
}
is_cross_target() {
case "$1" in
*linux*|*windows*) return 0 ;;
*) return 1 ;;
esac
}
native_target() {
[[ "$1" == "aarch64-apple-darwin" ]]
}
release_dir() {
if native_target "$1"; then
echo "target/release"
else
echo "target/$1/release"
fi
}
target_flag() {
if native_target "$1"; then
echo ""
else
echo "--target $1"
fi
}
builder_for() {
if is_cross_target "$1"; then
echo "cross"
else
echo "cargo"
fi
}
build_binary() {
local platform="$1"
shift
local builder
builder=$(builder_for "$platform")
local tf
tf=$(target_flag "$platform")
# shellcheck disable=SC2086
$builder build --release $tf "$@"
}
bundle_plugins_native() {
local platform="$1"
local tf
tf=$(target_flag "$platform")
# shellcheck disable=SC2086
cargo xtask bundle "$PLUGIN_NAME" --release $tf
}
bundle_desktop_native() {
local platform="$1"
local tf
tf=$(target_flag "$platform")
# shellcheck disable=SC2086
cargo bundle --release --features desktop --bin cagire-desktop $tf
}
bundle_plugins_cross() {
local platform="$1"
local rd
rd=$(release_dir "$platform")
local os
os=$(platform_os "$platform")
local arch
arch=$(platform_arch "$platform")
# Build the cdylib with cross
# shellcheck disable=SC2046
build_binary "$platform" -p "$PLUGIN_NAME"
# Determine source library file
local src_lib
case "$os" in
linux) src_lib="$rd/lib${LIB_NAME}.so" ;;
windows) src_lib="$rd/${LIB_NAME}.dll" ;;
esac
if [[ ! -f "$src_lib" ]]; then
echo " ERROR: Expected library not found: $src_lib"
return 1
fi
# Assemble CLAP bundle (flat file)
local clap_out="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap"
cp "$src_lib" "$clap_out"
echo " CLAP -> $clap_out"
# Assemble VST3 bundle (directory tree)
local vst3_dir="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3"
local vst3_contents
case "$os" in
linux)
vst3_contents="$vst3_dir/Contents/${arch}-linux"
mkdir -p "$vst3_contents"
cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.so"
;;
windows)
vst3_contents="$vst3_dir/Contents/${arch}-win"
mkdir -p "$vst3_contents"
cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.vst3"
;;
esac
echo " VST3 -> $vst3_dir/"
}
copy_artifacts() {
local platform="$1"
local rd
rd=$(release_dir "$platform")
local os
os=$(platform_os "$platform")
local arch
arch=$(platform_arch "$platform")
local suffix
suffix=$(platform_suffix "$platform")
if $build_cagire; then
local src="$rd/cagire${suffix}"
local dst="$OUT/cagire-${os}-${arch}${suffix}"
cp "$src" "$dst"
echo " cagire -> $dst"
fi
if $build_desktop; then
local src="$rd/cagire-desktop${suffix}"
local dst="$OUT/cagire-desktop-${os}-${arch}${suffix}"
cp "$src" "$dst"
echo " cagire-desktop -> $dst"
# macOS .app bundle
if [[ "$os" == "macos" ]]; then
local app_src="$rd/bundle/osx/Cagire.app"
if [[ ! -d "$app_src" ]]; then
echo " ERROR: .app bundle not found at $app_src"
echo " Did 'cargo bundle' succeed?"
return 1
fi
local app_dst="$OUT/Cagire-${arch}.app"
rm -rf "$app_dst"
cp -R "$app_src" "$app_dst"
echo " Cagire.app -> $app_dst"
scripts/make-dmg.sh "$app_dst" "$OUT"
fi
fi
# MSI installer for Windows targets
if [[ "$os" == "windows" ]] && command -v cargo-wix &>/dev/null; then
echo " Building MSI installer..."
cargo wix --no-build --nocapture --package cagire -C -arch -C x64
cp target/wix/*.msi "$OUT/" 2>/dev/null && echo " MSI -> $OUT/" || true
fi
# AppImage for Linux targets
if [[ "$os" == "linux" ]]; then
if $build_cagire; then
scripts/make-appimage.sh "$rd/cagire" "$arch" "$OUT"
fi
if $build_desktop; then
scripts/make-appimage.sh "$rd/cagire-desktop" "$arch" "$OUT"
fi
fi
# Plugin artifacts for native targets (cross handled in bundle_plugins_cross)
if $build_plugins && ! is_cross_target "$platform"; then
local bundle_dir="target/bundled"
# CLAP
local clap_src="$bundle_dir/${PLUGIN_NAME}.clap"
if [[ -e "$clap_src" ]]; then
local clap_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap"
cp -r "$clap_src" "$clap_dst"
echo " CLAP -> $clap_dst"
fi
# VST3
local vst3_src="$bundle_dir/${PLUGIN_NAME}.vst3"
if [[ -d "$vst3_src" ]]; then
local vst3_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3"
rm -rf "$vst3_dst"
cp -r "$vst3_src" "$vst3_dst"
echo " VST3 -> $vst3_dst/"
fi
fi
}
# --- Main ---
if $cli_all; then
selected_platforms=("${PLATFORMS[@]}")
selected_labels=("${PLATFORM_LABELS[@]}")
build_cagire=true
build_desktop=true
build_plugins=true
elif [[ -n "$cli_platforms" || -n "$cli_targets" ]]; then
# Resolve platforms from CLI
if [[ -n "$cli_platforms" ]]; then
selected_platforms=()
selected_labels=()
IFS=',' read -ra aliases <<< "$cli_platforms"
for alias in "${aliases[@]}"; do
alias="${alias// /}"
idx=$(resolve_platform_alias "$alias")
selected_platforms+=("${PLATFORMS[$idx]}")
selected_labels+=("${PLATFORM_LABELS[$idx]}")
done
else
selected_platforms=("${PLATFORMS[@]}")
selected_labels=("${PLATFORM_LABELS[@]}")
fi
# Resolve targets from CLI
build_cagire=false
build_desktop=false
build_plugins=false
if [[ -n "$cli_targets" ]]; then
IFS=',' read -ra tgts <<< "$cli_targets"
for t in "${tgts[@]}"; do
t="${t// /}"
case "$t" in
cli) build_cagire=true ;;
desktop) build_desktop=true ;;
plugins) build_plugins=true ;;
*) echo "Unknown target: $t (expected: cli, desktop, plugins)"; exit 1 ;;
esac
done
else
build_cagire=true
build_desktop=true
build_plugins=true
fi
else
prompt_platforms
prompt_targets
fi
if ! $cli_yes && [[ -z "$cli_platforms" ]] && ! $cli_all; then
confirm_summary
fi
mkdir -p "$OUT"
step=0
total=${#selected_platforms[@]}
for platform in "${selected_platforms[@]}"; do
step=$((step + 1))
echo ""
echo "=== [$step/$total] $platform ==="
if $build_cagire; then
echo " -> cagire"
build_binary "$platform"
fi
if $build_desktop; then
echo " -> cagire-desktop"
build_binary "$platform" --features desktop --bin cagire-desktop
if ! is_cross_target "$platform"; then
echo " -> bundling cagire-desktop .app"
bundle_desktop_native "$platform"
fi
fi
if $build_plugins; then
echo " -> cagire-plugins"
if is_cross_target "$platform"; then
bundle_plugins_cross "$platform"
else
bundle_plugins_native "$platform"
fi
fi
echo " Copying artifacts..."
copy_artifacts "$platform"
done
echo ""
echo "=== Done ==="
echo ""
ls -lhR "$OUT/"

1028
scripts/build.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
FROM ghcr.io/cross-rs/x86_64-pc-windows-gnu:main
RUN apt-get update && \
apt-get install -y --no-install-recommends \
cmake \
clang \
libclang-dev \
mingw-w64-tools \
mingw-w64-x86-64-dev \
g++-mingw-w64-x86-64 \
&& rm -rf /var/lib/apt/lists/* \
&& ln -sf windows.h /usr/x86_64-w64-mingw32/include/Windows.h \
&& ln -sf winsock2.h /usr/x86_64-w64-mingw32/include/WinSock2.h \
&& ln -sf ws2tcpip.h /usr/x86_64-w64-mingw32/include/WS2tcpip.h \
&& GCCDIR=$(ls -d /usr/lib/gcc/x86_64-w64-mingw32/*-posix 2>/dev/null | head -1) \
&& ln -sf "$GCCDIR/libstdc++.a" /usr/x86_64-w64-mingw32/lib/libstdc++.a \
&& ln -sf "$GCCDIR/libgcc.a" /usr/x86_64-w64-mingw32/lib/libgcc.a \
&& rm -f /usr/x86_64-w64-mingw32/lib/libstdc++.dll.a \
&& rm -f /usr/lib/gcc/x86_64-w64-mingw32/*/libstdc++.dll.a

View File

@@ -1,141 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage: scripts/make-appimage.sh <binary-path> <arch> <output-dir>
# Produces an AppImage from a Linux binary.
# On native Linux with matching arch: uses linuxdeploy.
# Otherwise (cross-compilation): builds AppImage via mksquashfs in Docker.
if [[ $# -ne 3 ]]; then
echo "Usage: $0 <binary-path> <arch> <output-dir>"
exit 1
fi
BINARY="$1"
ARCH="$2"
OUTDIR="$3"
REPO_ROOT="$(git rev-parse --show-toplevel)"
CACHE_DIR="$REPO_ROOT/.cache"
APP_NAME="$(basename "$BINARY")"
RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-${ARCH}"
RUNTIME="$CACHE_DIR/runtime-${ARCH}"
build_appdir() {
local appdir="$1"
mkdir -p "$appdir/usr/bin"
cp "$BINARY" "$appdir/usr/bin/cagire"
chmod +x "$appdir/usr/bin/cagire"
mkdir -p "$appdir/usr/share/icons/hicolor/512x512/apps"
cp "$REPO_ROOT/assets/Cagire.png" "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png"
cp "$REPO_ROOT/assets/cagire.desktop" "$appdir/cagire.desktop"
# AppRun entry point
cat > "$appdir/AppRun" <<'APPRUN'
#!/bin/sh
SELF="$(readlink -f "$0")"
HERE="$(dirname "$SELF")"
exec "$HERE/usr/bin/cagire" "$@"
APPRUN
chmod +x "$appdir/AppRun"
# Symlink icon at root for AppImage spec
ln -sf usr/share/icons/hicolor/512x512/apps/cagire.png "$appdir/cagire.png"
ln -sf cagire.desktop "$appdir/.DirIcon" 2>/dev/null || true
}
download_runtime() {
mkdir -p "$CACHE_DIR"
if [[ ! -f "$RUNTIME" ]]; then
echo " Downloading AppImage runtime for $ARCH..."
curl -fSL "$RUNTIME_URL" -o "$RUNTIME"
fi
}
run_native() {
local linuxdeploy_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
local linuxdeploy="$CACHE_DIR/linuxdeploy-${ARCH}.AppImage"
mkdir -p "$CACHE_DIR"
if [[ ! -f "$linuxdeploy" ]]; then
echo " Downloading linuxdeploy for $ARCH..."
curl -fSL "$linuxdeploy_url" -o "$linuxdeploy"
chmod +x "$linuxdeploy"
fi
local appdir
appdir="$(mktemp -d)/AppDir"
build_appdir "$appdir"
export ARCH
export LDAI_RUNTIME_FILE="$RUNTIME"
"$linuxdeploy" \
--appimage-extract-and-run \
--appdir "$appdir" \
--desktop-file "$appdir/cagire.desktop" \
--icon-file "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png" \
--output appimage
local appimage
appimage=$(ls -1t ./*.AppImage 2>/dev/null | head -1 || true)
if [[ -z "$appimage" ]]; then
echo " ERROR: No AppImage produced"
exit 1
fi
mkdir -p "$OUTDIR"
mv "$appimage" "$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
echo " AppImage -> $OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
}
run_docker() {
local platform
case "$ARCH" in
x86_64) platform="linux/amd64" ;;
aarch64) platform="linux/arm64" ;;
*) echo "Unsupported arch: $ARCH"; exit 1 ;;
esac
local appdir
appdir="$(mktemp -d)/AppDir"
build_appdir "$appdir"
local image_tag="cagire-appimage-${ARCH}"
echo " Building Docker image $image_tag ($platform)..."
docker build --platform "$platform" -q -t "$image_tag" - <<'DOCKERFILE'
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
squashfs-tools \
&& rm -rf /var/lib/apt/lists/*
DOCKERFILE
echo " Creating squashfs via Docker ($image_tag)..."
docker run --rm --platform "$platform" \
-v "$appdir:/appdir:ro" \
-v "$CACHE_DIR:/cache" \
"$image_tag" \
mksquashfs /appdir /cache/appimage-${ARCH}.squashfs \
-root-owned -noappend -comp gzip -no-progress
mkdir -p "$OUTDIR"
local final="$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
cat "$RUNTIME" "$CACHE_DIR/appimage-${ARCH}.squashfs" > "$final"
chmod +x "$final"
rm -f "$CACHE_DIR/appimage-${ARCH}.squashfs"
echo " AppImage -> $final"
}
HOST_ARCH="$(uname -m)"
download_runtime
echo " Building AppImage for ${APP_NAME} ($ARCH)..."
if [[ "$HOST_ARCH" == "$ARCH" ]] && [[ "$(uname -s)" == "Linux" ]]; then
run_native
else
run_docker
fi

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage: scripts/make-dmg.sh <app-path> <output-dir>
# Produces a .dmg from a macOS .app bundle using only hdiutil.
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <app-path> <output-dir>"
exit 1
fi
APP_PATH="$1"
OUTDIR="$2"
REPO_ROOT="$(git rev-parse --show-toplevel)"
if [[ ! -d "$APP_PATH" ]]; then
echo "ERROR: $APP_PATH is not a directory"
exit 1
fi
LIPO_OUTPUT=$(lipo -info "$APP_PATH/Contents/MacOS/cagire-desktop" 2>/dev/null)
if [[ -z "$LIPO_OUTPUT" ]]; then
echo "ERROR: could not determine architecture from $APP_PATH"
exit 1
fi
if echo "$LIPO_OUTPUT" | grep -q "Architectures in the fat file"; then
ARCH="universal"
else
ARCH=$(echo "$LIPO_OUTPUT" | awk '{print $NF}')
case "$ARCH" in
arm64) ARCH="aarch64" ;;
esac
fi
STAGING="$(mktemp -d)"
trap 'rm -rf "$STAGING"' EXIT
cp -R "$APP_PATH" "$STAGING/Cagire.app"
ln -s /Applications "$STAGING/Applications"
cp "$REPO_ROOT/assets/DMG-README.txt" "$STAGING/README.txt"
DMG_NAME="Cagire-${ARCH}.dmg"
mkdir -p "$OUTDIR"
hdiutil create -volname "Cagire" \
-srcfolder "$STAGING" \
-ov -format UDZO \
"$OUTDIR/$DMG_NAME"
echo " DMG -> $OUTDIR/$DMG_NAME"

9
scripts/platforms.toml Normal file
View File

@@ -0,0 +1,9 @@
# Cagire build targets — each triple defines a compilation platform.
# Everything else (os, arch, cross, alias, label) is derived by build.py.
triples = [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]

View File

@@ -222,7 +222,7 @@ impl App {
}
}
pub fn import_bank(&mut self, bank: usize) {
pub fn import_shared(&mut self, bank: usize, pattern: usize) {
let text = match arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()) {
Some(t) => t,
None => {
@@ -230,36 +230,8 @@ impl App {
return;
}
};
match model::share::import_bank(&text) {
Ok(imported) => {
self.project_state.project.banks[bank] = imported;
for pattern in 0..model::MAX_PATTERNS {
self.project_state.mark_dirty(bank, pattern);
}
if self.editor_ctx.bank == bank {
self.load_step_to_editor();
}
self.ui
.flash("Bank imported", 150, FlashKind::Success);
}
Err(e) => {
self.ui
.flash(&format!("Import failed: {e}"), 200, FlashKind::Error);
}
}
}
pub fn import_pattern(&mut self, bank: usize, pattern: usize) {
let text = match arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()) {
Some(t) => t,
None => {
self.ui
.flash("Clipboard empty", 150, FlashKind::Error);
return;
}
};
match model::share::import(&text) {
Ok(imported) => {
match model::share::import_auto(&text) {
Ok(model::share::ImportResult::Pattern(imported)) => {
self.project_state.project.banks[bank].patterns[pattern] = imported;
self.project_state.mark_dirty(bank, pattern);
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
@@ -268,6 +240,17 @@ impl App {
self.ui
.flash("Pattern imported", 150, FlashKind::Success);
}
Ok(model::share::ImportResult::Bank(imported)) => {
self.project_state.project.banks[bank] = imported;
for p in 0..model::MAX_PATTERNS {
self.project_state.mark_dirty(bank, p);
}
if self.editor_ctx.bank == bank {
self.load_step_to_editor();
}
self.ui
.flash("Bank imported", 150, FlashKind::Success);
}
Err(e) => {
self.ui
.flash(&format!("Import failed: {e}"), 200, FlashKind::Error);

View File

@@ -111,9 +111,8 @@ impl App {
AppCommand::CopyPattern { bank, pattern } => self.copy_pattern(bank, pattern),
AppCommand::PastePattern { bank, pattern } => self.paste_pattern(bank, pattern),
AppCommand::SharePattern { bank, pattern } => self.share_pattern(bank, pattern),
AppCommand::ImportPattern { bank, pattern } => self.import_pattern(bank, pattern),
AppCommand::ImportShared { bank, pattern } => self.import_shared(bank, pattern),
AppCommand::ShareBank { bank } => self.share_bank(bank),
AppCommand::ImportBank { bank } => self.import_bank(bank),
AppCommand::CopyPatterns { bank, patterns } => self.copy_patterns(bank, &patterns),
AppCommand::PastePatterns { bank, start } => self.paste_patterns(bank, start),
AppCommand::CopyBank { bank } => self.copy_bank(bank),
@@ -199,7 +198,6 @@ impl App {
length,
speed,
quantization,
sync_mode,
follow_up,
} => {
self.playback.staged_prop_changes.insert(
@@ -210,7 +208,6 @@ impl App {
length,
speed,
quantization,
sync_mode,
follow_up,
},
);
@@ -339,12 +336,6 @@ impl App {
}
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
// Panel
AppCommand::ClosePanel => {
self.panel.visible = false;
self.panel.focus = crate::state::PanelFocus::Main;
}
// Direct navigation (mouse)
AppCommand::GoToStep(step) => {
let len = self.current_edit_pattern().length;
@@ -384,7 +375,11 @@ impl App {
AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode),
AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name),
AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name),
AppCommand::SetDeviceKind(kind) => self.audio.device_kind = kind,
AppCommand::SetDevicesFocus(focus) => self.audio.devices_focus = focus,
AppCommand::CycleHost { right } => {
self.audio.cycle_host(right);
self.audio.trigger_restart();
}
AppCommand::AdjustAudioSetting { setting, delta } => {
use crate::state::SettingKind;
match setting {
@@ -475,6 +470,13 @@ impl App {
AppCommand::SavePrelude => self.save_prelude(),
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
AppCommand::OpenBankPreludeEditor => self.open_bank_prelude_editor(),
AppCommand::SaveBankPrelude => self.save_bank_prelude(),
AppCommand::EvaluateBankPrelude => {
let bank = self.editor_ctx.bank;
self.evaluate_bank_prelude(bank, link);
}
AppCommand::CloseBankPreludeEditor => self.close_bank_prelude_editor(),
// Periodic script
AppCommand::OpenScriptModal(field) => self.open_script_modal(field),

View File

@@ -14,7 +14,7 @@ use arc_swap::ArcSwap;
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, LazyLock};
use cagire_ratatui::CompletionCandidate;
@@ -25,11 +25,11 @@ use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables
use crate::page::Page;
use crate::state::{
undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal,
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
ProjectState, ScriptEditorState, UiState,
OptionsState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
ProjectState, SampleBrowserState, ScriptEditorState, UiState,
};
static COMPLETION_CANDIDATES: LazyLock<Vec<CompletionCandidate>> = LazyLock::new(|| {
static COMPLETION_CANDIDATES: LazyLock<Arc<[CompletionCandidate]>> = LazyLock::new(|| {
model::WORDS
.iter()
.map(|w| CompletionCandidate {
@@ -66,9 +66,10 @@ pub struct App {
pub audio: AudioSettings,
pub options: OptionsState,
pub panel: PanelState,
pub sample_browser: Option<SampleBrowserState>,
pub midi: MidiState,
pub plugin_mode: bool,
pub dict_keys: HashSet<String>,
}
impl Default for App {
@@ -123,9 +124,10 @@ impl App {
AudioSettings::default()
},
options: OptionsState::default(),
panel: PanelState::default(),
sample_browser: None,
midi: MidiState::new(),
plugin_mode,
dict_keys: HashSet::new(),
}
}
@@ -201,7 +203,6 @@ impl App {
length: pat.length.to_string(),
speed: pat.speed,
quantization: pat.quantization,
sync_mode: pat.sync_mode,
follow_up: pat.follow_up,
};
}
@@ -213,6 +214,9 @@ impl App {
if self.ui.modal != Modal::None {
return;
}
if crate::model::onboarding::for_page(self.page).is_empty() {
return;
}
let name = self.page.name();
if self.ui.onboarding_dismissed.iter().any(|d| d == name) {
return;

View File

@@ -16,6 +16,7 @@ impl App {
pub fn save_settings(&self, link: &LinkState) {
let settings = Settings {
audio: crate::settings::AudioSettings {
host: self.audio.config.selected_host.clone(),
output_device: self.audio.config.output_device.clone(),
input_device: self.audio.config.input_device.clone(),
channels: self.audio.config.channels,
@@ -52,7 +53,7 @@ impl App {
output_devices: {
let outputs = crate::midi::list_midi_outputs();
self.midi
.selected_outputs
.selected_outputs()
.iter()
.map(|opt| {
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
@@ -138,7 +139,6 @@ impl App {
self.playback.queued_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern },
quantization: crate::model::LaunchQuantization::Immediate,
sync_mode: crate::model::SyncMode::PhaseLock,
});
}

View File

@@ -1,5 +1,7 @@
//! Forth script compilation, evaluation, and editor ↔ step synchronization.
use std::sync::Arc;
use crossbeam_channel::Sender;
use crate::engine::LinkState;
@@ -32,6 +34,7 @@ impl App {
speed,
fill: false,
nudge_secs: 0.0,
sr: 0.0,
cc_access: None,
speed_key: "",
mouse_x: 0.5,
@@ -55,7 +58,7 @@ impl App {
script.lines().map(String::from).collect()
};
self.editor_ctx.editor.set_content(lines);
self.editor_ctx.editor.set_candidates(COMPLETION_CANDIDATES.clone());
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
self.editor_ctx
.editor
.set_completion_enabled(self.ui.show_completion);
@@ -87,7 +90,7 @@ impl App {
prelude.lines().map(String::from).collect()
};
self.editor_ctx.editor.set_content(lines);
self.editor_ctx.editor.set_candidates(COMPLETION_CANDIDATES.clone());
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
self.editor_ctx
.editor
.set_completion_enabled(self.ui.show_completion);
@@ -107,24 +110,104 @@ impl App {
self.load_step_to_editor();
}
/// Evaluate the project prelude to seed variables and definitions.
pub fn evaluate_prelude(&mut self, link: &LinkState) {
let prelude = &self.project_state.project.prelude;
/// Switch the editor to the current bank's prelude script.
pub fn open_bank_prelude_editor(&mut self) {
let bank = self.editor_ctx.bank;
let prelude = &self.project_state.project.banks[bank].prelude;
let lines: Vec<String> = if prelude.is_empty() {
vec![String::new()]
} else {
prelude.lines().map(String::from).collect()
};
self.editor_ctx.editor.set_content(lines);
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
self.editor_ctx
.editor
.set_completion_enabled(self.ui.show_completion);
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
self.editor_ctx.target = EditorTarget::BankPrelude;
self.ui.modal = Modal::Editor;
}
pub fn save_bank_prelude(&mut self) {
let bank = self.editor_ctx.bank;
let text = self.editor_ctx.editor.content();
self.project_state.project.banks[bank].prelude = text;
}
pub fn close_bank_prelude_editor(&mut self) {
self.editor_ctx.target = EditorTarget::Step;
self.load_step_to_editor();
}
/// Evaluate a single bank's prelude.
pub fn evaluate_bank_prelude(&mut self, bank: usize, link: &LinkState) {
let prelude = &self.project_state.project.banks[bank].prelude;
if prelude.trim().is_empty() {
return;
}
let ctx = self.create_step_context(0, link);
match self.script_engine.evaluate(prelude, &ctx) {
Ok(_) => {
self.ui.flash("Prelude evaluated", 150, FlashKind::Info);
self.dict_keys = self.dict.lock().keys().cloned().collect();
}
Err(e) => {
self.ui
.flash(&format!("Prelude error: {e}"), 300, FlashKind::Error);
let fallback = format!("Bank {}", bank + 1);
let bank_name = self.project_state.project.banks[bank]
.name
.as_deref()
.unwrap_or(&fallback);
self.ui.flash(
&format!("{bank_name} prelude error: {e}"),
300,
FlashKind::Error,
);
}
}
}
/// Evaluate the project prelude and all bank preludes.
pub fn evaluate_prelude(&mut self, link: &LinkState) {
let project_prelude = &self.project_state.project.prelude;
if !project_prelude.trim().is_empty() {
let ctx = self.create_step_context(0, link);
match self.script_engine.evaluate(project_prelude, &ctx) {
Ok(_) => {}
Err(e) => {
self.ui
.flash(&format!("Project prelude error: {e}"), 300, FlashKind::Error);
return;
}
}
}
for bank_idx in 0..self.project_state.project.banks.len() {
let prelude = &self.project_state.project.banks[bank_idx].prelude;
if prelude.trim().is_empty() {
continue;
}
let ctx = self.create_step_context(0, link);
match self.script_engine.evaluate(prelude, &ctx) {
Ok(_) => {}
Err(e) => {
let bank_name = self.project_state.project.banks[bank_idx]
.name
.as_deref()
.map(String::from)
.unwrap_or_else(|| format!("Bank {}", bank_idx + 1));
self.ui.flash(
&format!("{bank_name} prelude error: {e}"),
300,
FlashKind::Error,
);
return;
}
}
}
self.dict_keys = self.dict.lock().keys().cloned().collect();
self.ui.flash("Preludes evaluated", 150, FlashKind::Info);
}
/// Evaluate a script and immediately send its audio commands.
/// Returns collected `print` output, if any.
pub fn execute_script_oneshot(
@@ -146,7 +229,7 @@ impl App {
}
let _ = audio_tx
.load()
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
.send(crate::engine::AudioCommand::Evaluate { cmd, tick: None });
}
Ok(if print_output.is_empty() {
None
@@ -190,7 +273,7 @@ impl App {
script.lines().map(String::from).collect()
};
self.script_editor.editor.set_content(lines);
self.script_editor.editor.set_candidates(COMPLETION_CANDIDATES.clone());
self.script_editor.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
self.script_editor
.editor
.set_completion_enabled(self.ui.show_completion);

View File

@@ -16,7 +16,6 @@ impl App {
bank,
pattern,
quantization: staged.quantization,
sync_mode: staged.sync_mode,
});
}
PatternChange::Stop { bank, pattern } => {
@@ -68,7 +67,6 @@ impl App {
source: s.source,
})
.collect(),
sync_mode: pat.sync_mode,
follow_up: pat.follow_up,
};
let _ = cmd_tx.send(SeqCommand::PatternUpdate {

View File

@@ -29,7 +29,6 @@ impl App {
self.playback.staged_changes.push(StagedChange {
change: PatternChange::Stop { bank, pattern },
quantization: pattern_data.quantization,
sync_mode: pattern_data.sync_mode,
});
self.ui
.set_status(format!("{} armed to stop", bp_label(bank, pattern)));
@@ -37,7 +36,6 @@ impl App {
self.playback.staged_changes.push(StagedChange {
change: PatternChange::Start { bank, pattern },
quantization: pattern_data.quantization,
sync_mode: pattern_data.sync_mode,
});
self.ui
.set_status(format!("{} armed to play", bp_label(bank, pattern)));
@@ -84,7 +82,6 @@ impl App {
}
pat.speed = props.speed;
pat.quantization = props.quantization;
pat.sync_mode = props.sync_mode;
pat.follow_up = props.follow_up;
self.project_state.mark_dirty(bank, pattern);
}

View File

@@ -28,7 +28,6 @@ impl App {
| AppCommand::DeleteSteps { bank, pattern, .. }
| AppCommand::ResetPattern { bank, pattern }
| AppCommand::PastePattern { bank, pattern }
| AppCommand::ImportPattern { bank, pattern }
| AppCommand::RenamePattern { bank, pattern, .. } => {
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
@@ -42,9 +41,14 @@ impl App {
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
}
AppCommand::ResetBank { bank }
| AppCommand::PasteBank { bank }
| AppCommand::ImportBank { bank } => {
AppCommand::SaveBankPrelude => {
let bank = self.editor_ctx.bank;
let data = self.project_state.project.banks[bank].clone();
Some(UndoScope::Bank { bank, data })
}
AppCommand::ImportShared { bank, .. }
| AppCommand::ResetBank { bank }
| AppCommand::PasteBank { bank } => {
let data = self.project_state.project.banks[*bank].clone();
Some(UndoScope::Bank { bank: *bank, data })
}

View File

@@ -1,33 +1,24 @@
#![windows_subsystem = "windows"]
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use cagire::block_renderer::BlockCharBackend;
use arc_swap::ArcSwap;
use clap::Parser;
use doux::EngineMetrics;
use eframe::NativeOptions;
use egui_ratatui::RataguiBackend;
use ratatui::Terminal;
use soft_ratatui::embedded_graphics_unicodefonts::{
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
mono_9x18_atlas, mono_9x18_bold_atlas,
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
use cagire::engine::{
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
build_stream, AnalysisHandle, AudioRef, AudioStreamConfig, LinkState, ScopeBuffer,
SequencerHandle, SpectrumBuffer,
};
use cagire::terminal::{create_terminal, FontChoice, TerminalType};
use cagire::init::{init, InitArgs};
use cagire::input::{handle_key, handle_mouse, InputContext, InputResult};
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
use cagire::settings::Settings;
use cagire::views;
use crossbeam_channel::Receiver;
#[derive(Parser)]
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
@@ -48,103 +39,6 @@ struct Args {
buffer: Option<u32>,
}
#[derive(Clone, Copy, PartialEq)]
enum FontChoice {
Size6x13,
Size7x13,
Size8x13,
Size9x15,
Size9x18,
Size10x20,
}
impl FontChoice {
fn from_setting(s: &str) -> Self {
match s {
"6x13" => Self::Size6x13,
"7x13" => Self::Size7x13,
"9x15" => Self::Size9x15,
"9x18" => Self::Size9x18,
"10x20" => Self::Size10x20,
_ => Self::Size8x13,
}
}
fn to_setting(self) -> &'static str {
match self {
Self::Size6x13 => "6x13",
Self::Size7x13 => "7x13",
Self::Size8x13 => "8x13",
Self::Size9x15 => "9x15",
Self::Size9x18 => "9x18",
Self::Size10x20 => "10x20",
}
}
fn label(self) -> &'static str {
match self {
Self::Size6x13 => "6x13 (Compact)",
Self::Size7x13 => "7x13",
Self::Size8x13 => "8x13 (Default)",
Self::Size9x15 => "9x15",
Self::Size9x18 => "9x18",
Self::Size10x20 => "10x20 (Large)",
}
}
const ALL: [Self; 6] = [
Self::Size6x13,
Self::Size7x13,
Self::Size8x13,
Self::Size9x15,
Self::Size9x18,
Self::Size10x20,
];
}
type TerminalType = Terminal<RataguiBackend<BlockCharBackend>>;
fn create_terminal(font: FontChoice) -> TerminalType {
let (regular, bold, italic) = match font {
FontChoice::Size6x13 => (
mono_6x13_atlas(),
Some(mono_6x13_bold_atlas()),
Some(mono_6x13_italic_atlas()),
),
FontChoice::Size7x13 => (
mono_7x13_atlas(),
Some(mono_7x13_bold_atlas()),
Some(mono_7x13_italic_atlas()),
),
FontChoice::Size8x13 => (
mono_8x13_atlas(),
Some(mono_8x13_bold_atlas()),
Some(mono_8x13_italic_atlas()),
),
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
};
let eg = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
let soft = SoftBackend {
buffer: eg.buffer,
cursor: eg.cursor,
cursor_pos: eg.cursor_pos,
char_width: eg.char_width,
char_height: eg.char_height,
blink_counter: eg.blink_counter,
blinking_fast: eg.blinking_fast,
blinking_slow: eg.blinking_slow,
rgb_pixmap: eg.rgb_pixmap,
always_redraw_list: eg.always_redraw_list,
raster_backend: BlockCharBackend {
inner: eg.raster_backend,
},
};
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
}
struct CagireDesktop {
app: cagire::app::App,
terminal: TerminalType,
@@ -155,12 +49,11 @@ struct CagireDesktop {
metrics: Arc<EngineMetrics>,
scope_buffer: Arc<ScopeBuffer>,
spectrum_buffer: Arc<SpectrumBuffer>,
audio_sample_pos: Arc<AtomicU64>,
audio_ref: Arc<ArcSwap<AudioRef>>,
sample_rate_shared: Arc<AtomicU32>,
_stream: Option<cpal::Stream>,
_input_stream: Option<cpal::Stream>,
_analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>,
device_lost: Arc<AtomicBool>,
stream_error_rx: crossbeam_channel::Receiver<String>,
current_font: FontChoice,
@@ -202,12 +95,11 @@ impl CagireDesktop {
metrics: b.metrics,
scope_buffer: b.scope_buffer,
spectrum_buffer: b.spectrum_buffer,
audio_sample_pos: b.audio_sample_pos,
audio_ref: b.audio_ref,
sample_rate_shared: b.sample_rate_shared,
_stream: b.stream,
_input_stream: b.input_stream,
_analysis_handle: b.analysis_handle,
midi_rx: b.midi_rx,
device_lost: b.device_lost,
stream_error_rx: b.stream_error_rx,
current_font,
@@ -237,9 +129,9 @@ impl CagireDesktop {
return;
};
let new_audio_rx = sequencer.swap_audio_channel();
self.midi_rx = sequencer.swap_midi_channel();
let new_config = AudioStreamConfig {
host: self.app.audio.config.selected_host.clone(),
output_device: self.app.audio.config.output_device.clone(),
input_device: self.app.audio.config.input_device.clone(),
channels: self.app.audio.config.channels,
@@ -262,7 +154,11 @@ impl CagireDesktop {
}
}
self.audio_sample_pos.store(0, Ordering::Release);
self.audio_ref.store(Arc::new(AudioRef {
sample_pos: 0,
timestamp: std::time::Instant::now(),
sample_rate: 44100.0,
}));
let preload_entries: Vec<(String, std::path::PathBuf)> = restart_samples
.iter()
@@ -276,7 +172,7 @@ impl CagireDesktop {
Arc::clone(&self.spectrum_buffer),
Arc::clone(&self.metrics),
restart_samples,
Arc::clone(&self.audio_sample_pos),
Arc::clone(&self.audio_ref),
new_error_tx,
&self.app.audio.config.sample_paths,
Arc::clone(&self.device_lost),
@@ -288,6 +184,7 @@ impl CagireDesktop {
self.app.audio.config.sample_rate = info.sample_rate;
self.app.audio.config.host_name = info.host_name;
self.app.audio.config.channels = info.channels;
self.app.audio.config.input_sample_rate = info.input_sample_rate;
self.sample_rate_shared
.store(info.sample_rate as u32, Ordering::Relaxed);
self.app.audio.error = None;
@@ -339,30 +236,23 @@ impl CagireDesktop {
let term = self.terminal.get_frame().area();
let widget_rect = ctx.content_rect();
for mouse in convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse) {
let mut input_ctx = InputContext {
app: &mut self.app,
link: &self.link,
snapshot: &seq_snapshot,
playing: &self.playing,
audio_tx: &sequencer.audio_tx,
seq_cmd_tx: &sequencer.cmd_tx,
nudge_us: &self.nudge_us,
};
let mouse_events = convert_egui_mouse(ctx, widget_rect, term, &mut self.egui_mouse);
let key_events = convert_egui_events(ctx);
let mut input_ctx = InputContext {
app: &mut self.app,
link: &self.link,
snapshot: &seq_snapshot,
playing: &self.playing,
audio_tx: &sequencer.audio_tx,
seq_cmd_tx: &sequencer.cmd_tx,
nudge_us: &self.nudge_us,
};
for mouse in mouse_events {
handle_mouse(&mut input_ctx, mouse, term);
}
for key in convert_egui_events(ctx) {
let mut input_ctx = InputContext {
app: &mut self.app,
link: &self.link,
snapshot: &seq_snapshot,
playing: &self.playing,
audio_tx: &sequencer.audio_tx,
seq_cmd_tx: &sequencer.cmd_tx,
nudge_us: &self.nudge_us,
};
for key in key_events {
if let InputResult::Quit = handle_key(&mut input_ctx, key) {
return true;
}
@@ -414,59 +304,6 @@ impl eframe::App for CagireDesktop {
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
self.app.flush_dirty_script(&sequencer.cmd_tx);
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
match midi_cmd {
MidiCommand::NoteOn {
device,
channel,
note,
velocity,
} => {
self.app.midi.send_note_on(device, channel, note, velocity);
}
MidiCommand::NoteOff {
device,
channel,
note,
} => {
self.app.midi.send_note_off(device, channel, note);
}
MidiCommand::CC {
device,
channel,
cc,
value,
} => {
self.app.midi.send_cc(device, channel, cc, value);
}
MidiCommand::PitchBend {
device,
channel,
value,
} => {
self.app.midi.send_pitch_bend(device, channel, value);
}
MidiCommand::Pressure {
device,
channel,
value,
} => {
self.app.midi.send_pressure(device, channel, value);
}
MidiCommand::ProgramChange {
device,
channel,
program,
} => {
self.app.midi.send_program_change(device, channel, program);
}
MidiCommand::Clock { device } => self.app.midi.send_realtime(device, 0xF8),
MidiCommand::Start { device } => self.app.midi.send_realtime(device, 0xFA),
MidiCommand::Stop { device } => self.app.midi.send_realtime(device, 0xFC),
MidiCommand::Continue { device } => self.app.midi.send_realtime(device, 0xFB),
}
}
let should_quit = self.handle_input(ctx);
if should_quit {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
@@ -496,6 +333,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
@@ -616,7 +458,6 @@ fn load_icon() -> egui::IconData {
}
fn main() -> eframe::Result<()> {
#[cfg(unix)]
cagire::engine::realtime::lock_memory();
let args = Args::parse();

View File

@@ -242,7 +242,7 @@ fn color_to_rgb(color: &Color, is_fg: bool) -> [u8; 3] {
Color::Yellow => [255, 215, 0],
Color::Blue => [0, 0, 139],
Color::Magenta => [255, 0, 255],
Color::Cyan => [0, 0, 255],
Color::Cyan => [0, 255, 255],
Color::Gray => [128, 128, 128],
Color::DarkGray => [64, 64, 64],
Color::LightRed => [255, 0, 0],

View File

@@ -2,9 +2,9 @@
use std::path::PathBuf;
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
use crate::page::Page;
use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
use crate::state::{ColorScheme, DevicesFocus, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
pub enum AppCommand {
// Undo/Redo
@@ -75,16 +75,13 @@ pub enum AppCommand {
bank: usize,
pattern: usize,
},
ImportPattern {
ImportShared {
bank: usize,
pattern: usize,
},
ShareBank {
bank: usize,
},
ImportBank {
bank: usize,
},
CopyPatterns {
bank: usize,
patterns: Vec<usize>,
@@ -169,7 +166,6 @@ pub enum AppCommand {
length: Option<usize>,
speed: PatternSpeed,
quantization: LaunchQuantization,
sync_mode: SyncMode,
follow_up: FollowUp,
},
@@ -232,9 +228,6 @@ pub enum AppCommand {
// Live keys
ToggleLiveKeysFill,
// Panel
ClosePanel,
// Direct navigation (mouse)
GoToStep(usize),
PatternsSelectBank(usize),
@@ -259,7 +252,8 @@ pub enum AppCommand {
AudioSettingPrev,
SetOutputDevice(String),
SetInputDevice(String),
SetDeviceKind(DeviceKind),
SetDevicesFocus(DevicesFocus),
CycleHost { right: bool },
AdjustAudioSetting {
setting: SettingKind,
delta: i32,
@@ -304,6 +298,10 @@ pub enum AppCommand {
SavePrelude,
EvaluatePrelude,
ClosePreludeEditor,
OpenBankPreludeEditor,
SaveBankPrelude,
EvaluateBankPrelude,
CloseBankPreludeEditor,
// Onboarding
DismissOnboarding,

View File

@@ -1,13 +1,22 @@
//! Audio output stream (cpal) and FFT spectrum analysis.
use arc_swap::ArcSwap;
use ringbuf::{traits::*, HeapRb};
use rustfft::{num_complex::Complex, FftPlanner};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Instant;
#[cfg(feature = "cli")]
use std::sync::atomic::AtomicU64;
/// Timestamped audio position reference for jitter-free tick interpolation.
/// Published by the audio callback after each `process_block`, read by the
/// sequencer to compute the correct sample position at any instant.
#[derive(Clone)]
pub struct AudioRef {
pub sample_pos: u64,
pub timestamp: Instant,
pub sample_rate: f64,
}
pub struct ScopeBuffer {
pub samples: [AtomicU32; 256],
@@ -259,21 +268,20 @@ pub fn preload_sample_heads(
#[cfg(feature = "cli")]
use cpal::traits::{DeviceTrait, StreamTrait};
#[cfg(feature = "cli")]
use cpal::FromSample;
#[cfg(feature = "cli")]
use cpal::Stream;
#[cfg(feature = "cli")]
use crossbeam_channel::{Receiver, Sender};
#[cfg(feature = "cli")]
use doux::{Engine, EngineMetrics};
#[cfg(feature = "cli")]
use std::collections::VecDeque;
#[cfg(feature = "cli")]
use std::sync::Mutex;
#[cfg(feature = "cli")]
use super::AudioCommand;
#[cfg(feature = "cli")]
pub struct AudioStreamConfig {
pub host: Option<String>,
pub output_device: Option<String>,
pub input_device: Option<String>,
pub channels: u16,
@@ -286,6 +294,7 @@ pub struct AudioStreamInfo {
pub sample_rate: f32,
pub host_name: String,
pub channels: u16,
pub input_sample_rate: Option<f32>,
}
#[cfg(feature = "cli")]
@@ -306,15 +315,21 @@ pub fn build_stream(
spectrum_buffer: Arc<SpectrumBuffer>,
metrics: Arc<EngineMetrics>,
initial_samples: Vec<doux::sampling::SampleEntry>,
audio_sample_pos: Arc<AtomicU64>,
audio_ref: Arc<ArcSwap<AudioRef>>,
error_tx: Sender<String>,
sample_paths: &[std::path::PathBuf],
device_lost: Arc<AtomicBool>,
) -> Result<BuildStreamResult, String> {
let selection = match &config.host {
Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()),
None => doux::audio::HostSelection::Auto,
};
let host = doux::audio::get_host(selection).map_err(|e| format!("{e}"))?;
let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name)
Some(name) => doux::audio::find_output_device_for(&host, name)
.ok_or_else(|| format!("Device not found: {name}"))?,
None => doux::audio::default_output_device().ok_or("No default output device")?,
None => doux::audio::default_output_device_for(&host).ok_or("No default output device")?,
};
let default_config = device.default_output_config().map_err(|e| e.to_string())?;
@@ -323,10 +338,10 @@ pub fn build_stream(
let max_channels = doux::audio::max_output_channels(&device);
let channels = config.channels.min(max_channels).max(2);
let host_name = doux::audio::preferred_host().id().name().to_string();
let is_jack = host_name.to_lowercase().contains("jack");
let host_name = host.id().name().to_string();
let host_managed_buffer = doux::audio::host_controls_buffer_size(&host);
let buffer_size = if config.buffer_size > 0 && !is_jack {
let buffer_size = if config.buffer_size > 0 && !host_managed_buffer {
cpal::BufferSize::Fixed(config.buffer_size)
} else {
cpal::BufferSize::Default
@@ -343,8 +358,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 {
@@ -359,25 +375,33 @@ pub fn build_stream(
let registry = Arc::clone(&engine.sample_registry);
const INPUT_BUFFER_SIZE: usize = 8192;
let input_buffer: Arc<Mutex<VecDeque<f32>>> =
Arc::new(Mutex::new(VecDeque::with_capacity(INPUT_BUFFER_SIZE)));
let input_device = config
.input_device
.as_ref()
.and_then(|name| {
let dev = doux::audio::find_input_device(name);
let dev = doux::audio::find_input_device_for(&host, name);
if dev.is_none() {
eprintln!("input device not found: {name}");
}
dev
});
let input_channels: usize = input_device
let input_config = input_device
.as_ref()
.and_then(|dev| dev.default_input_config().ok());
let input_channels: usize = input_config
.as_ref()
.and_then(|dev| dev.default_input_config().ok())
.map_or(0, |cfg| cfg.channels() as usize);
let input_sample_rate = input_config.and_then(|cfg| {
let rate = cfg.sample_rate() as f32;
(rate != sample_rate).then_some(rate)
});
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() {
@@ -399,28 +423,46 @@ pub fn build_stream(
input_cfg.channels(),
input_cfg.sample_rate()
);
let buf = Arc::clone(&input_buffer);
let stream = dev
.build_input_stream(
&input_cfg.into(),
move |data: &[f32], _| {
let mut b = buf.lock().unwrap();
b.extend(data.iter().copied());
let excess = b.len().saturating_sub(INPUT_BUFFER_SIZE);
if excess > 0 {
drop(b.drain(..excess));
}
},
{
let device_lost = Arc::clone(&device_lost);
move |err| {
eprintln!("input stream error: {err}");
device_lost.store(true, Ordering::Release);
}
},
None,
)
.ok()?;
let input_format = input_cfg.sample_format();
let mut input_producer = input_producer;
macro_rules! build_input {
($T:ty) => {{
let mut scratch: Vec<f32> = Vec::new();
dev.build_input_stream(
&input_cfg.into(),
move |data: &[$T], _| {
scratch.resize(data.len(), 0.0);
for (dst, &src) in scratch.iter_mut().zip(data.iter()) {
*dst = <f32 as FromSample<$T>>::from_sample_(src);
}
input_producer.push_slice(&scratch);
},
{
let device_lost = Arc::clone(&device_lost);
move |err: cpal::StreamError| {
eprintln!("input stream error: {err}");
match err {
cpal::StreamError::DeviceNotAvailable
| cpal::StreamError::StreamInvalidated => {
device_lost.store(true, Ordering::Release);
}
_ => {}
}
}
},
None,
)
}};
}
let stream = match input_format {
cpal::SampleFormat::F32 => build_input!(f32),
cpal::SampleFormat::I32 => build_input!(i32),
cpal::SampleFormat::I16 => build_input!(i16),
_ => return None,
}
.ok()?;
stream.play().ok()?;
Some(stream)
});
@@ -430,111 +472,111 @@ pub fn build_stream(
let mut cmd_buffer = String::with_capacity(256);
let mut rt_set = false;
let mut live_scratch = vec![0.0f32; 4096];
let input_buf_clone = Arc::clone(&input_buffer);
let mut input_consumer = input_consumer;
let mut current_pos: u64 = 0;
let output_format = default_config.sample_format();
let stream = device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _| {
if !rt_set {
super::realtime::set_realtime_priority();
rt_set = true;
}
macro_rules! build_output {
($T:ty) => {{
let mut conv_buf: Vec<f32> = Vec::new();
device.build_output_stream(
&stream_config,
move |data: &mut [$T], _| {
conv_buf.resize(data.len(), 0.0f32);
let buffer_samples = data.len() / channels;
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
if !rt_set {
let ok = super::realtime::set_realtime_priority();
rt_set = true;
if !ok {
super::realtime::warn_no_rt("audio");
}
}
audio_sample_pos.fetch_add(buffer_samples as u64, Ordering::Release);
let buffer_samples = conv_buf.len() / channels;
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
while let Ok(cmd) = audio_rx.try_recv() {
match cmd {
AudioCommand::Evaluate { cmd, time } => {
let cmd_ref = match time {
Some(t) => {
cmd_buffer.clear();
use std::fmt::Write;
let _ = write!(&mut cmd_buffer, "{cmd}/time/{t:.6}");
cmd_buffer.as_str()
while let Ok(cmd) = audio_rx.try_recv() {
match cmd {
AudioCommand::Evaluate { cmd, tick } => {
let cmd_ref = match tick {
Some(t) => {
cmd_buffer.clear();
use std::fmt::Write;
let _ = write!(&mut cmd_buffer, "{cmd}/tick/{t}");
cmd_buffer.as_str()
}
None => &cmd,
};
engine.evaluate(cmd_ref);
}
AudioCommand::Hush => {
engine.hush();
}
AudioCommand::Panic => {
engine.panic();
}
AudioCommand::LoadSamples(samples) => {
engine.sample_index.extend(samples);
}
AudioCommand::LoadSoundfont(path) => {
if let Err(e) = engine.load_soundfont(&path) {
eprintln!("Failed to load soundfont: {e}");
}
None => &cmd,
};
engine.evaluate(cmd_ref);
}
AudioCommand::Hush => {
engine.hush();
}
AudioCommand::Panic => {
engine.panic();
}
AudioCommand::LoadSamples(samples) => {
engine.sample_index.extend(samples);
}
AudioCommand::LoadSoundfont(path) => {
if let Err(e) = engine.load_soundfont(&path) {
eprintln!("Failed to load soundfont: {e}");
}
}
}
}
// 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);
}
let mut buf = input_buf_clone.lock().unwrap();
match input_channels {
0 => {
live_scratch[..stereo_len].fill(0.0);
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);
}
1 => {
for i in 0..buffer_samples {
let s = buf.pop_front().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 = buf.pop_front().unwrap_or(0.0);
}
}
_ => {
for i in 0..buffer_samples {
let l = buf.pop_front().unwrap_or(0.0);
let r = buf.pop_front().unwrap_or(0.0);
for _ in 2..input_channels {
buf.pop_front();
}
live_scratch[i * 2] = l;
live_scratch[i * 2 + 1] = r;
}
}
}
// Discard excess if input produced more than we consumed
let excess = buf.len().saturating_sub(INPUT_BUFFER_SIZE / 2);
if excess > 0 {
drop(buf.drain(..excess));
}
drop(buf);
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]);
scope_buffer.write(data);
engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(&mut conv_buf, &[], &live_scratch[..raw_len]);
// Feed mono mix to analysis thread via ring buffer (non-blocking)
for chunk in data.chunks(channels) {
let mono = chunk.iter().sum::<f32>() / channels as f32;
let _ = fft_producer.try_push(mono);
}
},
move |err| {
let _ = error_tx.try_send(format!("stream error: {err}"));
device_lost.store(true, Ordering::Release);
},
None,
)
.map_err(|e| format!("Failed to build stream: {e}"))?;
current_pos += buffer_samples as u64;
audio_ref.store(Arc::new(AudioRef {
sample_pos: current_pos,
timestamp: Instant::now(),
sample_rate: sr as f64,
}));
scope_buffer.write(&conv_buf);
for chunk in conv_buf.chunks(channels) {
let mono = chunk.iter().sum::<f32>() / channels as f32;
let _ = fft_producer.try_push(mono);
}
for (out, &src) in data.iter_mut().zip(conv_buf.iter()) {
*out = <$T as FromSample<f32>>::from_sample_(src);
}
},
move |err: cpal::StreamError| {
let _ = error_tx.try_send(format!("stream error: {err}"));
match err {
cpal::StreamError::DeviceNotAvailable
| cpal::StreamError::StreamInvalidated => {
device_lost.store(true, Ordering::Release);
}
_ => {}
}
},
None,
)
}};
}
let stream = match output_format {
cpal::SampleFormat::F32 => build_output!(f32),
cpal::SampleFormat::I32 => build_output!(i32),
cpal::SampleFormat::I16 => build_output!(i16),
format => return Err(format!("unsupported output sample format: {format:?}")),
}
.map_err(|e| format!("Failed to build stream: {e}"))?;
stream
.play()
@@ -543,6 +585,7 @@ pub fn build_stream(
sample_rate,
host_name,
channels: effective_channels,
input_sample_rate,
};
Ok((stream, input_stream, info, analysis_handle, registry))
}

View File

@@ -1,14 +1,13 @@
use arc_swap::ArcSwap;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
use crossbeam_channel::{Receiver, RecvTimeoutError};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::sync::Arc;
use std::time::Duration;
use super::link::LinkState;
use super::realtime::{precise_sleep_us, set_realtime_priority};
use super::realtime::{precise_sleep_us, set_realtime_priority, warn_no_rt};
use super::sequencer::MidiCommand;
use super::timing::SyncTime;
use crate::midi::{MidiOutputPorts, MAX_MIDI_OUTPUTS};
/// A MIDI command scheduled for dispatch at a specific time.
#[derive(Clone)]
@@ -46,19 +45,17 @@ impl Eq for TimedMidiCommand {}
const SPIN_THRESHOLD_US: SyncTime = 100;
/// Dispatcher loop — handles MIDI timing only.
/// Dispatcher loop — handles MIDI timing and sends directly to MIDI ports.
/// Audio commands bypass the dispatcher entirely and go straight to doux's
/// sample-accurate scheduler via the audio thread channel.
pub fn dispatcher_loop(
cmd_rx: Receiver<TimedMidiCommand>,
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
link: Arc<LinkState>,
ports: MidiOutputPorts,
link: &LinkState,
) {
let has_rt = set_realtime_priority();
#[cfg(target_os = "linux")]
if !has_rt {
eprintln!("[cagire] Warning: Could not set realtime priority for dispatcher thread.");
warn_no_rt("dispatcher");
}
let mut queue: BinaryHeap<TimedMidiCommand> = BinaryHeap::with_capacity(256);
@@ -86,8 +83,8 @@ pub fn dispatcher_loop(
while let Some(cmd) = queue.peek() {
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
let cmd = queue.pop().expect("pop after peek");
wait_until_dispatch(cmd.target_time_us, &link, has_rt);
dispatch_midi(cmd.command, &midi_tx);
wait_until_dispatch(cmd.target_time_us, link, has_rt);
dispatch_midi(cmd.command, &ports);
} else {
break;
}
@@ -108,15 +105,15 @@ fn wait_until_dispatch(target_us: SyncTime, link: &LinkState, has_rt: bool) {
}
}
fn dispatch_midi(cmd: MidiDispatch, midi_tx: &Arc<ArcSwap<Sender<MidiCommand>>>) {
fn dispatch_midi(cmd: MidiDispatch, ports: &MidiOutputPorts) {
match cmd {
MidiDispatch::Send(midi_cmd) => {
let _ = midi_tx.load().try_send(midi_cmd);
ports.send_command(&midi_cmd);
}
MidiDispatch::FlushAll => {
for dev in 0..4u8 {
for dev in 0..MAX_MIDI_OUTPUTS as u8 {
for chan in 0..16u8 {
let _ = midi_tx.load().try_send(MidiCommand::CC {
ports.send_command(&MidiCommand::CC {
device: dev,
channel: chan,
cc: 123,

View File

@@ -81,6 +81,20 @@ impl LinkState {
self.link.commit_app_session_state(&state);
}
pub fn start_playing(&self, beat: f64, time: i64, quantum: f64) {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
state.set_is_playing_and_request_beat_at_time(true, time, beat, quantum);
self.link.commit_app_session_state(&state);
}
pub fn stop_playing(&self, time: i64) {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);
state.set_is_playing(false, time);
self.link.commit_app_session_state(&state);
}
pub fn capture_app_state(&self) -> SessionState {
let mut state = SessionState::new();
self.link.capture_app_session_state(&mut state);

View File

@@ -5,9 +5,9 @@ pub mod realtime;
pub mod sequencer;
mod timing;
pub use timing::{substeps_in_window, StepTiming, SyncTime};
pub use timing::{next_boundary, substeps_in_window, SyncTime};
pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer};
pub use audio::{preload_sample_heads, AnalysisHandle, AudioRef, ScopeBuffer, SpectrumBuffer};
// Re-exported for the plugin crate (not used by the terminal binary).
#[allow(unused_imports)]
@@ -21,13 +21,13 @@ pub use audio::AudioStreamInfo;
pub use link::LinkState;
pub use sequencer::{
spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand,
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand,
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
};
// Re-exported for the plugin crate (not used by the terminal binary).
#[allow(unused_imports)]
pub use sequencer::{
parse_midi_command, SequencerState, SharedSequencerState, TickInput, TickOutput,
parse_midi_command, MidiCommand, SequencerState, SharedSequencerState, TickInput, TickOutput,
TimestampedCommand,
};

View File

@@ -148,6 +148,17 @@ pub fn set_realtime_priority() -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn warn_no_rt(thread_name: &str) {
eprintln!(
"[cagire] Warning: No realtime priority for {thread_name} thread. \
Add user to 'audio' group and configure rtprio limits."
);
}
#[cfg(not(target_os = "linux"))]
pub fn warn_no_rt(_thread_name: &str) {}
/// High-precision sleep using clock_nanosleep on Linux.
/// Uses monotonic clock for jitter-free sleeping.
#[cfg(target_os = "linux")]

Some files were not shown because too many files have changed in this diff Show More