Compare commits
202 Commits
v0.1.4
...
5310b98542
| Author | SHA1 | Date | |
|---|---|---|---|
| 5310b98542 | |||
| 7099501130 | |||
| 730ddfb716 | |||
| 0a186f774c | |||
| 25a5c77344 | |||
| 3dba8213d6 | |||
| 299689e206 | |||
| a50059cf19 | |||
| 6cdb4d9d2c | |||
| 5b3252cc31 | |||
| b728b38d6e | |||
| 8af17c01d8 | |||
| c2eeebcfb7 | |||
| 7622e3d14c | |||
| e956346ae9 | |||
| 6892575a53 | |||
| 27b826ebaf | |||
| 0f0f13f2b8 | |||
| 6b94d6403a | |||
| f0de312d6b | |||
| 7632bc76f7 | |||
| 78b20b5ff9 | |||
| 4a8396670f | |||
| 77b7fa1f9e | |||
| 979b7639ac | |||
| 2a2b3c5651 | |||
| f6c7438886 | |||
| 057ba5b2f3 | |||
| 40e69b66da | |||
| 1ce5b8597a | |||
| 789dbb186b | |||
| 8ba98e8f3b | |||
| 003ee0518e | |||
| 52406c7374 | |||
| 0b78f15ef1 | |||
| 302f40c4ac | |||
| 79a4c3b6e2 | |||
| 12b90bc99b | |||
| a1190af494 | |||
| f85a20d9a7 | |||
| baa2aba381 | |||
| 75a8fd4401 | |||
| ac0ddc7fb9 | |||
| 07e95d5b6f | |||
| 00d6eb2f1f | |||
| 12752e0167 | |||
| 3b41a06d5e | |||
| f258358c8f | |||
| 2d8abe4af9 | |||
| 37f5f74ec1 | |||
| 58624b64cf | |||
| 5385bf675a | |||
| 211e71f5a9 | |||
| 23c7abb145 | |||
| 670ae0b6b6 | |||
| 10ca567ac5 | |||
| b2871ac251 | |||
| 8ba89f91a0 | |||
| 7d670dacb9 | |||
| 1de8c068f6 | |||
| d792f011ee | |||
| 897f1a776e | |||
| 869d3af244 | |||
| a5f17687f1 | |||
| 5b851751e5 | |||
| bc5d12e53a | |||
| d6bbae173b | |||
| 1f339f1503 | |||
| 8ffe2c22c7 | |||
| 20c32ce0d8 | |||
| a326d58d30 | |||
| c72733bac8 | |||
| 5758b18d58 | |||
| 52cc890a67 | |||
| 0f9d750069 | |||
| 66ee2e28ff | |||
| 6ec3a86568 | |||
| 51f52be4ce | |||
| 2c98a915fa | |||
| e42476dd4d | |||
| 3e364a6622 | |||
| 1248f74b25 | |||
| fc2ab0757b | |||
| 10ed5a629a | |||
| 88c2b51720 | |||
| 5cda1a8f95 | |||
| 200832f230 | |||
| 91bc9011b2 | |||
| de56598fca | |||
| abafea8ddf | |||
| e6f776bdf4 | |||
| d40d713649 | |||
| 767575b25d | |||
| 82b0668bcf | |||
| 6cf9d2eec1 | |||
| 2097997372 | |||
| 5579708f69 | |||
| 1b01491e87 | |||
| 5581ba1881 | |||
| 8983b3f21c | |||
| 4a7ae83019 | |||
| 61a6d7aad0 | |||
| 1b01e3b805 | |||
| 2a57cc415b | |||
| 7c76bdb8d6 | |||
| 1facc72a67 | |||
| 726ea16e92 | |||
| 154cac6547 | |||
| 3380e454df | |||
| 660f48216a | |||
| fb1f73ebd6 | |||
| cd223592a7 | |||
| af81c94207 | |||
| b53e4a76ab | |||
| 8c31ed4196 | |||
| 8024c18bb0 | |||
| 194030d953 | |||
| e4799c1f42 | |||
| 636129688d | |||
| a2ee0e5a50 | |||
| 96ed74c6fe | |||
| a67d982fcd | |||
| c9ab7a4f0b | |||
| 772d21a8ed | |||
| 4396147a8b | |||
| c396c39b6b | |||
| f6b43cb021 | |||
| 60d1d7ca74 | |||
| 9864cc6d61 | |||
| 985ab687d7 | |||
| 9b925d881e | |||
| 71146c7cea | |||
| 6b95f31afd | |||
| adee8d0d57 | |||
| f9c284effd | |||
| 57fd51be3e | |||
| ce70251057 | |||
| b47c789612 | |||
| dd853b8e1b | |||
| a0585b0814 | |||
| 2100b82dad | |||
| 15a4300db5 | |||
| fed39c01e8 | |||
| 0a4f1419eb | |||
| 793c83e18c | |||
| 20bc0ffcb4 | |||
| 8e09fd106e | |||
| 73ca0ff096 | |||
| 425f1c8627 | |||
| 730332cfb0 | |||
| 1d70a83759 | |||
| 0299012725 | |||
| 08029ec604 | |||
| 4f9b1f39f9 | |||
| 4772b02f77 | |||
| 4049c7787c | |||
| 4c635500dd | |||
| d0e37e13e6 | |||
| 7658cf9d51 | |||
| 584dbb6aad | |||
| 2731eea037 | |||
| 22ee5f97e6 | |||
| 5fb059ea20 | |||
| 705d93702b | |||
| 77a6aa9eb7 | |||
| d25b1317fc | |||
| 2851785e0d | |||
| a72772c8cc | |||
| 4d22bd5d2b | |||
| 495bfb3bdc | |||
| 73db616139 | |||
| 8efafffaff | |||
| 48f5920fed | |||
| d106711708 | |||
| 2be15d11f4 | |||
| 5952807240 | |||
| 0beed16c31 | |||
| c6860105a6 | |||
| f4eafdf5b2 | |||
| 935df84920 | |||
| a3a39ea28e | |||
| 574625735b | |||
| 40c509e295 | |||
| 61daa9d79d | |||
| 9e597258e4 | |||
| 223679acf8 | |||
| 2235a4b0a1 | |||
| 2453b78237 | |||
| fcb6adb6af | |||
| ce98acacd0 | |||
| d2d6ef5b06 | |||
| 6efcabd32d | |||
| 250e359fc5 | |||
| cf5994e604 | |||
| e1aff189cd | |||
| b3c56bc56c | |||
| 3bb19cbda8 | |||
| 42ad77d9ae | |||
| e853e67492 | |||
| f7e6f96cbf | |||
| 8af64fc4e2 | |||
| 183dd5b516 |
@@ -1,5 +1,5 @@
|
||||
[env]
|
||||
MACOSX_DEPLOYMENT_TARGET = "12.0"
|
||||
# Uncomment to use local doux for development
|
||||
paths = ["/Users/bubo/doux"]
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --release --"
|
||||
|
||||
@@ -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="${GITEA_REF_NAME#v}"
|
||||
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||
cp -R Cagire.app pkg-root/Applications/
|
||||
cp cagire pkg-root/usr/local/bin/
|
||||
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
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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/
|
||||
@@ -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/
|
||||
@@ -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/
|
||||
@@ -1,57 +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
|
||||
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so target/bundled/cagire-plugins.clap
|
||||
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/
|
||||
@@ -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/
|
||||
@@ -1,17 +0,0 @@
|
||||
name: Build Plugins
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.gitea/workflows/build-plugins-linux.yml
|
||||
|
||||
macos:
|
||||
uses: ./.gitea/workflows/build-plugins-macos.yml
|
||||
|
||||
windows:
|
||||
uses: ./.gitea/workflows/build-plugins-windows.yml
|
||||
|
||||
rpi:
|
||||
uses: ./.gitea/workflows/build-plugins-rpi.yml
|
||||
@@ -1,134 +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 --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop,asio --bin cagire-desktop --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Test
|
||||
if: inputs.run-tests
|
||||
run: cargo test --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Clippy
|
||||
if: inputs.run-clippy
|
||||
run: cargo clippy --features asio --target x86_64-pc-windows-msvc -- -D warnings
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install NSIS
|
||||
if: inputs.build-packages
|
||||
run: choco install nsis
|
||||
|
||||
- name: Build NSIS installer
|
||||
if: inputs.build-packages
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"(.+)"' | Select-Object -First 1).Matches.Groups[1].Value
|
||||
$root = (Get-Location).Path
|
||||
$target = "x86_64-pc-windows-msvc"
|
||||
& "C:\Program Files (x86)\NSIS\makensis.exe" `
|
||||
"-DVERSION=$version" `
|
||||
"-DCLI_EXE=$root\target\$target\release\cagire.exe" `
|
||||
"-DDESKTOP_EXE=$root\target\$target\release\cagire-desktop.exe" `
|
||||
"-DICON=$root\assets\Cagire.ico" `
|
||||
"-DOUTDIR=$root\target" `
|
||||
nsis/cagire.nsi
|
||||
|
||||
- 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 installer artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-installer
|
||||
path: target/cagire-*-setup.exe
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
if: inputs.build-packages
|
||||
shell: bash
|
||||
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/
|
||||
@@ -1,23 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.gitea/workflows/build-linux.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
macos:
|
||||
uses: ./.gitea/workflows/build-macos.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
windows:
|
||||
uses: ./.gitea/workflows/build-windows.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Deploy Website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'website/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
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
|
||||
|
||||
- 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/cagire-website-data/*
|
||||
cp -r website/dist/* /home/debian/my-services/cagire-website-data/
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.gitea/workflows/build-linux.yml
|
||||
with:
|
||||
build-packages: true
|
||||
|
||||
macos:
|
||||
uses: ./.gitea/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:
|
||||
uses: ./.gitea/workflows/build-windows.yml
|
||||
with:
|
||||
build-packages: true
|
||||
|
||||
cross:
|
||||
uses: ./.gitea/workflows/build-cross.yml
|
||||
|
||||
assemble-macos:
|
||||
needs: macos
|
||||
uses: ./.gitea/workflows/assemble-macos.yml
|
||||
|
||||
release:
|
||||
needs: [linux, macos, windows, cross, assemble-macos]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
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" == *-installer ]]; then
|
||||
cp "$dir"/*-setup.exe 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 Gitea release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
TAG="${GITEA_REF_NAME:-manual-$(date +%Y%m%d-%H%M%S)}"
|
||||
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||
|
||||
RELEASE_ID=$(curl -s -X POST "$API_URL" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"draft\": true}" \
|
||||
| jq -r '.id')
|
||||
|
||||
for file in release/*; do
|
||||
filename=$(basename "$file")
|
||||
curl -s -X POST "$API_URL/$RELEASE_ID/assets?name=$filename" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$file"
|
||||
done
|
||||
|
||||
echo "Release $TAG created as draft with $(ls release | wc -l) assets"
|
||||
75
.github/workflows/ci.yml
vendored
Normal file
75
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
|
||||
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 (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
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
|
||||
|
||||
- name: Install dependencies (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: brew list cmake &>/dev/null || brew install cmake
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
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 ${{ matrix.target }}
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||
|
||||
- name: Test
|
||||
run: cargo test --target ${{ matrix.target }}
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --target ${{ matrix.target }} -- -D warnings
|
||||
59
.github/workflows/pages.yml
vendored
Normal file
59
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
||||
408
.github/workflows/release.yml
vendored
Normal file
408
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,408 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.server_url == 'https://github.com'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact: cagire-linux-x86_64
|
||||
- os: macos-15-intel
|
||||
target: x86_64-apple-darwin
|
||||
artifact: cagire-macos-x86_64
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
artifact: cagire-macos-aarch64
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact: cagire-windows-x86_64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
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 }}
|
||||
|
||||
- name: Install cargo-binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
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
|
||||
cargo binstall -y cargo-bundle
|
||||
|
||||
- name: Install dependencies (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew list cmake &>/dev/null || brew install cmake
|
||||
cargo binstall -y cargo-bundle
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
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 ${{ matrix.target }}
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||
|
||||
- name: Bundle desktop app
|
||||
if: runner.os != 'Windows'
|
||||
run: cargo bundle --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||
|
||||
- name: Build AppImages (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
mkdir -p target/releases
|
||||
scripts/make-appimage.sh target/${{ matrix.target }}/release/cagire x86_64 target/releases
|
||||
scripts/make-appimage.sh target/${{ matrix.target }}/release/cagire-desktop x86_64 target/releases
|
||||
|
||||
- name: Upload AppImage artifacts (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-appimage
|
||||
path: target/releases/*.AppImage
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Zip macOS app bundle
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release/bundle/osx
|
||||
zip -r Cagire.app.zip Cagire.app
|
||||
|
||||
- name: Upload artifact (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target }}/release/cagire
|
||||
|
||||
- name: Upload artifact (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target }}/release/cagire.exe
|
||||
|
||||
- name: Upload desktop artifact (Linux deb)
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||
|
||||
- name: Upload desktop artifact (macOS app bundle)
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
|
||||
|
||||
- name: Upload desktop artifact (Windows exe)
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
||||
|
||||
- name: Install cargo-wix (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: cargo install cargo-wix
|
||||
|
||||
- name: Build MSI installer (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: cargo wix --no-build --nocapture -C -p -C x64
|
||||
|
||||
- name: Upload MSI installer (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-msi
|
||||
path: target/wix/*.msi
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-clap
|
||||
path: target/bundled/cagire-plugins.clap
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-vst3
|
||||
path: target/bundled/cagire-plugins.vst3
|
||||
|
||||
build-cross:
|
||||
if: github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
artifact: cagire-linux-aarch64
|
||||
|
||||
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 }}
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build
|
||||
run: cross build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Build desktop
|
||||
run: cross build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target }}/release/cagire
|
||||
|
||||
- name: Upload desktop artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/cagire-desktop
|
||||
|
||||
universal-macos:
|
||||
if: github.server_url == 'https://github.com'
|
||||
needs: build
|
||||
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: Upload universal CLAP plugin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-clap
|
||||
path: cagire-plugins.clap
|
||||
|
||||
- name: Upload universal VST3 plugin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-vst3
|
||||
path: cagire-plugins.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
|
||||
|
||||
release:
|
||||
needs: [build, build-cross, universal-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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
/.cache
|
||||
*.prof
|
||||
.DS_Store
|
||||
releases/
|
||||
|
||||
# Local cargo overrides (doux path patch)
|
||||
.cargo/config.local.toml
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://git.raphaelforment.fr/BuboBubo/cagire
|
||||
git clone --recursive https://github.com/Bubobubobubobubo/cagire
|
||||
cd cagire
|
||||
cargo build --release
|
||||
```
|
||||
@@ -163,7 +163,7 @@ scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop
|
||||
scripts/build-all.sh --all --yes
|
||||
```
|
||||
|
||||
Builds selected targets, producing binaries in `releases/`.
|
||||
Builds selected targets, producing binaries in `target/releases/`.
|
||||
|
||||
Platform aliases: `macos-arm64`, `macos-x86_64`, `linux-x86_64`, `linux-aarch64`, `windows-x86_64`.
|
||||
Target aliases: `cli`, `desktop`, `plugins`.
|
||||
@@ -175,7 +175,7 @@ Linux releases ship as AppImages — self-contained executables that bundle all
|
||||
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/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 target/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.
|
||||
|
||||
244
CHANGELOG.md
244
CHANGELOG.md
@@ -2,222 +2,110 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.4]
|
||||
|
||||
### Breaking
|
||||
- **Doux v0.0.12**: removed Mutable Instruments Plaits modes (`modal`, `va`, `analog`, `waveshape`, `grain`, `chord`, `swarm`, `pnoise`, etc.). Native percussion models retained; new models added: `tom`, `cowbell`, `cymbal`.
|
||||
- Simplified effects/filter API: removed per-filter envelope parameters in favor of the universal `env` word.
|
||||
- Recording commands simplified: removed `/sound/` path segment from `rec`, `overdub`, `orec`, `odub`.
|
||||
|
||||
### Forth Language
|
||||
- New modulation transition words: `islide` (swell), `oslide` (pluck), `pslide` (stair/stepped).
|
||||
- New `lpg` word (Low Pass Gate): pairs amplitude envelope with lowpass filter modulation.
|
||||
- New `inchan` word: select audio input channel by index.
|
||||
- New EQ frequency words: `eqlofreq`, `eqmidfreq`, `eqhifreq`.
|
||||
|
||||
### UI / UX
|
||||
- Redesigned top bar: consolidated transport, tempo, bar:beat display with visual beat segments.
|
||||
- CPU meter with color-coded fill bar (green/yellow/red).
|
||||
|
||||
### Engine
|
||||
- Audio input channel selection support.
|
||||
- Audio buffer sizing improved for multi-channel input.
|
||||
|
||||
### Packaging
|
||||
- CI migrated from GitHub Actions to Gitea Actions.
|
||||
- Removed WIX installer; Windows now distributed via zip and NSIS only.
|
||||
- Gitea Actions workflow for automatic website deployment.
|
||||
- Added LICENSE file.
|
||||
|
||||
### Documentation
|
||||
- Extensive documentation updates reflecting doux v0.0.12 API changes across sources, filters, modulation, wavetable, and audio modulation docs.
|
||||
|
||||
## [0.1.3]
|
||||
|
||||
### Forth Language
|
||||
- New `stretch` word: pitch-independent time stretching via phase vocoder (e.g., `kick sound 2 stretch .` plays at half speed, same pitch).
|
||||
- Automatic default release time on sounds when none is explicitly set.
|
||||
|
||||
### Engine
|
||||
- Sample-accurate timing: delta computation switched from float seconds to integer sample ticks, fixing precision issues.
|
||||
- Lock-free audio input buffer: replaced `Arc<Mutex<VecDeque>>` with `HeapRb` ring buffer.
|
||||
- Theme access optimized: `Rc<ThemeColors>` replaces deep cloning on every `get()`.
|
||||
- Dictionary keys cached in `App` to avoid repeated lock acquisitions during rendering.
|
||||
|
||||
### Fixed
|
||||
- Realtime priority diagnostics: dedicated `warn_no_rt()` on Linux, lookahead widened from 20ms to 40ms when RT priority unavailable.
|
||||
- Float epsilon precision in delta/nudge zero-comparisons.
|
||||
- Windows build fixes for standalone and plugin targets.
|
||||
|
||||
### Documentation
|
||||
- Time stretching usage guide added to `docs/engine/samples.md`.
|
||||
|
||||
## [0.1.2]
|
||||
|
||||
### Forth Language
|
||||
- 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 0–1 float range (was 0–127 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
|
||||
- `map` word: apply a quotation to each stack element (`1 2 3 ( 10 * ) map => 10 20 30`).
|
||||
- `loop` fix: now operates in steps instead of beats, uses `step_duration()` for correct timing.
|
||||
|
||||
### Fixed
|
||||
- Crash on missing sample directories: sample path scanning now validates directories exist before scanning.
|
||||
- Audio channel minimum enforced to 2, preventing crash on devices reporting fewer channels.
|
||||
- Audio device disconnect: automatic stream restart when device is lost (terminal and desktop).
|
||||
- Live keys (e.g. `f` for fill) no longer trigger while searching in dictionary or help views.
|
||||
- Side panel always uses horizontal layout (removed broken vertical fallback for narrow terminals).
|
||||
|
||||
### Changed
|
||||
- Runtime highlight enabled by default.
|
||||
|
||||
### Packaging
|
||||
- Modular CI: split monolithic release workflow into per-platform builds (Linux, macOS, Windows, cross-compilation).
|
||||
- Separate CI workflows for CLAP/VST plugin builds (Linux, macOS, Windows, Raspberry Pi).
|
||||
- Windows MSI installer workflow fixes.
|
||||
- Website download matrix updated.
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
### Breaking
|
||||
- **Quotation syntax changed from `{ }` to `( )`** — all deferred code blocks now use parentheses.
|
||||
|
||||
### Forth Language
|
||||
|
||||
**Syntax:**
|
||||
- `[ v1 v2 v3 ]` bracket lists with implicit count.
|
||||
- `( ... )` quotation syntax (replaces `{ }`).
|
||||
- `,varname` assignment syntax (SetKeep): assign without consuming.
|
||||
- `case/of/endof/endcase` control flow.
|
||||
- `print` — debug word, outputs top-of-stack as text.
|
||||
- Arithmetic and unary ops now lift over ArpList and CycleList element-wise.
|
||||
**Bracket syntax `[ ... ]`**
|
||||
- `[ v1 v2 v3 ]` pushes all items plus their count. Sugar for `v1 v2 v3 3`.
|
||||
|
||||
**New words:**
|
||||
- `index` — select item at explicit index (wraps with modulo).
|
||||
- `slice` / `pick` — sample slicing: divide a sample into N equal parts and select which slice to play.
|
||||
- `wave` / `waveform` — set drum synthesis waveform (0=sine, 0.5=triangle, 1=saw).
|
||||
- `pbounce` — ping-pong cycle keyed by pattern iteration.
|
||||
- `except` — inverse of `every`.
|
||||
- `every+` / `except+` — phase-offset variants.
|
||||
- `bjork` / `pbjork` — euclidean rhythm gates using quotations.
|
||||
- `arp` — arpeggio list type (spreads notes across time).
|
||||
- `all` / `noall` — apply params globally to all emitted sounds.
|
||||
- `pbounce` — ping-pong cycle keyed by pattern iteration (vs `bounce` which is step-keyed).
|
||||
- `except` — inverse of `every`: run quotation on all iterations except every nth.
|
||||
- `every+` / `except+` — `every`/`except` with a phase offset.
|
||||
- `all` / `noall` — apply current params globally to all emitted sounds; clear global params.
|
||||
- `linmap` / `expmap` — linear and exponential range mapping.
|
||||
- `rec` / `overdub` (`dub`) — record/overdub master audio to a named sample.
|
||||
- `orec` / `odub` — record/overdub a single orbit.
|
||||
- `rec` / `overdub` (`dub`) — toggle recording/overdubbing master audio to a named sample.
|
||||
- `orec` / `odub` — toggle recording/overdubbing a single orbit to a named sample.
|
||||
|
||||
**Harmony and voicing:**
|
||||
- `key!` — set tonal center.
|
||||
- `triad` / `seventh` — diatonic chord from scale degree.
|
||||
**Harmony and voicing words:**
|
||||
- `key!` — set tonal center for scale operations.
|
||||
- `triad` / `seventh` — diatonic triad/seventh from scale degree (follows a scale word).
|
||||
- `inv` / `dinv` — chord inversion / down inversion.
|
||||
- `drop2` / `drop3` — drop voicings.
|
||||
- `drop2` / `drop3` — drop-2 / drop-3 voicings.
|
||||
- `tp` — transpose all ints on stack by N semitones.
|
||||
|
||||
**New chord types:**
|
||||
- `pwr`, `augmaj7`, `7sus4`, `9sus4`, `maj69`, `min69`, `maj11`, `maj13`, `min13`, `dom7s11`.
|
||||
|
||||
**Effect parameters:**
|
||||
- Ducking compressor: `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
|
||||
- Smear effect: `smear`, `smearfreq`, `smearfb`.
|
||||
- Reverb: `verbtype`, `verbchorus`, `verbchorusfreq`, `verbprelow`, `verbprehigh`, `verblowcut`, `verbhighcut`, `verblowgain`.
|
||||
|
||||
**Behavior changes:**
|
||||
- All parameter words now accept varargs (100+ words updated to consume the full stack).
|
||||
- `every` reworked to accept quotations.
|
||||
- Removed `chain` word (replaced by pattern-level Follow Up setting).
|
||||
**Ducking compressor params:**
|
||||
- `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
|
||||
|
||||
### Engine
|
||||
- SF2 soundfont support: auto-scans sample directories for `.sf2` files.
|
||||
- Follow-up actions: patterns have configurable follow-up (Loop, Stop, Chain). Replaces the `chain` word with a declarative UI setting (`e` key).
|
||||
- Delta-time MIDI scheduling for tighter timing.
|
||||
- Audio stream errors surfaced as flash messages.
|
||||
- Prelude script evaluated on application startup (not only on play).
|
||||
- Global periodic script: a hidden script page runs alongside all patterns at its own speed/length.
|
||||
- RestartAll command: reset all active patterns to step 0 and clear state.
|
||||
- Tempo and current beat exposed in sequencer snapshot.
|
||||
- Spectrum analyzer rescaling.
|
||||
- SF2 soundfont support: auto-scans sample directories for `.sf2` files and loads them.
|
||||
- Audio stream errors surfaced as flash messages instead of printing to stderr.
|
||||
|
||||
### UI / UX
|
||||
- **Engine page redesign**: responsive narrow/wide layout, Link/MIDI/device settings moved here from Options.
|
||||
- **Patterns view redesign**: banks column with pattern counts, expandable detail rows, bottom preview strip with mini step grid.
|
||||
- **Mouse support**: click navigation on header/grid/panels/modals, text selection in code editor (click+drag), double-click on scope/spectrum/lissajous to cycle display modes.
|
||||
- Smooth playback progress bar interpolated between steps.
|
||||
- Dynamic step grid sizing adapts to terminal height.
|
||||
- Lissajous XY scope with Braille rendering and thermal trail mode.
|
||||
### UI / Visualization
|
||||
- Lissajous XY scope: stereo phase display using Braille characters, togglable via Options.
|
||||
- Gain boost (1x–16x) and normalize toggle for scope/lissajous/spectrum.
|
||||
- Pattern description field: editable via `d`, shown in pattern list and properties.
|
||||
- Bank/pattern import and export via clipboard (base64 serialization for sharing).
|
||||
- Pattern description field: editable via `d` on Patterns page, shown in pattern row and properties.
|
||||
- Mute/solo on main page now apply immediately (no staging).
|
||||
- Step name automatically cleared when deleting a step.
|
||||
- F1–F6 page navigation across the 3×2 page grid.
|
||||
- Collapsible help sections with code block copy.
|
||||
- Onboarding system for first-time users.
|
||||
- Show/hide preview pane toggle and zoom factor setting.
|
||||
- Reduced UI lag: sequencer snapshot moved after render call.
|
||||
- 10 bundled demo projects loaded on fresh startup (togglable in Options).
|
||||
- Options page: each option shows a description line below when focused.
|
||||
- Dictionary page: word list uses full page height (removed description box).
|
||||
|
||||
### Themes
|
||||
- 5 new themes: Iceberg, Everforest, Fauve, Tropicalia, Jaipur.
|
||||
- Palette-based generation: all 18 themes derived from a 14-field Palette via Oklab color space (definitions reduced from ~300 to ~20 lines each).
|
||||
|
||||
### Desktop (egui)
|
||||
- Fixed Alt/Option key on macOS (dead-key composition now works).
|
||||
- Fixed multi-character text paste.
|
||||
- Extended function key support (F13–F20).
|
||||
- No console window on Windows desktop build.
|
||||
|
||||
### Packaging
|
||||
- macOS: `.dmg` disk image with `.app` bundle (Intel + Apple Silicon fat binaries via `lipo`).
|
||||
- Windows: `.msi` installer via WiX.
|
||||
- Linux: improved AppImage build scripts and Docker cross-compilation.
|
||||
|
||||
### CLAP Plugin (experimental)
|
||||
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
|
||||
|
||||
### Documentation
|
||||
- Complete reorganization into `docs/` subdirectories.
|
||||
- 10 getting-started guides, 5 interactive tutorials.
|
||||
- New tutorials: Recording, Soundfonts, Sharing (import/export).
|
||||
- New topics: control flow, generators, harmony, randomness, variables, timing, bracket syntax.
|
||||
- Crate-level READMEs for forth, markdown, project, ratatui.
|
||||
|
||||
### Fixed
|
||||
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
|
||||
- Scope widget not drawing completely in some terminal sizes.
|
||||
|
||||
### Documentation
|
||||
- New tutorials: Recording (`docs/tutorials/recording.md`), Soundfonts (`docs/tutorials/soundfont.md`).
|
||||
|
||||
### UI / UX (breaking cosmetic changes)
|
||||
- **Options page**: Each option now shows a short description line below when focused, replacing the static header box.
|
||||
- **Dictionary page**: Removed the Forth description box at the top. The word list now uses the full page height.
|
||||
|
||||
### CLAP Plugin (experimental)
|
||||
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
|
||||
|
||||
### Forth Language
|
||||
- Removed `chain` word (replaced by pattern-level Follow Up setting).
|
||||
- `case/of/endof/endcase` control flow for pattern-matching dispatch.
|
||||
- `bjork` / `pbjork` — euclidean rhythm gates using quotations: execute a block only on Bjorklund-distributed hits.
|
||||
- `arp` — arpeggio list type that spreads notes across time positions instead of stacking them simultaneously.
|
||||
- `,varname` assignment syntax (SetKeep): assign to a variable without consuming the value from the stack.
|
||||
- `every` reworked to accept quotations for cleaner conditional step logic.
|
||||
- All parameter words now accept varargs — over 100 words updated to consume the full stack.
|
||||
- Reverb parameter words added.
|
||||
|
||||
### Engine
|
||||
- Follow-up actions: patterns now have a configurable follow-up behavior (Loop, Stop, or Chain to another pattern). Replaces the Forth `chain` word with a declarative setting in the Pattern Properties modal (`e` key). Chain targets specify bank and pattern via UI fields.
|
||||
- Delta-time MIDI scheduling for tighter, sample-accurate timing.
|
||||
- Tempo and current beat exposed in sequencer snapshot.
|
||||
- Spectrum analyzer rescaling.
|
||||
|
||||
### UI / UX
|
||||
- Patterns view redesign: new layout with banks column (showing pattern counts), expandable detail rows for the focused pattern (quantization, sync mode, progress bar), and a bottom preview strip with mini step grid and pattern properties.
|
||||
- Smooth playback progress: playing patterns display a real-time progress bar interpolated between steps.
|
||||
- Dynamic step grid sizing: `steps_per_page` adapts to terminal height instead of using a fixed constant.
|
||||
- Mouse support: click navigation on the pattern grid, panels, and modals.
|
||||
- F1–F6 page navigation across the 3×2 page grid.
|
||||
- Collapsible help sections with code block copy.
|
||||
- Onboarding system for first-time users.
|
||||
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
|
||||
- Show/hide preview pane toggle and zoom factor setting.
|
||||
|
||||
### Documentation
|
||||
- Complete reorganization into `docs/` subdirectories.
|
||||
- 10 getting-started guides, 5 interactive tutorials.
|
||||
- New topics: control flow, generators, harmony, randomness, variables, timing.
|
||||
|
||||
### Theme System
|
||||
- Palette-based generation: all 18 themes now derived from a 14-field Palette via Oklab color space.
|
||||
- Theme definitions reduced from ~300 lines each to ~20 lines.
|
||||
|
||||
### Codebase
|
||||
- `src/app.rs` split into 10 focused modules.
|
||||
- `src/app.rs` split into 10 focused modules (dispatch, clipboard, editing, navigation, persistence, scripting, sequencer, staging, undo).
|
||||
- `src/input.rs` split into 8 page-specific handlers.
|
||||
- Undo/redo system with scope-based tracking.
|
||||
- Feature-gated CLI vs plugin builds.
|
||||
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
|
||||
|
||||
## [0.0.9]
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ Contributions are welcome. There are many ways to contribute beyond code:
|
||||
## Prerequisites
|
||||
|
||||
- **Rust** (stable toolchain) - [rustup.rs](https://rustup.rs/)
|
||||
- **System libraries** - See [BUILDING.md](BUILDING.md) for platform-specific packages (cmake, ALSA, etc.)
|
||||
|
||||
## Quick start
|
||||
|
||||
|
||||
962
Cargo.lock
generated
962
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -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.4"
|
||||
version = "0.0.9"
|
||||
edition = "2021"
|
||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://git.raphaelforment.fr/BuboBubo/cagire"
|
||||
repository = "https://github.com/Bubobubobubobubo/cagire"
|
||||
homepage = "https://cagire.raphaelforment.fr"
|
||||
description = "Forth-based live coding music sequencer"
|
||||
|
||||
@@ -45,18 +45,17 @@ desktop = [
|
||||
"dep:egui_ratatui",
|
||||
"dep:image",
|
||||
]
|
||||
asio = ["doux/asio", "cpal/asio"]
|
||||
|
||||
[dependencies]
|
||||
cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
cpal = { version = "0.17", optional = true }
|
||||
cpal = { version = "0.17", features = ["jack"], optional = true }
|
||||
clap = { version = "4", features = ["derive"], optional = true }
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -84,10 +83,7 @@ rustc-hash = { version = "2", optional = true }
|
||||
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
cpal = { version = "0.17", optional = true, features = ["jack"] }
|
||||
|
||||
[build-dependencies]
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
@@ -113,4 +109,3 @@ icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
|
||||
copyright = "Copyright (c) 2025 Raphaël Forment"
|
||||
category = "Music"
|
||||
short_description = "Forth-based music sequencer"
|
||||
minimum_system_version = "12.0"
|
||||
|
||||
88
README.md
88
README.md
@@ -1,83 +1,37 @@
|
||||
<h1 align="center">Cagire</h1>
|
||||
|
||||
<p align="center"><em>A Forth-based live coding sequencer</em></p>
|
||||
<p align="center"><em>A Forth Music Sequencer</em></p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/Cagire.png" alt="Cagire" width="256">
|
||||
<img src="cagire_pixel.png" alt="Cagire" width="256">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cagire.raphaelforment.fr">Website</a> ·
|
||||
<a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a> ·
|
||||
AGPL-3.0
|
||||
</p>
|
||||
Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events.
|
||||
|
||||
Cagire is a terminal based step sequencer and live coding platform. Each step in a sequence is represented by a **Forth** script. It ships with a self-contained audio engine. No external software is needed, Cagire is a fully autonomous musical instrument that provides everything you need to perform.
|
||||
## Build
|
||||
|
||||
### Examples
|
||||
|
||||
A filtered sawtooth with reverb:
|
||||
|
||||
```forth
|
||||
saw sound
|
||||
200 199 freq
|
||||
400 lpf
|
||||
.8 lpq .3 verb
|
||||
.
|
||||
Terminal version:
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
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
|
||||
.
|
||||
Desktop version (with egui window):
|
||||
```
|
||||
cargo build --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
### Features
|
||||
## Run
|
||||
|
||||
- **Cagire's Forth**: a stack-based language made for live coding
|
||||
- Forth has almost no syntax, only words, numbers and spaces. Very easy to learn for beginners, quite deep for experienced programmers.
|
||||
- Nondeterminism and generative: randomness, probabilities, patterns thought as first-class features.
|
||||
- Quotations: code blocks `( ... )` that compose with probability, cycling, euclidean, and conditional words.
|
||||
- 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 (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. 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, 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.
|
||||
- **Cross-platform**: terminal and desktop interfaces on macOS, Linux, and Windows.
|
||||
- **Plugins**: run Cagire as a CLAP or VST3 plugin inside your DAW (separate version).
|
||||
Terminal version:
|
||||
```
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
### Getting started
|
||||
Desktop version:
|
||||
```
|
||||
cargo run --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
Download the latest release for your platform from the [website](https://cagire.raphaelforment.fr).
|
||||
## License
|
||||
|
||||
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.
|
||||
|
||||
- [Website](https://cagire.raphaelforment.fr)
|
||||
- [BUILDING.md](BUILDING.md) — build instructions and CLI flags
|
||||
- [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
### Credits
|
||||
|
||||
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
|
||||
|
||||
### License
|
||||
|
||||
[AGPL-3.0](LICENSE)
|
||||
AGPL-3.0
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# Cagire - A Forth-based music sequencer
|
||||
|
||||
## Installation
|
||||
Cagire - A Forth-based music sequencer
|
||||
Made by BuboBubo and his friends
|
||||
======================================
|
||||
|
||||
Installation
|
||||
------------
|
||||
Drag Cagire.app into the Applications folder.
|
||||
|
||||
## Unquarantine
|
||||
|
||||
Unquarantine
|
||||
------------
|
||||
Since this app is not signed with an Apple Developer certificate,
|
||||
macOS will block it from running. Thanks Apple! To fix this, open
|
||||
Terminal and run:
|
||||
macOS will block it from running. To fix this, open Terminal and run:
|
||||
|
||||
xattr -cr /Applications/Cagire.app
|
||||
|
||||
## Support
|
||||
|
||||
If you enjoy this software, consider supporting development:
|
||||
Support
|
||||
-------
|
||||
If you enjoy Cagire, consider supporting development:
|
||||
https://ko-fi.com/raphaelbubo
|
||||
|
||||
19
build.rs
19
build.rs
@@ -13,26 +13,13 @@ fn main() {
|
||||
println!("cargo:rustc-link-lib=oleaut32");
|
||||
}
|
||||
|
||||
if target_os == "windows" {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let icon = format!("{manifest_dir}/assets/Cagire.ico");
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
// Cross-compiling from Unix: use prefixed MinGW tools
|
||||
if cfg!(unix) {
|
||||
res.set_windres_path("x86_64-w64-mingw32-windres");
|
||||
res.set_ar_path("x86_64-w64-mingw32-ar");
|
||||
res.set_toolkit_path("/");
|
||||
}
|
||||
res.set_icon(&icon)
|
||||
res.set_icon("assets/Cagire.ico")
|
||||
.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");
|
||||
// GNU ld discards unreferenced sections from static archives,
|
||||
// so link the resource object directly to ensure .rsrc is kept.
|
||||
if cfg!(unix) {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
println!("cargo:rustc-link-arg-bins={out_dir}/resource.o");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# cagire-forth
|
||||
|
||||
Stack-based Forth VM for the Cagire sequencer. Tokenizes, compiles, and executes step scripts to produce audio and MIDI commands.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `vm` | Interpreter loop, `Forth::evaluate()` entry point |
|
||||
| `compiler` | Tokenization (with source spans) and single-pass compilation to ops |
|
||||
| `ops` | `Op` enum (~90 variants) |
|
||||
| `types` | `Value`, `StepContext`, shared state types |
|
||||
| `words/` | Built-in word definitions: `core`, `sound`, `music`, `midi`, `effects`, `sequencing`, `compile` |
|
||||
| `theory/` | Music theory lookups: `scales` (~200 patterns), `chords` (interval arrays) |
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`Forth`** — VM instance, holds stacks and compilation state
|
||||
- **`Value`** — Stack value (int, float, string, list, quotation, ...)
|
||||
- **`StepContext`** — Per-step evaluation context (step index, tempo, variables, ...)
|
||||
- **`Op`** — Compiled operation; nondeterministic variants carry `Option<SourceSpan>` for tracing
|
||||
- **`ExecutionTrace`** — Records executed/selected spans and resolved values during evaluation
|
||||
@@ -31,7 +31,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '{' || c == '}' {
|
||||
if c == '(' || c == ')' {
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
@@ -133,7 +133,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
|
||||
Token::Word(w, span) => {
|
||||
let word = w.as_str();
|
||||
if word == "(" {
|
||||
if word == "{" {
|
||||
let (quote_ops, consumed, end_span) =
|
||||
compile_quotation(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
@@ -142,8 +142,8 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
end: end_span.end,
|
||||
};
|
||||
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
||||
} else if word == ")" {
|
||||
return Err("unexpected )".into());
|
||||
} else if word == "}" {
|
||||
return Err("unexpected }".into());
|
||||
} else if word == "[" {
|
||||
let (bracket_ops, consumed, end_span) =
|
||||
compile_bracket(&tokens[i + 1..], dict)?;
|
||||
@@ -203,8 +203,8 @@ fn compile_quotation(
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"(" => depth += 1,
|
||||
")" => {
|
||||
"{" => depth += 1,
|
||||
"}" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_idx = Some(i);
|
||||
@@ -216,7 +216,7 @@ fn compile_quotation(
|
||||
}
|
||||
}
|
||||
|
||||
let end_idx = end_idx.ok_or("missing )")?;
|
||||
let end_idx = end_idx.ok_or("missing }")?;
|
||||
let end_span = match &tokens[end_idx] {
|
||||
Token::Word(_, span) => *span,
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -65,7 +65,6 @@ pub enum Op {
|
||||
NewCmd,
|
||||
SetParam(&'static str),
|
||||
Emit,
|
||||
Print,
|
||||
Get,
|
||||
Set,
|
||||
SetKeep,
|
||||
@@ -118,7 +117,6 @@ pub enum Op {
|
||||
Euclid,
|
||||
EuclidRot,
|
||||
Times,
|
||||
Map,
|
||||
Chord(&'static [i64]),
|
||||
Transpose,
|
||||
Invert,
|
||||
@@ -133,9 +131,6 @@ pub enum Op {
|
||||
ModSlide(u8),
|
||||
ModRnd(u8),
|
||||
ModEnv,
|
||||
ModEnvAd,
|
||||
ModEnvAdr,
|
||||
Lpg,
|
||||
// Global params
|
||||
EmitAll,
|
||||
ClearGlobal,
|
||||
|
||||
@@ -63,7 +63,6 @@ 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,
|
||||
|
||||
@@ -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(std::mem::take(&mut *self.global_params.lock()));
|
||||
cmd.set_global(self.global_params.lock().clone());
|
||||
|
||||
self.execute_ops(
|
||||
ops,
|
||||
@@ -302,7 +302,6 @@ impl Forth {
|
||||
&resolved_params,
|
||||
ctx.step_duration(),
|
||||
delta_secs,
|
||||
ctx.sr,
|
||||
outputs,
|
||||
);
|
||||
Ok(resolved_sound_val.map(|v| v.into_owned()))
|
||||
@@ -316,7 +315,7 @@ impl Forth {
|
||||
|
||||
Op::Dup => {
|
||||
ensure(stack, 1)?;
|
||||
let v = stack.last().expect("stack non-empty after ensure").clone();
|
||||
let v = stack.last().unwrap().clone();
|
||||
stack.push(v);
|
||||
}
|
||||
Op::Dupn => {
|
||||
@@ -329,16 +328,6 @@ impl Forth {
|
||||
Op::Drop => {
|
||||
pop(stack)?;
|
||||
}
|
||||
Op::Print => {
|
||||
let val = pop(stack)?;
|
||||
let text = match &val {
|
||||
Value::Int(n, _) => n.to_string(),
|
||||
Value::Float(f, _) => format!("{f}"),
|
||||
Value::Str(s, _) => s.to_string(),
|
||||
_ => format!("{val:?}"),
|
||||
};
|
||||
outputs.push(format!("print:{text}"));
|
||||
}
|
||||
Op::Swap => {
|
||||
ensure(stack, 2)?;
|
||||
let len = stack.len();
|
||||
@@ -460,7 +449,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)?;
|
||||
@@ -468,47 +457,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)?,
|
||||
@@ -569,10 +558,7 @@ impl Forth {
|
||||
|
||||
Op::NewCmd => {
|
||||
ensure(stack, 1)?;
|
||||
let values = drain_skip_quotations(stack);
|
||||
if values.is_empty() {
|
||||
return Err("expected sound name".into());
|
||||
}
|
||||
let values = std::mem::take(stack);
|
||||
let val = if values.len() == 1 {
|
||||
values.into_iter().next().unwrap()
|
||||
} else {
|
||||
@@ -582,10 +568,7 @@ impl Forth {
|
||||
}
|
||||
Op::SetParam(param) => {
|
||||
ensure(stack, 1)?;
|
||||
let values = drain_skip_quotations(stack);
|
||||
if values.is_empty() {
|
||||
return Err("expected parameter value".into());
|
||||
}
|
||||
let values = std::mem::take(stack);
|
||||
let val = if values.len() == 1 {
|
||||
values.into_iter().next().unwrap()
|
||||
} else {
|
||||
@@ -1056,7 +1039,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]
|
||||
@@ -1156,7 +1139,7 @@ impl Forth {
|
||||
Op::Oct => {
|
||||
let shift = pop(stack)?;
|
||||
let note = pop(stack)?;
|
||||
let result = lift_binary(¬e, &shift, |n, s| n + s * 12.0)?;
|
||||
let result = lift_binary(note, shift, |n, s| n + s * 12.0)?;
|
||||
stack.push(result);
|
||||
}
|
||||
|
||||
@@ -1181,11 +1164,11 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Loop => {
|
||||
let steps = pop_float(stack)?;
|
||||
let beats = pop_float(stack)?;
|
||||
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
||||
return Err("tempo and speed must be non-zero".into());
|
||||
}
|
||||
let dur = steps * ctx.step_duration();
|
||||
let dur = beats * 60.0 / ctx.tempo / ctx.speed;
|
||||
cmd.set_param("fit", Value::Float(dur, None));
|
||||
cmd.set_param("dur", Value::Float(dur, None));
|
||||
}
|
||||
@@ -1375,15 +1358,6 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
|
||||
Op::Map => {
|
||||
let quot = pop(stack)?;
|
||||
let items = std::mem::take(stack);
|
||||
for item in items {
|
||||
stack.push(item);
|
||||
run_quotation(quot.clone(), stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::GeomRange => {
|
||||
let count = pop_int(stack)?;
|
||||
let ratio = pop_float(stack)?;
|
||||
@@ -1433,7 +1407,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", 3 => "i", 4 => "o", 5 => "p", _ => "" };
|
||||
let suffix = match curve { 1 => "e", 2 => "s", _ => "" };
|
||||
let s = format!("{start}>{end}:{dur}{suffix}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
@@ -1446,57 +1420,25 @@ impl Forth {
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
Op::ModEnv => {
|
||||
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)?;
|
||||
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();
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:{sustain}:{release}");
|
||||
let _ = write!(&mut s, "{}", floats[0]);
|
||||
for pair in floats[1..].chunks(2) {
|
||||
let _ = write!(&mut s, ">{}:{}", pair[0], pair[1] * step_dur);
|
||||
}
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
|
||||
Op::ModEnvAd => {
|
||||
let decay = pop_float(stack)? * ctx.step_duration();
|
||||
let attack = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:0:0");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
|
||||
Op::ModEnvAdr => {
|
||||
let release = pop_float(stack)? * ctx.step_duration();
|
||||
let decay = pop_float(stack)? * ctx.step_duration();
|
||||
let attack = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:0:{release}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
|
||||
Op::Lpg => {
|
||||
let depth = pop_float(stack)?.clamp(0.0, 1.0);
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
let effective_max = min + (max - min) * depth;
|
||||
let sd = ctx.step_duration();
|
||||
let a = cmd_param_float(cmd, "attack").unwrap_or(0.0) * sd;
|
||||
let d = cmd_param_float(cmd, "decay").unwrap_or(1.0) * sd;
|
||||
let s = cmd_param_float(cmd, "sustain").unwrap_or(0.0);
|
||||
let r = cmd_param_float(cmd, "release").unwrap_or(0.0) * sd;
|
||||
use std::fmt::Write;
|
||||
let mut mod_str = String::new();
|
||||
let _ = write!(&mut mod_str, "{min}^{effective_max}:{a}:{d}:{s}:{r}");
|
||||
cmd.set_param("lpf", Value::Str(mod_str.into(), None));
|
||||
}
|
||||
|
||||
// MIDI operations
|
||||
Op::MidiEmit => {
|
||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||
@@ -1575,7 +1517,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.abs() > 1e-9 {
|
||||
let delta_suffix = if delta_secs > 0.0 {
|
||||
format!("/delta/{delta_secs}")
|
||||
} else {
|
||||
String::new()
|
||||
@@ -1606,7 +1548,7 @@ impl Forth {
|
||||
} else {
|
||||
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
||||
let velocity =
|
||||
(get_float("velocity").unwrap_or(0.8) * 127.0).clamp(0.0, 127.0) as u8;
|
||||
get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
|
||||
let dur = get_float("dur").unwrap_or(1.0);
|
||||
let dur_secs = dur * ctx.step_duration();
|
||||
outputs.push(format!(
|
||||
@@ -1674,21 +1616,21 @@ impl Forth {
|
||||
}
|
||||
Op::Rec => {
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/{}", name.as_str()?));
|
||||
outputs.push(format!("/doux/rec/sound/{}", name.as_str()?));
|
||||
}
|
||||
Op::Overdub => {
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/{}/overdub/1", name.as_str()?));
|
||||
outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?));
|
||||
}
|
||||
Op::Orec => {
|
||||
let orbit = pop(stack)?.as_int()?;
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/{}/orbit/{}", name.as_str()?, orbit));
|
||||
outputs.push(format!("/doux/rec/sound/{}/orbit/{}", name.as_str()?, orbit));
|
||||
}
|
||||
Op::Odub => {
|
||||
let orbit = pop(stack)?.as_int()?;
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
|
||||
outputs.push(format!("/doux/rec/sound/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
|
||||
}
|
||||
Op::Forget => {
|
||||
let name = pop(stack)?;
|
||||
@@ -1742,18 +1684,30 @@ 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" | "envdelay" | "hold" | "chorusdelay"
|
||||
"attack"
|
||||
| "decay"
|
||||
| "release"
|
||||
| "lpa"
|
||||
| "lpd"
|
||||
| "lpr"
|
||||
| "hpa"
|
||||
| "hpd"
|
||||
| "hpr"
|
||||
| "bpa"
|
||||
| "bpd"
|
||||
| "bpr"
|
||||
| "patt"
|
||||
| "pdec"
|
||||
| "prel"
|
||||
| "fma"
|
||||
| "fmd"
|
||||
| "fmr"
|
||||
| "glide"
|
||||
| "chorusdelay"
|
||||
| "duration"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1762,7 +1716,6 @@ fn emit_output(
|
||||
params: &[(&str, String)],
|
||||
step_duration: f64,
|
||||
nudge_secs: f64,
|
||||
sr: f64,
|
||||
outputs: &mut Vec<String>,
|
||||
) {
|
||||
use std::fmt::Write;
|
||||
@@ -1770,7 +1723,6 @@ fn emit_output(
|
||||
out.push('/');
|
||||
|
||||
let has_dur = params.iter().any(|(k, _)| *k == "dur");
|
||||
let has_release = params.iter().any(|(k, _)| *k == "release");
|
||||
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
|
||||
|
||||
if let Some(s) = sound {
|
||||
@@ -1778,9 +1730,6 @@ fn emit_output(
|
||||
}
|
||||
|
||||
for (i, (k, v)) in params.iter().enumerate() {
|
||||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !out.ends_with('/') {
|
||||
out.push('/');
|
||||
}
|
||||
@@ -1798,12 +1747,11 @@ fn emit_output(
|
||||
}
|
||||
}
|
||||
|
||||
if nudge_secs.abs() > 1e-9 {
|
||||
if nudge_secs > 0.0 {
|
||||
if !out.ends_with('/') {
|
||||
out.push('/');
|
||||
}
|
||||
let delta_ticks = (nudge_secs * sr).round() as i64;
|
||||
let _ = write!(&mut out, "delta/{delta_ticks}");
|
||||
let _ = write!(&mut out, "delta/{nudge_secs}");
|
||||
}
|
||||
|
||||
if !has_dur {
|
||||
@@ -1813,13 +1761,6 @@ fn emit_output(
|
||||
let _ = write!(&mut out, "dur/{}", 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() {
|
||||
if !out.ends_with('/') {
|
||||
out.push('/');
|
||||
@@ -1863,8 +1804,8 @@ fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
|
||||
groups.into_iter().partition(|g| g[0]);
|
||||
|
||||
for _ in 0..min_count {
|
||||
let mut one = ones.pop().expect("ones sufficient for min_count");
|
||||
one.extend(zeros.pop().expect("zeros sufficient for min_count"));
|
||||
let mut one = ones.pop().unwrap();
|
||||
one.extend(zeros.pop().unwrap());
|
||||
new_groups.push(one);
|
||||
}
|
||||
new_groups.extend(ones);
|
||||
@@ -1925,21 +1866,6 @@ fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, String> {
|
||||
Ok(pop(stack)?.is_truthy())
|
||||
}
|
||||
|
||||
/// Drain the stack, returning non-quotation values.
|
||||
/// Quotations are pushed back onto the stack (transparent).
|
||||
fn drain_skip_quotations(stack: &mut Vec<Value>) -> Vec<Value> {
|
||||
let values = std::mem::take(stack);
|
||||
let mut result = Vec::new();
|
||||
for v in values {
|
||||
if matches!(v, Value::Quotation(..)) {
|
||||
stack.push(v);
|
||||
} else {
|
||||
result.push(v);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn ensure(stack: &[Value], n: usize) -> Result<(), String> {
|
||||
if stack.len() < n {
|
||||
return Err("stack underflow".into());
|
||||
@@ -1955,65 +1881,65 @@ 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, f)).collect();
|
||||
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, f)).collect();
|
||||
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), 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, f)).collect();
|
||||
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, f)).collect();
|
||||
items.iter().map(|x| lift_unary_int(x.clone(), 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, b, f)).collect();
|
||||
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, x, f)).collect();
|
||||
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, b, f)).collect();
|
||||
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
|
||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
||||
}
|
||||
(a, Value::CycleList(items)) => {
|
||||
let mapped: Result<Vec<_>, _> =
|
||||
items.iter().map(|x| lift_binary(a, x, f)).collect();
|
||||
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
|
||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
||||
}
|
||||
(a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))),
|
||||
@@ -2026,7 +1952,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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"dup" => Op::Dup,
|
||||
"dupn" => Op::Dupn,
|
||||
"drop" => Op::Drop,
|
||||
"print" => Op::Print,
|
||||
"swap" => Op::Swap,
|
||||
"over" => Op::Over,
|
||||
"rot" => Op::Rot,
|
||||
@@ -110,7 +109,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"euclid" => Op::Euclid,
|
||||
"euclidrot" => Op::EuclidRot,
|
||||
"times" => Op::Times,
|
||||
"map" => Op::Map,
|
||||
"m." => Op::MidiEmit,
|
||||
"ccval" => Op::GetMidiCC,
|
||||
"mclock" => Op::MidiClock,
|
||||
@@ -136,16 +134,10 @@ 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),
|
||||
"ead" => Op::ModEnvAd,
|
||||
"eadr" => Op::ModEnvAdr,
|
||||
"eadsr" | "env" => Op::ModEnv,
|
||||
"lpg" => Op::Lpg,
|
||||
"env" => Op::ModEnv,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,16 +33,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "print",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(x --)",
|
||||
desc: "Print top of stack to footer bar",
|
||||
example: "42 print",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "swap",
|
||||
aliases: &[],
|
||||
@@ -512,7 +502,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Logic",
|
||||
stack: "(true-quot false-quot bool --)",
|
||||
desc: "Execute true-quot if true, else false-quot",
|
||||
example: "( 1 ) ( 2 ) coin ifelse",
|
||||
example: "{ 1 } { 2 } coin ifelse",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -522,7 +512,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Logic",
|
||||
stack: "(..quots n --)",
|
||||
desc: "Execute nth quotation (0-indexed)",
|
||||
example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
|
||||
example: "{ 1 } { 2 } { 3 } 2 select => 3",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
@@ -532,7 +522,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Logic",
|
||||
stack: "(quot bool --)",
|
||||
desc: "Execute quotation if true",
|
||||
example: "( 2 distort ) 0.5 chance ?",
|
||||
example: "{ 2 distort } 0.5 chance ?",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -542,7 +532,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Logic",
|
||||
stack: "(quot bool --)",
|
||||
desc: "Execute quotation if false",
|
||||
example: "( 1 distort ) 0.5 chance !?",
|
||||
example: "{ 1 distort } 0.5 chance !?",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -552,7 +542,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Logic",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation unconditionally",
|
||||
example: "( 2 * ) apply",
|
||||
example: "{ 2 * } apply",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -563,17 +553,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Control",
|
||||
stack: "(n quot --)",
|
||||
desc: "Execute quotation n times, @i holds current index",
|
||||
example: "4 ( @i . ) times => 0 1 2 3",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "map",
|
||||
aliases: &[],
|
||||
category: "Control",
|
||||
stack: "(..vals quot -- ..results)",
|
||||
desc: "Apply quotation to each stack element",
|
||||
example: "1 2 3 ( 10 * ) map => 10 20 30",
|
||||
example: "4 { @i . } times => 0 1 2 3",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
|
||||
@@ -28,14 +28,14 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set velocity (0-1)",
|
||||
example: "0.8 velocity",
|
||||
desc: "Set velocity",
|
||||
example: "100 velocity",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "attack",
|
||||
aliases: &["att", "a"],
|
||||
aliases: &["att"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set attack time",
|
||||
@@ -45,7 +45,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
},
|
||||
Word {
|
||||
name: "decay",
|
||||
aliases: &["dec", "d"],
|
||||
aliases: &["dec"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set decay time",
|
||||
@@ -55,7 +55,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
},
|
||||
Word {
|
||||
name: "sustain",
|
||||
aliases: &["sus", "s"],
|
||||
aliases: &["sus"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sustain level",
|
||||
@@ -65,7 +65,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
},
|
||||
Word {
|
||||
name: "release",
|
||||
aliases: &["rel", "r"],
|
||||
aliases: &["rel"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set release time",
|
||||
@@ -73,26 +73,6 @@ 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: &[],
|
||||
@@ -113,6 +93,56 @@ 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",
|
||||
@@ -134,6 +164,56 @@ 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: &[],
|
||||
@@ -154,6 +234,56 @@ 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: &[],
|
||||
@@ -174,6 +304,56 @@ 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: &[],
|
||||
@@ -274,36 +454,6 @@ 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: &[],
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot prob --)",
|
||||
desc: "Execute quotation with probability (0.0-1.0)",
|
||||
example: "( 2 distort ) 0.75 chance",
|
||||
example: "{ 2 distort } 0.75 chance",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -70,7 +70,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot pct --)",
|
||||
desc: "Execute quotation with probability (0-100)",
|
||||
example: "( 2 distort ) 75 prob",
|
||||
example: "{ 2 distort } 75 prob",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -150,7 +150,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Always execute quotation",
|
||||
example: "( 2 distort ) always",
|
||||
example: "{ 2 distort } always",
|
||||
compile: Probability(1.0),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -160,7 +160,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Never execute quotation",
|
||||
example: "( 2 distort ) never",
|
||||
example: "{ 2 distort } never",
|
||||
compile: Probability(0.0),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -170,7 +170,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 75% of the time",
|
||||
example: "( 2 distort ) often",
|
||||
example: "{ 2 distort } often",
|
||||
compile: Probability(0.75),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -180,7 +180,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 50% of the time",
|
||||
example: "( 2 distort ) sometimes",
|
||||
example: "{ 2 distort } sometimes",
|
||||
compile: Probability(0.5),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -190,7 +190,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 25% of the time",
|
||||
example: "( 2 distort ) rarely",
|
||||
example: "{ 2 distort } rarely",
|
||||
compile: Probability(0.25),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -200,7 +200,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 10% of the time",
|
||||
example: "( 2 distort ) almostNever",
|
||||
example: "{ 2 distort } almostNever",
|
||||
compile: Probability(0.1),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -210,7 +210,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 90% of the time",
|
||||
example: "( 2 distort ) almostAlways",
|
||||
example: "{ 2 distort } almostAlways",
|
||||
compile: Probability(0.9),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -221,7 +221,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Time",
|
||||
stack: "(quot n --)",
|
||||
desc: "Execute quotation every nth iteration",
|
||||
example: "( 2 distort ) 4 every",
|
||||
example: "{ 2 distort } 4 every",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -231,7 +231,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Time",
|
||||
stack: "(quot n --)",
|
||||
desc: "Execute quotation on all iterations except every nth",
|
||||
example: "( 2 distort ) 4 except",
|
||||
example: "{ 2 distort } 4 except",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -241,7 +241,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Time",
|
||||
stack: "(quot n offset --)",
|
||||
desc: "Execute quotation every nth iteration with phase offset",
|
||||
example: "( snare ) 4 2 every+ => fires at iter 2, 6, 10...",
|
||||
example: "{ snare } 4 2 every+ => fires at iter 2, 6, 10...",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -251,7 +251,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Time",
|
||||
stack: "(quot n offset --)",
|
||||
desc: "Skip quotation every nth iteration with phase offset",
|
||||
example: "( snare ) 4 2 except+ => skips at iter 2, 6, 10...",
|
||||
example: "{ snare } 4 2 except+ => skips at iter 2, 6, 10...",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -261,7 +261,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Time",
|
||||
stack: "(quot k n --)",
|
||||
desc: "Execute quotation using Euclidean distribution over step runs",
|
||||
example: "( 2 distort ) 3 8 bjork",
|
||||
example: "{ 2 distort } 3 8 bjork",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -271,7 +271,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Time",
|
||||
stack: "(quot k n --)",
|
||||
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
||||
example: "( 2 distort ) 3 8 pbjork",
|
||||
example: "{ 2 distort } 3 8 pbjork",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -280,8 +280,8 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(n --)",
|
||||
desc: "Fit sample to n steps",
|
||||
example: "\"break\" s 16 loop @",
|
||||
desc: "Fit sample to n beats",
|
||||
example: "\"break\" s 4 loop @",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
@@ -456,7 +456,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Desktop",
|
||||
stack: "(-- bool)",
|
||||
desc: "1 when mouse button held, 0 otherwise",
|
||||
example: "mdown ( \"crash\" s . ) ?",
|
||||
example: "mdown { \"crash\" s . } ?",
|
||||
compile: Context("mdown"),
|
||||
varargs: false,
|
||||
},
|
||||
@@ -487,7 +487,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
category: "Generator",
|
||||
stack: "(quot n -- results...)",
|
||||
desc: "Execute quotation n times, push all results",
|
||||
example: "( 1 6 rand ) 4 gen => 4 random values",
|
||||
example: "{ 1 6 rand } 4 gen => 4 random values",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
// Sound
|
||||
Word {
|
||||
name: "sound",
|
||||
aliases: &["snd"],
|
||||
aliases: &["s"],
|
||||
category: "Sound",
|
||||
stack: "(name --)",
|
||||
desc: "Begin sound command",
|
||||
@@ -126,6 +126,16 @@ 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: &[],
|
||||
@@ -141,7 +151,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set gate duration (total note length, 0 = infinite sustain)",
|
||||
desc: "Set gate time",
|
||||
example: "0.8 gate",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -156,16 +166,6 @@ 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,16 +236,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "inchan",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Select input channel for live input (0-indexed)",
|
||||
example: "0 inchan",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "cut",
|
||||
aliases: &[],
|
||||
@@ -287,6 +277,16 @@ 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 +352,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set harmonics (add source)",
|
||||
desc: "Set harmonics (mutable only)",
|
||||
example: "4 harmonics",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -362,7 +362,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set timbre (add source)",
|
||||
desc: "Set timbre (mutable only)",
|
||||
example: "0.5 timbre",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -372,21 +372,11 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set morph (add source)",
|
||||
desc: "Set morph (mutable only)",
|
||||
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: &[],
|
||||
@@ -458,6 +448,36 @@ 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",
|
||||
@@ -489,6 +509,56 @@ 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: &[],
|
||||
@@ -762,36 +832,6 @@ 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: &[],
|
||||
@@ -822,53 +862,13 @@ 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: "(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 .",
|
||||
stack: "(start t1 d1 ... -- str)",
|
||||
desc: "Multi-segment envelope: start>t1:d1>...",
|
||||
example: "0 1 0.01 0.7 0.1 0 2 env gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# cagire-markdown
|
||||
|
||||
Markdown parser and renderer that produces ratatui-styled lines. Used for the built-in help/documentation views.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `parser` | Markdown-to-styled-lines conversion |
|
||||
| `highlighter` | `CodeHighlighter` trait for syntax highlighting in fenced code blocks |
|
||||
| `theme` | Color mappings for markdown elements |
|
||||
|
||||
## Key Trait
|
||||
|
||||
- **`CodeHighlighter`** — Implement to provide language-specific syntax highlighting. Returns `Vec<(Style, String)>` per line.
|
||||
@@ -1,22 +0,0 @@
|
||||
# cagire-project
|
||||
|
||||
Project data model and persistence for Cagire.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `project` | `Project`, `Bank`, `Pattern`, `Step` structs and constants |
|
||||
| `file` | File I/O (save/load) |
|
||||
| `share` | Project sharing/export |
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`Project`** — Top-level container: banks of patterns
|
||||
- **`Bank`** — Collection of patterns
|
||||
- **`Pattern`** — Sequence of steps with metadata
|
||||
- **`Step`** — Single step holding a Forth script
|
||||
|
||||
## Constants
|
||||
|
||||
`MAX_BANKS=32`, `MAX_PATTERNS=32`, `MAX_STEPS=1024`
|
||||
@@ -170,17 +170,6 @@ 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 {
|
||||
@@ -223,13 +212,6 @@ impl SyncMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Rst",
|
||||
Self::PhaseLock => "Plk",
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between Reset and PhaseLock.
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
@@ -543,8 +525,6 @@ pub struct Bank {
|
||||
pub patterns: Vec<Pattern>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
@@ -562,7 +542,6 @@ impl Default for Bank {
|
||||
Self {
|
||||
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||
name: None,
|
||||
prelude: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +96,9 @@ mod tests {
|
||||
#[test]
|
||||
fn roundtrip_empty() {
|
||||
let pattern = Pattern::default();
|
||||
let encoded = export(&pattern).expect("export pattern");
|
||||
let encoded = export(&pattern).unwrap();
|
||||
assert!(encoded.starts_with("cgr:"));
|
||||
let decoded = import(&encoded).expect("import pattern");
|
||||
let decoded = import(&encoded).unwrap();
|
||||
assert_eq!(decoded.length, pattern.length);
|
||||
assert_eq!(decoded.steps.len(), pattern.steps.len());
|
||||
}
|
||||
@@ -127,8 +127,8 @@ mod tests {
|
||||
pattern.length = 8;
|
||||
pattern.name = Some("Test".to_string());
|
||||
|
||||
let encoded = export(&pattern).expect("export pattern");
|
||||
let decoded = import(&encoded).expect("import pattern");
|
||||
let encoded = export(&pattern).unwrap();
|
||||
let decoded = import(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.length, 8);
|
||||
assert_eq!(decoded.name.as_deref(), Some("Test"));
|
||||
@@ -152,9 +152,9 @@ mod tests {
|
||||
#[test]
|
||||
fn whitespace_trimming() {
|
||||
let pattern = Pattern::default();
|
||||
let encoded = export(&pattern).expect("export pattern");
|
||||
let encoded = export(&pattern).unwrap();
|
||||
let padded = format!(" {encoded} \n");
|
||||
let decoded = import(&padded).expect("import padded pattern");
|
||||
let decoded = import(&padded).unwrap();
|
||||
assert_eq!(decoded.length, pattern.length);
|
||||
}
|
||||
|
||||
@@ -172,15 +172,15 @@ mod tests {
|
||||
pattern.length = 16;
|
||||
|
||||
// Current (msgpack+brotli)
|
||||
let new_encoded = export(&pattern).expect("export pattern");
|
||||
let new_encoded = export(&pattern).unwrap();
|
||||
|
||||
// Old pipeline (json+deflate) for comparison
|
||||
use std::io::Write;
|
||||
let json = serde_json::to_vec(&pattern).expect("serialize json");
|
||||
let json = serde_json::to_vec(&pattern).unwrap();
|
||||
let mut encoder =
|
||||
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
encoder.write_all(&json).expect("write to encoder");
|
||||
let old_compressed = encoder.finish().expect("finish encoder");
|
||||
encoder.write_all(&json).unwrap();
|
||||
let old_compressed = encoder.finish().unwrap();
|
||||
let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));
|
||||
|
||||
assert!(
|
||||
@@ -203,9 +203,9 @@ mod tests {
|
||||
bank.patterns[0].length = 8;
|
||||
bank.name = Some("Drums".to_string());
|
||||
|
||||
let encoded = export_bank(&bank).expect("export bank");
|
||||
let encoded = export_bank(&bank).unwrap();
|
||||
assert!(encoded.starts_with("cgrb:"));
|
||||
let decoded = import_bank(&encoded).expect("import bank");
|
||||
let decoded = import_bank(&encoded).unwrap();
|
||||
|
||||
assert_eq!(decoded.name.as_deref(), Some("Drums"));
|
||||
assert_eq!(decoded.patterns[0].length, 8);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# cagire-ratatui
|
||||
|
||||
TUI widget library and theme system for Cagire.
|
||||
|
||||
## Widgets
|
||||
|
||||
`category_list`, `confirm`, `editor`, `file_browser`, `hint_bar`, `lissajous`, `list_select`, `modal`, `nav_minimap`, `props_form`, `sample_browser`, `scope`, `scroll_indicators`, `search_bar`, `section_header`, `sparkles`, `spectrum`, `text_input`, `vu_meter`, `waveform`
|
||||
|
||||
## Theme System
|
||||
|
||||
The `theme/` module provides a palette-based theming system using Oklab color space.
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `mod` | `THEMES` array, `CURRENT_THEME` thread-local, `get()`/`set()` |
|
||||
| `palette` | `Palette` (14 fields), color manipulation helpers (`shift`, `mix`, `tint_bg`, ...) |
|
||||
| `build` | Derives ~190 `ThemeColors` fields from a `Palette` |
|
||||
| `transform` | HSV-based hue rotation for generated palettes |
|
||||
|
||||
25 built-in themes.
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`Palette`** — 14-field color definition, input to theme generation
|
||||
- **`ThemeColors`** — ~190 derived semantic colors used throughout the UI
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Script editor widget with completion, search, and sample finder popups.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::{
|
||||
@@ -26,7 +25,7 @@ pub struct CompletionCandidate {
|
||||
}
|
||||
|
||||
struct CompletionState {
|
||||
candidates: Arc<[CompletionCandidate]>,
|
||||
candidates: Vec<CompletionCandidate>,
|
||||
matches: Vec<usize>,
|
||||
cursor: usize,
|
||||
prefix: String,
|
||||
@@ -38,7 +37,7 @@ struct CompletionState {
|
||||
impl CompletionState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
candidates: Arc::from([]),
|
||||
candidates: Vec::new(),
|
||||
matches: Vec::new(),
|
||||
cursor: 0,
|
||||
prefix: String::new(),
|
||||
@@ -172,7 +171,7 @@ impl Editor {
|
||||
self.scroll_offset.set(0);
|
||||
}
|
||||
|
||||
pub fn set_candidates(&mut self, candidates: Arc<[CompletionCandidate]>) {
|
||||
pub fn set_candidates(&mut self, candidates: Vec<CompletionCandidate>) {
|
||||
self.completion.candidates = candidates;
|
||||
}
|
||||
|
||||
@@ -488,7 +487,7 @@ impl Editor {
|
||||
if is_cursor {
|
||||
cursor_style
|
||||
} else if is_selected {
|
||||
base_style.bg(selection_style.bg.expect("selection style has bg"))
|
||||
base_style.bg(selection_style.bg.unwrap())
|
||||
} else {
|
||||
base_style
|
||||
}
|
||||
|
||||
@@ -14,14 +14,11 @@ 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> {
|
||||
@@ -30,14 +27,11 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,21 +60,6 @@ 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);
|
||||
@@ -91,61 +70,37 @@ impl<'a> FileBrowserModal<'a> {
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
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);
|
||||
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
|
||||
|
||||
// Input line
|
||||
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![
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||
]
|
||||
};
|
||||
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,
|
||||
);
|
||||
}
|
||||
])),
|
||||
rows[0],
|
||||
);
|
||||
|
||||
// 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
|
||||
.map(|(abs_idx, (name, is_dir, is_cagire))| {
|
||||
.enumerate()
|
||||
.map(|(i, (name, is_dir, is_cagire))| {
|
||||
let abs_idx = i + self.scroll_offset;
|
||||
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 {
|
||||
@@ -155,21 +110,7 @@ impl<'a> FileBrowserModal<'a> {
|
||||
} else {
|
||||
colors.browser.file
|
||||
};
|
||||
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)
|
||||
Line::from(Span::styled(display, Style::new().fg(color)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::theme;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Node type in the sample tree.
|
||||
@@ -23,7 +23,6 @@ pub struct TreeLine {
|
||||
pub label: String,
|
||||
pub folder: String,
|
||||
pub index: usize,
|
||||
pub child_count: usize,
|
||||
}
|
||||
|
||||
/// Tree-view browser for navigating sample folders.
|
||||
@@ -117,13 +116,13 @@ impl<'a> SampleBrowser<'a> {
|
||||
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||
let height = area.height as usize;
|
||||
if self.entries.is_empty() {
|
||||
if self.search_query.is_empty() {
|
||||
self.render_empty_guide(frame, area, colors);
|
||||
let msg = if self.search_query.is_empty() {
|
||||
"No samples loaded"
|
||||
} else {
|
||||
let line =
|
||||
Line::from(Span::styled("No matches", Style::new().fg(colors.browser.empty_text)));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
}
|
||||
"No matches"
|
||||
};
|
||||
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,10 +136,10 @@ impl<'a> SampleBrowser<'a> {
|
||||
|
||||
let (icon, icon_color) = match entry.kind {
|
||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
||||
("\u{2212} ", colors.browser.folder_icon)
|
||||
("\u{25BC} ", colors.browser.folder_icon)
|
||||
}
|
||||
TreeLineKind::Root { expanded: false }
|
||||
| TreeLineKind::Folder { expanded: false } => ("+ ", colors.browser.folder_icon),
|
||||
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
|
||||
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
||||
};
|
||||
|
||||
@@ -164,43 +163,15 @@ 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(label, label_style),
|
||||
Span::styled(&entry.label, label_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));
|
||||
}
|
||||
_ => {}
|
||||
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));
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
@@ -208,47 +179,4 @@ impl<'a> SampleBrowser<'a> {
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
|
||||
fn render_empty_guide(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||
let muted = Style::new().fg(colors.browser.empty_text);
|
||||
let heading = Style::new().fg(colors.ui.text_primary);
|
||||
let key = Style::new().fg(colors.hint.key);
|
||||
let desc = Style::new().fg(colors.hint.text);
|
||||
let code = Style::new().fg(colors.ui.accent);
|
||||
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(" No samples loaded.", muted)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Load from the Engine page:", heading)),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" F6 ", key),
|
||||
Span::styled("Go to Engine page", desc),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" A ", key),
|
||||
Span::styled("Add a sample folder", desc),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Organize samples like this:", heading)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" samples/", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} kick/", code)),
|
||||
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} kick.wav", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} snare/", code)),
|
||||
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} snare.wav", code)),
|
||||
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} hats/", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} closed.wav", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} open.wav", code)),
|
||||
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} pedal.wav", code)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Folders become Forth words:", heading)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" kick sound .", code)),
|
||||
Line::from(Span::styled(" hats sound 2 n .", code)),
|
||||
Line::from(Span::styled(" snare sound 0.5 speed .", code)),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ 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)),
|
||||
|
||||
@@ -30,7 +30,6 @@ 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 {
|
||||
@@ -67,17 +66,17 @@ pub const THEMES: &[ThemeEntry] = &[
|
||||
];
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_THEME: RefCell<Rc<ThemeColors>> = RefCell::new(Rc::new(build::build(&(THEMES[0].palette)())));
|
||||
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new(build::build(&(THEMES[0].palette)()));
|
||||
}
|
||||
|
||||
/// 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()))
|
||||
/// Return the current thread-local theme.
|
||||
pub fn get() -> ThemeColors {
|
||||
CURRENT_THEME.with(|t| t.borrow().clone())
|
||||
}
|
||||
|
||||
/// Set the current thread-local theme.
|
||||
pub fn set(theme: ThemeColors) {
|
||||
CURRENT_THEME.with(|t| *t.borrow_mut() = Rc::new(theme));
|
||||
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
|
||||
}
|
||||
|
||||
/// Complete set of resolved colors for all UI components.
|
||||
@@ -175,7 +174,6 @@ 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,
|
||||
|
||||
8376
demos/02.cagire
8376
demos/02.cagire
File diff suppressed because it is too large
Load Diff
8371
demos/03.cagire
8371
demos/03.cagire
File diff suppressed because it is too large
Load Diff
@@ -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 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 )
|
||||
saw s 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
|
||||
saw s 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
|
||||
```
|
||||
|
||||
| Word | Shape | Output |
|
||||
@@ -27,8 +27,8 @@ Stack effect: `( min max period -- str )`
|
||||
Transition from one value to another over a duration.
|
||||
|
||||
```forth
|
||||
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 )
|
||||
saw s 0 1 0.5 slide gain . ( fade in over half a step )
|
||||
saw s 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
|
||||
```
|
||||
|
||||
| Word | Curve | Output |
|
||||
@@ -44,9 +44,9 @@ Stack effect: `( start end dur -- str )`
|
||||
Randomize a parameter within a range, retriggering at a given period.
|
||||
|
||||
```forth
|
||||
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 )
|
||||
saw s 200 4000 2 jit lpf . ( new random value every 2 steps )
|
||||
saw s 200 4000 2 sjit lpf . ( same but smoothly interpolated )
|
||||
saw s 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
```
|
||||
|
||||
| Word | Behavior | Output |
|
||||
@@ -57,62 +57,26 @@ saw snd 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
|
||||
Stack effect: `( min max period -- str )`
|
||||
|
||||
## Envelope Modulation
|
||||
## Envelopes
|
||||
|
||||
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.
|
||||
Define a multi-segment envelope for a parameter. Provide a start value, then pairs of target and duration.
|
||||
|
||||
```forth
|
||||
saw snd 200 8000 0.01 0.1 0.5 0.3 env lpf .
|
||||
saw s 0 1 0.1 0.7 0.5 0 8 env gain .
|
||||
```
|
||||
|
||||
Stack effect: `( min max attack decay sustain release -- str )`
|
||||
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.
|
||||
|
||||
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.
|
||||
Stack effect: `( start target1 dur1 [target2 dur2 ...] -- str )`
|
||||
|
||||
## Combining
|
||||
|
||||
Modulation words return strings, so they compose naturally with the rest of the language. Use them anywhere a parameter value is expected.
|
||||
|
||||
```forth
|
||||
saw snd
|
||||
saw s
|
||||
200 4000 4 lfo lpf
|
||||
0.3 0.7 8 tlfo pan
|
||||
0 1 0.01 0.1 ead gain
|
||||
0 1 0.1 0.7 0.5 0 8 env gain
|
||||
.
|
||||
```
|
||||
|
||||
@@ -57,15 +57,29 @@ The `ftype` parameter sets the filter slope (rolloff steepness).
|
||||
saw 800 lpf 3 ftype . ( 48 dB/oct lowpass )
|
||||
```
|
||||
|
||||
## Filter Envelope Modulation
|
||||
## Filter Envelope
|
||||
|
||||
Use the `env` word to apply a DAHDSR envelope to any filter cutoff:
|
||||
Filters can be modulated by an ADSR envelope. The envelope multiplies the base cutoff:
|
||||
|
||||
```forth
|
||||
saw 200 8000 0.01 0.3 0.5 0.3 env lpf . ( cutoff sweeps from 200 to 8000 Hz )
|
||||
```
|
||||
final_cutoff = lpf + (lpe × envelope × lpf)
|
||||
```
|
||||
|
||||
The same works for highpass and bandpass: `env hpf`, `env bpf`.
|
||||
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`.
|
||||
|
||||
```forth
|
||||
saw 200 lpf 2 lpe 0.01 lpa 0.3 lpd . ( cutoff sweeps from 600 Hz down to 200 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.).
|
||||
|
||||
## Ladder Filters
|
||||
|
||||
@@ -86,7 +100,7 @@ saw 1000 lbpf 0.8 lbpq . ( ladder bandpass )
|
||||
| `lbpf` | Hz | Ladder bandpass cutoff |
|
||||
| `lbpq` | 0-1 | Ladder bandpass resonance |
|
||||
|
||||
Ladder filter cutoffs can also be modulated with `env`, `lfo`, `slide`, etc.
|
||||
Ladder filters share the lowpass envelope parameters (`lpe`, `lpa`, etc.).
|
||||
|
||||
## EQ
|
||||
|
||||
|
||||
@@ -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 snd c4 note 0.8 gain 0.3 verb .
|
||||
saw s 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 snd
|
||||
saw s
|
||||
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 snd 60 note . ;; gets lpf=500 verb=0.5
|
||||
hat snd 70 note . ;; gets lpf=500 verb=0.5
|
||||
kick s 60 note . ;; gets lpf=500 verb=0.5
|
||||
hat s 70 note . ;; gets lpf=500 verb=0.5
|
||||
```
|
||||
|
||||
```forth
|
||||
;; Retroactive: patch already-emitted sounds
|
||||
kick snd 60 note .
|
||||
hat snd 70 note .
|
||||
kick s 60 note .
|
||||
hat s 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 snd 2000 lpf . ;; lpf=2000 (per-sound wins)
|
||||
hat snd . ;; lpf=500 (global)
|
||||
kick s 2000 lpf . ;; lpf=2000 (per-sound wins)
|
||||
hat s . ;; lpf=500 (global)
|
||||
```
|
||||
|
||||
Use `noall` to clear global parameters:
|
||||
|
||||
```forth
|
||||
500 lpf all
|
||||
kick snd . ;; gets lpf
|
||||
kick s . ;; gets lpf
|
||||
noall
|
||||
hat snd . ;; no lpf
|
||||
hat s . ;; no lpf
|
||||
```
|
||||
|
||||
## Controlling Existing Voices
|
||||
|
||||
@@ -16,6 +16,34 @@ 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.
|
||||
@@ -30,7 +58,7 @@ sine 440 freq 2 fm 2 fmh . ( modulator at 2× carrier frequency )
|
||||
| `fmh` | ratio | Harmonic ratio (modulator / carrier) |
|
||||
| `fmshape` | shape | Modulator waveform |
|
||||
|
||||
Use `env` to apply a DAHDSR envelope to FM depth: `0 5 0.01 0.1 0.3 0.5 env fm`.
|
||||
FM has its own envelope (`fme`, `fma`, `fmd`, `fms`, `fmr`).
|
||||
|
||||
## Amplitude Modulation
|
||||
|
||||
|
||||
@@ -20,17 +20,15 @@ The engine scans these directories and builds a registry of available samples. S
|
||||
|
||||
```
|
||||
samples/
|
||||
├── kick/ → "kick"
|
||||
│ └── kick.wav
|
||||
├── snare/ → "snare"
|
||||
│ └── snare.wav
|
||||
├── kick.wav → "kick"
|
||||
├── snare.wav → "snare"
|
||||
└── hats/
|
||||
├── closed.wav → "hats" n 0
|
||||
├── open.wav → "hats" n 1
|
||||
└── pedal.wav → "hats" n 2
|
||||
```
|
||||
|
||||
Folders at the root of your sample directory become sample banks named after the folder. Each file within a folder gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
|
||||
Folders at the root of your directory are used as the name of a sample bank. Folders create sample banks where each file gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
|
||||
|
||||
## Playing Samples
|
||||
|
||||
@@ -50,7 +48,6 @@ 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 |
|
||||
@@ -106,24 +103,6 @@ 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.
|
||||
|
||||
@@ -79,16 +79,16 @@ Top-level files are named by their filename (without extension). Files inside fo
|
||||
Reference samples by name:
|
||||
|
||||
```forth
|
||||
kick snd . ;; play kick.wav
|
||||
snare snd 0.5 gain . ;; play snare at half volume
|
||||
kick s . ;; play kick.wav
|
||||
snare s 0.5 gain . ;; play snare at half volume
|
||||
```
|
||||
|
||||
For samples in folders, use `n` to select which one:
|
||||
|
||||
```forth
|
||||
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)
|
||||
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)
|
||||
```
|
||||
|
||||
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 snd . ;; plays kick.wav
|
||||
kick snd a bank . ;; plays kick_a.wav
|
||||
kick snd hard bank . ;; plays kick_hard.wav
|
||||
kick s . ;; plays kick.wav
|
||||
kick s a bank . ;; plays kick_a.wav
|
||||
kick s hard bank . ;; plays kick_hard.wav
|
||||
```
|
||||
|
||||
If the banked version does not exist, it falls back to the default.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sources
|
||||
|
||||
The audio engine provides a variety of sound sources. Use the `sound` word (or `snd` for short) to select one.
|
||||
The audio engine provides a variety of sound sources. Use the `sound` word (or `s` for short) to select one.
|
||||
|
||||
## Basic Oscillators
|
||||
|
||||
@@ -56,29 +56,37 @@ Noise sources ignore pitch. Use filters to shape the spectrum.
|
||||
|
||||
All filter and effect parameters apply to the input signal.
|
||||
|
||||
## Additive
|
||||
## Plaits Engines
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `add` | Stacks 1-32 sine partials with spectral tilt, even/odd morph, harmonic stretching, phase shaping. |
|
||||
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`):
|
||||
|
||||
| Parameter | Controls |
|
||||
|-----------|----------|
|
||||
| `harmonics` | Harmonic content / structure. |
|
||||
| `timbre` | Brightness / tonal color. |
|
||||
| `harmonics` | Harmonic content, structure, detuning. |
|
||||
| `timbre` | Brightness, tonal color. |
|
||||
| `morph` | Smooth transitions between variations. |
|
||||
| `partials` | Number of active harmonics (1-32). |
|
||||
|
||||
## Percussion
|
||||
|
||||
Native drum synthesis with timbral morphing. All share `wave` (waveform: 0=sine, 0.5=tri, 1=saw), `morph`, `harmonics`, and `timbre` parameters.
|
||||
### Pitched
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `kick` | Bass drum. |
|
||||
| `snare` | Snare drum with tone/noise balance. |
|
||||
| `hat` | Hi-hat. |
|
||||
| `tom` | Tom drum. |
|
||||
| `rim` | Rimshot. |
|
||||
| `cowbell` | Cowbell. |
|
||||
| `cymbal` | Cymbal. |
|
||||
| `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.
|
||||
|
||||
@@ -32,6 +32,9 @@ 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
|
||||
|
||||
@@ -54,16 +57,24 @@ pad 0.5 scan . ( blend between middle cycles )
|
||||
pad 1 scan . ( last cycle only )
|
||||
```
|
||||
|
||||
## Scan Modulation
|
||||
## LFO Modulation
|
||||
|
||||
Use audio-rate modulation words to automate the scan position:
|
||||
Automate the scan position with a built-in LFO:
|
||||
|
||||
```forth
|
||||
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 )
|
||||
pad 0 scan 2 scanlfo 0.3 scandepth . ( 2 Hz modulation, 30% depth )
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
@@ -5,7 +5,7 @@ Word definitions let you abstract sound design into reusable units.
|
||||
## Defining Sounds
|
||||
|
||||
```forth
|
||||
: lead "saw" snd 0.3 gain 1200 lpf ;
|
||||
: lead "saw" s 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" snd 1 decay . ;
|
||||
: hh "hihat" snd 0.5 gain 0.5 decay . ;
|
||||
: kk "kick" s 1 decay . ;
|
||||
: hh "hihat" s 0.5 gain 0.5 decay . ;
|
||||
```
|
||||
|
||||
Steps become simple:
|
||||
@@ -39,5 +39,5 @@ kk
|
||||
```
|
||||
|
||||
```forth
|
||||
c4 note saw snd dark wet .
|
||||
c4 note saw s dark wet .
|
||||
```
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# About Forth
|
||||
|
||||
Forth is a _stack-based_ programming language created by Charles H. Moore in the early 1970s. It was designed with simplicity, directness, and interactive exploration in mind. Forth has been used for scientific work and embedded systems: it controlled telescopes and even ran on hardware aboard space missions. It evolved into many implementations targeting various architectures, but none of them really caught on. Nonetheless, the ideas behind Forth continue to attract people from very different, often unrelated fields. Today, Forth languages are used by hackers and artists for their unconventional nature. Forth is simple, direct, and beautiful to implement. Forth is an elegant, minimal language, easy to understand, extend, and tailor to a specific task. The Forth we use in Cagire is specialized in making live music. It is used as a DSL: a _Domain Specific Language_.
|
||||
|
||||
**TLDR:** Forth is a really nice language to play music with.
|
||||
Forth is a _stack-based_ programming language created by Charles H. Moore in the early 1970s. It was designed with simplicity, directness, and interactive exploration in mind. Forth has been used for scientific work and embedded systems: it controlled telescopes and even ran on hardware aboard space missions. It evolved into many implementations targeting various architectures, but none of them really caught on. Nonetheless, the ideas behind Forth continue to attract people from very different, often unrelated fields. Today, Forth languages are used by hackers and artists for their unconventional nature. Forth is simple, direct, and beautiful to implement. Forth is an elegant, minimal language, easy to understand, extend, and tailor to a specific task. The Forth we use in Cagire is specialized in making live music. It is used as a DSL: a _Domain Specific Language_.
|
||||
|
||||
## Why Forth?
|
||||
|
||||
Most programming languages rely on a complex syntax of `variables`, `expressions` and `statements` like `x = 3 + 4` or `do_something(()=>bob(4))`. Forth works differently. It has almost no syntax at all. Instead, you push values onto a `stack` and apply `words` that transform them:
|
||||
|
||||
```forth
|
||||
3 4 + print
|
||||
3 4 +
|
||||
```
|
||||
|
||||
The program above leaves the number `7` on the stack. There are no variables, no parentheses, no syntax to remember. You just end up with words and numbers separated by spaces. For live coding music, this directness is quite exciting. All you do is think in terms of transformations and add things to the stack: take a note, shift it up, add reverb, play it.
|
||||
@@ -22,7 +20,6 @@ The stack is where values live. When you type a number, it goes on the stack. Wh
|
||||
3 ;; stack: 3
|
||||
4 ;; stack: 3 4
|
||||
+ ;; stack: 7
|
||||
print
|
||||
```
|
||||
|
||||
The stack is `last-in, first-out`. The most recent value is always on top. This means that it's often better to read Forth programs from right to left, bottom to top.
|
||||
@@ -41,7 +38,7 @@ Words compose naturally on the stack. To double a number:
|
||||
|
||||
```forth
|
||||
;; 3 3 +
|
||||
3 dup + print
|
||||
3 dup +
|
||||
```
|
||||
|
||||
Forth has a large vocabulary, so Cagire includes a `Dictionary` directly in the application. You can also create your own words. They will work just like existing words. The only difference is that these words will not be included in the dictionary. There are good reasons to create new words on-the-fly:
|
||||
@@ -57,28 +54,19 @@ Four basic types of values can live on the stack:
|
||||
- **Integers**: `42`, `-7`, `0`
|
||||
- **Floats**: `0.5`, `3.14`, `-1.0`
|
||||
- **Strings**: `"kick"`, `"hello"`
|
||||
- **Quotations**: `( dup + )` (code as data)
|
||||
- **Quotations**: `{ dup + }` (code as data)
|
||||
|
||||
Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`.
|
||||
|
||||
Parentheses are used to "quote" a section of a program. The code inside does not run immediately — it is pushed onto the stack as a value. A quotation only runs when a consuming word decides to execute it. This is how conditionals and loops work:
|
||||
Parentheses are ignored by the parser. You can use them freely for visual grouping without affecting execution:
|
||||
|
||||
```forth
|
||||
( 60 note 0.3 verb ) 1 ?
|
||||
(c4 note) (0.5 gain) "sine" s .
|
||||
```
|
||||
|
||||
Here `?` pops the quotation and the condition. The code inside runs only when the condition is truthy. Words like `?`, `!?`, `times`, `cycle`, `choose`, `ifelse`, `every`, `chance`, and `apply` all consume quotations this way.
|
||||
Quotations are special. They let you pass code around as a value. This is how conditionals and loops work. Don't worry about them for now — you'll learn how to use them later.
|
||||
|
||||
Because parentheses defer execution, wrapping code in `( ... )` without a consuming word means it never runs. Quotations are transparent to sound and parameter words — they stay on the stack untouched. This is a useful trick for temporarily disabling part of a step:
|
||||
|
||||
```forth
|
||||
( 0.5 gain ) ;; this quotation is ignored
|
||||
"kick" sound
|
||||
0.3 decay
|
||||
.
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## The Command Register
|
||||
|
||||
@@ -94,7 +82,7 @@ kick sound ;; sets the sound name
|
||||
. ;; emits the command and clears the register
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
This design lets you build sounds incrementally:
|
||||
|
||||
@@ -110,14 +98,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" snd c4 note 0.5 gain 0.3 decay 0.4 verb .
|
||||
"sine" s 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" snd 0.5 gain clear ;; nothing plays, register is emptied
|
||||
"hat" snd . ;; only the hat plays
|
||||
"kick" s 0.5 gain clear ;; nothing plays, register is emptied
|
||||
"hat" s . ;; only the hat plays
|
||||
```
|
||||
|
||||
This is useful when conditionals might cancel a sound before it emits.
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# Brackets
|
||||
|
||||
Cagire uses three bracket forms. Each one behaves differently.
|
||||
|
||||
## ( ... ) — Quotations
|
||||
|
||||
Parentheses create quotations: deferred code. The contents are not executed immediately — they are pushed onto the stack as a single value.
|
||||
|
||||
```forth
|
||||
( dup + )
|
||||
```
|
||||
|
||||
This pushes a block of code. You can store it in a variable, pass it to other words, or execute it later. Quotations are what make Cagire's control flow work.
|
||||
|
||||
### Words that consume quotations
|
||||
|
||||
Many built-in words expect a quotation on the stack:
|
||||
|
||||
| Word | Effect |
|
||||
|------|--------|
|
||||
| `?` | Execute if condition is truthy |
|
||||
| `!?` | Execute if condition is falsy |
|
||||
| `ifelse` | Choose between two quotations |
|
||||
| `select` | Pick the nth quotation from a list |
|
||||
| `apply` | Execute unconditionally |
|
||||
| `times` | Loop n times |
|
||||
| `cycle` / `pcycle` | Rotate through quotations |
|
||||
| `choose` | Pick one at random |
|
||||
| `every` | Execute on every nth iteration |
|
||||
| `chance` / `prob` | Execute with probability |
|
||||
| `bjork` / `pbjork` | Euclidean rhythm gate |
|
||||
|
||||
When a word like `cycle` or `choose` selects a quotation, it executes it. When it selects a plain value, it pushes it.
|
||||
|
||||
### Nesting
|
||||
|
||||
Quotations nest freely:
|
||||
|
||||
```forth
|
||||
( ( c4 note ) ( e4 note ) coin ifelse ) 4 every
|
||||
```
|
||||
|
||||
The outer quotation runs every 4th iteration. Inside, a coin flip picks the note.
|
||||
|
||||
### The mute trick
|
||||
|
||||
Wrapping code in a quotation without consuming it is a quick way to disable it:
|
||||
|
||||
```forth
|
||||
( kick snd . )
|
||||
```
|
||||
|
||||
Nothing will execute this quotation — it just sits on the stack and gets discarded. Useful for temporarily silencing a line while editing.
|
||||
|
||||
## [ ... ] — Square Brackets
|
||||
|
||||
Square brackets execute their contents immediately, then push a count of how many values were produced. The values themselves stay on the stack.
|
||||
|
||||
```forth
|
||||
[ 60 64 67 ]
|
||||
```
|
||||
|
||||
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 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.
|
||||
|
||||
Square brackets work with any word that takes a count:
|
||||
|
||||
```forth
|
||||
[ 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
|
||||
|
||||
Square brackets can nest. Each pair produces its own count:
|
||||
|
||||
```forth
|
||||
[ [ 60 64 67 ] cycle [ 0.3 0.5 0.8 ] cycle ] choose
|
||||
```
|
||||
|
||||
### Expressions inside brackets
|
||||
|
||||
The contents are compiled and executed normally, so you can use any Forth code:
|
||||
|
||||
```forth
|
||||
[ c4 c4 3 + c4 7 + ] note sine snd . ;; root, minor third, fifth
|
||||
```
|
||||
|
||||
## { ... } — Curly Braces
|
||||
|
||||
Curly braces are ignored by the compiler. They do nothing. Use them as a visual aid to group related code:
|
||||
|
||||
```forth
|
||||
{ kick snd } { 0.5 gain } { 0.3 verb } .
|
||||
```
|
||||
|
||||
This compiles to exactly the same thing as:
|
||||
|
||||
```forth
|
||||
kick snd 0.5 gain 0.3 verb .
|
||||
```
|
||||
|
||||
They can help readability in dense one-liners but have no semantic meaning.
|
||||
@@ -1,143 +1,140 @@
|
||||
# Control Flow
|
||||
|
||||
Control flow in Cagire's Forth comes in two families. The first is compiled syntax — `if/then` and `case` — which the compiler handles directly as branch instructions. The second is quotation words — `?`, `!?`, `ifelse`, `select`, `apply` — which pop `( ... )` quotations from the stack and decide whether to run them. Probability and periodic execution (`chance`, `every`, `bjork`) are covered in the Randomness tutorial.
|
||||
Sometimes a step should behave differently depending on context — a coin flip, a fill, which iteration of the pattern is playing. Control flow words let you branch, choose, and repeat inside a step's script. Control structures are essential for programming and allow you to create complex and dynamic patterns.
|
||||
|
||||
## Branching with if / else / then
|
||||
## if / else / then
|
||||
|
||||
Push a condition, then `if`. Everything between `if` and `then` runs only when the condition is truthy:
|
||||
The simplest branch. Push a condition, then `if`:
|
||||
|
||||
```forth
|
||||
;; degrade sound if true
|
||||
coin if
|
||||
7 crush
|
||||
then
|
||||
sine sound
|
||||
c4 note
|
||||
1 decay
|
||||
.
|
||||
coin if 0.8 gain then
|
||||
saw s c4 note .
|
||||
```
|
||||
|
||||
The crush is applied on half the hits. The sound always plays. Add `else` for a two-way split:
|
||||
The gain is applied if the coin flip is true. The sound will always plays. Add `else` for a two-way split:
|
||||
|
||||
```forth
|
||||
coin if
|
||||
c5 note
|
||||
c4 note
|
||||
else
|
||||
c3 note
|
||||
then
|
||||
saw sound
|
||||
0.3 verb
|
||||
0.5 decay
|
||||
0.6 gain
|
||||
.
|
||||
saw s 0.6 gain .
|
||||
```
|
||||
|
||||
These are compiled directly into branch instructions — they will not appear in the dictionary. This is a "low level" way to use conditionals in Cagire.
|
||||
These are compiled directly into branch instructions. For that reason, these words will not appear in the dictionary.
|
||||
|
||||
## Matching with case
|
||||
## ? and !?
|
||||
|
||||
When you already have a quotation, `?` executes it if the condition is truthy:
|
||||
|
||||
```forth
|
||||
{ 0.4 verb } coin ?
|
||||
saw s c4 note 0.5 gain . ;; reverb on half the hits
|
||||
```
|
||||
|
||||
`!?` is the opposite — executes when falsy:
|
||||
|
||||
```forth
|
||||
{ 0.2 gain } coin !?
|
||||
saw s c4 note . ;; quiet on half the hits
|
||||
```
|
||||
|
||||
These pair well with `chance`, `prob`, and the other probability words:
|
||||
|
||||
```forth
|
||||
{ 0.5 verb } 0.3 chance ? ;; occasional reverb wash
|
||||
{ 12 + } fill ? ;; octave up during fills
|
||||
```
|
||||
|
||||
## ifelse
|
||||
|
||||
Two quotations, one condition. The true branch comes first:
|
||||
|
||||
```forth
|
||||
{ c3 note } { c4 note } coin ifelse
|
||||
saw s 0.6 gain . ;; bass or lead, coin flip
|
||||
```
|
||||
|
||||
Reads naturally: "c3 or c4, depending on the coin."
|
||||
|
||||
```forth
|
||||
{ 0.8 gain } { 0.3 gain } fill ifelse
|
||||
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
|
||||
```
|
||||
|
||||
## select
|
||||
|
||||
Choose the nth option from a list of quotations:
|
||||
|
||||
```forth
|
||||
{ c4 } { e4 } { g4 } { b4 } iter 4 mod select
|
||||
note sine s 0.5 decay .
|
||||
```
|
||||
|
||||
Four notes cycling through a major seventh chord, one per pattern iteration. The index is 0-based.
|
||||
|
||||
## apply
|
||||
|
||||
When you have a quotation and want to execute it unconditionally, use `apply`:
|
||||
|
||||
```forth
|
||||
{ dup + } apply ;; doubles the top value
|
||||
```
|
||||
|
||||
This is simpler than `?` when there is no condition to check. It pops the quotation and runs it.
|
||||
|
||||
## case / of / endof / endcase
|
||||
|
||||
For matching a value against several options. Cleaner than a chain of `if`s when you have more than two branches:
|
||||
|
||||
```forth
|
||||
1 8 rand 4 mod case
|
||||
iter 4 mod case
|
||||
0 of c3 note endof
|
||||
1 of e3 note endof
|
||||
2 of g3 note endof
|
||||
3 of a3 note endof
|
||||
endcase
|
||||
tri s
|
||||
2 fm 0.99 fmh
|
||||
0.6 gain 0.2 chorus
|
||||
1 decay
|
||||
800 lpf
|
||||
.
|
||||
saw s 0.6 gain 800 lpf .
|
||||
```
|
||||
|
||||
A different root note each time the pattern loops. The last line before `endcase` is the default — it runs when no `of` matched:
|
||||
A different root note each time the pattern loops.
|
||||
|
||||
The last line before `endcase` is the default — it runs when no `of` matched:
|
||||
|
||||
```forth
|
||||
iter 3 mod case
|
||||
0 of 0.9 gain endof
|
||||
0.4 gain
|
||||
0.4 gain ;; default: quieter
|
||||
endcase
|
||||
saw s
|
||||
.5 decay
|
||||
c4 note
|
||||
.
|
||||
saw s c4 note .
|
||||
```
|
||||
|
||||
Like `if/then`, `case` is compiled syntax and does not appear in the dictionary.
|
||||
## times
|
||||
|
||||
## Quotation Words
|
||||
|
||||
The remaining control flow words operate on quotations — `( ... )` blocks sitting on the stack. Each word pops one or more quotations and decides whether or how to execute them.
|
||||
|
||||
### ? and !?
|
||||
|
||||
`?` executes a quotation if the condition is truthy:
|
||||
Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0):
|
||||
|
||||
```forth
|
||||
( 0.4 verb 6 crush ) coin ?
|
||||
tri sound 2 fm 0.5 fmh
|
||||
c3 note 0.5 gain 2 decay
|
||||
.
|
||||
3 { c4 @i 4 * + note } times
|
||||
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 — a chord
|
||||
```
|
||||
|
||||
Reverb on half the hits. `!?` is the opposite — executes when falsy:
|
||||
Subdivide with `at`:
|
||||
|
||||
```forth
|
||||
( 0.5 delay 0.9 delayfeedback ) coin !?
|
||||
saw sound
|
||||
c4 note
|
||||
500 lpf
|
||||
0.5 decay
|
||||
0.5 gain
|
||||
.
|
||||
4 { @i 4 / at sine s c4 note 0.3 gain . } times
|
||||
```
|
||||
|
||||
Quiet on half the hits. These pair well with `chance` and `fill` from the Randomness tutorial.
|
||||
Four evenly spaced notes within the step.
|
||||
|
||||
### ifelse
|
||||
|
||||
Two quotations, one condition. The true branch comes first:
|
||||
Vary intensity per iteration:
|
||||
|
||||
```forth
|
||||
( c3 note ) ( c5 note ) coin ifelse
|
||||
saw sound 0.3 verb
|
||||
0.5 decay 0.6 gain
|
||||
.
|
||||
8 {
|
||||
@i 8 / at
|
||||
@i 4 mod 0 = if 0.7 else 0.2 then gain
|
||||
tri s c5 note 0.1 decay .
|
||||
} times
|
||||
```
|
||||
|
||||
Reads naturally: "c3 or c5, depending on the coin."
|
||||
|
||||
```forth
|
||||
( 0.8 gain ) ( 0.3 gain ) fill ifelse
|
||||
tri snd c4 note 0.2 decay .
|
||||
```
|
||||
|
||||
Loud during fills, quiet otherwise.
|
||||
|
||||
### select
|
||||
|
||||
Choose the nth quotation from a list. The index is 0-based:
|
||||
|
||||
```forth
|
||||
( c4 ) ( e4 ) ( g4 ) ( b4 ) 0 3 rand select
|
||||
note sine snd 0.5 decay .
|
||||
```
|
||||
|
||||
Four notes of a major seventh chord picked randomly. Note that this is unnecessarily complex :)
|
||||
|
||||
### apply
|
||||
|
||||
When you have a quotation and want to execute it unconditionally:
|
||||
|
||||
```forth
|
||||
( dup + ) apply
|
||||
```
|
||||
|
||||
Pops the quotation and runs it. Simpler than `?` when there is no condition to check.
|
||||
|
||||
## More!
|
||||
|
||||
For probability gates, periodic execution, and euclidean rhythms, see the Randomness tutorial. For generators and ranges, see the Generators tutorial.
|
||||
Eight notes per step. Every fourth one louder.
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# Cycling & Selection
|
||||
|
||||
These words all share a pattern: push values onto the stack, then select one. If the selected item is a quotation, it gets executed. If it is a plain value, it gets pushed. All of them support `[ ]` brackets for auto-counting.
|
||||
|
||||
## cycle / pcycle
|
||||
|
||||
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 snd . ;; 60, 64, 67, 60, 64, 67, ...
|
||||
```
|
||||
|
||||
`pcycle` advances based on `iter` — the pattern iteration count:
|
||||
|
||||
```forth
|
||||
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.
|
||||
|
||||
## bounce / pbounce
|
||||
|
||||
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 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`.
|
||||
|
||||
## choose
|
||||
|
||||
Uniform random selection:
|
||||
|
||||
```forth
|
||||
kick snare hat 3 choose snd . ;; random drum hit each time
|
||||
```
|
||||
|
||||
Unlike the cycling words, `choose` is nondeterministic — every evaluation picks independently.
|
||||
|
||||
## wchoose
|
||||
|
||||
Weighted random. Push value/weight pairs, then the count:
|
||||
|
||||
```forth
|
||||
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.
|
||||
|
||||
## index
|
||||
|
||||
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 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.
|
||||
|
||||
## Using with brackets
|
||||
|
||||
All these words take a count argument `n`. Square brackets compute that count for you:
|
||||
|
||||
```forth
|
||||
[ 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.
|
||||
|
||||
## Quotations
|
||||
|
||||
When any of these words selects a quotation, it executes it instead of pushing it:
|
||||
|
||||
```forth
|
||||
[ ( c4 note ) ( e4 note ) ( g4 note ) ] cycle
|
||||
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.
|
||||
|
||||
This works with all selection words. Mix plain values and quotations freely:
|
||||
|
||||
```forth
|
||||
[ ( hat snd 0.3 gain . ) ( snare snd . ) ( kick snd . ) ] choose
|
||||
```
|
||||
@@ -13,7 +13,8 @@ Use `:` to start a definition and `;` to end it:
|
||||
This creates a word called `double` that duplicates the top value and adds it to itself. Now you can use it:
|
||||
|
||||
```forth
|
||||
3 double print ;; leaves 6 on the stack
|
||||
3 double ;; leaves 6 on the stack
|
||||
5 double ;; leaves 10 on the stack
|
||||
```
|
||||
|
||||
The definition is simple: everything between `:` and `;` becomes the body of the word.
|
||||
@@ -24,7 +25,7 @@ When you define a word in one step, it becomes available to all other steps. Thi
|
||||
|
||||
Step 0:
|
||||
```forth
|
||||
: bass "saw" snd 0.8 gain 800 lpf ;
|
||||
: bass "saw" s 0.8 gain 800 lpf ;
|
||||
```
|
||||
|
||||
Step 4:
|
||||
@@ -75,7 +76,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" snd 0.3 gain 2 attack 0.5 verb ;
|
||||
: pad "sine" s 0.3 gain 2 attack 0.5 verb ;
|
||||
```
|
||||
|
||||
**Transpositions** and musical helpers:
|
||||
@@ -90,8 +91,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" snd . ;
|
||||
: hat "hat" snd 0.4 gain . ;
|
||||
: kick "kick" s . ;
|
||||
: hat "hat" s 0.4 gain . ;
|
||||
```
|
||||
|
||||
Then a step becomes trivial:
|
||||
|
||||
@@ -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` → `snd`). 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` → `s`). You will learn aliases quite naturally, because aliases are usually reserved for very common words.
|
||||
|
||||
@@ -10,7 +10,7 @@ Classic Forth uses parentheses for comments:
|
||||
( this is a comment )
|
||||
```
|
||||
|
||||
In Cagire, parentheses create quotations, so comments use double semicolons instead:
|
||||
Cagire uses double semicolons:
|
||||
|
||||
```forth
|
||||
;; this is a comment
|
||||
@@ -18,6 +18,18 @@ In Cagire, parentheses create quotations, so comments use double semicolons inst
|
||||
|
||||
Everything after `;;` until the end of the line is ignored.
|
||||
|
||||
## Quotations
|
||||
|
||||
Classic Forth has no quotations. Code is not a value you can pass around.
|
||||
|
||||
Cagire has first-class quotations using curly braces:
|
||||
|
||||
```forth
|
||||
{ dup + }
|
||||
```
|
||||
|
||||
This pushes a block of code onto the stack. You can store it, pass it to other words, and execute it later. Quotations enable conditionals, probability, and cycling.
|
||||
|
||||
## Conditionals
|
||||
|
||||
Classic Forth uses `IF ... ELSE ... THEN`:
|
||||
@@ -29,14 +41,14 @@ x 0 > IF 1 ELSE -1 THEN
|
||||
Cagire supports this syntax but also provides quotation-based conditionals:
|
||||
|
||||
```forth
|
||||
( 1 ) ( -1 ) x 0 > ifelse
|
||||
{ 1 } { -1 } x 0 > ifelse
|
||||
```
|
||||
|
||||
The words `?` and `!?` execute a quotation based on a condition:
|
||||
|
||||
```forth
|
||||
( "kick" snd . ) coin ? ;; execute if coin is 1
|
||||
( "snare" snd . ) coin !? ;; execute if coin is 0
|
||||
{ "kick" s . } coin ? ;; execute if coin is 1
|
||||
{ "snare" s . } coin !? ;; execute if coin is 0
|
||||
```
|
||||
|
||||
## Strings
|
||||
@@ -56,7 +68,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 snd . ;; "kick" is not a word, so it becomes the string "kick"
|
||||
kick s . ;; "kick" is not a word, so it becomes the string "kick"
|
||||
myweirdname ;; pushes "myweirdname" onto the stack
|
||||
```
|
||||
|
||||
@@ -104,21 +116,21 @@ Classic Forth has `DO ... LOOP`:
|
||||
Cagire uses a quotation-based loop with `times`:
|
||||
|
||||
```forth
|
||||
4 ( @i . ) times ;; prints 0 1 2 3
|
||||
4 { @i . } times ;; prints 0 1 2 3
|
||||
```
|
||||
|
||||
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 snd . ) times ;; hat at 0, 0.25, 0.5, 0.75
|
||||
4 ( c4 @i + note sine snd . ) times ;; ascending notes
|
||||
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
|
||||
```
|
||||
|
||||
For generating sequences without side effects, use `..` or `gen`:
|
||||
|
||||
```forth
|
||||
1 5 .. ;; pushes 1 2 3 4 5
|
||||
( dup * ) 4 gen ;; pushes 0 1 4 9 (squares)
|
||||
{ dup * } 4 gen ;; pushes 0 1 4 9 (squares)
|
||||
```
|
||||
|
||||
## The Command Register
|
||||
@@ -155,11 +167,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" 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
|
||||
{ "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
|
||||
```
|
||||
|
||||
These words take a quotation and execute it probabilistically.
|
||||
@@ -169,27 +181,52 @@ These words take a quotation and execute it probabilistically.
|
||||
Execute a quotation on specific iterations:
|
||||
|
||||
```forth
|
||||
( "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
|
||||
{ "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
|
||||
```
|
||||
|
||||
`every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped.
|
||||
|
||||
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits as evenly as possible across n positions. `bjork` counts by step runs, `pbjork` counts by pattern iterations. Classic Euclidean rhythms: tresillo (3,8), cinquillo (5,8), son clave (5,16).
|
||||
|
||||
## Cycling
|
||||
|
||||
Cagire has built-in support for cycling through values. Push values onto the stack, then select one based on pattern state:
|
||||
|
||||
```forth
|
||||
60 64 67 3 cycle note
|
||||
```
|
||||
|
||||
Each time the step runs, a different note is selected. The `3` tells `cycle` how many values to pick from.
|
||||
|
||||
You can also use quotations if you need to execute code:
|
||||
|
||||
```forth
|
||||
{ c4 note } { e4 note } { g4 note } 3 cycle
|
||||
```
|
||||
|
||||
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.
|
||||
|
||||
Two cycling words exist:
|
||||
|
||||
- `cycle` - selects based on `runs` (how many times this step has played)
|
||||
- `pcycle` - selects based on `iter` (how many times the pattern has looped)
|
||||
|
||||
The difference between `cycle` and `pcycle` matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
|
||||
|
||||
## Polyphonic Parameters
|
||||
|
||||
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 snd . ;; emits 3 voices with notes 60, 64, 67
|
||||
60 64 67 note sine s . ;; 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 snd . ;; 2 voices: sine at 440, tri at 880
|
||||
440 880 freq sine tri s . ;; 2 voices: sine at 440, tri at 880
|
||||
```
|
||||
|
||||
When params have different lengths, shorter lists cycle:
|
||||
@@ -197,7 +234,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 snd . ;; emits 3 voices
|
||||
sine s . ;; emits 3 voices
|
||||
```
|
||||
|
||||
Polyphony multiplies with `at` deltas:
|
||||
@@ -205,7 +242,7 @@ Polyphony multiplies with `at` deltas:
|
||||
```forth
|
||||
0 0.5 at ;; 2 time points
|
||||
60 64 note ;; 2 notes
|
||||
sine snd . ;; emits 4 voices (2 notes × 2 times)
|
||||
sine s . ;; emits 4 voices (2 notes × 2 times)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1,69 +1,44 @@
|
||||
# Preludes
|
||||
# The Prelude
|
||||
|
||||
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) |
|
||||
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.
|
||||
|
||||
## Naming Your Sounds
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```forth
|
||||
pulse sound c2 note 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width .
|
||||
pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
```
|
||||
|
||||
In the bank prelude, define it once:
|
||||
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:
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 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 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 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 . ;
|
||||
|
||||
@@ -74,11 +49,11 @@ Now every step just writes `c2 note bass`. Change the sound in one place, every
|
||||
: loud 0.9 gain ;
|
||||
```
|
||||
|
||||
Steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
|
||||
By using the prelude and predefined words, steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
|
||||
|
||||
## Setting Initial State
|
||||
|
||||
The project prelude is the right place for global state:
|
||||
The prelude also runs plain Forth, not just definitions. You can use it to set variables and seed the random generator:
|
||||
|
||||
```forth
|
||||
c2 !root
|
||||
@@ -86,18 +61,18 @@ c2 !root
|
||||
42 seed
|
||||
```
|
||||
|
||||
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible.
|
||||
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible — same seed, same sequence every time you hit play.
|
||||
|
||||
## When Preludes Run
|
||||
## When It Runs
|
||||
|
||||
Preludes evaluate at three moments:
|
||||
The prelude evaluates 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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## What Not to Put Here
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -89,7 +89,7 @@ The fix is simple: make sure you push enough values before calling a word. Check
|
||||
* **Leftover values** are the opposite problem: values remain on the stack after your script finishes. This is less critical but indicates sloppy code. If your script leaves unused values behind, you probably made a mistake somewhere.
|
||||
|
||||
```forth
|
||||
3 4 5 + ;; 3 is still on the stack, unconsumed
|
||||
3 4 5 + . ;; plays a sound, but 3 is still on the stack
|
||||
```
|
||||
|
||||
The `3` was never used. Either it should not be there, or you forgot a word that consumes it.
|
||||
|
||||
@@ -6,8 +6,6 @@ Cagire organizes all your patterns and data following a strict hierarchy:
|
||||
- **Banks** contain **Patterns**.
|
||||
- **Patterns** contain **Steps**.
|
||||
|
||||
If strict organization isn't your style, don't worry, you can ignore banks entirely and just work in a single pattern. You can also escape the strict metric using sub-step timing and randomness.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
@@ -17,7 +15,7 @@ Project
|
||||
└── 1024 Steps (per pattern)
|
||||
```
|
||||
|
||||
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps. This means that you can create a staggering amount of music. Don't hesitate to create copies, variations, and explore the pattern system thoroughly. The more you add, the more surprising it becomes.
|
||||
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps. This means that you can create a staggering amount of things. Don't hesitate to create copies, variations, and explore the pattern system thoroughly.
|
||||
|
||||
## Patterns
|
||||
|
||||
@@ -31,7 +29,7 @@ Each pattern is an independent sequence of steps with its own properties:
|
||||
| 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!
|
||||
Press `e` in the patterns view to edit these settings.
|
||||
|
||||
### Follow Up
|
||||
|
||||
@@ -46,26 +44,22 @@ The follow-up action determines what happens when a pattern reaches the end of i
|
||||
Access the patterns view with `F2` (or `Ctrl+Up` from the sequencer). The view shows all banks and patterns in a grid. Indicators show pattern state:
|
||||
|
||||
- `>` Currently playing
|
||||
- `+` Armed to play
|
||||
- `-` Armed to stop
|
||||
- `+` Staged to play
|
||||
- `-` Staged to stop
|
||||
- `M` Muted
|
||||
- `S` Soloed
|
||||
|
||||
It is quite essential for you to understand the arm / launch system in order to use patterns. Please read the next section carefully!
|
||||
|
||||
### Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Arrows` | Navigate banks and patterns |
|
||||
| `Enter` | Select and return to sequencer |
|
||||
| `p` | Arm pattern to play/stop |
|
||||
| `c` | Launch armed changes |
|
||||
| `m` / `x` | Arm mute / solo toggle |
|
||||
| `p` | Stage pattern to play/stop |
|
||||
| `c` | Commit staged changes |
|
||||
| `m` / `x` | Stage mute / solo toggle |
|
||||
| `e` | Edit pattern properties |
|
||||
| `r` | Rename bank or pattern |
|
||||
| `Ctrl+c` / `Ctrl+v` | Copy / Paste |
|
||||
| `Delete` | Reset to empty pattern |
|
||||
| `Esc` | Cancel armed changes |
|
||||
|
||||
|
||||
| `Esc` | Cancel staged changes |
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Big Picture
|
||||
|
||||
> **What exactly is Cagire? What purpose does it serve?**
|
||||
|
||||
Cagire is a small and simple software that allows you to create music live while programming short scripts. At heart, it is really nothing more than a classic step sequencer, the kind you can buy in a music store. It is deliberately kept small and simple in form, but it goes rather deep if you take the time to discover the audio engine and all its capabilities. Adding the Forth language to program steps allows you to create patterns and behaviors of any complexity. Forth also makes it easy to extend and to customize Cagire while keeping the core mechanisms and the logic simple.
|
||||
Let's answer some basic questions: what exactly is Cagire? What purpose does it serve? Cagire is a small and simple piece of software that allows you to create music live while playing with scripts. At heart, it is really nothing more than a classic step sequencer, the kind you can buy in a music store. It is deliberately kept small and simple. Adding the Forth language to program steps allows you to create patterns and behaviors of any complexity. Forth also makes it super easy to extend and to customize Cagire while keeping the core mechanisms and the logic simple.
|
||||
|
||||
Cagire is not complex, it is just very peculiar. It has been created as a hybrid between a step sequencer and a programming environment. It allows you to create music live and to extend and customize it using the power of Forth. It has been designed to be fast and responsive, low-tech in the sense that you can run it on any decent computer. You can think of it as a musical instrument. You learn it by getting into the flow and practicing. What you ultimately do with it is up to you: improvisation, composition, etc. Cagire is also made to be autonomous, self-contained, and self-sustaining: it contains all the necessary components to make music without relying on external software or hardware.
|
||||
|
||||
@@ -10,40 +8,29 @@ Cagire is not complex, it is just very peculiar. It has been created as a hybrid
|
||||
|
||||
A traditional step sequencer would offer the musician a grid where each step represents a note or a single musical event. Cagire replaces notes and/or events in favour of **Forth scripts**. When the sequencer reaches a step to play, it runs the script associated with it. A script can do whatever it is programmed to do: play a note, trigger a sample, apply effects, generate randomness, or all of the above. Scripts can share code and data with each other. Everything else works like a regular step sequencer: you can toggle, copy, paste, and rearrange steps freely.
|
||||
|
||||
```forth
|
||||
0.0 8.0 rand at
|
||||
sine sound
|
||||
200 2000 rand 100 4000 rand
|
||||
4 slide freq 0.6 verb 2 vib
|
||||
0.125 vibmod 0.2 chorus
|
||||
0.4 0.6 rand gain
|
||||
.
|
||||
```
|
||||
|
||||
## What Does a Script Look Like?
|
||||
|
||||
A Forth script is generally kind of small, and it solves a simple problem: playing a chord, tweaking some parameters, etc. The more focused it is, the better. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. Here is a program that plays a middle C note for two steps using a sine wave:
|
||||
A Forth script is generally kind of small, and it solves a simple problem: playing a chord, tweaking some parameters, etc. The more focused it is, the better. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. Here is a program that plays a middle C note using a sine wave:
|
||||
|
||||
```forth
|
||||
c4 note sine sound 2 decay .
|
||||
c4 note sine sound .
|
||||
```
|
||||
|
||||
Read it backwards and you will understand what it does:
|
||||
|
||||
- `.` — play a sound.
|
||||
- `2 decay` — the sound takes two steps to die.
|
||||
- `sine sound` — the sound is a sine wave.
|
||||
- `c4 note` — the pitch is C4 (middle C).
|
||||
|
||||
There is pretty much no syntax to learn, just three rules:
|
||||
Five tokens separated by spaces. There is pretty much no syntax to learn, just three rules:
|
||||
|
||||
- There are `words` and `numbers`.
|
||||
- A `word` is anything that is not a space or a number (can include symbols).
|
||||
- A `word` is anything that is not a space or a number.
|
||||
- A `number` is anything that is not a space or a word.
|
||||
- They are separated by spaces.
|
||||
- Everything piles up on the **stack**.
|
||||
|
||||
The stack is what makes Forth tick. Think of it as a pile of things. `c4` puts a pitch on the pile. `note` picks it up. `sine` chooses a waveform. `sound` assembles everything into a voice. `.` plays it. Each word picks up what the previous ones left behind and leaves something for the next. Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. Cagire requires you to understand what the stack is. The good thing is that it will take five minutes for you to make sense of it. See the **Forth** section for details.
|
||||
The stack is what makes Forth tick. Think of it as a pile of things. `c4` puts a pitch on the pile. `note` picks it up. `sine` chooses a waveform. `sound` assembles everything into a voice. `.` plays it. Each word picks up what the previous ones left behind and leaves something for the next. Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. You will need to understand the stack, but it will take five minutes. See the **Forth** section for details.
|
||||
|
||||
## The Audio Engine
|
||||
|
||||
@@ -51,7 +38,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 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 dur .
|
||||
100 199 freq saw sound 250 lpf 8 lpe 1 lpd 0.2 chorus 0.8 verb 2 dur .
|
||||
```
|
||||
|
||||
```forth
|
||||
@@ -61,12 +48,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 2 dur .
|
||||
white sine sound 100 freq 0.5 decay 24 penv 0.5 pdec 2 dur .
|
||||
```
|
||||
|
||||
```forth
|
||||
;; random robot noises: sine + randomized freq + ring modulation
|
||||
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmdepth .
|
||||
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmddepth .
|
||||
```
|
||||
|
||||
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!
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Editing a Step
|
||||
|
||||
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the code editor. The editor appears as a modal overlay with the step number in the title bar. If the step is a mirrored step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
|
||||
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the code editor. The editor appears as a modal overlay with the step number in the title bar. If the step is a linked step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
|
||||
|
||||
## Writing Scripts
|
||||
|
||||
@@ -18,7 +18,7 @@ Add parameters before words to modify them:
|
||||
c4 note 0.75 decay sine sound .
|
||||
```
|
||||
|
||||
Writing long lines can become tedious. Instead, break your code into multiple lines for clarity:
|
||||
Writing long lines is not recommended because it can become quite unmanageable. Instead, break them into multiple lines for clarity:
|
||||
|
||||
```forth
|
||||
;; the same sound on multiple lines
|
||||
@@ -29,12 +29,6 @@ sine sound
|
||||
.
|
||||
```
|
||||
|
||||
Forth has no special rule about what a line should look like and space has no meaning.
|
||||
|
||||
## Adding comments to your code
|
||||
|
||||
You can comment a line using `;;`. This is not very common for people that are used to Forth. There are no multiline comments.
|
||||
|
||||
## Saving
|
||||
|
||||
- `Esc` — Save, compile, and close the editor.
|
||||
@@ -70,6 +64,27 @@ Press `Ctrl+F` to open the search bar. Type your query, then navigate matches:
|
||||
- `Enter` — Confirm and close search.
|
||||
- `Esc` — Cancel search.
|
||||
|
||||
## Script preview
|
||||
## Debugging
|
||||
|
||||
Press `Ctrl+R` to execute the script immediately as a one-shot, without waiting for the sequencer to reach the step. A green flash indicates success, red indicates an error. This is super useful for sound design. It also works when hovering on a step with the editor closed.
|
||||
Press `Ctrl+S` to toggle the stack display. This shows the stack state evaluated up to the cursor line, useful for understanding how values flow through your script.
|
||||
|
||||
Press `Ctrl+R` to execute the script immediately as a one-shot, without waiting for the sequencer to reach the step. A green flash indicates success, red indicates an error.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Esc` | Save and close |
|
||||
| `Ctrl+E` | Evaluate (save + compile in place) |
|
||||
| `Ctrl+R` | Execute script once |
|
||||
| `Ctrl+S` | Toggle stack display |
|
||||
| `Ctrl+B` | Open sample finder |
|
||||
| `Ctrl+F` | Search |
|
||||
| `Ctrl+N` | Next match / next suggestion |
|
||||
| `Ctrl+P` | Previous match / previous suggestion |
|
||||
| `Ctrl+A` | Select all |
|
||||
| `Ctrl+C` | Copy |
|
||||
| `Ctrl+X` | Cut |
|
||||
| `Ctrl+V` | Paste |
|
||||
| `Shift+Arrows` | Extend selection |
|
||||
| `Tab` | Accept completion / sample |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Audio Engine
|
||||
|
||||
The Engine page (`F6`) is where you configure audio hardware, manage MIDI connections, set up Ableton Link, and manage your sample library. The left column holds six configuration sections — press `Tab` to move between them, `Shift+Tab` to go back. The right column is a read-only monitoring panel with VU meters, status metrics, and an oscilloscope.
|
||||
The Engine page (`F6`) is where you configure audio hardware, adjust performance settings, and manage your sample library. The right side of the page shows a real-time oscilloscope and spectrum analyzer. The page is divided into three sections. Press `Tab` to move between them, `Shift+Tab` to go back.
|
||||
|
||||
## Devices
|
||||
|
||||
@@ -17,29 +17,11 @@ Four audio parameters are adjustable with `Left`/`Right`:
|
||||
| Voices | 1–128 | Maximum polyphony (simultaneous sounds) |
|
||||
| Nudge | -100 to +100 ms | Timing offset to compensate for latency |
|
||||
|
||||
After changing the buffer size or channel count, press `Shift+r` to restart the audio engine for changes to take effect.
|
||||
|
||||
## Link
|
||||
|
||||
Ableton Link synchronizes tempo across devices and applications on the same network. Three settings are adjustable with `Left`/`Right`:
|
||||
|
||||
- **Enabled** — Turn Link on or off. A status badge next to the header shows DISABLED, LISTENING, or CONNECTED.
|
||||
- **Start/Stop Sync** — Whether play/stop commands are shared with other Link peers.
|
||||
- **Quantum** — Number of beats per phrase, used for phase alignment.
|
||||
|
||||
Below the settings, three read-only session values update in real time: Tempo, Beat, and Phase.
|
||||
|
||||
## MIDI Outputs
|
||||
|
||||
Four output slots (0–3). Browse with `Up`/`Down`, cycle available devices with `Left`/`Right`. A slot shows "(not connected)" until you assign a device.
|
||||
|
||||
## MIDI Inputs
|
||||
|
||||
Same layout as outputs — four input slots (0–3) with the same navigation.
|
||||
The last two rows — sample rate and audio host — are read-only values reported by your system. After changing the buffer size or channel count, press `Shift+r` to restart the audio engine for changes to take effect.
|
||||
|
||||
## Samples
|
||||
|
||||
This section shows how many sample directories are registered and how many files have been indexed. Browse existing paths with `Up`/`Down`. Press `A` to open a file browser and add a new sample directory. Press `D` to remove the selected path. Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
|
||||
This section shows how many sample directories are registered and how many files have been indexed. Press `A` to open a file browser and add a new sample directory. Press `D` to remove the last one. Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
|
||||
|
||||
Sample directories must be added here before you can use the sample browser or reference samples in your scripts.
|
||||
|
||||
@@ -48,17 +30,23 @@ Sample directories must be added here before you can use the sample browser or r
|
||||
A few keys work from anywhere on the Engine page:
|
||||
|
||||
- `h` — Hush. Silence all audio immediately.
|
||||
- `p` — Panic. Hard stop, clears all active voices, stop all patterns.
|
||||
- `p` — Panic. Hard stop, clears all active voices.
|
||||
- `t` — Test tone. Plays a brief sine wave to verify audio output.
|
||||
- `r` — Reset the peak voice counter.
|
||||
- `Shift+r` — Restart the audio engine.
|
||||
|
||||
## Monitoring
|
||||
## Keybindings
|
||||
|
||||
The right column displays a live overview of the engine state. Everything here is read-only.
|
||||
|
||||
- **VU Meters** — Left and right channel levels with horizontal bars and dB readouts. Green below -12 dB, yellow approaching 0 dB, red above.
|
||||
|
||||
- **Status** — CPU load (with bar graph), active voices and peak count, scheduled events, schedule depth, nudge offset, sample rate, audio host, and Link peers (when connected).
|
||||
|
||||
- **Scope** — An oscilloscope showing the current audio output waveform.
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Tab` / `Shift+Tab` | Next / previous section |
|
||||
| `Up` / `Down` | Navigate within section |
|
||||
| `Left` / `Right` | Switch device column / adjust setting |
|
||||
| `PageUp` / `PageDown` | Scroll device list |
|
||||
| `Enter` | Select device |
|
||||
| `D` | Refresh devices / remove last sample path |
|
||||
| `A` | Add sample directory |
|
||||
| `Shift+r` | Restart audio engine |
|
||||
| `h` | Hush |
|
||||
| `p` | Panic |
|
||||
| `t` | Test tone |
|
||||
| `r` | Reset peak voices |
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
# The Sequencer Grid
|
||||
|
||||
The sequencer grid (`F5`) is where you spend most of your time in Cagire. It shows the step sequencer and lets you edit each step using the code editor. This is the first view you see when you open the application. Optional widgets — oscilloscope, spectrum analyzer, goniometer, prelude preview, and step preview — can be toggled on for visual feedback while you work.
|
||||
The sequencer grid is the main view of Cagire (`F5`). This is the one you see when you open the application. On this view, you can see the step sequencer grid and edit each step using the code editor. You can optionally display the following widgets:
|
||||
- **an oscilloscope**: visualize the current audio output.
|
||||
- **a spectrum analyzer**: 32 bands spectrum analyze (mostly cosmetic).
|
||||
- **a step preview**: visualize the content of the hovered script.
|
||||
|
||||
You can press `o` to cycle through layouts. It will basically rotate the sequencer around. Use it to find the view that makes the more sense for you.
|
||||
|
||||
## Navigation
|
||||
|
||||
Use arrow keys to move between steps. `Shift+arrows` selects multiple steps, and `Esc` clears any selection. The grid wraps around at pattern boundaries. Press `:` to jump directly to a step by number.
|
||||
Use arrow keys to move between steps. The grid wraps around at pattern boundaries. Press `:` to jump directly to a step by number. This keybinding is useful for very long patterns.
|
||||
|
||||
- `Alt+Up` / `Alt+Down` — Previous / next pattern
|
||||
- `Alt+Left` / `Alt+Right` — Previous / next bank
|
||||
## Preview
|
||||
|
||||
Press `p` to enter preview mode. A read-only code editor opens showing the script of the step under the cursor. You can still navigate the grid while previewing. Press `Esc` to exit preview mode.
|
||||
|
||||
## Selection
|
||||
|
||||
Hold `Shift` while pressing arrow keys to select multiple steps. Press `Esc` to clear the selection.
|
||||
|
||||
## Editing Steps
|
||||
|
||||
- `Enter` — Open the script editor
|
||||
- `t` — Make a step active / inactive
|
||||
- `t` — Toggle step active/inactive
|
||||
- `r` — Rename a step
|
||||
- `Del` — Delete selected steps
|
||||
|
||||
## Mirrored Steps
|
||||
|
||||
Imagine a drum pattern where four steps play the same kick script. You tweak the sound on one of them — now you have to find and edit all four. Mirrored steps solve this: one step is the source, the others are mirrors that always reflect its script. Edit the source once, every mirror follows.
|
||||
|
||||
On the grid, mirrors are easy to spot. They show an arrow prefix like `→05`, meaning "I mirror step 05." Steps that share a source also share a background color, so clusters of linked steps are visible at a glance.
|
||||
|
||||
To create mirrors: copy a step with `Ctrl+C`, then paste with `Ctrl+B` instead of `Ctrl+V`. The pasted steps become mirrors of the original. Pressing `Enter` on a mirror jumps to its source and opens the editor there. If you want to break the link and make a mirror independent again, press `Ctrl+H` to harden it back into a regular copy.
|
||||
|
||||
## Copy & Paste
|
||||
|
||||
- `Ctrl+C` — Copy selected steps
|
||||
- `Ctrl+V` — Paste as independent copies
|
||||
- `Ctrl+B` — Paste as mirrored steps
|
||||
- `Ctrl+V` — Paste as copies
|
||||
- `Ctrl+B` — Paste as linked steps
|
||||
- `Ctrl+D` — Duplicate selection
|
||||
- `Ctrl+H` — Harden mirrors (convert to independent copies)
|
||||
- `Ctrl+H` — Harden links (convert to independent copies)
|
||||
|
||||
## Preludes
|
||||
|
||||
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 current bank's prelude editor
|
||||
- `P` — Open project prelude editor
|
||||
- `d` — Evaluate all preludes (project + all banks)
|
||||
Linked steps share the same script as their source. When you edit the source, all linked steps update automatically. This is an extremely important and powerful feature. It allows you to create complex patterns with minimal effort. `Ctrl+H` converts linked steps back to independent copies.
|
||||
|
||||
## Pattern Controls
|
||||
|
||||
Each pattern has its own length and speed. Length sets how many steps it cycles through. Speed is a multiplier on the global tempo.
|
||||
|
||||
- `<` / `>` — Decrease / increase pattern length
|
||||
- `[` / `]` — Decrease / increase pattern speed
|
||||
- `L` — Set length directly
|
||||
@@ -51,8 +45,6 @@ Each pattern has its own length and speed. Length sets how many steps it cycles
|
||||
|
||||
## Playback
|
||||
|
||||
Playback starts and stops globally across all unmuted patterns. The highlighted cell on the grid marks the currently playing step.
|
||||
|
||||
- `Space` — Toggle play / stop
|
||||
- `+` / `-` — Adjust tempo
|
||||
- `T` — Set tempo directly
|
||||
@@ -60,24 +52,57 @@ Playback starts and stops globally across all unmuted patterns. The highlighted
|
||||
|
||||
## Mute & Solo
|
||||
|
||||
Mute silences a pattern; solo silences everything except it. Both work while playing.
|
||||
|
||||
- `m` — Mute current pattern
|
||||
- `x` — Solo current pattern
|
||||
- `Shift+m` — Clear all mutes
|
||||
- `Shift+x` — Clear all solos
|
||||
|
||||
## Project
|
||||
## Prelude
|
||||
|
||||
- `s` — Save project
|
||||
- `l` — Load project
|
||||
- `q` — Quit
|
||||
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
|
||||
|
||||
- `d` — Open the prelude editor
|
||||
- `Shift+d` — Evaluate the prelude
|
||||
|
||||
## Tools
|
||||
|
||||
A few utilities accessible from the grid.
|
||||
|
||||
- `e` — Euclidean rhythm distribution
|
||||
- `?` — Show keybindings help
|
||||
- `o` — Cycle layout
|
||||
- `Tab` — Toggle sample browser panel
|
||||
|
||||
## Visual Indicators
|
||||
|
||||
- **Highlighted cell** — Currently playing step
|
||||
- **Colored backgrounds** — Linked steps share colors by source
|
||||
- **Arrow prefix** (`→05`) — Step is linked to step 05
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Arrows` | Navigate grid |
|
||||
| `Shift+Arrows` | Extend selection |
|
||||
| `:` | Jump to step |
|
||||
| `Enter` | Open editor |
|
||||
| `p` | Preview step |
|
||||
| `t` | Toggle step active |
|
||||
| `r` | Rename step |
|
||||
| `Del` | Delete steps |
|
||||
| `Ctrl+C` / `Ctrl+V` | Copy / Paste |
|
||||
| `Ctrl+B` | Paste as links |
|
||||
| `Ctrl+D` | Duplicate |
|
||||
| `Ctrl+H` | Harden links |
|
||||
| `<` / `>` | Pattern length |
|
||||
| `[` / `]` | Pattern speed |
|
||||
| `L` / `S` | Set length / speed |
|
||||
| `Space` | Play / Stop |
|
||||
| `+` / `-` | Tempo up / down |
|
||||
| `T` | Set tempo |
|
||||
| `Ctrl+R` | Execute step once |
|
||||
| `m` / `x` | Mute / Solo |
|
||||
| `d` | Prelude editor |
|
||||
| `e` | Euclidean distribution |
|
||||
| `o` | Cycle layout |
|
||||
| `Tab` | Sample browser |
|
||||
| `Ctrl+Z` | Undo |
|
||||
| `Ctrl+Shift+Z` | Redo |
|
||||
| `?` | Show keybindings |
|
||||
|
||||
@@ -1,6 +1,41 @@
|
||||
# Navigation
|
||||
|
||||
Press `?` on any view to see its keybindings. The most important shortcuts are always displayed in the footer bar. Press `Esc` to close the keybindings panel. These shortcuts work on every view:
|
||||
Cagire's interface is organized as a 3x2 grid of six views:
|
||||
|
||||
```
|
||||
Dict Patterns Options
|
||||
Help Sequencer Engine
|
||||
```
|
||||
|
||||
- *Dict* : Forth dictionary — learn about the language.
|
||||
- *Help* : Help and tutorials — learn about the tool.
|
||||
- *Patterns* : Manage your current session / project.
|
||||
- *Sequencer* : The main view, where you edit sequences and play music.
|
||||
- *Options* : Configuration settings for the application.
|
||||
- *Engine* : Configuration settings for the audio engine.
|
||||
|
||||
## Switching Views
|
||||
|
||||
Use `Ctrl+Arrow` keys to move between views. A minimap will briefly appear to show your position in the grid. You can also click on the view name at the bottom left to open the switch view panel.
|
||||
|
||||
- `Ctrl+Left` / `Ctrl+Right` — move horizontally (wraps around)
|
||||
- `Ctrl+Up` / `Ctrl+Down` — move vertically (does not wrap)
|
||||
- `Click` at bottom left — select a view
|
||||
|
||||
You can also jump directly to any view with the F-keys:
|
||||
|
||||
| Key | View |
|
||||
|------|------------|
|
||||
| `F1` | Dict |
|
||||
| `F2` | Patterns |
|
||||
| `F3` | Options |
|
||||
| `F4` | Help |
|
||||
| `F5` | Sequencer |
|
||||
| `F6` | Engine |
|
||||
|
||||
## Common Keys
|
||||
|
||||
These shortcuts work on every view:
|
||||
|
||||
| Key | Action |
|
||||
|---------|---------------------------|
|
||||
@@ -10,29 +45,6 @@ Press `?` on any view to see its keybindings. The most important shortcuts are a
|
||||
| `l` | Load project |
|
||||
| `?` | Show keybindings for view |
|
||||
|
||||
## Views
|
||||
## Getting Help
|
||||
|
||||
Cagire's interface is organized as a 3x2 grid of six views. Jump to any view with its F-key or `Ctrl+Arrow` keys:
|
||||
|
||||
```
|
||||
F1 Dict F2 Patterns F3 Options
|
||||
F4 Help F5 Sequencer F6 Engine
|
||||
```
|
||||
|
||||
| Key | View | Description |
|
||||
|------|------------|-------------|
|
||||
| `F1` | Dict | Forth dictionary — learn about the language |
|
||||
| `F2` | Patterns | Manage your current session / project |
|
||||
| `F3` | Options | Configuration settings for the application |
|
||||
| `F4` | Help | Help and tutorials — learn about the tool |
|
||||
| `F5` | Sequencer | The main view, where you edit sequences and play music |
|
||||
| `F6` | Engine | Configuration settings for the audio engine |
|
||||
|
||||
Use `Ctrl+Arrow` keys to move between adjacent views. A minimap will briefly appear to show your position in the grid. You can also click on the view name at the bottom left or in the top left corner of the header bar to open the switch view panel.
|
||||
|
||||
- `Ctrl+Left` / `Ctrl+Right` — move horizontally (wraps around)
|
||||
- `Ctrl+Up` / `Ctrl+Down` — move vertically (does not wrap)
|
||||
|
||||
## Secrets
|
||||
|
||||
There is a hidden seventh view: the **Periodic Script**. Press `F11` to open it. The periodic script is a free-running Forth script evaluated at every step, independent of any pattern. It is useful for drones, global effects, control logic, and experimentation. See the **Periodic Script** tutorial for details.
|
||||
Press `?` on any view to see the associated keybindings. This shows all available shortcuts for the current context. The most important keybindings are displayed in the footer bar. Press `Esc` to close the keybindings panel.
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
# Options
|
||||
|
||||
The Options page (`F3`) gathers display and onboarding settings in one place. Navigate with `Up`/`Down` or `Tab`, change values with `Left`/`Right`. All changes are saved automatically. A description line appears below the focused option to explain what it does.
|
||||
The Options page (`F3`) gathers all configuration settings in one place: display, synchronization and MIDI. Navigate options with `Up`/`Down` or `Tab`, change values with `Left`/`Right`. All changes are saved automatically.
|
||||
|
||||
## Display
|
||||
|
||||
| Option | Values | Description |
|
||||
|--------|--------|-------------|
|
||||
| Theme | (cycle) | Color scheme for the entire interface |
|
||||
| Hue rotation | 0–360° | Shift all theme colors by a hue angle (±5° per step) |
|
||||
| Hue rotation | 0–360° | Shift theme colors by a hue angle (±5° per step) |
|
||||
| Refresh rate | 60 / 30 / 15 fps | Lower values reduce CPU usage |
|
||||
| Runtime highlight | on / off | Highlight executed code spans during playback |
|
||||
| Show scope | on / off | Oscilloscope on the main view |
|
||||
| Show spectrum | on / off | Spectrum analyzer on the main view |
|
||||
| Show lissajous | on / off | XY stereo phase scope |
|
||||
| Gain boost | 1x – 16x | Amplify scope and lissajous waveforms |
|
||||
| Normalize | on / off | Auto-scale visualizations to fill the display |
|
||||
| Show scope | on / off | Oscilloscope on the engine page |
|
||||
| Show spectrum | on / off | Spectrum analyzer on the engine page |
|
||||
| Completion | on / off | Word completion popup in the editor |
|
||||
| Show preview | on / off | Step script preview on the sequencer grid |
|
||||
| Performance mode | on / off | Hide header and footer bars |
|
||||
| Font | 6x13 – 10x20 | Bitmap font size (plugin mode only) |
|
||||
| Zoom | 50% – 200% | Interface zoom factor (plugin mode only) |
|
||||
| Zoom | 0.5x – 2.0x | Interface zoom factor (plugin mode only) |
|
||||
| Window | (presets) | Window size presets (plugin mode only) |
|
||||
|
||||
## Ableton Link
|
||||
|
||||
Cagire uses Ableton Link to synchronize tempo with other applications on the same network. Three settings control the connection:
|
||||
|
||||
- **Enabled** — Turn Link on or off. When enabled, Cagire listens for peers and shares its tempo.
|
||||
- **Start/Stop sync** — When on, pressing play or stop in one app affects all peers.
|
||||
- **Quantum** — The beat subdivision used for phase alignment.
|
||||
|
||||
Below these settings, a read-only session display shows the current tempo, beat position, and phase. The status line at the top shows the connection state: disabled, listening, or connected with peer count.
|
||||
|
||||
## MIDI
|
||||
|
||||
Four output slots and four input slots let you connect to MIDI devices. Cycle through available devices with `Left`/`Right`. Each slot can hold one device, and the same device cannot be assigned to multiple slots.
|
||||
|
||||
## Onboarding
|
||||
|
||||
- **Reset guides** — Re-enable all dismissed guide popups.
|
||||
- **Demo on startup** — Load a rotating demo song on fresh startup.
|
||||
At the bottom, you can reset the onboarding guides if you dismissed them earlier and want to see them again.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Up` / `Down` / `Tab` | Navigate options |
|
||||
| `Left` / `Right` | Change value |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Sample Browser
|
||||
|
||||
Press `Tab` on the sequencer grid to open the sample browser. It appears as a side panel showing a tree of all your sample directories and files. Press `Tab` again to close it. Before using the browser, you need to register at least one sample directory on the Engine page (`F6`). Cagire indexes audio files (`.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`) from all registered paths.
|
||||
Press `Tab` on the sequencer grid to open the sample browser. It appears as a side panel showing a tree of all your sample directories and files. Press `Tab` again to close it. Before using the browser, you need to register at least one sample directory on the Engine page (`F6`). Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
|
||||
|
||||
## Browsing
|
||||
|
||||
@@ -28,3 +28,15 @@ kick sound .
|
||||
|
||||
See the **Samples** section in the Audio Engine documentation for details on how sample playback works.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Tab` | Open / close browser |
|
||||
| `Up` / `Down` | Navigate |
|
||||
| `Right` | Expand folder / play file |
|
||||
| `Left` | Collapse folder |
|
||||
| `Enter` | Play file |
|
||||
| `PageUp` / `PageDown` | Fast scroll |
|
||||
| `/` | Search |
|
||||
| `Esc` | Clear search / close |
|
||||
|
||||
@@ -23,3 +23,15 @@ When saving, type a filename and press `Enter`. Parent directories are created a
|
||||
|
||||
When loading, browse to a `.cagire` file and press `Enter`. The project replaces the current session entirely.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `s` | Save (from any view) |
|
||||
| `l` | Load (from any view) |
|
||||
| `Up` / `Down` | Browse entries |
|
||||
| `Right` | Enter directory |
|
||||
| `Left` | Parent directory |
|
||||
| `Tab` | Autocomplete path |
|
||||
| `Enter` | Confirm |
|
||||
| `Esc` | Cancel |
|
||||
|
||||
@@ -1,58 +1,46 @@
|
||||
# Arm / Launch
|
||||
# Stage / Commit
|
||||
|
||||
In Cagire, changes to playback happen in two steps. First you **arm**: you mark what you want to happen. Then you **launch**: you apply all armed changes at once. Nothing changes until you launch. It is simpler than it sounds.
|
||||
Cagire requires you to `stage` changes you wish to make to the playback state and then `commit` it. It is way more simple than it seems. For instance, you mark pattern `04` and `05` to start playing, and _then_ you send the order to start the playback (`commit`). The same goes for stopping patterns. You mark which pattern to stop (`stage`) and then you give the order to stop them (`commit`). Why is staging useful? Here are some reasons why this design choice was made:
|
||||
|
||||
Say you want patterns `04` and `05` to start playing together. You arm both (`p` on each), then launch (`c`). Both start at the same time. Want to stop them later? Arm them again, launch again. That's it.
|
||||
- **To apply multiple changes**: Queue several patterns to start/stop, commit them together.
|
||||
- **To get clean timing**: All changes happen on beat/bar boundaries.
|
||||
- **To help with live performance**: Prepare the next section without affecting current playback.
|
||||
|
||||
This two-step process exists for good reasons:
|
||||
|
||||
- **Multiple changes at once**: queue several patterns to start/stop, launch them together.
|
||||
- **Clean timing**: all changes land on beat or bar boundaries, never mid-step.
|
||||
- **Safe preparation**: set up the next section while the current one keeps playing.
|
||||
|
||||
## Arm changes, then launch
|
||||
|
||||
Arming is an essential feature to understand to be effective when doing live performances:
|
||||
Staging is an essential feature to understand to be effective when doing live performances:
|
||||
|
||||
1. Open the **Patterns** view (`F2` or `Ctrl+Up` from sequencer)
|
||||
2. Navigate to a pattern you wish to change/play
|
||||
3. Press `p` to arm it. The pending change is going to be displayed:
|
||||
- `+` (armed to play)
|
||||
- `-` (armed to stop)
|
||||
- `m` (armed to mute)
|
||||
- `s` (armed to solo)
|
||||
- etc.
|
||||
3. Press `p` to stage it. The pending change is going to be displayed:
|
||||
- `+` (staged to play)
|
||||
- `-` (staged to stop)
|
||||
4. Repeat for other patterns you want to change
|
||||
5. Press `c` to launch all changes
|
||||
5. Press `c` to commit all changes
|
||||
6. Or press `Esc` to cancel
|
||||
|
||||
You can also arm mute/solo changes:
|
||||
You can also stage mute/solo changes:
|
||||
|
||||
- Press `m` to arm a mute toggle
|
||||
- Press `x` to arm a solo toggle
|
||||
- Press `m` to stage a mute toggle
|
||||
- Press `x` to stage a solo toggle
|
||||
- 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.
|
||||
It might wait for the next beat/bar boundary.
|
||||
A pattern might not start immediately depending on the sync mode you have chosen. It might wait for the next beat/bar boundary.
|
||||
|
||||
## Status Indicators
|
||||
|
||||
| Indicator | Meaning |
|
||||
|-----------|---------|
|
||||
| `>` | Currently playing |
|
||||
| `+` | Armed to play |
|
||||
| `-` | Armed to stop |
|
||||
| `+` | Staged to play |
|
||||
| `-` | Staged to stop |
|
||||
| `M` | Muted |
|
||||
| `S` | Soloed |
|
||||
|
||||
A pattern can show combined indicators, e.g. `>` (playing) and `-` (armed to stop), or `>M` (playing and muted).
|
||||
|
||||
Armed patterns blink to make pending changes impossible to miss.
|
||||
A pattern can show combined indicators, e.g. `>` (playing) and `-` (staged to stop), or `>M` (playing and muted).
|
||||
|
||||
## Quantization
|
||||
|
||||
Launched changes don't execute immediately. They wait for a quantization boundary:
|
||||
Committed changes don't execute immediately. They wait for a quantization boundary:
|
||||
|
||||
| Setting | Behavior |
|
||||
|---------|----------|
|
||||
|
||||
@@ -42,8 +42,8 @@ Crossfade between two sounds:
|
||||
|
||||
```forth
|
||||
1 1 ccval 127 / ;; normalize to 0.0-1.0
|
||||
dup saw snd swap gain .
|
||||
1 swap - tri snd swap gain .
|
||||
dup saw s swap gain .
|
||||
1 swap - tri s swap gain .
|
||||
```
|
||||
|
||||
## Scaling Values
|
||||
|
||||
@@ -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 snd c4 note 0.5 gain . ;; audio
|
||||
saw s c4 note 0.5 gain . ;; audio
|
||||
60 note 100 velocity m. ;; MIDI
|
||||
```
|
||||
|
||||
|
||||
@@ -7,31 +7,24 @@ 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 snd . ;; kick at the midpoint
|
||||
0.5 at kick s . ;; kick at the midpoint
|
||||
```
|
||||
|
||||
Push multiple values before calling `at` to get multiple emits from a single `.`:
|
||||
|
||||
```forth
|
||||
0 0.5 at kick snd .
|
||||
0 0.5 at kick s . ;; two kicks: one at start, one at midpoint
|
||||
0 0.25 0.5 0.75 at hat s . ;; four hats, evenly spaced
|
||||
```
|
||||
|
||||
Two kicks: one at start, one at midpoint.
|
||||
|
||||
```forth
|
||||
0 0.25 0.5 0.75 at hat snd .
|
||||
```
|
||||
|
||||
Four hats, evenly spaced.
|
||||
|
||||
The deltas persist across multiple `.` calls until `clear` or a new `at`:
|
||||
|
||||
```forth
|
||||
0 0.5 at
|
||||
kick snd . ;; 2 kicks
|
||||
hat snd . ;; 2 hats (same timing)
|
||||
kick s . ;; 2 kicks
|
||||
hat s . ;; 2 hats (same timing)
|
||||
clear
|
||||
snare snd . ;; 1 snare (deltas cleared)
|
||||
snare s . ;; 1 snare (deltas cleared)
|
||||
```
|
||||
|
||||
## Cross-product: at Without arp
|
||||
@@ -40,10 +33,10 @@ Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2
|
||||
|
||||
```forth
|
||||
0 0.5 at
|
||||
c4 e4 g4 note 1.5 decay sine snd .
|
||||
c4 e4 g4 note sine s . ;; 6 emits: 3 notes x 2 deltas
|
||||
```
|
||||
|
||||
6 emits: 3 notes x 2 deltas. A chord played twice per step.
|
||||
This is a chord played twice per step.
|
||||
|
||||
## 1:1 Pairing: at With arp
|
||||
|
||||
@@ -51,20 +44,16 @@ c4 e4 g4 note 1.5 decay sine snd .
|
||||
|
||||
```forth
|
||||
0 0.33 0.66 at
|
||||
c4 e4 g4 arp note 0.5 decay sine snd .
|
||||
c4 e4 g4 arp note sine s . ;; c4 at 0, e4 at 0.33, g4 at 0.66
|
||||
```
|
||||
|
||||
C4 at 0, E4 at 0.33, G4 at 0.66.
|
||||
|
||||
If the lists differ in length, the shorter one wraps around:
|
||||
|
||||
```forth
|
||||
0 0.25 0.5 0.75 at
|
||||
c4 e4 arp note 0.3 decay sine snd .
|
||||
c4 e4 arp note sine s . ;; c4, e4, c4, e4 at 4 time points
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Generating Deltas
|
||||
@@ -74,25 +63,25 @@ You rarely type deltas by hand. Use generators:
|
||||
Evenly spaced via `.,`:
|
||||
|
||||
```forth
|
||||
0 1 0.25 ., at hat snd . ;; 0 0.25 0.5 0.75 1.0
|
||||
0 1 0.25 ., at hat s . ;; 0 0.25 0.5 0.75 1.0
|
||||
```
|
||||
|
||||
Euclidean distribution via `euclid`:
|
||||
|
||||
```forth
|
||||
3 8 euclid at hat snd . ;; 3 hats at positions 0, 3, 5
|
||||
3 8 euclid at hat s . ;; 3 hats at positions 0, 3, 5
|
||||
```
|
||||
|
||||
Random timing via `gen`:
|
||||
|
||||
```forth
|
||||
( 0.0 1.0 rand ) 4 gen at hat snd . ;; 4 hats at random positions
|
||||
{ 0.0 1.0 rand } 4 gen at hat s . ;; 4 hats at random positions
|
||||
```
|
||||
|
||||
Geometric spacing via `geom..`:
|
||||
|
||||
```forth
|
||||
0.0 2.0 4 geom.. at hat snd . ;; exponentially spaced
|
||||
0.0 2.0 4 geom.. at hat s . ;; exponentially spaced
|
||||
```
|
||||
|
||||
## Gating at
|
||||
@@ -100,18 +89,12 @@ Geometric spacing via `geom..`:
|
||||
Wrap `at` expressions in quotations for conditional timing:
|
||||
|
||||
```forth
|
||||
( 0 0.25 0.5 0.75 at ) 2 every
|
||||
hat snd .
|
||||
{ 0 0.25 0.5 0.75 at } 2 every ;; 16th-note hats every other bar
|
||||
hat s .
|
||||
|
||||
{ 0 0.5 at } 0.5 chance ;; 50% chance of double-hit
|
||||
kick s .
|
||||
```
|
||||
|
||||
16th-note hats every other bar.
|
||||
|
||||
```forth
|
||||
( 0 0.5 at ) 0.5 chance
|
||||
kick snd .
|
||||
```
|
||||
|
||||
50% chance of double-hit.
|
||||
|
||||
When the quotation doesn't execute, no deltas are set -- you get the default single emit at beat start.
|
||||
|
||||
|
||||
@@ -40,15 +40,15 @@ That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `freq`.
|
||||
`gen` executes a quotation n times and collects all results. The quotation must push exactly one value per call:
|
||||
|
||||
```forth
|
||||
( 1 6 rand ) 4 gen ;; 4 random values between 1 and 6
|
||||
( coin ) 8 gen ;; 8 random 0s and 1s
|
||||
{ 1 6 rand } 4 gen ;; 4 random values between 1 and 6
|
||||
{ coin } 8 gen ;; 8 random 0s and 1s
|
||||
```
|
||||
|
||||
Contrast with `times`, which executes for side effects and does not collect. `times` sets `@i` to the current index:
|
||||
|
||||
```forth
|
||||
4 ( @i ) times ;; 0 1 2 3 (pushes @i each iteration)
|
||||
4 ( @i 60 + note sine snd . ) times ;; plays 4 notes, collects nothing
|
||||
4 { @i } times ;; 0 1 2 3 (pushes @i each iteration)
|
||||
4 { @i 60 + note sine s . } times ;; plays 4 notes, collects nothing
|
||||
```
|
||||
|
||||
The distinction: `gen` is for building data. `times` is for doing things.
|
||||
@@ -109,7 +109,7 @@ c4 e4 g4 b4 4 shuffle ;; random permutation each time
|
||||
Useful for computing averages or accumulating values:
|
||||
|
||||
```forth
|
||||
( 1 6 rand ) 4 gen 4 sum ;; sum of 4 dice rolls
|
||||
{ 1 6 rand } 4 gen 4 sum ;; sum of 4 dice rolls
|
||||
```
|
||||
|
||||
## Replication
|
||||
@@ -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 snd .
|
||||
c3 note 12 + 0.5 gain sine snd .
|
||||
c3 note 24 + 0.3 gain sine snd .
|
||||
c3 note 0.5 gain sine s .
|
||||
c3 note 12 + 0.5 gain sine s .
|
||||
c3 note 24 + 0.3 gain sine s .
|
||||
```
|
||||
|
||||
Or replicate a value for batch processing:
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# Notes & Harmony
|
||||
|
||||
This tutorial covers everything pitch-related: notes, intervals, chords, voicings, transposition, scales, and diatonic harmony. Each section builds on the previous one.
|
||||
Cagire speaks music theory. Notes, intervals, chords, and scales are all first-class words that compile to stack operations on MIDI values. This tutorial covers every pitch-related feature.
|
||||
|
||||
## Notes
|
||||
## MIDI Notes
|
||||
|
||||
A note name followed by an octave number compiles to a MIDI integer:
|
||||
Write a note name followed by an octave number. It compiles to a MIDI integer:
|
||||
|
||||
```forth
|
||||
c4 note sine snd .
|
||||
c4 ;; 60 (middle C)
|
||||
a4 ;; 69 (concert A)
|
||||
e3 ;; 52
|
||||
```
|
||||
|
||||
That plays middle C (MIDI 60). `a4` is concert A (69), `e3` is 52. Sharps use `s` or `#`, flats use `b`:
|
||||
Sharps use `s` or `#`. Flats use `b`:
|
||||
|
||||
```forth
|
||||
fs4 note 0.5 decay saw snd .
|
||||
fs4 ;; 66 (F sharp 4)
|
||||
f#4 ;; 66 (same thing)
|
||||
bb3 ;; 58 (B flat 3)
|
||||
eb4 ;; 63
|
||||
```
|
||||
|
||||
Octave range is -1 to 9. The formula is `(octave + 1) * 12 + base + modifier`, where C=0, D=2, E=4, F=5, G=7, A=9, B=11.
|
||||
|
||||
Note literals push a single integer onto the stack, just like writing `60` directly. They work everywhere an integer works:
|
||||
|
||||
```forth
|
||||
eb4 note 0.8 decay tri snd .
|
||||
c4 note sine s . ;; play middle C as a sine
|
||||
a4 note 0.5 gain modal s . ;; concert A, quieter
|
||||
```
|
||||
|
||||
`fs4` and `f#4` both mean F sharp 4 (MIDI 66). `bb3` is B flat 3 (58). Octave range is -1 to 9.
|
||||
|
||||
Notes are just integers. They work anywhere an integer works — you can do arithmetic on them, store them in variables, pass them to any word that expects a number.
|
||||
|
||||
## Intervals
|
||||
|
||||
An interval duplicates the top of the stack and adds semitones. Stack two intervals to build a chord by hand:
|
||||
An interval duplicates the top of the stack and adds semitones. This lets you build chords by stacking:
|
||||
|
||||
```forth
|
||||
c4 M3 P5 note 1.5 decay sine snd .
|
||||
c4 M3 P5 ;; stack: 60 64 67 (C major triad)
|
||||
c4 m3 P5 ;; stack: 60 63 67 (C minor triad)
|
||||
a3 P5 ;; stack: 57 64 (A plus a fifth)
|
||||
```
|
||||
|
||||
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 saw snd .
|
||||
```
|
||||
|
||||
A minor triad: A3, C4, E4.
|
||||
|
||||
**Simple intervals** (within one octave):
|
||||
Simple intervals (within one octave):
|
||||
|
||||
| Interval | Semitones | Name |
|
||||
|----------|-----------|------|
|
||||
@@ -58,7 +58,7 @@ A minor triad: A3, C4, E4.
|
||||
| `M7` | 11 | Major 7th |
|
||||
| `P8` | 12 | Octave |
|
||||
|
||||
**Compound intervals** (beyond one octave):
|
||||
Compound intervals (beyond one octave):
|
||||
|
||||
| Interval | Semitones |
|
||||
|----------|-----------|
|
||||
@@ -75,333 +75,108 @@ A minor triad: A3, C4, E4.
|
||||
| `M14` | 23 |
|
||||
| `P15` | 24 |
|
||||
|
||||
Custom voicings with wide intervals:
|
||||
|
||||
```forth
|
||||
c3 P5 P8 M10 note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
C3, G3, C4, E4 — an open-voiced C major spread across two octaves.
|
||||
|
||||
## Chords
|
||||
|
||||
Chord words replace a root note with all the chord tones. They're shortcuts for what intervals do manually:
|
||||
Chord words take a root note and push all the chord tones. They eat the root and replace it with the full voicing:
|
||||
|
||||
```forth
|
||||
c4 maj note 1.5 decay sine snd .
|
||||
c4 maj ;; stack: 60 64 67
|
||||
c4 min7 ;; stack: 60 63 67 70
|
||||
c4 dom9 ;; stack: 60 64 67 70 74
|
||||
```
|
||||
|
||||
That's the same C major triad, but in one word instead of `M3 P5`. A few more:
|
||||
**Triads:**
|
||||
|
||||
| Word | Intervals | Example (C4) |
|
||||
|------|-----------|-------------|
|
||||
| `maj` | 0 4 7 | 60 64 67 |
|
||||
| `m` | 0 3 7 | 60 63 67 |
|
||||
| `dim` | 0 3 6 | 60 63 66 |
|
||||
| `aug` | 0 4 8 | 60 64 68 |
|
||||
| `sus2` | 0 2 7 | 60 62 67 |
|
||||
| `sus4` | 0 5 7 | 60 65 67 |
|
||||
|
||||
**Seventh chords:**
|
||||
|
||||
| Word | Intervals | Example (C4) |
|
||||
|------|-----------|-------------|
|
||||
| `maj7` | 0 4 7 11 | 60 64 67 71 |
|
||||
| `min7` | 0 3 7 10 | 60 63 67 70 |
|
||||
| `dom7` | 0 4 7 10 | 60 64 67 70 |
|
||||
| `dim7` | 0 3 6 9 | 60 63 66 69 |
|
||||
| `m7b5` | 0 3 6 10 | 60 63 66 70 |
|
||||
| `minmaj7` | 0 3 7 11 | 60 63 67 71 |
|
||||
| `aug7` | 0 4 8 10 | 60 64 68 70 |
|
||||
|
||||
**Sixth chords:**
|
||||
|
||||
| Word | Intervals | Example (C4) |
|
||||
|------|-----------|-------------|
|
||||
| `maj6` | 0 4 7 9 | 60 64 67 69 |
|
||||
| `min6` | 0 3 7 9 | 60 63 67 69 |
|
||||
|
||||
**Extended chords:**
|
||||
|
||||
| Word | Intervals | Example (C4) |
|
||||
|------|-----------|-------------|
|
||||
| `dom9` | 0 4 7 10 14 | 60 64 67 70 74 |
|
||||
| `maj9` | 0 4 7 11 14 | 60 64 67 71 74 |
|
||||
| `min9` | 0 3 7 10 14 | 60 63 67 70 74 |
|
||||
| `dom11` | 0 4 7 10 14 17 | 60 64 67 70 74 77 |
|
||||
| `min11` | 0 3 7 10 14 17 | 60 63 67 70 74 77 |
|
||||
| `dom13` | 0 4 7 10 14 21 | 60 64 67 70 74 81 |
|
||||
|
||||
**Add chords:**
|
||||
|
||||
| Word | Intervals | Example (C4) |
|
||||
|------|-----------|-------------|
|
||||
| `add9` | 0 4 7 14 | 60 64 67 74 |
|
||||
| `add11` | 0 4 7 17 | 60 64 67 77 |
|
||||
| `madd9` | 0 3 7 14 | 60 63 67 74 |
|
||||
|
||||
**Altered dominants:**
|
||||
|
||||
| Word | Intervals | Example (C4) |
|
||||
|------|-----------|-------------|
|
||||
| `dom7b9` | 0 4 7 10 13 | 60 64 67 70 73 |
|
||||
| `dom7s9` | 0 4 7 10 15 | 60 64 67 70 75 |
|
||||
| `dom7b5` | 0 4 6 10 | 60 64 66 70 |
|
||||
| `dom7s5` | 0 4 8 10 | 60 64 68 70 |
|
||||
|
||||
Chord tones are varargs -- they eat the entire stack. So a chord word should come right after the root note:
|
||||
|
||||
```forth
|
||||
d3 min7 note 1.5 decay saw snd .
|
||||
```
|
||||
|
||||
```forth
|
||||
e3 dom9 note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
```forth
|
||||
a3 sus2 note 1.5 decay tri snd .
|
||||
```
|
||||
|
||||
Common triads:
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `maj` | 0 4 7 |
|
||||
| `m` | 0 3 7 |
|
||||
| `dim` | 0 3 6 |
|
||||
| `aug` | 0 4 8 |
|
||||
| `sus2` | 0 2 7 |
|
||||
| `sus4` | 0 5 7 |
|
||||
| `pwr` | 0 7 |
|
||||
|
||||
Common seventh chords:
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `maj7` | 0 4 7 11 |
|
||||
| `min7` | 0 3 7 10 |
|
||||
| `dom7` | 0 4 7 10 |
|
||||
| `dim7` | 0 3 6 9 |
|
||||
| `m7b5` | 0 3 6 10 |
|
||||
| `minmaj7` | 0 3 7 11 |
|
||||
| `aug7` | 0 4 8 10 |
|
||||
| `augmaj7` | 0 4 8 11 |
|
||||
| `7sus4` | 0 5 7 10 |
|
||||
|
||||
Extended, add, altered, and other chord types are listed in the Reference section at the end.
|
||||
|
||||
## Voicings
|
||||
|
||||
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 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 snd .
|
||||
```
|
||||
|
||||
G4 C5 E5. `dinv` does the opposite — moves the top note down an octave:
|
||||
|
||||
```forth
|
||||
c4 maj dinv note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
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 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 saw snd .
|
||||
```
|
||||
|
||||
E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jazz guitar and piano.
|
||||
|
||||
## Transposition
|
||||
|
||||
`tp` shifts every note on the stack by N semitones:
|
||||
|
||||
```forth
|
||||
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 saw snd .
|
||||
```
|
||||
|
||||
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 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 snd .
|
||||
c4 maj note sine s . ;; plays all 3 notes as one chord
|
||||
```
|
||||
|
||||
## Scales
|
||||
|
||||
Scale words convert a degree index into a MIDI note. By default the root is C4 (MIDI 60):
|
||||
Scale words convert a degree index into a MIDI note. The base note is C4 (MIDI 60). Degrees wrap around with octave transposition:
|
||||
|
||||
```forth
|
||||
0 major note 0.5 decay sine snd .
|
||||
0 major ;; 60 (C4 -- degree 0)
|
||||
4 major ;; 67 (G4 -- degree 4)
|
||||
7 major ;; 72 (C5 -- degree 7, wraps to next octave)
|
||||
-1 major ;; 59 (B3 -- negative degrees go down)
|
||||
```
|
||||
|
||||
Degree 0 of the major scale: C4. Degrees wrap with octave transposition — degree 7 gives C5 (72), degree -1 gives B3 (59).
|
||||
|
||||
Walk through a scale with `cycle`:
|
||||
Use scales with `cycle` or `rand` to walk through pitches:
|
||||
|
||||
```forth
|
||||
0 1 2 3 4 5 6 7 8 cycle minor note 0.5 decay sine snd .
|
||||
0 1 2 3 4 5 6 7 8 cycle minor note sine s .
|
||||
```
|
||||
|
||||
Random notes from a scale:
|
||||
**Standard modes:**
|
||||
|
||||
```forth
|
||||
0 7 rand pentatonic note 0.8 decay saw snd .
|
||||
```
|
||||
|
||||
### Setting the key
|
||||
|
||||
By default scales are rooted at C4. Use `key!` to change the tonal center:
|
||||
|
||||
```forth
|
||||
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 snd .
|
||||
```
|
||||
|
||||
A minor melody starting from A3.
|
||||
|
||||
**Common modes:**
|
||||
|
||||
| Word | Pattern |
|
||||
|------|---------|
|
||||
| Word | Pattern (semitones) |
|
||||
|------|-------------------|
|
||||
| `major` | 0 2 4 5 7 9 11 |
|
||||
| `minor` | 0 2 3 5 7 8 10 |
|
||||
| `dorian` | 0 2 3 5 7 9 10 |
|
||||
| `phrygian` | 0 1 3 5 7 8 10 |
|
||||
| `lydian` | 0 2 4 6 7 9 11 |
|
||||
| `mixolydian` | 0 2 4 5 7 9 10 |
|
||||
| `pentatonic` | 0 2 4 7 9 |
|
||||
| `minpent` | 0 3 5 7 10 |
|
||||
| `blues` | 0 3 5 6 7 10 |
|
||||
| `harmonicminor` | 0 2 3 5 7 8 11 |
|
||||
| `melodicminor` | 0 2 3 5 7 9 11 |
|
||||
|
||||
Jazz, symmetric, and modal variant scales are listed in the Reference section.
|
||||
|
||||
## Diatonic Harmony
|
||||
|
||||
`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 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 snd .
|
||||
```
|
||||
|
||||
`seventh` adds a fourth note:
|
||||
|
||||
```forth
|
||||
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.
|
||||
|
||||
Combine with `key!` to play diatonic chords in any key:
|
||||
|
||||
```forth
|
||||
g3 key! 0 major triad note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
G major triad rooted at G3.
|
||||
|
||||
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 saw snd .
|
||||
```
|
||||
|
||||
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 saw snd .
|
||||
```
|
||||
|
||||
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):
|
||||
|
||||
```forth
|
||||
0 major seventh arp note 0.5 decay sine snd .
|
||||
```
|
||||
|
||||
## Frequency Conversion
|
||||
|
||||
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
|
||||
|
||||
```forth
|
||||
c4 mtof freq sine snd .
|
||||
```
|
||||
|
||||
Useful when a synth parameter expects Hz rather than MIDI.
|
||||
|
||||
## Reference
|
||||
|
||||
### All Chords
|
||||
|
||||
**Triads:**
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `maj` | 0 4 7 |
|
||||
| `m` | 0 3 7 |
|
||||
| `dim` | 0 3 6 |
|
||||
| `aug` | 0 4 8 |
|
||||
| `sus2` | 0 2 7 |
|
||||
| `sus4` | 0 5 7 |
|
||||
| `pwr` | 0 7 |
|
||||
|
||||
**Seventh chords:**
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `maj7` | 0 4 7 11 |
|
||||
| `min7` | 0 3 7 10 |
|
||||
| `dom7` | 0 4 7 10 |
|
||||
| `dim7` | 0 3 6 9 |
|
||||
| `m7b5` | 0 3 6 10 |
|
||||
| `minmaj7` | 0 3 7 11 |
|
||||
| `aug7` | 0 4 8 10 |
|
||||
| `augmaj7` | 0 4 8 11 |
|
||||
| `7sus4` | 0 5 7 10 |
|
||||
|
||||
**Sixth chords:**
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `maj6` | 0 4 7 9 |
|
||||
| `min6` | 0 3 7 9 |
|
||||
| `maj69` | 0 4 7 9 14 |
|
||||
| `min69` | 0 3 7 9 14 |
|
||||
|
||||
**Extended chords:**
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `dom9` | 0 4 7 10 14 |
|
||||
| `maj9` | 0 4 7 11 14 |
|
||||
| `min9` | 0 3 7 10 14 |
|
||||
| `9sus4` | 0 5 7 10 14 |
|
||||
| `dom11` | 0 4 7 10 14 17 |
|
||||
| `maj11` | 0 4 7 11 14 17 |
|
||||
| `min11` | 0 3 7 10 14 17 |
|
||||
| `dom13` | 0 4 7 10 14 21 |
|
||||
| `maj13` | 0 4 7 11 14 21 |
|
||||
| `min13` | 0 3 7 10 14 21 |
|
||||
|
||||
**Add chords:**
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `add9` | 0 4 7 14 |
|
||||
| `add11` | 0 4 7 17 |
|
||||
| `madd9` | 0 3 7 14 |
|
||||
|
||||
**Altered dominants:**
|
||||
|
||||
| Word | Intervals |
|
||||
|------|-----------|
|
||||
| `dom7b9` | 0 4 7 10 13 |
|
||||
| `dom7s9` | 0 4 7 10 15 |
|
||||
| `dom7b5` | 0 4 6 10 |
|
||||
| `dom7s5` | 0 4 8 10 |
|
||||
| `dom7s11` | 0 4 7 10 18 |
|
||||
|
||||
### All Scales
|
||||
|
||||
**Modes:**
|
||||
|
||||
| Word | Pattern |
|
||||
|------|---------|
|
||||
| `major` | 0 2 4 5 7 9 11 |
|
||||
| `minor` / `aeolian` | 0 2 3 5 7 8 10 |
|
||||
| `dorian` | 0 2 3 5 7 9 10 |
|
||||
| `phrygian` | 0 1 3 5 7 8 10 |
|
||||
| `lydian` | 0 2 4 6 7 9 11 |
|
||||
| `mixolydian` | 0 2 4 5 7 9 10 |
|
||||
| `aeolian` | 0 2 3 5 7 8 10 |
|
||||
| `locrian` | 0 1 3 5 6 8 10 |
|
||||
|
||||
**Pentatonic and blues:**
|
||||
@@ -412,13 +187,6 @@ Useful when a synth parameter expects Hz rather than MIDI.
|
||||
| `minpent` | 0 3 5 7 10 |
|
||||
| `blues` | 0 3 5 6 7 10 |
|
||||
|
||||
**Harmonic and melodic minor:**
|
||||
|
||||
| Word | Pattern |
|
||||
|------|---------|
|
||||
| `harmonicminor` | 0 2 3 5 7 8 11 |
|
||||
| `melodicminor` | 0 2 3 5 7 9 11 |
|
||||
|
||||
**Chromatic and whole tone:**
|
||||
|
||||
| Word | Pattern |
|
||||
@@ -426,6 +194,13 @@ Useful when a synth parameter expects Hz rather than MIDI.
|
||||
| `chromatic` | 0 1 2 3 4 5 6 7 8 9 10 11 |
|
||||
| `wholetone` | 0 2 4 6 8 10 |
|
||||
|
||||
**Harmonic and melodic minor:**
|
||||
|
||||
| Word | Pattern |
|
||||
|------|---------|
|
||||
| `harmonicminor` | 0 2 3 5 7 8 11 |
|
||||
| `melodicminor` | 0 2 3 5 7 9 11 |
|
||||
|
||||
**Jazz / Bebop:**
|
||||
|
||||
| Word | Pattern |
|
||||
@@ -454,3 +229,74 @@ Useful when a synth parameter expects Hz rather than MIDI.
|
||||
| `lydianaug` | 0 2 4 6 8 9 11 |
|
||||
| `mixb6` | 0 2 4 5 7 8 10 |
|
||||
| `locrian2` | 0 2 3 5 6 8 10 |
|
||||
|
||||
## Octave Shifting
|
||||
|
||||
`oct` transposes a note by octaves:
|
||||
|
||||
```forth
|
||||
c4 1 oct ;; 72 (C5)
|
||||
c4 -1 oct ;; 48 (C3)
|
||||
c4 2 oct ;; 84 (C6)
|
||||
```
|
||||
|
||||
Stack effect: `(note shift -- transposed)`. The shift is multiplied by 12 and added to the note.
|
||||
|
||||
## Frequency Conversion
|
||||
|
||||
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
|
||||
|
||||
```forth
|
||||
69 mtof ;; 440.0 (A4)
|
||||
60 mtof ;; 261.63 (C4)
|
||||
440 ftom ;; 69.0
|
||||
```
|
||||
|
||||
Useful when a synth parameter expects Hz rather than MIDI:
|
||||
|
||||
```forth
|
||||
c4 mtof freq sine s .
|
||||
```
|
||||
|
||||
## Putting It Together
|
||||
|
||||
A chord progression cycling every pattern iteration:
|
||||
|
||||
```forth
|
||||
{ c3 maj7 } { f3 maj7 } { g3 dom7 } { c3 maj7 } 4 pcycle
|
||||
note sine s .
|
||||
```
|
||||
|
||||
Arpeggiate a chord across the step's time divisions:
|
||||
|
||||
```forth
|
||||
c4 min7 arp note 0.5 decay sine s .
|
||||
```
|
||||
|
||||
Random notes from a scale:
|
||||
|
||||
```forth
|
||||
0 7 rand minor note sine s .
|
||||
```
|
||||
|
||||
A bass line walking scale degrees:
|
||||
|
||||
```forth
|
||||
0 2 4 5 7 5 4 2 8 cycle minor note
|
||||
-2 oct 0.8 gain sine s .
|
||||
```
|
||||
|
||||
Chord voicings with random inversion:
|
||||
|
||||
```forth
|
||||
e3 min9
|
||||
{ } { 1 oct } 2 choose
|
||||
note modal s .
|
||||
```
|
||||
|
||||
Stacked intervals for custom voicings:
|
||||
|
||||
```forth
|
||||
c3 P5 P8 M10 ;; C3, G3, C4, E4
|
||||
note sine s .
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
# The Periodic Script
|
||||
|
||||
The periodic script is a hidden seventh view accessible with `F11`. It is a Forth script that runs continuously alongside your patterns, evaluated at every step like a pattern would be. Think of it as a free-running pattern with no grid — just code.
|
||||
|
||||
## What is it for?
|
||||
|
||||
The periodic script is useful for things that don't belong to any specific pattern:
|
||||
|
||||
- **Global effects**: apply a filter sweep or reverb tail across everything.
|
||||
- **Drones**: run a sustained sound that keeps going regardless of which patterns are playing.
|
||||
- **Control logic**: update variables, send MIDI clock, or modulate global parameters.
|
||||
- **Experimentation**: sketch ideas without touching your pattern grid.
|
||||
|
||||
## Opening the Script
|
||||
|
||||
Press `F11` from any view. The script page appears with an editor on the left and visualizations on the right (scope, spectrum, prelude preview). The script is saved with your project.
|
||||
|
||||
## Editing
|
||||
|
||||
Press `Enter` to focus the editor. Write Forth code as you would in any step. Press `Esc` to unfocus and save. Press `Ctrl+E` to evaluate without unfocusing.
|
||||
|
||||
```forth
|
||||
;; a simple drone
|
||||
saw snd c2 note 0.3 gain 0.4 verb .
|
||||
```
|
||||
|
||||
## Speed and Length
|
||||
|
||||
The periodic script has its own speed and length settings, independent of any pattern. Press `S` (unfocused) to set the speed and `L` to set the length (1-256 steps). Speed and length are displayed in the editor title bar.
|
||||
|
||||
The script loops over its length just like a pattern. Context words like `step`, `iter`, and `phase` work as expected, counting within the script's own cycle.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `F11` | Open periodic script view |
|
||||
| `Enter` | Focus editor |
|
||||
| `Esc` | Unfocus and save |
|
||||
| `Ctrl+E` | Evaluate |
|
||||
| `Ctrl+S` | Toggle stack display |
|
||||
| `S` | Set speed (unfocused) |
|
||||
| `L` | Set length (unfocused) |
|
||||
@@ -7,25 +7,21 @@ Music needs surprise. A pattern that plays identically every time gets boring fa
|
||||
`coin` pushes 0 or 1 with equal probability:
|
||||
|
||||
```forth
|
||||
;; sometimes, reverb
|
||||
sine sound
|
||||
( 0.5 verb ) coin ?
|
||||
1 decay
|
||||
.
|
||||
coin note sine s . ;; either 0 or 1 as the note
|
||||
```
|
||||
|
||||
`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 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
|
||||
60 72 rand note sine s . ;; random MIDI note from 60 to 72
|
||||
0.3 0.9 rand gain sine s . ;; 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 snd 0.5 decay . ;; mostly low frequencies
|
||||
200.0 8000.0 logrand freq sine snd 0.5 decay . ;; mostly high frequencies
|
||||
200.0 8000.0 exprand freq sine s . ;; mostly low frequencies
|
||||
200.0 8000.0 logrand freq sine s . ;; mostly high frequencies
|
||||
```
|
||||
|
||||
These are useful for parameters where perception is logarithmic, like frequency and duration.
|
||||
@@ -35,8 +31,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 snd . ) 0.25 chance ;; 25% chance
|
||||
( kick snd . ) 75 prob ;; 75% chance
|
||||
{ hat s . } 0.25 chance ;; 25% chance
|
||||
{ hat s . } 75 prob ;; 75% chance
|
||||
```
|
||||
|
||||
Named probability words save you from remembering numbers:
|
||||
@@ -52,9 +48,9 @@ Named probability words save you from remembering numbers:
|
||||
| `never` | 0% |
|
||||
|
||||
```forth
|
||||
( hat snd . ) often ;; 75%
|
||||
( snare snd . ) sometimes ;; 50%
|
||||
( clap snd . ) rarely ;; 25%
|
||||
{ hat s . } often ;; 75%
|
||||
{ snare s . } sometimes ;; 50%
|
||||
{ clap s . } 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 +58,8 @@ Named probability words save you from remembering numbers:
|
||||
Use `?` and `!?` with `coin` for quick coin-flip decisions:
|
||||
|
||||
```forth
|
||||
( hat snd . ) coin ? ;; execute if coin is 1
|
||||
( rim snd . ) coin !? ;; execute if coin is 0
|
||||
{ hat s . } coin ? ;; execute if coin is 1
|
||||
{ rim s . } coin !? ;; execute if coin is 0
|
||||
```
|
||||
|
||||
## Selection
|
||||
@@ -71,21 +67,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 snd . ;; random drum hit
|
||||
60 64 67 72 4 choose note sine snd 0.5 decay . ;; random note from a set
|
||||
kick snare hat 3 choose s . ;; random drum hit
|
||||
60 64 67 72 4 choose note sine s . ;; 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 snd .
|
||||
{ 0.1 decay } { 0.5 decay } { 0.9 decay } 3 choose
|
||||
sine s .
|
||||
```
|
||||
|
||||
`wchoose` lets you assign weights to each option. Push value/weight pairs:
|
||||
|
||||
```forth
|
||||
kick 0.5 snare 0.3 hat 0.2 3 wchoose snd .
|
||||
kick 0.5 snare 0.3 hat 0.2 3 wchoose s .
|
||||
```
|
||||
|
||||
Kick plays 50% of the time, snare 30%, hat 20%. Weights don't need to sum to 1 -- they're normalized automatically.
|
||||
@@ -98,39 +94,56 @@ Kick plays 50% of the time, snare 30%, hat 20%. Weights don't need to sum to 1 -
|
||||
|
||||
Combined with `note`, this gives you a random permutation of a chord every time the step runs.
|
||||
|
||||
## Cycling
|
||||
|
||||
Cycling steps through values deterministically. No randomness -- pure rotation.
|
||||
|
||||
`cycle` selects based on how many times this step has played (its `runs` count):
|
||||
|
||||
```forth
|
||||
60 64 67 3 cycle note sine s . ;; 60, 64, 67, 60, 64, 67, ...
|
||||
```
|
||||
|
||||
`pcycle` selects based on the pattern iteration count (`iter`):
|
||||
|
||||
```forth
|
||||
kick snare 2 pcycle s . ;; kick on even iterations, snare on odd
|
||||
```
|
||||
|
||||
The difference matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
|
||||
|
||||
Quotations work here too:
|
||||
|
||||
```forth
|
||||
{ c4 note } { e4 note } { g4 note } 3 cycle
|
||||
sine s .
|
||||
```
|
||||
|
||||
`bounce` ping-pongs instead of wrapping around:
|
||||
|
||||
```forth
|
||||
60 64 67 72 4 bounce note sine s . ;; 60, 64, 67, 72, 67, 64, 60, 64, ...
|
||||
```
|
||||
|
||||
## Periodic Execution
|
||||
|
||||
`every` runs a quotation once every n pattern iterations:
|
||||
|
||||
```forth
|
||||
( crash snd . ) 4 every ;; crash cymbal every 4th iteration
|
||||
{ crash s . } 4 every ;; crash cymbal every 4th iteration
|
||||
```
|
||||
|
||||
`except` is the inverse -- it runs a quotation on all iterations *except* every nth:
|
||||
|
||||
```forth
|
||||
( 2 distort ) 4 except ;; distort on all iterations except every 4th
|
||||
```
|
||||
|
||||
`every+` and `except+` take an extra offset argument to shift the phase:
|
||||
|
||||
```forth
|
||||
( 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 snd . ) 4 every ;; kick at 0, 4, 8...
|
||||
( snare snd . ) 4 2 every+ ;; snare at 2, 6, 10...
|
||||
{ 2 distort } 4 except ;; distort on all iterations except every 4th
|
||||
```
|
||||
|
||||
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms:
|
||||
|
||||
```forth
|
||||
( hat snd . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
|
||||
( hat snd . ) 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
|
||||
{ hat s . } 3 8 bjork ;; tresillo: x..x..x. (by step runs)
|
||||
{ hat s . } 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 +161,7 @@ By default, every run produces different random values. Use `seed` to make rando
|
||||
|
||||
```forth
|
||||
42 seed
|
||||
60 72 rand note sine snd . ;; always the same "random" note
|
||||
60 72 rand note sine s . ;; 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,8 +171,8 @@ 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 snd
|
||||
( 0.3 0.6 rand gain ) ( 0.8 gain ) 2 cycle
|
||||
hat s
|
||||
{ 0.3 0.6 rand gain } { 0.8 gain } 2 cycle
|
||||
.
|
||||
```
|
||||
|
||||
@@ -168,18 +181,18 @@ Full volume on even runs, random quiet on odd runs.
|
||||
A bass line that changes every 4 bars:
|
||||
|
||||
```forth
|
||||
( c2 note ) ( e2 note ) ( g2 note ) ( a2 note ) 4 pcycle
|
||||
( 0.5 decay ) often
|
||||
sine snd .
|
||||
{ c2 note } { e2 note } { g2 note } { a2 note } 4 pcycle
|
||||
{ 0.5 decay } often
|
||||
sine s .
|
||||
```
|
||||
|
||||
Layered percussion with different densities:
|
||||
|
||||
```forth
|
||||
( kick snd . ) always
|
||||
( snare snd . ) 2 every
|
||||
( hat snd . ) 5 8 bjork
|
||||
( rim snd . ) rarely
|
||||
{ kick s . } always
|
||||
{ snare s . } 2 every
|
||||
{ hat s . } 5 8 bjork
|
||||
{ rim s . } rarely
|
||||
```
|
||||
|
||||
A melodic step with weighted note selection and random timbre:
|
||||
@@ -188,7 +201,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
|
||||
add snd .
|
||||
modal s .
|
||||
```
|
||||
|
||||
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.
|
||||
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.
|
||||
@@ -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 snd .
|
||||
drums s .
|
||||
```
|
||||
|
||||
## Playback
|
||||
@@ -27,10 +27,10 @@ drums snd .
|
||||
Recorded samples are ordinary samples. Everything you can do with a loaded sample works here:
|
||||
|
||||
```forth
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
## Overdub
|
||||
@@ -70,7 +70,7 @@ Record a foundation, then overdub to build up:
|
||||
"loop" dub
|
||||
|
||||
;; 4. play the result
|
||||
loop snd .
|
||||
loop s .
|
||||
```
|
||||
|
||||
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 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
|
||||
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
|
||||
```
|
||||
|
||||
Combine with randomness for variation:
|
||||
|
||||
```forth
|
||||
loop snd
|
||||
loop s
|
||||
0.0 0.25 0.5 0.75 4 choose begin
|
||||
0.5 speed
|
||||
.
|
||||
|
||||
@@ -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 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
|
||||
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
|
||||
```
|
||||
|
||||
## Drums
|
||||
@@ -21,10 +21,10 @@ gm snd 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 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
|
||||
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
|
||||
```
|
||||
|
||||
## Envelope
|
||||
@@ -32,8 +32,8 @@ gm snd 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 snd piano n 0.01 attack 0.3 decay .
|
||||
gm snd strings n 0.5 attack 2.0 release .
|
||||
gm s piano n 0.01 attack 0.3 decay .
|
||||
gm s 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 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 .
|
||||
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 .
|
||||
```
|
||||
|
||||
## Preset Names
|
||||
@@ -79,22 +79,22 @@ A simple GM drum pattern across four steps:
|
||||
|
||||
```forth
|
||||
;; step 1: kick
|
||||
gm snd drums n 36 note .
|
||||
gm s drums n 36 note .
|
||||
|
||||
;; step 2: closed hat
|
||||
gm snd drums n 42 note 0.6 gain .
|
||||
gm s drums n 42 note 0.6 gain .
|
||||
|
||||
;; step 3: snare
|
||||
gm snd drums n 38 note .
|
||||
gm s drums n 38 note .
|
||||
|
||||
;; step 4: closed hat
|
||||
gm snd drums n 42 note 0.6 gain .
|
||||
gm s drums n 42 note 0.6 gain .
|
||||
```
|
||||
|
||||
Layer piano chords with randomness:
|
||||
|
||||
```forth
|
||||
gm snd piano n
|
||||
gm s 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 snd bass n
|
||||
gm s bass n
|
||||
c2 e2 g2 a2 4 cycle note
|
||||
0.01 attack 0.2 decay 0.0 sustain
|
||||
.
|
||||
|
||||
@@ -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 snd . ;; stores 440 in freq AND passes it to the pipeline
|
||||
440 ,freq sine s . ;; stores 440 in freq AND passes it to the pipeline
|
||||
```
|
||||
|
||||
Without `,`, you'd need `dup`:
|
||||
|
||||
```forth
|
||||
440 dup !freq sine snd . ;; equivalent, but noisier
|
||||
440 dup !freq sine s . ;; 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 snd .
|
||||
@root 7 + note sine s .
|
||||
```
|
||||
|
||||
Every time the pattern loops, step 0 picks a new root. Step 4 always harmonizes with it.
|
||||
@@ -46,14 +46,14 @@ 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 snd . ;; cycle through 12 notes
|
||||
@n 12 mod note sine s . ;; cycle through 12 notes
|
||||
```
|
||||
|
||||
Reset on some condition:
|
||||
|
||||
```forth
|
||||
@n 1 + !n
|
||||
( 0 !n ) @n 16 > ? ;; reset after 16
|
||||
{ 0 !n } @n 16 > ? ;; reset after 16
|
||||
```
|
||||
|
||||
## When Changes Take Effect
|
||||
@@ -69,7 +69,7 @@ Store a sound name in a variable, reuse it across steps:
|
||||
"sine" !synth
|
||||
|
||||
;; step 1, 2, 3...
|
||||
c4 note @synth snd .
|
||||
c4 note @synth s .
|
||||
```
|
||||
|
||||
Change one step, all steps follow.
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
# Welcome to Cagire
|
||||
|
||||
Cagire is a live-codable step sequencer. Each sequencer step is defined by a **Forth** script that gets evaluated at the right time. **Forth** is a minimal, fun and rewarding programming language. It has almost no syntax but provides infinite fun. It rewards exploration, creativity and curiosity.
|
||||
|
||||
This documentation is both a _tutorial_ and a _reference_. All the code examples in the documentation are interactive. **You can run them!** Use `n` and `p` (next/previous) to navigate through the examples. Press `Enter` to evaluate an example! Try to evaluate the following example using `n`, `p` and `Enter`:
|
||||
Cagire is a live-codable step sequencer. Each sequencer step is defined by a **Forth** script that gets evaluated at the right time. **Forth** is a minimal, fun and rewarding programming language. It has almost no syntax but provides infinite fun. It rewards exploration, creativity and curiosity. This documentation is both a _tutorial_ and a _reference_. All the code examples in the documentation are interactive. **You can run them!** Use `n` and `p` (next/previous) to navigate through the examples. Press `Enter` to evaluate an example! Try to evaluate the following example using `n`, `p` and `Enter`:
|
||||
|
||||
```forth
|
||||
saw sound
|
||||
c4 note
|
||||
0.5 decay
|
||||
400 freq
|
||||
1 decay
|
||||
.
|
||||
```
|
||||
|
||||
## What is live coding?
|
||||
|
||||
Live coding is a technique where you write code in real-time to create audiovisual performances. Most often, it is practiced in front of an audience. Live coding is a way to experiment with code, to share things and thoughts openly, to think through code. It can be technical, poetic, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media with a strong emphasis on _improvisation_. Learn more about live coding on [toplap.org](https://toplap.org) or [livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: the act of doing it is its own reward. There are no errors, only fun.
|
||||
Live coding is a technique where you write code in real-time to create audiovisual performances. Most often, it is practiced in front of an audience. Live coding is a way to experiment with code, to share things and thoughts openly, to think through code. It can be technical, poetic, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media with a strong emphasis on _improvisation_. Learn more about live coding on [https://toplap.org](https://toplap.org) or [https://livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: the act of doing it is its own reward. There are no errors, only fun.
|
||||
|
||||
## About
|
||||
|
||||
Cagire is mainly developed by BuboBubo (Raphaël Maurice Forment, [raphaelforment.fr](https://raphaelforment.fr)). It is a free and open-source project licensed under the `AGPL-3.0 License`. You are free to contribute to the project by contributing to the codebase or by sharing feedback. Help and feedback are welcome!
|
||||
Cagire is mainly developed by BuboBubo (Raphaël Maurice Forment, [https://raphaelforment.fr](https://raphaelforment.fr)). It is a free and open-source project licensed under the `AGPL-3.0 License`. You are free to contribute to the project by contributing to the codebase or by sharing feedback. Help and feedback are welcome!
|
||||
|
||||
### 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.
|
||||
* **Author**: Oliver Rockstedt [info@sourcebox.de](info@sourcebox.de).
|
||||
* **Original author**: Emilie Gillet [emilie.o.gillet@gmail.com](emilie.o.gillet@gmail.com).
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
; Cagire NSIS Installer Script
|
||||
; Receives defines from command line:
|
||||
; -DVERSION=x.y.z
|
||||
; -DCLI_EXE=/path/to/cagire.exe
|
||||
; -DDESKTOP_EXE=/path/to/cagire-desktop.exe
|
||||
; -DICON=/path/to/Cagire.ico
|
||||
; -DOUTDIR=/path/to/releases
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "WordFunc.nsh"
|
||||
|
||||
Name "Cagire ${VERSION}"
|
||||
OutFile "${OUTDIR}\cagire-${VERSION}-windows-x86_64-setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\Cagire"
|
||||
InstallDirRegKey HKLM "Software\Cagire" "InstallDir"
|
||||
RequestExecutionLevel admin
|
||||
Unicode True
|
||||
|
||||
!define MUI_ICON "${ICON}"
|
||||
!define MUI_UNICON "${ICON}"
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_HEADERIMAGE_BITMAP "header.bmp"
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
|
||||
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Section "Cagire (required)" SecCore
|
||||
SectionIn RO
|
||||
SetOutPath "$INSTDIR"
|
||||
File "/oname=cagire.exe" "${CLI_EXE}"
|
||||
File "/oname=cagire-desktop.exe" "${DESKTOP_EXE}"
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Cagire" "InstallDir" "$INSTDIR"
|
||||
|
||||
; Add/Remove Programs entry
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayName" "Cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "Publisher" "Raphael Forment"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayIcon" '"$INSTDIR\cagire-desktop.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://git.raphaelforment.fr/BuboBubo/cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "HelpLink" "https://cagire.raphaelforment.fr"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoRepair" 1
|
||||
SectionEnd
|
||||
|
||||
Section "Add to PATH" SecPath
|
||||
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
|
||||
StrCpy $0 "$0;$INSTDIR"
|
||||
WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0"
|
||||
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||
SectionEnd
|
||||
|
||||
Section "Start Menu Shortcut" SecStartMenu
|
||||
CreateDirectory "$SMPROGRAMS\Cagire"
|
||||
CreateShortCut "$SMPROGRAMS\Cagire\Cagire.lnk" "$INSTDIR\cagire-desktop.exe" "" "$INSTDIR\cagire-desktop.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\Cagire\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
||||
SectionEnd
|
||||
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} "Installs Cagire CLI and Desktop binaries."
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecPath} "Add the install location to the PATH system environment variable."
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} "Add a Cagire shortcut to the Start Menu."
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$INSTDIR\cagire.exe"
|
||||
Delete "$INSTDIR\cagire-desktop.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
Delete "$SMPROGRAMS\Cagire\Cagire.lnk"
|
||||
Delete "$SMPROGRAMS\Cagire\Uninstall.lnk"
|
||||
RMDir "$SMPROGRAMS\Cagire"
|
||||
|
||||
; Remove from PATH
|
||||
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
|
||||
; Remove ";$INSTDIR" from the path string
|
||||
${WordReplace} $0 ";$INSTDIR" "" "+" $0
|
||||
WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0"
|
||||
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire"
|
||||
DeleteRegKey HKLM "Software\Cagire"
|
||||
SectionEnd
|
||||
BIN
nsis/header.bmp
BIN
nsis/header.bmp
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
BIN
nsis/sidebar.bmp
BIN
nsis/sidebar.bmp
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB |
@@ -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", tag = "v0.0.13", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", 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"
|
||||
|
||||
@@ -234,7 +234,6 @@ pub fn create_editor(
|
||||
// Read live snapshot from the audio thread
|
||||
let shared = editor.bridge.shared_state.load();
|
||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
||||
editor.app.playback.playing = editor.snapshot.playing;
|
||||
|
||||
// Sync host tempo into LinkState so title bar shows real tempo
|
||||
if shared.tempo > 0.0 {
|
||||
@@ -299,11 +298,6 @@ 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;
|
||||
|
||||
@@ -219,6 +219,7 @@ impl Plugin for CagirePlugin {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
@@ -279,6 +280,8 @@ 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();
|
||||
|
||||
@@ -292,8 +295,7 @@ impl Plugin for CagirePlugin {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
audio_sample_pos: self.sample_pos,
|
||||
sr: self.sample_rate as f64,
|
||||
engine_time,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
@@ -309,12 +311,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, tick } => {
|
||||
let cmd_ref = match tick {
|
||||
AudioCommand::Evaluate { ref cmd, time } => {
|
||||
let cmd_ref = match time {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{cmd}/tick/{t}");
|
||||
let _ = write!(&mut self.cmd_buffer, "{cmd}/time/{t:.6}");
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => cmd.as_str(),
|
||||
@@ -418,11 +420,11 @@ impl Plugin for CagirePlugin {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let cmd_ref = match tsc.tick {
|
||||
let cmd_ref = match tsc.time {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{}/tick/{t}", tsc.cmd);
|
||||
let _ = write!(&mut self.cmd_buffer, "{}/time/{t:.6}", tsc.cmd);
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => &tsc.cmd,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
publish = false
|
||||
@@ -1,13 +1,11 @@
|
||||
#!/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"
|
||||
OUT="target/releases"
|
||||
|
||||
PLATFORMS=(
|
||||
"aarch64-apple-darwin"
|
||||
@@ -314,33 +312,21 @@ copy_artifacts() {
|
||||
# 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
|
||||
if [[ -d "$app_src" ]]; then
|
||||
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
|
||||
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
|
||||
|
||||
# NSIS installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then
|
||||
echo " Building NSIS installer..."
|
||||
local version
|
||||
version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
local abs_root
|
||||
abs_root=$(pwd)
|
||||
makensis -DVERSION="$version" \
|
||||
-DCLI_EXE="$abs_root/$rd/cagire.exe" \
|
||||
-DDESKTOP_EXE="$abs_root/$rd/cagire-desktop.exe" \
|
||||
-DICON="$abs_root/assets/Cagire.ico" \
|
||||
-DOUTDIR="$abs_root/$OUT" \
|
||||
nsis/cagire.nsi
|
||||
echo " Installer -> $OUT/cagire-${version}-windows-x86_64-setup.exe"
|
||||
# MSI installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v cargo-wix &>/dev/null; then
|
||||
echo " Building MSI installer..."
|
||||
cargo wix --no-build --nocapture -C -p -C x64
|
||||
cp target/wix/*.msi "$OUT/" 2>/dev/null && echo " MSI -> $OUT/" || true
|
||||
fi
|
||||
|
||||
# AppImage for Linux targets
|
||||
|
||||
@@ -14,8 +14,4 @@ RUN dpkg --add-architecture arm64 && \
|
||||
libxcb-xfixes0-dev:arm64 \
|
||||
libxkbcommon-dev:arm64 \
|
||||
libgl1-mesa-dev:arm64 \
|
||||
libxcursor-dev:arm64 \
|
||||
libxrandr-dev:arm64 \
|
||||
libxi-dev:arm64 \
|
||||
libwayland-dev:arm64 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -13,8 +13,4 @@ RUN apt-get update && \
|
||||
libxcb-xfixes0-dev \
|
||||
libxkbcommon-dev \
|
||||
libgl1-mesa-dev \
|
||||
libxcursor-dev \
|
||||
libxrandr-dev \
|
||||
libxi-dev \
|
||||
libwayland-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Usage: scripts/make-app-bundle.sh <target>
|
||||
# Creates a macOS .app bundle at target/<target>/release/bundle/osx/Cagire.app
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <target>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET="$1"
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
BINARY="$REPO_ROOT/target/$TARGET/release/cagire-desktop"
|
||||
ICON="$REPO_ROOT/assets/Cagire.icns"
|
||||
VERSION="0.1.0"
|
||||
|
||||
if [[ ! -f "$BINARY" ]]; then
|
||||
echo "ERROR: binary not found at $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APP_DIR="$REPO_ROOT/target/$TARGET/release/bundle/osx/Cagire.app"
|
||||
CONTENTS="$APP_DIR/Contents"
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
|
||||
|
||||
cp "$BINARY" "$CONTENTS/MacOS/cagire-desktop"
|
||||
[[ -f "$ICON" ]] && cp "$ICON" "$CONTENTS/Resources/Cagire.icns"
|
||||
|
||||
cat > "$CONTENTS/Info.plist" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Cagire</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Cagire</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.sova.cagire</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>cagire-desktop</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Cagire.icns</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.music</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright (c) 2025 Raphaël Forment</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Cagire needs microphone access for audio input.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
echo " APP -> $APP_DIR"
|
||||
@@ -1,26 +0,0 @@
|
||||
# cagire (main application)
|
||||
|
||||
Terminal UI application — ties together the Forth VM, audio engine, and project model.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `app/` | `App` struct and submodules: dispatch, editing, navigation, persistence, scripting, sequencer, clipboard, staging, undo |
|
||||
| `engine/` | Audio engine: `sequencer`, `audio`, `link` (Ableton Link), `dispatcher`, `realtime`, `timing` |
|
||||
| `input/` | Keyboard/mouse handling: per-page handlers, modal input, `InputContext` |
|
||||
| `views/` | Pure rendering functions taking `&App` |
|
||||
| `state/` | UI state modules (audio, editor, modals, panels, playback, ...) |
|
||||
| `services/` | Domain logic: clipboard, dict navigation, euclidean, help navigation, pattern editor, stack preview |
|
||||
| `model/` | Domain models: docs, categories, onboarding, script |
|
||||
| `commands` | `AppCommand` enum (~150 variants) |
|
||||
| `page` | `Page` navigation enum |
|
||||
| `midi` | MIDI I/O (up to 4 inputs/outputs) |
|
||||
| `settings` | Confy-based persistent settings |
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`App`** — Central application state, coordinates all subsystems
|
||||
- **`AppCommand`** — Enum of all user actions, dispatched via `App::dispatch()`
|
||||
- **`InputContext`** — Holds `&mut App` + channel senders, bridges input to commands
|
||||
- **`Page`** — 3x2 page grid (Dict, Patterns, Options, Help, Main, Engine)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user