Compare commits
178 Commits
v0.1.4
...
979b7639ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,10 +0,0 @@
|
|||||||
[env]
|
|
||||||
MACOSX_DEPLOYMENT_TARGET = "12.0"
|
|
||||||
|
|
||||||
[alias]
|
|
||||||
xtask = "run --package xtask --release --"
|
|
||||||
|
|
||||||
[target.x86_64-pc-windows-gnu]
|
|
||||||
rustflags = [
|
|
||||||
"-C", "link-args=-Wl,-Bstatic -lstdc++ -lgcc -lgcc_eh -lpthread -Wl,-Bdynamic -lmingwex -lmsvcrt -lws2_32 -liphlpapi -lwinmm -lole32 -loleaut32 -luuid -lkernel32",
|
|
||||||
]
|
|
||||||
@@ -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"
|
|
||||||
305
.github/workflows/ci.yml
vendored
Normal file
305
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
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 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 install cargo-bundle
|
||||||
|
|
||||||
|
- name: Install dependencies (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew list cmake &>/dev/null || brew install cmake
|
||||||
|
cargo install 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: 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: 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
|
||||||
|
|
||||||
|
universal-macos:
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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 .pkg installer
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal-pkg
|
||||||
|
path: Cagire-*-universal.pkg
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [build, universal-macos]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
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-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" == *-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
|
||||||
58
.github/workflows/pages.yml
vendored
Normal file
58
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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:
|
||||||
|
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
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,11 +1,10 @@
|
|||||||
/target
|
/target
|
||||||
/.cache
|
Cargo.lock
|
||||||
*.prof
|
*.prof
|
||||||
.DS_Store
|
.DS_Store
|
||||||
releases/
|
|
||||||
|
|
||||||
# Local cargo overrides (doux path patch)
|
# Cargo config
|
||||||
.cargo/config.local.toml
|
.cargo/config.toml
|
||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
187
BUILDING.md
187
BUILDING.md
@@ -1,187 +0,0 @@
|
|||||||
# Building Cagire
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.raphaelforment.fr/BuboBubo/cagire
|
|
||||||
cd cagire
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
The `doux` audio engine is fetched automatically from git. No local path setup needed.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
**Rust** (stable toolchain): https://rustup.rs
|
|
||||||
|
|
||||||
## System Dependencies
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install cmake
|
|
||||||
```
|
|
||||||
|
|
||||||
cmake is required by `rusty_link` (Ableton Link C++ bindings). Xcode Command Line Tools provide the C++ compiler. CoreAudio and CoreMIDI are built-in. The desktop build needs no additional dependencies on macOS (Cocoa/Metal are provided by the system).
|
|
||||||
|
|
||||||
### Linux (Debian/Ubuntu)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install cmake g++ pkg-config libasound2-dev libjack-jackd2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
For the desktop build (egui/eframe), also install:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install libgl-dev libxkbcommon-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux (Arch)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo pacman -S cmake gcc pkgconf alsa-lib jack2
|
|
||||||
```
|
|
||||||
|
|
||||||
For the desktop build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo pacman -S libxkbcommon libx11 libxcursor libxrandr libxi wayland mesa
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux (Fedora)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install cmake gcc-c++ pkgconf-pkg-config alsa-lib-devel jack-audio-connection-kit-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
For the desktop build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install libxkbcommon-devel libX11-devel libXcursor-devel libXrandr-devel libXi-devel wayland-devel mesa-libGL-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
Install Visual Studio Build Tools (MSVC) and CMake. Everything else is provided by the Windows SDK.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
Terminal (default):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Desktop (egui window):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --release --features desktop --bin cagire-desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
Plugins (CLAP/VST3):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo xtask bundle cagire-plugins --release
|
|
||||||
```
|
|
||||||
|
|
||||||
The xtask alias is defined in `.cargo/config.toml` (committed). Plugin bundles are output to `target/bundled/`.
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
Terminal (default):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --release -- [OPTIONS]
|
|
||||||
```
|
|
||||||
|
|
||||||
Desktop (egui window):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --release --features desktop --bin cagire-desktop
|
|
||||||
```
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `-s, --samples <path>` | Sample directory (repeatable) |
|
|
||||||
| `-o, --output <device>` | Output audio device |
|
|
||||||
| `-i, --input <device>` | Input audio device |
|
|
||||||
| `-c, --channels <n>` | Output channel count |
|
|
||||||
| `-b, --buffer <size>` | Audio buffer size |
|
|
||||||
|
|
||||||
## Cross-Compilation
|
|
||||||
|
|
||||||
[cross](https://github.com/cross-rs/cross) uses Docker to build for other platforms without installing their toolchains locally. It works on any OS that runs Docker.
|
|
||||||
|
|
||||||
### Targets
|
|
||||||
|
|
||||||
| Target | Method | Binaries |
|
|
||||||
|--------|--------|----------|
|
|
||||||
| aarch64-apple-darwin | Native (macOS ARM only) | `cagire`, `cagire-desktop` |
|
|
||||||
| x86_64-apple-darwin | Native (macOS only) | `cagire`, `cagire-desktop` |
|
|
||||||
| x86_64-unknown-linux-gnu | `cross build` | `cagire`, `cagire-desktop` |
|
|
||||||
| aarch64-unknown-linux-gnu (RPi 64-bit) | `cross build` | `cagire`, `cagire-desktop` |
|
|
||||||
| x86_64-pc-windows-gnu | `cross build` | `cagire`, `cagire-desktop` |
|
|
||||||
|
|
||||||
macOS targets can only be built on macOS — Apple does not support cross-compilation to macOS from other platforms. Linux and Windows targets can be cross-compiled from any OS. The aarch64-unknown-linux-gnu target covers Raspberry Pi (64-bit OS).
|
|
||||||
|
|
||||||
### Windows ABI
|
|
||||||
|
|
||||||
CI produces `x86_64-pc-windows-msvc` binaries (native Windows build, better compatibility). Local cross-compilation from non-Windows hosts produces `x86_64-pc-windows-gnu` binaries (MinGW via Docker). Both work; MSVC is preferred for releases.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
1. **Docker**: https://docs.docker.com/get-docker/
|
|
||||||
2. **cross**: `cargo install cross --git https://github.com/cross-rs/cross`
|
|
||||||
3. On macOS, add the Intel target: `rustup target add x86_64-apple-darwin`
|
|
||||||
|
|
||||||
Docker must be running before invoking `cross` or `scripts/build-all.sh`.
|
|
||||||
|
|
||||||
### Building Individual Targets
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux x86_64
|
|
||||||
cross build --release --target x86_64-unknown-linux-gnu
|
|
||||||
cross build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
# Linux aarch64
|
|
||||||
cross build --release --target aarch64-unknown-linux-gnu
|
|
||||||
cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
|
|
||||||
|
|
||||||
# Windows x86_64
|
|
||||||
cross build --release --target x86_64-pc-windows-gnu
|
|
||||||
cross build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-gnu
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building All Targets (macOS only)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Interactive (prompts for platform/target selection):
|
|
||||||
scripts/build-all.sh
|
|
||||||
|
|
||||||
# Non-interactive:
|
|
||||||
scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop --yes
|
|
||||||
scripts/build-all.sh --all --yes
|
|
||||||
```
|
|
||||||
|
|
||||||
Builds selected targets, producing binaries in `releases/`.
|
|
||||||
|
|
||||||
Platform aliases: `macos-arm64`, `macos-x86_64`, `linux-x86_64`, `linux-aarch64`, `windows-x86_64`.
|
|
||||||
Target aliases: `cli`, `desktop`, `plugins`.
|
|
||||||
|
|
||||||
### Linux AppImage Packaging
|
|
||||||
|
|
||||||
Linux releases ship as AppImages — self-contained executables that bundle all shared library dependencies (ALSA, JACK, X11, OpenGL). No runtime dependencies required.
|
|
||||||
|
|
||||||
After building a Linux target, produce an AppImage with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 releases
|
|
||||||
```
|
|
||||||
|
|
||||||
`scripts/build-all.sh` does this automatically for every Linux target selected. The CI pipeline produces AppImages for the x86_64 Linux build. Cross-arch AppImage building (e.g. aarch64 on x86_64) is not supported — run on a matching host or in CI.
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- Custom Dockerfiles in `cross/` install the native libraries Cagire depends on (ALSA, JACK, X11, cmake, libclang, etc.). `Cross.toml` maps each target to its Dockerfile.
|
|
||||||
- The first build per target downloads Docker base images and installs packages. Subsequent builds use cached layers.
|
|
||||||
- Cross-architecture Docker builds (e.g. aarch64 on x86_64 or vice versa) run under QEMU emulation and are significantly slower.
|
|
||||||
236
CHANGELOG.md
236
CHANGELOG.md
@@ -2,222 +2,56 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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]
|
## [0.1.0]
|
||||||
|
|
||||||
### Breaking
|
### UI / UX (breaking cosmetic changes)
|
||||||
- **Quotation syntax changed from `{ }` to `( )`** — all deferred code blocks now use parentheses.
|
- **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.
|
||||||
### 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.
|
|
||||||
|
|
||||||
**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.
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
**Harmony and voicing:**
|
|
||||||
- `key!` — set tonal center.
|
|
||||||
- `triad` / `seventh` — diatonic chord from scale degree.
|
|
||||||
- `inv` / `dinv` — chord inversion / down inversion.
|
|
||||||
- `drop2` / `drop3` — drop 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).
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
- 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).
|
|
||||||
- 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)
|
### CLAP Plugin (experimental)
|
||||||
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
|
- 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
|
### Documentation
|
||||||
- Complete reorganization into `docs/` subdirectories.
|
- Complete reorganization into `docs/` subdirectories.
|
||||||
- 10 getting-started guides, 5 interactive tutorials.
|
- 10 getting-started guides, 5 interactive tutorials.
|
||||||
- New tutorials: Recording, Soundfonts, Sharing (import/export).
|
- New topics: control flow, generators, harmony, randomness, variables, timing.
|
||||||
- New topics: control flow, generators, harmony, randomness, variables, timing, bracket syntax.
|
|
||||||
- Crate-level READMEs for forth, markdown, project, ratatui.
|
|
||||||
|
|
||||||
### Fixed
|
### Theme System
|
||||||
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
|
- Palette-based generation: all 18 themes now derived from a 14-field Palette via Oklab color space.
|
||||||
- Scope widget not drawing completely in some terminal sizes.
|
- Theme definitions reduced from ~300 lines each to ~20 lines.
|
||||||
|
|
||||||
### Codebase
|
### 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.
|
- `src/input.rs` split into 8 page-specific handlers.
|
||||||
- Undo/redo system with scope-based tracking.
|
- Undo/redo system with scope-based tracking.
|
||||||
- Feature-gated CLI vs plugin builds.
|
- Feature-gated CLI vs plugin builds.
|
||||||
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
|
|
||||||
|
|
||||||
## [0.0.9]
|
## [0.0.9]
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ Contributions are welcome. There are many ways to contribute beyond code:
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Rust** (stable toolchain) - [rustup.rs](https://rustup.rs/)
|
- **Rust** (stable toolchain) - [rustup.rs](https://rustup.rs/)
|
||||||
- **System libraries** - See [BUILDING.md](BUILDING.md) for platform-specific packages (cmake, ALSA, etc.)
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|||||||
7551
Cargo.lock
generated
7551
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"]
|
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.4"
|
version = "0.0.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
repository = "https://git.raphaelforment.fr/BuboBubo/cagire"
|
repository = "https://github.com/Bubobubobubobubo/cagire"
|
||||||
homepage = "https://cagire.raphaelforment.fr"
|
homepage = "https://cagire.raphaelforment.fr"
|
||||||
description = "Forth-based live coding music sequencer"
|
description = "Forth-based live coding music sequencer"
|
||||||
|
|
||||||
@@ -45,18 +45,17 @@ desktop = [
|
|||||||
"dep:egui_ratatui",
|
"dep:egui_ratatui",
|
||||||
"dep:image",
|
"dep:image",
|
||||||
]
|
]
|
||||||
asio = ["doux/asio", "cpal/asio"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cagire-forth = { path = "crates/forth" }
|
cagire-forth = { path = "crates/forth" }
|
||||||
cagire-markdown = { path = "crates/markdown" }
|
cagire-markdown = { path = "crates/markdown" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
cagire-ratatui = { path = "crates/ratatui" }
|
||||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
|
doux = { path = "/Users/bubo/doux", features = ["native"] }
|
||||||
rusty_link = "0.4"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
cpal = { version = "0.17", optional = true }
|
cpal = { version = "0.17", features = ["jack"], optional = true }
|
||||||
clap = { version = "4", features = ["derive"], optional = true }
|
clap = { version = "4", features = ["derive"], optional = true }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
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 }
|
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
cpal = { version = "0.17", optional = true, features = ["jack"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
winres = "0.1"
|
winres = "0.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -113,4 +109,3 @@ icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
|
|||||||
copyright = "Copyright (c) 2025 Raphaël Forment"
|
copyright = "Copyright (c) 2025 Raphaël Forment"
|
||||||
category = "Music"
|
category = "Music"
|
||||||
short_description = "Forth-based music sequencer"
|
short_description = "Forth-based music sequencer"
|
||||||
minimum_system_version = "12.0"
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
[target.aarch64-unknown-linux-gnu]
|
|
||||||
dockerfile = "./scripts/cross/aarch64-linux.Dockerfile"
|
|
||||||
|
|
||||||
[target.x86_64-unknown-linux-gnu]
|
|
||||||
dockerfile = "./scripts/cross/x86_64-linux.Dockerfile"
|
|
||||||
|
|
||||||
[target.x86_64-pc-windows-gnu]
|
|
||||||
dockerfile = "./scripts/cross/x86_64-windows.Dockerfile"
|
|
||||||
88
README.md
88
README.md
@@ -1,83 +1,37 @@
|
|||||||
<h1 align="center">Cagire</h1>
|
<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">
|
<p align="center">
|
||||||
<img src="assets/Cagire.png" alt="Cagire" width="256">
|
<img src="cagire_pixel.png" alt="Cagire" width="256">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
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.
|
||||||
<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 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
|
Terminal version:
|
||||||
|
```
|
||||||
A filtered sawtooth with reverb:
|
cargo build --release
|
||||||
|
|
||||||
```forth
|
|
||||||
saw sound
|
|
||||||
200 199 freq
|
|
||||||
400 lpf
|
|
||||||
.8 lpq .3 verb
|
|
||||||
.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A generative pattern using randomness, scales, and effects:
|
Desktop version (with egui window):
|
||||||
|
```
|
||||||
```forth
|
cargo build --release --features desktop --bin cagire-desktop
|
||||||
sine sound 2 fm 0.5 fmh
|
|
||||||
0 7 rand minor 50 + note
|
|
||||||
.1 .8 rrand cutoff
|
|
||||||
1 4 irand 10 * delay .5 delayfb
|
|
||||||
.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Features
|
## Run
|
||||||
|
|
||||||
- **Cagire's Forth**: a stack-based language made for live coding
|
Terminal version:
|
||||||
- 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.
|
cargo run --release
|
||||||
- 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).
|
|
||||||
|
|
||||||
### 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).
|
AGPL-3.0
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,18 +0,0 @@
|
|||||||
# Cagire - A Forth-based music sequencer
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Drag Cagire.app into the Applications folder.
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
|
|
||||||
xattr -cr /Applications/Cagire.app
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
If you enjoy this software, consider supporting development:
|
|
||||||
https://ko-fi.com/raphaelbubo
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=Cagire
|
|
||||||
Comment=Forth-based music sequencer
|
|
||||||
Exec=cagire
|
|
||||||
Icon=cagire
|
|
||||||
Categories=Audio;Music;AudioVideo;
|
|
||||||
33
build.rs
33
build.rs
@@ -1,38 +1,11 @@
|
|||||||
//! Build script — embeds Windows application resources (icon, metadata).
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
if target_os == "windows" {
|
|
||||||
// C++ runtime (stdc++, gcc, gcc_eh, pthread) linked statically via .cargo/config.toml
|
|
||||||
// using -Wl,-Bstatic. Only Windows system DLLs go here.
|
|
||||||
println!("cargo:rustc-link-lib=ws2_32");
|
|
||||||
println!("cargo:rustc-link-lib=iphlpapi");
|
|
||||||
println!("cargo:rustc-link-lib=winmm");
|
|
||||||
println!("cargo:rustc-link-lib=ole32");
|
|
||||||
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");
|
|
||||||
let mut res = winres::WindowsResource::new();
|
let mut res = winres::WindowsResource::new();
|
||||||
// Cross-compiling from Unix: use prefixed MinGW tools
|
res.set_icon("assets/Cagire.ico")
|
||||||
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)
|
|
||||||
.set("ProductName", "Cagire")
|
.set("ProductName", "Cagire")
|
||||||
.set("FileDescription", "Forth-based music sequencer")
|
.set("FileDescription", "Forth-based music sequencer")
|
||||||
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment");
|
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment");
|
||||||
res.compile().expect("Failed to compile Windows resources");
|
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
|
|
||||||
@@ -15,7 +15,6 @@ enum Token {
|
|||||||
Word(String, SourceSpan),
|
Word(String, SourceSpan),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile Forth source text into an executable Op sequence.
|
|
||||||
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||||
let tokens = tokenize(input);
|
let tokens = tokenize(input);
|
||||||
compile(&tokens, dict)
|
compile(&tokens, dict)
|
||||||
@@ -31,7 +30,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if c == '{' || c == '}' {
|
if c == '(' || c == ')' {
|
||||||
chars.next();
|
chars.next();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,7 +132,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::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
|
||||||
Token::Word(w, span) => {
|
Token::Word(w, span) => {
|
||||||
let word = w.as_str();
|
let word = w.as_str();
|
||||||
if word == "(" {
|
if word == "{" {
|
||||||
let (quote_ops, consumed, end_span) =
|
let (quote_ops, consumed, end_span) =
|
||||||
compile_quotation(&tokens[i + 1..], dict)?;
|
compile_quotation(&tokens[i + 1..], dict)?;
|
||||||
i += consumed;
|
i += consumed;
|
||||||
@@ -142,21 +141,8 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
end: end_span.end,
|
end: end_span.end,
|
||||||
};
|
};
|
||||||
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
||||||
} else if word == ")" {
|
} else if word == "}" {
|
||||||
return Err("unexpected )".into());
|
return Err("unexpected }".into());
|
||||||
} else if word == "[" {
|
|
||||||
let (bracket_ops, consumed, end_span) =
|
|
||||||
compile_bracket(&tokens[i + 1..], dict)?;
|
|
||||||
i += consumed;
|
|
||||||
ops.push(Op::Mark);
|
|
||||||
ops.extend(bracket_ops);
|
|
||||||
let count_span = SourceSpan {
|
|
||||||
start: span.start,
|
|
||||||
end: end_span.end,
|
|
||||||
};
|
|
||||||
ops.push(Op::Count(Some(count_span)));
|
|
||||||
} else if word == "]" {
|
|
||||||
return Err("unexpected ]".into());
|
|
||||||
} else if word == ":" {
|
} else if word == ":" {
|
||||||
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
|
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
|
||||||
i += consumed;
|
i += consumed;
|
||||||
@@ -203,8 +189,8 @@ fn compile_quotation(
|
|||||||
for (i, tok) in tokens.iter().enumerate() {
|
for (i, tok) in tokens.iter().enumerate() {
|
||||||
if let Token::Word(w, _) = tok {
|
if let Token::Word(w, _) = tok {
|
||||||
match w.as_str() {
|
match w.as_str() {
|
||||||
"(" => depth += 1,
|
"{" => depth += 1,
|
||||||
")" => {
|
"}" => {
|
||||||
depth -= 1;
|
depth -= 1;
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
end_idx = Some(i);
|
end_idx = Some(i);
|
||||||
@@ -216,7 +202,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] {
|
let end_span = match &tokens[end_idx] {
|
||||||
Token::Word(_, span) => *span,
|
Token::Word(_, span) => *span,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -225,38 +211,6 @@ fn compile_quotation(
|
|||||||
Ok((quote_ops, end_idx + 1, end_span))
|
Ok((quote_ops, end_idx + 1, end_span))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_bracket(
|
|
||||||
tokens: &[Token],
|
|
||||||
dict: &Dictionary,
|
|
||||||
) -> Result<(Vec<Op>, usize, SourceSpan), String> {
|
|
||||||
let mut depth = 1;
|
|
||||||
let mut end_idx = None;
|
|
||||||
|
|
||||||
for (i, tok) in tokens.iter().enumerate() {
|
|
||||||
if let Token::Word(w, _) = tok {
|
|
||||||
match w.as_str() {
|
|
||||||
"[" => depth += 1,
|
|
||||||
"]" => {
|
|
||||||
depth -= 1;
|
|
||||||
if depth == 0 {
|
|
||||||
end_idx = Some(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let end_idx = end_idx.ok_or("missing ]")?;
|
|
||||||
let end_span = match &tokens[end_idx] {
|
|
||||||
Token::Word(_, span) => *span,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let body_ops = compile(&tokens[..end_idx], dict)?;
|
|
||||||
Ok((body_ops, end_idx + 1, end_span))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn token_span(tok: &Token) -> Option<SourceSpan> {
|
fn token_span(tok: &Token) -> Option<SourceSpan> {
|
||||||
match tok {
|
match tok {
|
||||||
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
|
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use super::types::SourceSpan;
|
use super::types::SourceSpan;
|
||||||
|
|
||||||
/// Single VM instruction produced by the compiler.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Op {
|
pub enum Op {
|
||||||
PushInt(i64, Option<SourceSpan>),
|
PushInt(i64, Option<SourceSpan>),
|
||||||
@@ -65,7 +64,6 @@ pub enum Op {
|
|||||||
NewCmd,
|
NewCmd,
|
||||||
SetParam(&'static str),
|
SetParam(&'static str),
|
||||||
Emit,
|
Emit,
|
||||||
Print,
|
|
||||||
Get,
|
Get,
|
||||||
Set,
|
Set,
|
||||||
SetKeep,
|
SetKeep,
|
||||||
@@ -78,7 +76,6 @@ pub enum Op {
|
|||||||
PCycle(Option<SourceSpan>),
|
PCycle(Option<SourceSpan>),
|
||||||
Choose(Option<SourceSpan>),
|
Choose(Option<SourceSpan>),
|
||||||
Bounce(Option<SourceSpan>),
|
Bounce(Option<SourceSpan>),
|
||||||
PBounce(Option<SourceSpan>),
|
|
||||||
WChoose(Option<SourceSpan>),
|
WChoose(Option<SourceSpan>),
|
||||||
ChanceExec(Option<SourceSpan>),
|
ChanceExec(Option<SourceSpan>),
|
||||||
ProbExec(Option<SourceSpan>),
|
ProbExec(Option<SourceSpan>),
|
||||||
@@ -87,9 +84,6 @@ pub enum Op {
|
|||||||
Ftom,
|
Ftom,
|
||||||
SetTempo,
|
SetTempo,
|
||||||
Every(Option<SourceSpan>),
|
Every(Option<SourceSpan>),
|
||||||
Except(Option<SourceSpan>),
|
|
||||||
EveryOffset(Option<SourceSpan>),
|
|
||||||
ExceptOffset(Option<SourceSpan>),
|
|
||||||
Bjork(Option<SourceSpan>),
|
Bjork(Option<SourceSpan>),
|
||||||
PBjork(Option<SourceSpan>),
|
PBjork(Option<SourceSpan>),
|
||||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||||
@@ -118,7 +112,6 @@ pub enum Op {
|
|||||||
Euclid,
|
Euclid,
|
||||||
EuclidRot,
|
EuclidRot,
|
||||||
Times,
|
Times,
|
||||||
Map,
|
|
||||||
Chord(&'static [i64]),
|
Chord(&'static [i64]),
|
||||||
Transpose,
|
Transpose,
|
||||||
Invert,
|
Invert,
|
||||||
@@ -133,12 +126,6 @@ pub enum Op {
|
|||||||
ModSlide(u8),
|
ModSlide(u8),
|
||||||
ModRnd(u8),
|
ModRnd(u8),
|
||||||
ModEnv,
|
ModEnv,
|
||||||
ModEnvAd,
|
|
||||||
ModEnvAdr,
|
|
||||||
Lpg,
|
|
||||||
// Global params
|
|
||||||
EmitAll,
|
|
||||||
ClearGlobal,
|
|
||||||
// MIDI
|
// MIDI
|
||||||
MidiEmit,
|
MidiEmit,
|
||||||
GetMidiCC,
|
GetMidiCC,
|
||||||
@@ -146,13 +133,4 @@ pub enum Op {
|
|||||||
MidiStart,
|
MidiStart,
|
||||||
MidiStop,
|
MidiStop,
|
||||||
MidiContinue,
|
MidiContinue,
|
||||||
// Recording
|
|
||||||
Rec,
|
|
||||||
Overdub,
|
|
||||||
Orec,
|
|
||||||
Odub,
|
|
||||||
// Bracket syntax (mark/count for auto-counting)
|
|
||||||
Mark,
|
|
||||||
Count(Option<SourceSpan>),
|
|
||||||
Index(Option<SourceSpan>),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
//! Chord definitions as semitone interval arrays.
|
|
||||||
|
|
||||||
/// Named chord with its interval pattern.
|
|
||||||
pub struct Chord {
|
pub struct Chord {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub intervals: &'static [i64],
|
pub intervals: &'static [i64],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All built-in chord types.
|
|
||||||
pub static CHORDS: &[Chord] = &[
|
pub static CHORDS: &[Chord] = &[
|
||||||
// Triads
|
// Triads
|
||||||
Chord {
|
Chord {
|
||||||
@@ -173,7 +169,6 @@ pub static CHORDS: &[Chord] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Find a chord's intervals by name.
|
|
||||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||||
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
|
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Music theory data — chord and scale lookup tables.
|
|
||||||
|
|
||||||
pub mod chords;
|
pub mod chords;
|
||||||
mod scales;
|
mod scales;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
//! Scale definitions as semitone offset arrays.
|
|
||||||
|
|
||||||
/// Named scale with its semitone pattern.
|
|
||||||
pub struct Scale {
|
pub struct Scale {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub pattern: &'static [i64],
|
pub pattern: &'static [i64],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All built-in scale types.
|
|
||||||
pub static SCALES: &[Scale] = &[
|
pub static SCALES: &[Scale] = &[
|
||||||
Scale {
|
Scale {
|
||||||
name: "major",
|
name: "major",
|
||||||
@@ -129,7 +125,6 @@ pub static SCALES: &[Scale] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Find a scale's pattern by name.
|
|
||||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||||
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
|
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ pub trait CcAccess: Send + Sync {
|
|||||||
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
|
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Byte range in source text.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct SourceSpan {
|
pub struct SourceSpan {
|
||||||
pub start: u32,
|
pub start: u32,
|
||||||
pub end: u32,
|
pub end: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Concrete value resolved from a nondeterministic op, used for trace annotations.
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ResolvedValue {
|
pub enum ResolvedValue {
|
||||||
Int(i64),
|
Int(i64),
|
||||||
@@ -41,7 +39,6 @@ impl ResolvedValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spans and resolved values collected during a single evaluation, used for UI highlighting.
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ExecutionTrace {
|
pub struct ExecutionTrace {
|
||||||
pub executed_spans: Vec<SourceSpan>,
|
pub executed_spans: Vec<SourceSpan>,
|
||||||
@@ -49,7 +46,6 @@ pub struct ExecutionTrace {
|
|||||||
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
|
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-step sequencer state passed into the VM.
|
|
||||||
pub struct StepContext<'a> {
|
pub struct StepContext<'a> {
|
||||||
pub step: usize,
|
pub step: usize,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
@@ -63,7 +59,6 @@ pub struct StepContext<'a> {
|
|||||||
pub speed: f64,
|
pub speed: f64,
|
||||||
pub fill: bool,
|
pub fill: bool,
|
||||||
pub nudge_secs: f64,
|
pub nudge_secs: f64,
|
||||||
pub sr: f64,
|
|
||||||
pub cc_access: Option<&'a dyn CcAccess>,
|
pub cc_access: Option<&'a dyn CcAccess>,
|
||||||
pub speed_key: &'a str,
|
pub speed_key: &'a str,
|
||||||
pub mouse_x: f64,
|
pub mouse_x: f64,
|
||||||
@@ -77,18 +72,13 @@ impl StepContext<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Underlying map for user-defined variables.
|
|
||||||
pub type VariablesMap = HashMap<String, Value>;
|
pub type VariablesMap = HashMap<String, Value>;
|
||||||
/// Shared variable store, swapped atomically after each step.
|
|
||||||
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
||||||
/// Shared user-defined word dictionary.
|
|
||||||
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
||||||
/// Shared random number generator.
|
|
||||||
pub type Rng = Arc<Mutex<StdRng>>;
|
pub type Rng = Arc<Mutex<StdRng>>;
|
||||||
pub type Stack = Mutex<Vec<Value>>;
|
pub type Stack = Mutex<Vec<Value>>;
|
||||||
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
|
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
|
||||||
|
|
||||||
/// Stack value in the Forth VM.
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Int(i64, Option<SourceSpan>),
|
Int(i64, Option<SourceSpan>),
|
||||||
@@ -170,7 +160,6 @@ pub(super) struct CmdRegister {
|
|||||||
sound: Option<Value>,
|
sound: Option<Value>,
|
||||||
params: Vec<(&'static str, Value)>,
|
params: Vec<(&'static str, Value)>,
|
||||||
deltas: Vec<Value>,
|
deltas: Vec<Value>,
|
||||||
global_params: Vec<(&'static str, Value)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CmdRegister {
|
impl CmdRegister {
|
||||||
@@ -179,7 +168,6 @@ impl CmdRegister {
|
|||||||
sound: None,
|
sound: None,
|
||||||
params: Vec::with_capacity(16),
|
params: Vec::with_capacity(16),
|
||||||
deltas: Vec::with_capacity(4),
|
deltas: Vec::with_capacity(4),
|
||||||
global_params: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,28 +203,6 @@ impl CmdRegister {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn global_params(&self) -> &[(&'static str, Value)] {
|
|
||||||
&self.global_params
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn commit_global(&mut self) {
|
|
||||||
self.global_params.append(&mut self.params);
|
|
||||||
self.sound = None;
|
|
||||||
self.deltas.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn clear_global(&mut self) {
|
|
||||||
self.global_params.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_global(&mut self, params: Vec<(&'static str, Value)>) {
|
|
||||||
self.global_params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_global(&mut self) -> Vec<(&'static str, Value)> {
|
|
||||||
std::mem::take(&mut self.global_params)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn clear(&mut self) {
|
pub(super) fn clear(&mut self) {
|
||||||
self.sound = None;
|
self.sound = None;
|
||||||
self.params.clear();
|
self.params.clear();
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ use super::types::{
|
|||||||
Value, Variables, VariablesMap,
|
Value, Variables, VariablesMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Forth VM instance. Holds the stack, variables, dictionary, and RNG.
|
|
||||||
pub struct Forth {
|
pub struct Forth {
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
vars: Variables,
|
vars: Variables,
|
||||||
dict: Dictionary,
|
dict: Dictionary,
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
global_params: Mutex<Vec<(&'static str, Value)>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Forth {
|
impl Forth {
|
||||||
@@ -30,7 +28,6 @@ impl Forth {
|
|||||||
vars,
|
vars,
|
||||||
dict,
|
dict,
|
||||||
rng,
|
rng,
|
||||||
global_params: Mutex::new(Vec::new()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +39,12 @@ impl Forth {
|
|||||||
self.stack.lock().clear();
|
self.stack.lock().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_global_params(&self) {
|
|
||||||
self.global_params.lock().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate a Forth script and return audio command strings.
|
|
||||||
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
||||||
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
|
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
|
||||||
self.apply_var_writes(var_writes);
|
self.apply_var_writes(var_writes);
|
||||||
Ok(outputs)
|
Ok(outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate and collect an execution trace for UI highlighting.
|
|
||||||
pub fn evaluate_with_trace(
|
pub fn evaluate_with_trace(
|
||||||
&self,
|
&self,
|
||||||
script: &str,
|
script: &str,
|
||||||
@@ -65,7 +56,6 @@ impl Forth {
|
|||||||
Ok(outputs)
|
Ok(outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate and return both outputs and pending variable writes (without applying them).
|
|
||||||
pub fn evaluate_raw(
|
pub fn evaluate_raw(
|
||||||
&self,
|
&self,
|
||||||
script: &str,
|
script: &str,
|
||||||
@@ -112,8 +102,6 @@ impl Forth {
|
|||||||
let vars_snapshot = self.vars.load_full();
|
let vars_snapshot = self.vars.load_full();
|
||||||
let mut var_writes: HashMap<String, Value> = HashMap::new();
|
let mut var_writes: HashMap<String, Value> = HashMap::new();
|
||||||
|
|
||||||
cmd.set_global(std::mem::take(&mut *self.global_params.lock()));
|
|
||||||
|
|
||||||
self.execute_ops(
|
self.execute_ops(
|
||||||
ops,
|
ops,
|
||||||
ctx,
|
ctx,
|
||||||
@@ -125,8 +113,6 @@ impl Forth {
|
|||||||
&mut var_writes,
|
&mut var_writes,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
*self.global_params.lock() = cmd.take_global();
|
|
||||||
|
|
||||||
Ok((outputs, var_writes))
|
Ok((outputs, var_writes))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +130,6 @@ impl Forth {
|
|||||||
var_writes: &mut HashMap<String, Value>,
|
var_writes: &mut HashMap<String, Value>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut pc = 0;
|
let mut pc = 0;
|
||||||
let mut marks: Vec<usize> = Vec::new();
|
|
||||||
let trace_cell = std::cell::RefCell::new(trace);
|
let trace_cell = std::cell::RefCell::new(trace);
|
||||||
let var_writes_cell = std::cell::RefCell::new(Some(var_writes));
|
let var_writes_cell = std::cell::RefCell::new(Some(var_writes));
|
||||||
|
|
||||||
@@ -229,9 +214,8 @@ impl Forth {
|
|||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
let param_max = cmd
|
let param_max = cmd
|
||||||
.global_params()
|
.params()
|
||||||
.iter()
|
.iter()
|
||||||
.chain(cmd.params().iter())
|
|
||||||
.map(|(_, v)| match v {
|
.map(|(_, v)| match v {
|
||||||
Value::CycleList(items) => items.len(),
|
Value::CycleList(items) => items.len(),
|
||||||
_ => 1,
|
_ => 1,
|
||||||
@@ -243,8 +227,7 @@ impl Forth {
|
|||||||
|
|
||||||
let has_arp_list = |cmd: &CmdRegister| -> bool {
|
let has_arp_list = |cmd: &CmdRegister| -> bool {
|
||||||
matches!(cmd.sound(), Some(Value::ArpList(_)))
|
matches!(cmd.sound(), Some(Value::ArpList(_)))
|
||||||
|| cmd.global_params().iter().chain(cmd.params().iter())
|
|| cmd.params().iter().any(|(_, v)| matches!(v, Value::ArpList(_)))
|
||||||
.any(|(_, v)| matches!(v, Value::ArpList(_)))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let compute_arp_count = |cmd: &CmdRegister| -> usize {
|
let compute_arp_count = |cmd: &CmdRegister| -> usize {
|
||||||
@@ -270,21 +253,15 @@ impl Forth {
|
|||||||
delta_secs: f64,
|
delta_secs: f64,
|
||||||
outputs: &mut Vec<String>|
|
outputs: &mut Vec<String>|
|
||||||
-> Result<Option<Value>, String> {
|
-> Result<Option<Value>, String> {
|
||||||
let has_sound = cmd.sound().is_some();
|
let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?;
|
||||||
let has_params = !cmd.params().is_empty();
|
|
||||||
let has_global = !cmd.global_params().is_empty();
|
|
||||||
if !has_sound && !has_params && !has_global {
|
|
||||||
return Err("nothing to emit".into());
|
|
||||||
}
|
|
||||||
let resolved_sound_val =
|
let resolved_sound_val =
|
||||||
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
|
sound_opt.map(|sv| resolve_value(sv, arp_idx, poly_idx));
|
||||||
let sound_str = match &resolved_sound_val {
|
let sound_str = match &resolved_sound_val {
|
||||||
Some(v) => Some(v.as_str()?.to_string()),
|
Some(v) => Some(v.as_str()?.to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let resolved_params: Vec<(&str, String)> = cmd.global_params()
|
let resolved_params: Vec<(&str, String)> = params
|
||||||
.iter()
|
.iter()
|
||||||
.chain(cmd.params().iter())
|
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let resolved = resolve_value(v, arp_idx, poly_idx);
|
let resolved = resolve_value(v, arp_idx, poly_idx);
|
||||||
if let Value::CycleList(_) | Value::ArpList(_) = v {
|
if let Value::CycleList(_) | Value::ArpList(_) = v {
|
||||||
@@ -302,7 +279,6 @@ impl Forth {
|
|||||||
&resolved_params,
|
&resolved_params,
|
||||||
ctx.step_duration(),
|
ctx.step_duration(),
|
||||||
delta_secs,
|
delta_secs,
|
||||||
ctx.sr,
|
|
||||||
outputs,
|
outputs,
|
||||||
);
|
);
|
||||||
Ok(resolved_sound_val.map(|v| v.into_owned()))
|
Ok(resolved_sound_val.map(|v| v.into_owned()))
|
||||||
@@ -316,7 +292,7 @@ impl Forth {
|
|||||||
|
|
||||||
Op::Dup => {
|
Op::Dup => {
|
||||||
ensure(stack, 1)?;
|
ensure(stack, 1)?;
|
||||||
let v = stack.last().expect("stack non-empty after ensure").clone();
|
let v = stack.last().unwrap().clone();
|
||||||
stack.push(v);
|
stack.push(v);
|
||||||
}
|
}
|
||||||
Op::Dupn => {
|
Op::Dupn => {
|
||||||
@@ -329,16 +305,6 @@ impl Forth {
|
|||||||
Op::Drop => {
|
Op::Drop => {
|
||||||
pop(stack)?;
|
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 => {
|
Op::Swap => {
|
||||||
ensure(stack, 2)?;
|
ensure(stack, 2)?;
|
||||||
let len = stack.len();
|
let len = stack.len();
|
||||||
@@ -460,7 +426,7 @@ impl Forth {
|
|||||||
if b.as_float().map_or(true, |v| v == 0.0) {
|
if b.as_float().map_or(true, |v| v == 0.0) {
|
||||||
return Err("division by zero".into());
|
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 => {
|
Op::Mod => {
|
||||||
let b = pop(stack)?;
|
let b = pop(stack)?;
|
||||||
@@ -468,47 +434,47 @@ impl Forth {
|
|||||||
if b.as_float().map_or(true, |v| v == 0.0) {
|
if b.as_float().map_or(true, |v| v == 0.0) {
|
||||||
return Err("modulo by zero".into());
|
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);
|
stack.push(result);
|
||||||
}
|
}
|
||||||
Op::Neg => {
|
Op::Neg => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| -x)?);
|
stack.push(lift_unary(v, |x| -x)?);
|
||||||
}
|
}
|
||||||
Op::Abs => {
|
Op::Abs => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| x.abs())?);
|
stack.push(lift_unary(v, |x| x.abs())?);
|
||||||
}
|
}
|
||||||
Op::Floor => {
|
Op::Floor => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| x.floor())?);
|
stack.push(lift_unary(v, |x| x.floor())?);
|
||||||
}
|
}
|
||||||
Op::Ceil => {
|
Op::Ceil => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| x.ceil())?);
|
stack.push(lift_unary(v, |x| x.ceil())?);
|
||||||
}
|
}
|
||||||
Op::Round => {
|
Op::Round => {
|
||||||
let v = pop(stack)?;
|
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::Min => binary_op(stack, |a, b| a.min(b))?,
|
||||||
Op::Max => binary_op(stack, |a, b| a.max(b))?,
|
Op::Max => binary_op(stack, |a, b| a.max(b))?,
|
||||||
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
|
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
|
||||||
Op::Sqrt => {
|
Op::Sqrt => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| x.sqrt())?);
|
stack.push(lift_unary(v, |x| x.sqrt())?);
|
||||||
}
|
}
|
||||||
Op::Sin => {
|
Op::Sin => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| x.sin())?);
|
stack.push(lift_unary(v, |x| x.sin())?);
|
||||||
}
|
}
|
||||||
Op::Cos => {
|
Op::Cos => {
|
||||||
let v = pop(stack)?;
|
let v = pop(stack)?;
|
||||||
stack.push(lift_unary(&v, |x| x.cos())?);
|
stack.push(lift_unary(v, |x| x.cos())?);
|
||||||
}
|
}
|
||||||
Op::Log => {
|
Op::Log => {
|
||||||
let v = pop(stack)?;
|
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)?,
|
Op::Eq => cmp_op(stack, |a, b| (a - b).abs() < f64::EPSILON)?,
|
||||||
@@ -569,10 +535,7 @@ impl Forth {
|
|||||||
|
|
||||||
Op::NewCmd => {
|
Op::NewCmd => {
|
||||||
ensure(stack, 1)?;
|
ensure(stack, 1)?;
|
||||||
let values = drain_skip_quotations(stack);
|
let values = std::mem::take(stack);
|
||||||
if values.is_empty() {
|
|
||||||
return Err("expected sound name".into());
|
|
||||||
}
|
|
||||||
let val = if values.len() == 1 {
|
let val = if values.len() == 1 {
|
||||||
values.into_iter().next().unwrap()
|
values.into_iter().next().unwrap()
|
||||||
} else {
|
} else {
|
||||||
@@ -582,10 +545,7 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
Op::SetParam(param) => {
|
Op::SetParam(param) => {
|
||||||
ensure(stack, 1)?;
|
ensure(stack, 1)?;
|
||||||
let values = drain_skip_quotations(stack);
|
let values = std::mem::take(stack);
|
||||||
if values.is_empty() {
|
|
||||||
return Err("expected parameter value".into());
|
|
||||||
}
|
|
||||||
let val = if values.len() == 1 {
|
let val = if values.len() == 1 {
|
||||||
values.into_iter().next().unwrap()
|
values.into_iter().next().unwrap()
|
||||||
} else {
|
} else {
|
||||||
@@ -820,20 +780,16 @@ impl Forth {
|
|||||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Bounce(word_span) | Op::PBounce(word_span) => {
|
Op::Bounce(word_span) => {
|
||||||
let count = pop_int(stack)? as usize;
|
let count = pop_int(stack)? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return Err("bounce count must be > 0".into());
|
return Err("bounce count must be > 0".into());
|
||||||
}
|
}
|
||||||
let counter = match &ops[pc] {
|
|
||||||
Op::Bounce(_) => ctx.runs,
|
|
||||||
_ => ctx.iter,
|
|
||||||
};
|
|
||||||
let idx = if count == 1 {
|
let idx = if count == 1 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
let period = 2 * (count - 1);
|
let period = 2 * (count - 1);
|
||||||
let raw = counter % period;
|
let raw = ctx.runs % period;
|
||||||
if raw < count { raw } else { period - raw }
|
if raw < count { raw } else { period - raw }
|
||||||
};
|
};
|
||||||
if let Some(span) = word_span {
|
if let Some(span) = word_span {
|
||||||
@@ -920,47 +876,6 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Except(word_span) => {
|
|
||||||
let n = pop_int(stack)?;
|
|
||||||
let quot = pop(stack)?;
|
|
||||||
if n <= 0 {
|
|
||||||
return Err("except count must be > 0".into());
|
|
||||||
}
|
|
||||||
let result = ctx.iter as i64 % n != 0;
|
|
||||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
|
||||||
if result {
|
|
||||||
run_quotation(quot, stack, outputs, cmd)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::EveryOffset(word_span) => {
|
|
||||||
let offset = pop_int(stack)?;
|
|
||||||
let n = pop_int(stack)?;
|
|
||||||
let quot = pop(stack)?;
|
|
||||||
if n <= 0 {
|
|
||||||
return Err("every+ count must be > 0".into());
|
|
||||||
}
|
|
||||||
let result = ctx.iter as i64 % n == offset.rem_euclid(n);
|
|
||||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
|
||||||
if result {
|
|
||||||
run_quotation(quot, stack, outputs, cmd)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::ExceptOffset(word_span) => {
|
|
||||||
let offset = pop_int(stack)?;
|
|
||||||
let n = pop_int(stack)?;
|
|
||||||
let quot = pop(stack)?;
|
|
||||||
if n <= 0 {
|
|
||||||
return Err("except+ count must be > 0".into());
|
|
||||||
}
|
|
||||||
let result = ctx.iter as i64 % n != offset.rem_euclid(n);
|
|
||||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
|
||||||
if result {
|
|
||||||
run_quotation(quot, stack, outputs, cmd)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::Bjork(word_span) | Op::PBjork(word_span) => {
|
Op::Bjork(word_span) | Op::PBjork(word_span) => {
|
||||||
let n = pop_int(stack)?;
|
let n = pop_int(stack)?;
|
||||||
let k = pop_int(stack)?;
|
let k = pop_int(stack)?;
|
||||||
@@ -1056,7 +971,7 @@ impl Forth {
|
|||||||
let key = read_key(&var_writes_cell, vars_snapshot);
|
let key = read_key(&var_writes_cell, vars_snapshot);
|
||||||
let values = std::mem::take(stack);
|
let values = std::mem::take(stack);
|
||||||
for val in values {
|
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 octave_offset = degree.div_euclid(len);
|
||||||
let idx = degree.rem_euclid(len) as usize;
|
let idx = degree.rem_euclid(len) as usize;
|
||||||
key + octave_offset * 12 + pattern[idx]
|
key + octave_offset * 12 + pattern[idx]
|
||||||
@@ -1156,7 +1071,7 @@ impl Forth {
|
|||||||
Op::Oct => {
|
Op::Oct => {
|
||||||
let shift = pop(stack)?;
|
let shift = pop(stack)?;
|
||||||
let note = 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);
|
stack.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,11 +1096,11 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::Loop => {
|
Op::Loop => {
|
||||||
let steps = pop_float(stack)?;
|
let beats = pop_float(stack)?;
|
||||||
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
||||||
return Err("tempo and speed must be non-zero".into());
|
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("fit", Value::Float(dur, None));
|
||||||
cmd.set_param("dur", Value::Float(dur, None));
|
cmd.set_param("dur", Value::Float(dur, None));
|
||||||
}
|
}
|
||||||
@@ -1279,37 +1194,6 @@ impl Forth {
|
|||||||
cmd.clear();
|
cmd.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::EmitAll => {
|
|
||||||
// Retroactive: patch existing sound outputs with current params
|
|
||||||
if !cmd.params().is_empty() {
|
|
||||||
let step_duration = ctx.step_duration();
|
|
||||||
for output in outputs.iter_mut() {
|
|
||||||
if output.starts_with("/sound/") {
|
|
||||||
use std::fmt::Write;
|
|
||||||
for (k, v) in cmd.params() {
|
|
||||||
let val_str = v.to_param_string();
|
|
||||||
if !output.ends_with('/') {
|
|
||||||
output.push('/');
|
|
||||||
}
|
|
||||||
if is_tempo_scaled_param(k) {
|
|
||||||
if let Ok(val) = val_str.parse::<f64>() {
|
|
||||||
let _ = write!(output, "{k}/{}", val * step_duration);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = write!(output, "{k}/{val_str}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Prospective: store for future emits
|
|
||||||
cmd.commit_global();
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::ClearGlobal => {
|
|
||||||
cmd.clear_global();
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::IntRange => {
|
Op::IntRange => {
|
||||||
let end = pop_int(stack)?;
|
let end = pop_int(stack)?;
|
||||||
let start = pop_int(stack)?;
|
let start = pop_int(stack)?;
|
||||||
@@ -1375,15 +1259,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 => {
|
Op::GeomRange => {
|
||||||
let count = pop_int(stack)?;
|
let count = pop_int(stack)?;
|
||||||
let ratio = pop_float(stack)?;
|
let ratio = pop_float(stack)?;
|
||||||
@@ -1433,7 +1308,7 @@ impl Forth {
|
|||||||
let dur = pop_float(stack)? * ctx.step_duration();
|
let dur = pop_float(stack)? * ctx.step_duration();
|
||||||
let end = pop_float(stack)?;
|
let end = pop_float(stack)?;
|
||||||
let start = 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}");
|
let s = format!("{start}>{end}:{dur}{suffix}");
|
||||||
stack.push(Value::Str(s.into(), None));
|
stack.push(Value::Str(s.into(), None));
|
||||||
}
|
}
|
||||||
@@ -1446,57 +1321,25 @@ impl Forth {
|
|||||||
stack.push(Value::Str(s.into(), None));
|
stack.push(Value::Str(s.into(), None));
|
||||||
}
|
}
|
||||||
Op::ModEnv => {
|
Op::ModEnv => {
|
||||||
let release = pop_float(stack)? * ctx.step_duration();
|
ensure(stack, 1)?;
|
||||||
let sustain = pop_float(stack)?;
|
let values = std::mem::take(stack);
|
||||||
let decay = pop_float(stack)? * ctx.step_duration();
|
let mut floats = Vec::with_capacity(values.len());
|
||||||
let attack = pop_float(stack)? * ctx.step_duration();
|
for v in &values {
|
||||||
let max = pop_float(stack)?;
|
floats.push(v.as_float()?);
|
||||||
let min = pop_float(stack)?;
|
}
|
||||||
|
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;
|
use std::fmt::Write;
|
||||||
let mut s = String::new();
|
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));
|
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
|
// MIDI operations
|
||||||
Op::MidiEmit => {
|
Op::MidiEmit => {
|
||||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||||
@@ -1575,7 +1418,7 @@ impl Forth {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let dev =
|
let dev =
|
||||||
get_int("dev").map(|d| d.clamp(0, 3) as u8).unwrap_or(0);
|
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}")
|
format!("/delta/{delta_secs}")
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
@@ -1606,7 +1449,7 @@ impl Forth {
|
|||||||
} else {
|
} else {
|
||||||
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
||||||
let velocity =
|
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 = get_float("dur").unwrap_or(1.0);
|
||||||
let dur_secs = dur * ctx.step_duration();
|
let dur_secs = dur * ctx.step_duration();
|
||||||
outputs.push(format!(
|
outputs.push(format!(
|
||||||
@@ -1649,47 +1492,6 @@ impl Forth {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
stack.push(Value::Int(val as i64, None));
|
stack.push(Value::Int(val as i64, None));
|
||||||
}
|
}
|
||||||
Op::Mark => {
|
|
||||||
marks.push(stack.len());
|
|
||||||
}
|
|
||||||
Op::Count(span) => {
|
|
||||||
let mark = marks.pop().ok_or("count without mark")?;
|
|
||||||
stack.push(Value::Int((stack.len() - mark) as i64, *span));
|
|
||||||
}
|
|
||||||
Op::Index(word_span) => {
|
|
||||||
let idx = pop_int(stack)?;
|
|
||||||
let count = pop_int(stack)? as usize;
|
|
||||||
if count == 0 {
|
|
||||||
return Err("index count must be > 0".into());
|
|
||||||
}
|
|
||||||
let resolved_idx = ((idx % count as i64 + count as i64) % count as i64) as usize;
|
|
||||||
if let Some(span) = word_span {
|
|
||||||
if stack.len() >= count {
|
|
||||||
let start = stack.len() - count;
|
|
||||||
let selected = &stack[start + resolved_idx];
|
|
||||||
record_resolved_from_value(&trace_cell, Some(*span), selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drain_select_run(count, resolved_idx, stack, outputs, cmd)?;
|
|
||||||
}
|
|
||||||
Op::Rec => {
|
|
||||||
let name = pop(stack)?;
|
|
||||||
outputs.push(format!("/doux/rec/{}", name.as_str()?));
|
|
||||||
}
|
|
||||||
Op::Overdub => {
|
|
||||||
let name = pop(stack)?;
|
|
||||||
outputs.push(format!("/doux/rec/{}/overdub/1", name.as_str()?));
|
|
||||||
}
|
|
||||||
Op::Orec => {
|
|
||||||
let orbit = pop(stack)?.as_int()?;
|
|
||||||
let name = pop(stack)?;
|
|
||||||
outputs.push(format!("/doux/rec/{}/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));
|
|
||||||
}
|
|
||||||
Op::Forget => {
|
Op::Forget => {
|
||||||
let name = pop(stack)?;
|
let name = pop(stack)?;
|
||||||
self.dict.lock().remove(name.as_str()?);
|
self.dict.lock().remove(name.as_str()?);
|
||||||
@@ -1742,18 +1544,30 @@ fn extract_dev_param(params: &[(&str, Value)]) -> u8 {
|
|||||||
.unwrap_or(0)
|
.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 {
|
fn is_tempo_scaled_param(name: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
name,
|
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 +1576,6 @@ fn emit_output(
|
|||||||
params: &[(&str, String)],
|
params: &[(&str, String)],
|
||||||
step_duration: f64,
|
step_duration: f64,
|
||||||
nudge_secs: f64,
|
nudge_secs: f64,
|
||||||
sr: f64,
|
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
) {
|
) {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
@@ -1770,7 +1583,6 @@ fn emit_output(
|
|||||||
out.push('/');
|
out.push('/');
|
||||||
|
|
||||||
let has_dur = params.iter().any(|(k, _)| *k == "dur");
|
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");
|
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
|
||||||
|
|
||||||
if let Some(s) = sound {
|
if let Some(s) = sound {
|
||||||
@@ -1778,9 +1590,6 @@ fn emit_output(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (i, (k, v)) in params.iter().enumerate() {
|
for (i, (k, v)) in params.iter().enumerate() {
|
||||||
if v.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !out.ends_with('/') {
|
if !out.ends_with('/') {
|
||||||
out.push('/');
|
out.push('/');
|
||||||
}
|
}
|
||||||
@@ -1798,12 +1607,11 @@ fn emit_output(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if nudge_secs.abs() > 1e-9 {
|
if nudge_secs > 0.0 {
|
||||||
if !out.ends_with('/') {
|
if !out.ends_with('/') {
|
||||||
out.push('/');
|
out.push('/');
|
||||||
}
|
}
|
||||||
let delta_ticks = (nudge_secs * sr).round() as i64;
|
let _ = write!(&mut out, "delta/{nudge_secs}");
|
||||||
let _ = write!(&mut out, "delta/{delta_ticks}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_dur {
|
if !has_dur {
|
||||||
@@ -1813,13 +1621,6 @@ fn emit_output(
|
|||||||
let _ = write!(&mut out, "dur/{}", step_duration * 4.0);
|
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 sound.is_some() && delaytime_idx.is_none() {
|
||||||
if !out.ends_with('/') {
|
if !out.ends_with('/') {
|
||||||
out.push('/');
|
out.push('/');
|
||||||
@@ -1863,8 +1664,8 @@ fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
|
|||||||
groups.into_iter().partition(|g| g[0]);
|
groups.into_iter().partition(|g| g[0]);
|
||||||
|
|
||||||
for _ in 0..min_count {
|
for _ in 0..min_count {
|
||||||
let mut one = ones.pop().expect("ones sufficient for min_count");
|
let mut one = ones.pop().unwrap();
|
||||||
one.extend(zeros.pop().expect("zeros sufficient for min_count"));
|
one.extend(zeros.pop().unwrap());
|
||||||
new_groups.push(one);
|
new_groups.push(one);
|
||||||
}
|
}
|
||||||
new_groups.extend(ones);
|
new_groups.extend(ones);
|
||||||
@@ -1925,21 +1726,6 @@ fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, String> {
|
|||||||
Ok(pop(stack)?.is_truthy())
|
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> {
|
fn ensure(stack: &[Value], n: usize) -> Result<(), String> {
|
||||||
if stack.len() < n {
|
if stack.len() < n {
|
||||||
return Err("stack underflow".into());
|
return Err("stack underflow".into());
|
||||||
@@ -1955,69 +1741,25 @@ 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
|
where
|
||||||
F: Fn(f64) -> f64 + Copy,
|
F: Fn(f64) -> f64,
|
||||||
{
|
{
|
||||||
match val {
|
Ok(float_to_value(f(val.as_float()?)))
|
||||||
Value::ArpList(items) => {
|
|
||||||
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
|
|
||||||
Ok(Value::ArpList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
Value::CycleList(items) => {
|
|
||||||
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x, f)).collect();
|
|
||||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
v => Ok(float_to_value(f(v.as_float()?))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lift_unary_int<F>(val: &Value, f: F) -> Result<Value, String>
|
fn lift_unary_int<F>(val: Value, f: F) -> Result<Value, String>
|
||||||
where
|
where
|
||||||
F: Fn(i64) -> i64 + Copy,
|
F: Fn(i64) -> i64,
|
||||||
{
|
{
|
||||||
match val {
|
Ok(Value::Int(f(val.as_int()?), None))
|
||||||
Value::ArpList(items) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_unary_int(x, 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();
|
|
||||||
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
|
where
|
||||||
F: Fn(f64, f64) -> f64 + Copy,
|
F: Fn(f64, f64) -> f64,
|
||||||
{
|
{
|
||||||
match (a, b) {
|
Ok(float_to_value(f(a.as_float()?, b.as_float()?)))
|
||||||
(Value::ArpList(items), b) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_binary(x, b, 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();
|
|
||||||
Ok(Value::ArpList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
(Value::CycleList(items), b) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_binary(x, b, f)).collect();
|
|
||||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
(a, Value::CycleList(items)) => {
|
|
||||||
let mapped: Result<Vec<_>, _> =
|
|
||||||
items.iter().map(|x| lift_binary(a, x, f)).collect();
|
|
||||||
Ok(Value::CycleList(Arc::from(mapped?)))
|
|
||||||
}
|
|
||||||
(a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||||
@@ -2026,7 +1768,7 @@ where
|
|||||||
{
|
{
|
||||||
let b = pop(stack)?;
|
let b = pop(stack)?;
|
||||||
let a = pop(stack)?;
|
let a = pop(stack)?;
|
||||||
stack.push(lift_binary(&a, &b, f)?);
|
stack.push(lift_binary(a, b, f)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Word-to-Op translation: maps Forth word names to compiled instructions.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::ops::Op;
|
use crate::ops::Op;
|
||||||
@@ -13,7 +11,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"dup" => Op::Dup,
|
"dup" => Op::Dup,
|
||||||
"dupn" => Op::Dupn,
|
"dupn" => Op::Dupn,
|
||||||
"drop" => Op::Drop,
|
"drop" => Op::Drop,
|
||||||
"print" => Op::Print,
|
|
||||||
"swap" => Op::Swap,
|
"swap" => Op::Swap,
|
||||||
"over" => Op::Over,
|
"over" => Op::Over,
|
||||||
"rot" => Op::Rot,
|
"rot" => Op::Rot,
|
||||||
@@ -59,7 +56,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"nand" => Op::Nand,
|
"nand" => Op::Nand,
|
||||||
"nor" => Op::Nor,
|
"nor" => Op::Nor,
|
||||||
"ifelse" => Op::IfElse,
|
"ifelse" => Op::IfElse,
|
||||||
"select" => Op::Pick,
|
"pick" => Op::Pick,
|
||||||
"sound" => Op::NewCmd,
|
"sound" => Op::NewCmd,
|
||||||
"." => Op::Emit,
|
"." => Op::Emit,
|
||||||
"rand" => Op::Rand(None),
|
"rand" => Op::Rand(None),
|
||||||
@@ -70,12 +67,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"pcycle" => Op::PCycle(None),
|
"pcycle" => Op::PCycle(None),
|
||||||
"choose" => Op::Choose(None),
|
"choose" => Op::Choose(None),
|
||||||
"bounce" => Op::Bounce(None),
|
"bounce" => Op::Bounce(None),
|
||||||
"pbounce" => Op::PBounce(None),
|
|
||||||
"wchoose" => Op::WChoose(None),
|
"wchoose" => Op::WChoose(None),
|
||||||
"every" => Op::Every(None),
|
"every" => Op::Every(None),
|
||||||
"except" => Op::Except(None),
|
|
||||||
"every+" => Op::EveryOffset(None),
|
|
||||||
"except+" => Op::ExceptOffset(None),
|
|
||||||
"bjork" => Op::Bjork(None),
|
"bjork" => Op::Bjork(None),
|
||||||
"pbjork" => Op::PBjork(None),
|
"pbjork" => Op::PBjork(None),
|
||||||
"chance" => Op::ChanceExec(None),
|
"chance" => Op::ChanceExec(None),
|
||||||
@@ -101,8 +94,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"loop" => Op::Loop,
|
"loop" => Op::Loop,
|
||||||
"oct" => Op::Oct,
|
"oct" => Op::Oct,
|
||||||
"clear" => Op::ClearCmd,
|
"clear" => Op::ClearCmd,
|
||||||
"all" => Op::EmitAll,
|
|
||||||
"noall" => Op::ClearGlobal,
|
|
||||||
".." => Op::IntRange,
|
".." => Op::IntRange,
|
||||||
".," => Op::StepRange,
|
".," => Op::StepRange,
|
||||||
"gen" => Op::Generate,
|
"gen" => Op::Generate,
|
||||||
@@ -110,19 +101,13 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"euclid" => Op::Euclid,
|
"euclid" => Op::Euclid,
|
||||||
"euclidrot" => Op::EuclidRot,
|
"euclidrot" => Op::EuclidRot,
|
||||||
"times" => Op::Times,
|
"times" => Op::Times,
|
||||||
"map" => Op::Map,
|
|
||||||
"m." => Op::MidiEmit,
|
"m." => Op::MidiEmit,
|
||||||
"ccval" => Op::GetMidiCC,
|
"ccval" => Op::GetMidiCC,
|
||||||
"mclock" => Op::MidiClock,
|
"mclock" => Op::MidiClock,
|
||||||
"mstart" => Op::MidiStart,
|
"mstart" => Op::MidiStart,
|
||||||
"mstop" => Op::MidiStop,
|
"mstop" => Op::MidiStop,
|
||||||
"mcont" => Op::MidiContinue,
|
"mcont" => Op::MidiContinue,
|
||||||
"rec" => Op::Rec,
|
|
||||||
"overdub" | "dub" => Op::Overdub,
|
|
||||||
"orec" => Op::Orec,
|
|
||||||
"odub" => Op::Odub,
|
|
||||||
"forget" => Op::Forget,
|
"forget" => Op::Forget,
|
||||||
"index" => Op::Index(None),
|
|
||||||
"key!" => Op::SetKey,
|
"key!" => Op::SetKey,
|
||||||
"tp" => Op::Transpose,
|
"tp" => Op::Transpose,
|
||||||
"inv" => Op::Invert,
|
"inv" => Op::Invert,
|
||||||
@@ -136,16 +121,10 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"slide" => Op::ModSlide(0),
|
"slide" => Op::ModSlide(0),
|
||||||
"expslide" => Op::ModSlide(1),
|
"expslide" => Op::ModSlide(1),
|
||||||
"sslide" => Op::ModSlide(2),
|
"sslide" => Op::ModSlide(2),
|
||||||
"islide" => Op::ModSlide(3),
|
|
||||||
"oslide" => Op::ModSlide(4),
|
|
||||||
"pslide" => Op::ModSlide(5),
|
|
||||||
"jit" => Op::ModRnd(0),
|
"jit" => Op::ModRnd(0),
|
||||||
"sjit" => Op::ModRnd(1),
|
"sjit" => Op::ModRnd(1),
|
||||||
"drunk" => Op::ModRnd(2),
|
"drunk" => Op::ModRnd(2),
|
||||||
"ead" => Op::ModEnvAd,
|
"env" => Op::ModEnv,
|
||||||
"eadr" => Op::ModEnvAdr,
|
|
||||||
"eadsr" | "env" => Op::ModEnv,
|
|
||||||
"lpg" => Op::Lpg,
|
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -222,10 +201,9 @@ fn attach_span(op: &mut Op, span: SourceSpan) {
|
|||||||
match op {
|
match op {
|
||||||
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
|
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
|
||||||
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
|
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
|
||||||
| Op::Bounce(s) | Op::PBounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
|
| Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
|
||||||
| Op::Every(s) | Op::Except(s) | Op::EveryOffset(s) | Op::ExceptOffset(s)
|
| Op::Every(s)
|
||||||
| Op::Bjork(s) | Op::PBjork(s)
|
| Op::Bjork(s) | Op::PBjork(s) => *s = Some(span),
|
||||||
| Op::Count(s) | Op::Index(s) => *s = Some(span),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Word metadata for core language primitives (stack, arithmetic, logic, variables, definitions).
|
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
|
// Stack, Arithmetic, Comparison, Logic, Control, Variables, Definitions
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Stack manipulation
|
// Stack manipulation
|
||||||
Word {
|
Word {
|
||||||
@@ -33,16 +33,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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 {
|
Word {
|
||||||
name: "swap",
|
name: "swap",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -512,17 +502,17 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(true-quot false-quot bool --)",
|
stack: "(true-quot false-quot bool --)",
|
||||||
desc: "Execute true-quot if true, else false-quot",
|
desc: "Execute true-quot if true, else false-quot",
|
||||||
example: "( 1 ) ( 2 ) coin ifelse",
|
example: "{ 1 } { 2 } coin ifelse",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "select",
|
name: "pick",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(..quots n --)",
|
stack: "(..quots n --)",
|
||||||
desc: "Execute nth quotation (0-indexed)",
|
desc: "Execute nth quotation (0-indexed)",
|
||||||
example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
|
example: "{ 1 } { 2 } { 3 } 2 pick => 3",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
@@ -532,7 +522,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(quot bool --)",
|
stack: "(quot bool --)",
|
||||||
desc: "Execute quotation if true",
|
desc: "Execute quotation if true",
|
||||||
example: "( 2 distort ) 0.5 chance ?",
|
example: "{ 2 distort } 0.5 chance ?",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -542,7 +532,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(quot bool --)",
|
stack: "(quot bool --)",
|
||||||
desc: "Execute quotation if false",
|
desc: "Execute quotation if false",
|
||||||
example: "( 1 distort ) 0.5 chance !?",
|
example: "{ 1 distort } 0.5 chance !?",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -552,7 +542,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation unconditionally",
|
desc: "Execute quotation unconditionally",
|
||||||
example: "( 2 * ) apply",
|
example: "{ 2 * } apply",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -563,17 +553,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Control",
|
category: "Control",
|
||||||
stack: "(n quot --)",
|
stack: "(n quot --)",
|
||||||
desc: "Execute quotation n times, @i holds current index",
|
desc: "Execute quotation n times, @i holds current index",
|
||||||
example: "4 ( @i . ) times => 0 1 2 3",
|
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",
|
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Word metadata for audio effect parameters (filter, envelope, reverb, delay, lo-fi, stereo, mod FX).
|
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
|
// Filter, Envelope, Reverb, Delay, Lo-fi, Stereo, Mod FX
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Envelope
|
// Envelope
|
||||||
Word {
|
Word {
|
||||||
@@ -28,14 +28,14 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set velocity (0-1)",
|
desc: "Set velocity",
|
||||||
example: "0.8 velocity",
|
example: "100 velocity",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "attack",
|
name: "attack",
|
||||||
aliases: &["att", "a"],
|
aliases: &["att"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set attack time",
|
desc: "Set attack time",
|
||||||
@@ -45,7 +45,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "decay",
|
name: "decay",
|
||||||
aliases: &["dec", "d"],
|
aliases: &["dec"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set decay time",
|
desc: "Set decay time",
|
||||||
@@ -55,7 +55,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "sustain",
|
name: "sustain",
|
||||||
aliases: &["sus", "s"],
|
aliases: &["sus"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set sustain level",
|
desc: "Set sustain level",
|
||||||
@@ -65,7 +65,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "release",
|
name: "release",
|
||||||
aliases: &["rel", "r"],
|
aliases: &["rel"],
|
||||||
category: "Envelope",
|
category: "Envelope",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set release time",
|
desc: "Set release time",
|
||||||
@@ -73,26 +73,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "adsr",
|
name: "adsr",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -113,6 +93,56 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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
|
// Filter
|
||||||
Word {
|
Word {
|
||||||
name: "lpf",
|
name: "lpf",
|
||||||
@@ -134,6 +164,56 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "hpf",
|
name: "hpf",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -154,6 +234,56 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "bpf",
|
name: "bpf",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -174,6 +304,56 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "llpf",
|
name: "llpf",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -274,36 +454,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "tilt",
|
name: "tilt",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -809,45 +959,4 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
// Compressor
|
|
||||||
Word {
|
|
||||||
name: "comp",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Compressor",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Set sidechain duck amount (0-1)",
|
|
||||||
example: "0.8 comp",
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "compattack",
|
|
||||||
aliases: &["cattack"],
|
|
||||||
category: "Compressor",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Set compressor attack time in seconds",
|
|
||||||
example: "0.01 compattack",
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "comprelease",
|
|
||||||
aliases: &["crelease"],
|
|
||||||
category: "Compressor",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Set compressor release time in seconds",
|
|
||||||
example: "0.15 comprelease",
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "comporbit",
|
|
||||||
aliases: &["corbit"],
|
|
||||||
category: "Compressor",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Set sidechain source orbit",
|
|
||||||
example: "0 comporbit",
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//! MIDI word definitions: channel, CC, pitch bend, transport, and device routing.
|
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
|
// MIDI
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
Word {
|
Word {
|
||||||
name: "chan",
|
name: "chan",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Built-in word definitions and lookup for the Forth VM.
|
|
||||||
|
|
||||||
mod compile;
|
mod compile;
|
||||||
mod core;
|
mod core;
|
||||||
mod effects;
|
mod effects;
|
||||||
@@ -13,7 +11,6 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
pub(crate) use compile::compile_word;
|
pub(crate) use compile::compile_word;
|
||||||
|
|
||||||
/// How a word is compiled into ops.
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum WordCompile {
|
pub enum WordCompile {
|
||||||
Simple,
|
Simple,
|
||||||
@@ -22,7 +19,6 @@ pub enum WordCompile {
|
|||||||
Probability(f64),
|
Probability(f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for a built-in Forth word.
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Word {
|
pub struct Word {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
@@ -35,7 +31,6 @@ pub struct Word {
|
|||||||
pub varargs: bool,
|
pub varargs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All built-in words, aggregated from every category module.
|
|
||||||
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
||||||
let mut words = Vec::new();
|
let mut words = Vec::new();
|
||||||
words.extend_from_slice(self::core::WORDS);
|
words.extend_from_slice(self::core::WORDS);
|
||||||
@@ -47,7 +42,6 @@ pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
|||||||
words
|
words
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Index mapping word names and aliases to their definitions.
|
|
||||||
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
|
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
|
||||||
let mut map = HashMap::with_capacity(WORDS.len() * 2);
|
let mut map = HashMap::with_capacity(WORDS.len() * 2);
|
||||||
for word in WORDS.iter() {
|
for word in WORDS.iter() {
|
||||||
@@ -59,7 +53,6 @@ static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(
|
|||||||
map
|
map
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Find a word by name or alias.
|
|
||||||
pub fn lookup_word(name: &str) -> Option<&'static Word> {
|
pub fn lookup_word(name: &str) -> Option<&'static Word> {
|
||||||
WORD_MAP.get(name).copied()
|
WORD_MAP.get(name).copied()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//! Word definitions for music theory, harmony, and chord construction.
|
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
|
// Music, Chord
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Music
|
// Music
|
||||||
Word {
|
Word {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//! Word metadata for sequencing: probability, timing, context queries, generators.
|
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
|
// Time, Context, Probability, Generator, Desktop
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Probability
|
// Probability
|
||||||
Word {
|
Word {
|
||||||
@@ -60,7 +59,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot prob --)",
|
stack: "(quot prob --)",
|
||||||
desc: "Execute quotation with probability (0.0-1.0)",
|
desc: "Execute quotation with probability (0.0-1.0)",
|
||||||
example: "( 2 distort ) 0.75 chance",
|
example: "{ 2 distort } 0.75 chance",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -70,7 +69,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot pct --)",
|
stack: "(quot pct --)",
|
||||||
desc: "Execute quotation with probability (0-100)",
|
desc: "Execute quotation with probability (0-100)",
|
||||||
example: "( 2 distort ) 75 prob",
|
example: "{ 2 distort } 75 prob",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -114,26 +113,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "pbounce",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Probability",
|
|
||||||
stack: "(v1..vn n -- selected)",
|
|
||||||
desc: "Ping-pong cycle through n items by pattern iteration",
|
|
||||||
example: "60 64 67 72 4 pbounce",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "index",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Probability",
|
|
||||||
stack: "(v1..vn n idx -- selected)",
|
|
||||||
desc: "Select item at explicit index",
|
|
||||||
example: "[ c4 e4 g4 ] step index",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "wchoose",
|
name: "wchoose",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -150,7 +129,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Always execute quotation",
|
desc: "Always execute quotation",
|
||||||
example: "( 2 distort ) always",
|
example: "{ 2 distort } always",
|
||||||
compile: Probability(1.0),
|
compile: Probability(1.0),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -160,7 +139,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Never execute quotation",
|
desc: "Never execute quotation",
|
||||||
example: "( 2 distort ) never",
|
example: "{ 2 distort } never",
|
||||||
compile: Probability(0.0),
|
compile: Probability(0.0),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -170,7 +149,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 75% of the time",
|
desc: "Execute quotation 75% of the time",
|
||||||
example: "( 2 distort ) often",
|
example: "{ 2 distort } often",
|
||||||
compile: Probability(0.75),
|
compile: Probability(0.75),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -180,7 +159,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 50% of the time",
|
desc: "Execute quotation 50% of the time",
|
||||||
example: "( 2 distort ) sometimes",
|
example: "{ 2 distort } sometimes",
|
||||||
compile: Probability(0.5),
|
compile: Probability(0.5),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -190,7 +169,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 25% of the time",
|
desc: "Execute quotation 25% of the time",
|
||||||
example: "( 2 distort ) rarely",
|
example: "{ 2 distort } rarely",
|
||||||
compile: Probability(0.25),
|
compile: Probability(0.25),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -200,7 +179,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 10% of the time",
|
desc: "Execute quotation 10% of the time",
|
||||||
example: "( 2 distort ) almostNever",
|
example: "{ 2 distort } almostNever",
|
||||||
compile: Probability(0.1),
|
compile: Probability(0.1),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -210,7 +189,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 90% of the time",
|
desc: "Execute quotation 90% of the time",
|
||||||
example: "( 2 distort ) almostAlways",
|
example: "{ 2 distort } almostAlways",
|
||||||
compile: Probability(0.9),
|
compile: Probability(0.9),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -221,37 +200,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot n --)",
|
stack: "(quot n --)",
|
||||||
desc: "Execute quotation every nth iteration",
|
desc: "Execute quotation every nth iteration",
|
||||||
example: "( 2 distort ) 4 every",
|
example: "{ 2 distort } 4 every",
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "except",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Time",
|
|
||||||
stack: "(quot n --)",
|
|
||||||
desc: "Execute quotation on all iterations except every nth",
|
|
||||||
example: "( 2 distort ) 4 except",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "every+",
|
|
||||||
aliases: &[],
|
|
||||||
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...",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "except+",
|
|
||||||
aliases: &[],
|
|
||||||
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...",
|
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -261,7 +210,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot k n --)",
|
stack: "(quot k n --)",
|
||||||
desc: "Execute quotation using Euclidean distribution over step runs",
|
desc: "Execute quotation using Euclidean distribution over step runs",
|
||||||
example: "( 2 distort ) 3 8 bjork",
|
example: "{ 2 distort } 3 8 bjork",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -271,7 +220,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot k n --)",
|
stack: "(quot k n --)",
|
||||||
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
||||||
example: "( 2 distort ) 3 8 pbjork",
|
example: "{ 2 distort } 3 8 pbjork",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -280,8 +229,8 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(n --)",
|
stack: "(n --)",
|
||||||
desc: "Fit sample to n steps",
|
desc: "Fit sample to n beats",
|
||||||
example: "\"break\" s 16 loop @",
|
example: "\"break\" s 4 loop @",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -456,7 +405,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Desktop",
|
category: "Desktop",
|
||||||
stack: "(-- bool)",
|
stack: "(-- bool)",
|
||||||
desc: "1 when mouse button held, 0 otherwise",
|
desc: "1 when mouse button held, 0 otherwise",
|
||||||
example: "mdown ( \"crash\" s . ) ?",
|
example: "mdown { \"crash\" s . } ?",
|
||||||
compile: Context("mdown"),
|
compile: Context("mdown"),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -487,7 +436,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Generator",
|
category: "Generator",
|
||||||
stack: "(quot n -- results...)",
|
stack: "(quot n -- results...)",
|
||||||
desc: "Execute quotation n times, push all 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,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
//! Word metadata for sound commands, sample/oscillator params, FM, modulation, and LFO.
|
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
|
// Sound, Oscillator, Sample, Wavetable, FM, Modulation, LFO
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Sound
|
// Sound
|
||||||
Word {
|
Word {
|
||||||
name: "sound",
|
name: "sound",
|
||||||
aliases: &["snd"],
|
aliases: &["s"],
|
||||||
category: "Sound",
|
category: "Sound",
|
||||||
stack: "(name --)",
|
stack: "(name --)",
|
||||||
desc: "Begin sound command",
|
desc: "Begin sound command",
|
||||||
@@ -44,67 +43,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "all",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(--)",
|
|
||||||
desc: "Apply current params to all sounds",
|
|
||||||
example: "500 lpf 0.5 verb all",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "noall",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(--)",
|
|
||||||
desc: "Clear global params",
|
|
||||||
example: "noall",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
// Recording
|
|
||||||
Word {
|
|
||||||
name: "rec",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(name --)",
|
|
||||||
desc: "Toggle recording audio output to named sample",
|
|
||||||
example: "\"loop1\" rec",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "overdub",
|
|
||||||
aliases: &["dub"],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(name --)",
|
|
||||||
desc: "Toggle overdub recording onto existing named sample",
|
|
||||||
example: "\"loop1\" overdub",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "orec",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(name orbit --)",
|
|
||||||
desc: "Toggle recording a single orbit into named sample",
|
|
||||||
example: "\"drums\" 0 orec",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "odub",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sound",
|
|
||||||
stack: "(name orbit --)",
|
|
||||||
desc: "Toggle overdub recording a single orbit onto named sample",
|
|
||||||
example: "\"drums\" 0 odub",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
// Sample
|
// Sample
|
||||||
Word {
|
Word {
|
||||||
name: "bank",
|
name: "bank",
|
||||||
@@ -126,6 +64,16 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "repeat",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Sample",
|
||||||
|
stack: "(v.. --)",
|
||||||
|
desc: "Set repeat count",
|
||||||
|
example: "4 repeat",
|
||||||
|
compile: Param,
|
||||||
|
varargs: true,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "dur",
|
name: "dur",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -141,7 +89,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Sample",
|
category: "Sample",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set gate duration (total note length, 0 = infinite sustain)",
|
desc: "Set gate time",
|
||||||
example: "0.8 gate",
|
example: "0.8 gate",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
@@ -156,16 +104,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "stretch",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sample",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Time stretch factor (pitch-independent)",
|
|
||||||
example: "2 stretch",
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "begin",
|
name: "begin",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -186,26 +124,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "slice",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sample",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Divide sample into N equal slices",
|
|
||||||
example: r#""break" s 8 slice 3 pick ."#,
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "pick",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Sample",
|
|
||||||
stack: "(v.. --)",
|
|
||||||
desc: "Select which slice to play (0-indexed, wraps)",
|
|
||||||
example: r#""break" s 8 slice 3 pick ."#,
|
|
||||||
compile: Param,
|
|
||||||
varargs: true,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "voice",
|
name: "voice",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -236,16 +154,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "cut",
|
name: "cut",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -287,6 +195,16 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "glide",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Oscillator",
|
||||||
|
stack: "(v.. --)",
|
||||||
|
desc: "Set glide/portamento",
|
||||||
|
example: "0.1 glide",
|
||||||
|
compile: Param,
|
||||||
|
varargs: true,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "pw",
|
name: "pw",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -352,7 +270,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Oscillator",
|
category: "Oscillator",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set harmonics (add source)",
|
desc: "Set harmonics (mutable only)",
|
||||||
example: "4 harmonics",
|
example: "4 harmonics",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
@@ -362,7 +280,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Oscillator",
|
category: "Oscillator",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set timbre (add source)",
|
desc: "Set timbre (mutable only)",
|
||||||
example: "0.5 timbre",
|
example: "0.5 timbre",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
@@ -372,21 +290,11 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Oscillator",
|
category: "Oscillator",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set morph (add source)",
|
desc: "Set morph (mutable only)",
|
||||||
example: "0.5 morph",
|
example: "0.5 morph",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "coarse",
|
name: "coarse",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -458,6 +366,36 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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
|
// FM
|
||||||
Word {
|
Word {
|
||||||
name: "fm",
|
name: "fm",
|
||||||
@@ -489,6 +427,56 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
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 {
|
Word {
|
||||||
name: "fm2",
|
name: "fm2",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -762,36 +750,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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 {
|
Word {
|
||||||
name: "jit",
|
name: "jit",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
@@ -822,53 +780,13 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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 {
|
Word {
|
||||||
name: "env",
|
name: "env",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Audio Modulation",
|
category: "Audio Modulation",
|
||||||
stack: "(min max a d s r -- str)",
|
stack: "(start t1 d1 ... -- str)",
|
||||||
desc: "DAHDSR envelope modulation: min^max:a:d:s:r",
|
desc: "Multi-segment envelope: start>t1:d1>...",
|
||||||
example: "200 8000 0.01 0.1 0.5 0.3 env lpf",
|
example: "0 1 0.01 0.7 0.1 0 2 env gain",
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
|
||||||
name: "lpg",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Audio Modulation",
|
|
||||||
stack: "(min max depth --)",
|
|
||||||
desc: "Low pass gate: pairs amp envelope with lpf modulation",
|
|
||||||
example: "0.01 0.1 ad 200 8000 1 lpg .",
|
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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,13 +1,9 @@
|
|||||||
//! Syntax highlighting trait for fenced code blocks in markdown.
|
|
||||||
|
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
|
|
||||||
/// Produce styled spans from a single line of source code.
|
|
||||||
pub trait CodeHighlighter {
|
pub trait CodeHighlighter {
|
||||||
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
|
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pass-through highlighter that applies no styling.
|
|
||||||
pub struct NoHighlight;
|
pub struct NoHighlight;
|
||||||
|
|
||||||
impl CodeHighlighter for NoHighlight {
|
impl CodeHighlighter for NoHighlight {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Parse markdown into styled ratatui lines with pluggable syntax highlighting.
|
|
||||||
|
|
||||||
mod highlighter;
|
mod highlighter;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Parse markdown text into styled ratatui lines with syntax-highlighted code blocks.
|
|
||||||
|
|
||||||
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line as RLine, Span};
|
use ratatui::text::{Line as RLine, Span};
|
||||||
@@ -7,20 +5,17 @@ use ratatui::text::{Line as RLine, Span};
|
|||||||
use crate::highlighter::CodeHighlighter;
|
use crate::highlighter::CodeHighlighter;
|
||||||
use crate::theme::MarkdownTheme;
|
use crate::theme::MarkdownTheme;
|
||||||
|
|
||||||
/// Span of lines within a parsed document that form a fenced code block.
|
|
||||||
pub struct CodeBlock {
|
pub struct CodeBlock {
|
||||||
pub start_line: usize,
|
pub start_line: usize,
|
||||||
pub end_line: usize,
|
pub end_line: usize,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of parsing a markdown string: styled lines and extracted code blocks.
|
|
||||||
pub struct ParsedMarkdown {
|
pub struct ParsedMarkdown {
|
||||||
pub lines: Vec<RLine<'static>>,
|
pub lines: Vec<RLine<'static>>,
|
||||||
pub code_blocks: Vec<CodeBlock>,
|
pub code_blocks: Vec<CodeBlock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse markdown text into themed, syntax-highlighted ratatui lines.
|
|
||||||
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||||
md: &str,
|
md: &str,
|
||||||
theme: &T,
|
theme: &T,
|
||||||
@@ -49,7 +44,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
|||||||
let close_block = |start: Option<usize>,
|
let close_block = |start: Option<usize>,
|
||||||
source: &mut Vec<String>,
|
source: &mut Vec<String>,
|
||||||
blocks: &mut Vec<CodeBlock>,
|
blocks: &mut Vec<CodeBlock>,
|
||||||
lines: &[RLine<'static>]| {
|
lines: &Vec<RLine<'static>>| {
|
||||||
if let Some(start) = start {
|
if let Some(start) = start {
|
||||||
blocks.push(CodeBlock {
|
blocks.push(CodeBlock {
|
||||||
start_line: start,
|
start_line: start,
|
||||||
@@ -123,7 +118,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
|||||||
ParsedMarkdown { lines, code_blocks }
|
ParsedMarkdown { lines, code_blocks }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preprocess_markdown(md: &str) -> String {
|
pub fn preprocess_markdown(md: &str) -> String {
|
||||||
let mut out = String::with_capacity(md.len());
|
let mut out = String::with_capacity(md.len());
|
||||||
for line in md.lines() {
|
for line in md.lines() {
|
||||||
let line = convert_dash_lists(line);
|
let line = convert_dash_lists(line);
|
||||||
@@ -167,7 +162,7 @@ fn preprocess_markdown(md: &str) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_dash_lists(line: &str) -> String {
|
pub fn convert_dash_lists(line: &str) -> String {
|
||||||
let trimmed = line.trim_start();
|
let trimmed = line.trim_start();
|
||||||
if let Some(rest) = trimmed.strip_prefix("- ") {
|
if let Some(rest) = trimmed.strip_prefix("- ") {
|
||||||
let indent = line.len() - trimmed.len();
|
let indent = line.len() - trimmed.len();
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
//! Style provider trait for markdown rendering.
|
|
||||||
|
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
|
||||||
/// Style provider for each markdown element type.
|
|
||||||
pub trait MarkdownTheme {
|
pub trait MarkdownTheme {
|
||||||
fn h1(&self) -> Style;
|
fn h1(&self) -> Style;
|
||||||
fn h2(&self) -> Style;
|
fn h2(&self) -> Style;
|
||||||
@@ -19,7 +16,6 @@ pub trait MarkdownTheme {
|
|||||||
fn table_row_odd(&self) -> Color;
|
fn table_row_odd(&self) -> Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fallback theme with hardcoded terminal colors, used in tests.
|
|
||||||
pub struct DefaultTheme;
|
pub struct DefaultTheme;
|
||||||
|
|
||||||
impl MarkdownTheme for DefaultTheme {
|
impl MarkdownTheme for DefaultTheme {
|
||||||
|
|||||||
@@ -10,9 +10,3 @@ description = "Project data structures for cagire sequencer"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rmp-serde = "1"
|
|
||||||
brotli = "7"
|
|
||||||
base64 = "0.22"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
flate2 = "1"
|
|
||||||
|
|||||||
@@ -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`
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
//! JSON-based project file persistence with versioned format.
|
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::project::{Bank, PatternSpeed, Project};
|
use crate::project::{Bank, Project};
|
||||||
|
|
||||||
const VERSION: u8 = 1;
|
const VERSION: u8 = 1;
|
||||||
const EXTENSION: &str = "cagire";
|
pub const EXTENSION: &str = "cagire";
|
||||||
|
|
||||||
fn ensure_extension(path: &Path) -> PathBuf {
|
pub fn ensure_extension(path: &Path) -> PathBuf {
|
||||||
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
||||||
path.to_path_buf()
|
path.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
@@ -31,24 +29,6 @@ struct ProjectFile {
|
|||||||
playing_patterns: Vec<(usize, usize)>,
|
playing_patterns: Vec<(usize, usize)>,
|
||||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||||
prelude: String,
|
prelude: String,
|
||||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
|
||||||
script: String,
|
|
||||||
#[serde(default, skip_serializing_if = "is_default_speed")]
|
|
||||||
script_speed: PatternSpeed,
|
|
||||||
#[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")]
|
|
||||||
script_length: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_default_speed(s: &PatternSpeed) -> bool {
|
|
||||||
*s == PatternSpeed::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_script_length() -> usize {
|
|
||||||
16
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_default_script_length(n: &usize) -> bool {
|
|
||||||
*n == default_script_length()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tempo() -> f64 {
|
fn default_tempo() -> f64 {
|
||||||
@@ -64,9 +44,6 @@ impl From<&Project> for ProjectFile {
|
|||||||
tempo: project.tempo,
|
tempo: project.tempo,
|
||||||
playing_patterns: project.playing_patterns.clone(),
|
playing_patterns: project.playing_patterns.clone(),
|
||||||
prelude: project.prelude.clone(),
|
prelude: project.prelude.clone(),
|
||||||
script: project.script.clone(),
|
|
||||||
script_speed: project.script_speed,
|
|
||||||
script_length: project.script_length,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,16 +56,12 @@ impl From<ProjectFile> for Project {
|
|||||||
tempo: file.tempo,
|
tempo: file.tempo,
|
||||||
playing_patterns: file.playing_patterns,
|
playing_patterns: file.playing_patterns,
|
||||||
prelude: file.prelude,
|
prelude: file.prelude,
|
||||||
script: file.script,
|
|
||||||
script_speed: file.script_speed,
|
|
||||||
script_length: file.script_length,
|
|
||||||
};
|
};
|
||||||
project.normalize();
|
project.normalize();
|
||||||
project
|
project
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error returned by project save/load operations.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FileError {
|
pub enum FileError {
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
@@ -118,7 +91,6 @@ impl From<serde_json::Error> for FileError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a project to disk as pretty-printed JSON, returning the final path.
|
|
||||||
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
||||||
let path = ensure_extension(path);
|
let path = ensure_extension(path);
|
||||||
let file = ProjectFile::from(project);
|
let file = ProjectFile::from(project);
|
||||||
@@ -127,13 +99,11 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a project from a `.cagire` file on disk.
|
|
||||||
pub fn load(path: &Path) -> Result<Project, FileError> {
|
pub fn load(path: &Path) -> Result<Project, FileError> {
|
||||||
let json = fs::read_to_string(path)?;
|
let json = fs::read_to_string(path)?;
|
||||||
load_str(&json)
|
load_str(&json)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a project from a JSON string.
|
|
||||||
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
||||||
let file: ProjectFile = serde_json::from_str(json)?;
|
let file: ProjectFile = serde_json::from_str(json)?;
|
||||||
if file.version > VERSION {
|
if file.version > VERSION {
|
||||||
|
|||||||
@@ -2,15 +2,10 @@
|
|||||||
|
|
||||||
mod file;
|
mod file;
|
||||||
mod project;
|
mod project;
|
||||||
pub mod share;
|
|
||||||
|
|
||||||
/// Maximum number of banks in a project.
|
|
||||||
pub const MAX_BANKS: usize = 32;
|
pub const MAX_BANKS: usize = 32;
|
||||||
/// Maximum number of patterns per bank.
|
|
||||||
pub const MAX_PATTERNS: usize = 32;
|
pub const MAX_PATTERNS: usize = 32;
|
||||||
/// Maximum number of steps per pattern.
|
|
||||||
pub const MAX_STEPS: usize = 1024;
|
pub const MAX_STEPS: usize = 1024;
|
||||||
/// Default pattern length in steps.
|
|
||||||
pub const DEFAULT_LENGTH: usize = 16;
|
pub const DEFAULT_LENGTH: usize = 16;
|
||||||
|
|
||||||
pub use file::{load, load_str, save, FileError};
|
pub use file::{load, load_str, save, FileError};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|||||||
|
|
||||||
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||||
|
|
||||||
/// Speed multiplier for a pattern, expressed as a rational fraction.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PatternSpeed {
|
pub struct PatternSpeed {
|
||||||
pub num: u8,
|
pub num: u8,
|
||||||
@@ -38,12 +37,10 @@ impl PatternSpeed {
|
|||||||
Self::OCTO,
|
Self::OCTO,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Return the speed as a floating-point multiplier.
|
|
||||||
pub fn multiplier(&self) -> f64 {
|
pub fn multiplier(&self) -> f64 {
|
||||||
self.num as f64 / self.denom as f64
|
self.num as f64 / self.denom as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format as a human-readable label (e.g. "2x", "1/4x").
|
|
||||||
pub fn label(&self) -> String {
|
pub fn label(&self) -> String {
|
||||||
if self.denom == 1 {
|
if self.denom == 1 {
|
||||||
format!("{}x", self.num)
|
format!("{}x", self.num)
|
||||||
@@ -52,7 +49,6 @@ impl PatternSpeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the next faster preset, or self if already at maximum.
|
|
||||||
pub fn next(&self) -> Self {
|
pub fn next(&self) -> Self {
|
||||||
let current = self.multiplier();
|
let current = self.multiplier();
|
||||||
Self::PRESETS
|
Self::PRESETS
|
||||||
@@ -62,7 +58,6 @@ impl PatternSpeed {
|
|||||||
.unwrap_or(*self)
|
.unwrap_or(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the next slower preset, or self if already at minimum.
|
|
||||||
pub fn prev(&self) -> Self {
|
pub fn prev(&self) -> Self {
|
||||||
let current = self.multiplier();
|
let current = self.multiplier();
|
||||||
Self::PRESETS
|
Self::PRESETS
|
||||||
@@ -73,7 +68,6 @@ impl PatternSpeed {
|
|||||||
.unwrap_or(*self)
|
.unwrap_or(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
|
|
||||||
pub fn from_label(s: &str) -> Option<Self> {
|
pub fn from_label(s: &str) -> Option<Self> {
|
||||||
let s = s.trim().trim_end_matches('x');
|
let s = s.trim().trim_end_matches('x');
|
||||||
if let Some((num, denom)) = s.split_once('/') {
|
if let Some((num, denom)) = s.split_once('/') {
|
||||||
@@ -145,7 +139,6 @@ impl<'de> Deserialize<'de> for PatternSpeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quantization grid for launching patterns.
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum LaunchQuantization {
|
pub enum LaunchQuantization {
|
||||||
Immediate,
|
Immediate,
|
||||||
@@ -158,7 +151,6 @@ pub enum LaunchQuantization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LaunchQuantization {
|
impl LaunchQuantization {
|
||||||
/// Human-readable label for display.
|
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Immediate => "Immediate",
|
Self::Immediate => "Immediate",
|
||||||
@@ -170,18 +162,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 {
|
pub fn next(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Immediate => Self::Beat,
|
Self::Immediate => Self::Beat,
|
||||||
@@ -193,7 +173,6 @@ impl LaunchQuantization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cycle to the next shorter quantization, clamped at `Immediate`.
|
|
||||||
pub fn prev(&self) -> Self {
|
pub fn prev(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Immediate => Self::Immediate,
|
Self::Immediate => Self::Immediate,
|
||||||
@@ -206,7 +185,6 @@ impl LaunchQuantization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How a pattern synchronizes when launched: restart or phase-lock.
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum SyncMode {
|
pub enum SyncMode {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -215,7 +193,6 @@ pub enum SyncMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SyncMode {
|
impl SyncMode {
|
||||||
/// Human-readable label for display.
|
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Reset => "Reset",
|
Self::Reset => "Reset",
|
||||||
@@ -223,14 +200,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 {
|
pub fn toggle(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Reset => Self::PhaseLock,
|
Self::Reset => Self::PhaseLock,
|
||||||
@@ -239,7 +208,6 @@ impl SyncMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum FollowUp {
|
pub enum FollowUp {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -249,7 +217,6 @@ pub enum FollowUp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FollowUp {
|
impl FollowUp {
|
||||||
/// Human-readable label for display.
|
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Loop => "Loop",
|
Self::Loop => "Loop",
|
||||||
@@ -258,7 +225,6 @@ impl FollowUp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cycle forward through follow-up modes.
|
|
||||||
pub fn next_mode(&self) -> Self {
|
pub fn next_mode(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Loop => Self::Stop,
|
Self::Loop => Self::Stop,
|
||||||
@@ -267,7 +233,6 @@ impl FollowUp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cycle backward through follow-up modes.
|
|
||||||
pub fn prev_mode(&self) -> Self {
|
pub fn prev_mode(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
||||||
@@ -281,7 +246,6 @@ fn is_default_follow_up(f: &FollowUp) -> bool {
|
|||||||
*f == FollowUp::default()
|
*f == FollowUp::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single step in a pattern, holding a Forth script and optional metadata.
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Step {
|
pub struct Step {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
@@ -293,12 +257,10 @@ pub struct Step {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Step {
|
impl Step {
|
||||||
/// True if all fields are at their default values.
|
|
||||||
pub fn is_default(&self) -> bool {
|
pub fn is_default(&self) -> bool {
|
||||||
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if the script is non-empty.
|
|
||||||
pub fn has_content(&self) -> bool {
|
pub fn has_content(&self) -> bool {
|
||||||
!self.script.is_empty()
|
!self.script.is_empty()
|
||||||
}
|
}
|
||||||
@@ -315,14 +277,12 @@ impl Default for Step {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Pattern {
|
pub struct Pattern {
|
||||||
pub steps: Vec<Step>,
|
pub steps: Vec<Step>,
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
pub speed: PatternSpeed,
|
pub speed: PatternSpeed,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub description: Option<String>,
|
|
||||||
pub quantization: LaunchQuantization,
|
pub quantization: LaunchQuantization,
|
||||||
pub sync_mode: SyncMode,
|
pub sync_mode: SyncMode,
|
||||||
pub follow_up: FollowUp,
|
pub follow_up: FollowUp,
|
||||||
@@ -357,8 +317,6 @@ struct SparsePattern {
|
|||||||
speed: PatternSpeed,
|
speed: PatternSpeed,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
description: Option<String>,
|
|
||||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||||
@@ -384,8 +342,6 @@ struct LegacyPattern {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
description: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
sync_mode: SyncMode,
|
sync_mode: SyncMode,
|
||||||
@@ -414,7 +370,6 @@ impl Serialize for Pattern {
|
|||||||
length: self.length,
|
length: self.length,
|
||||||
speed: self.speed,
|
speed: self.speed,
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
description: self.description.clone(),
|
|
||||||
quantization: self.quantization,
|
quantization: self.quantization,
|
||||||
sync_mode: self.sync_mode,
|
sync_mode: self.sync_mode,
|
||||||
follow_up: self.follow_up,
|
follow_up: self.follow_up,
|
||||||
@@ -450,7 +405,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
length: sparse.length,
|
length: sparse.length,
|
||||||
speed: sparse.speed,
|
speed: sparse.speed,
|
||||||
name: sparse.name,
|
name: sparse.name,
|
||||||
description: sparse.description,
|
|
||||||
quantization: sparse.quantization,
|
quantization: sparse.quantization,
|
||||||
sync_mode: sparse.sync_mode,
|
sync_mode: sparse.sync_mode,
|
||||||
follow_up: sparse.follow_up,
|
follow_up: sparse.follow_up,
|
||||||
@@ -461,7 +415,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
length: legacy.length,
|
length: legacy.length,
|
||||||
speed: legacy.speed,
|
speed: legacy.speed,
|
||||||
name: legacy.name,
|
name: legacy.name,
|
||||||
description: legacy.description,
|
|
||||||
quantization: legacy.quantization,
|
quantization: legacy.quantization,
|
||||||
sync_mode: legacy.sync_mode,
|
sync_mode: legacy.sync_mode,
|
||||||
follow_up: legacy.follow_up,
|
follow_up: legacy.follow_up,
|
||||||
@@ -477,7 +430,6 @@ impl Default for Pattern {
|
|||||||
length: DEFAULT_LENGTH,
|
length: DEFAULT_LENGTH,
|
||||||
speed: PatternSpeed::default(),
|
speed: PatternSpeed::default(),
|
||||||
name: None,
|
name: None,
|
||||||
description: None,
|
|
||||||
quantization: LaunchQuantization::default(),
|
quantization: LaunchQuantization::default(),
|
||||||
sync_mode: SyncMode::default(),
|
sync_mode: SyncMode::default(),
|
||||||
follow_up: FollowUp::default(),
|
follow_up: FollowUp::default(),
|
||||||
@@ -486,17 +438,14 @@ impl Default for Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Pattern {
|
impl Pattern {
|
||||||
/// Borrow a step by index.
|
|
||||||
pub fn step(&self, index: usize) -> Option<&Step> {
|
pub fn step(&self, index: usize) -> Option<&Step> {
|
||||||
self.steps.get(index)
|
self.steps.get(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mutably borrow a step by index.
|
|
||||||
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
||||||
self.steps.get_mut(index)
|
self.steps.get_mut(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the active length, clamped to `[1, MAX_STEPS]`.
|
|
||||||
pub fn set_length(&mut self, length: usize) {
|
pub fn set_length(&mut self, length: usize) {
|
||||||
let length = length.clamp(1, MAX_STEPS);
|
let length = length.clamp(1, MAX_STEPS);
|
||||||
while self.steps.len() < length {
|
while self.steps.len() < length {
|
||||||
@@ -505,7 +454,6 @@ impl Pattern {
|
|||||||
self.length = length;
|
self.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Follow the source chain from `index` to find the originating step.
|
|
||||||
pub fn resolve_source(&self, index: usize) -> usize {
|
pub fn resolve_source(&self, index: usize) -> usize {
|
||||||
let mut current = index;
|
let mut current = index;
|
||||||
for _ in 0..self.steps.len() {
|
for _ in 0..self.steps.len() {
|
||||||
@@ -522,33 +470,28 @@ impl Pattern {
|
|||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the script at the resolved source of `index`.
|
|
||||||
pub fn resolve_script(&self, index: usize) -> Option<&str> {
|
pub fn resolve_script(&self, index: usize) -> Option<&str> {
|
||||||
let source_idx = self.resolve_source(index);
|
let source_idx = self.resolve_source(index);
|
||||||
self.steps.get(source_idx).map(|s| s.script.as_str())
|
self.steps.get(source_idx).map(|s| s.script.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count active-length steps that have a script or a source reference.
|
|
||||||
pub fn content_step_count(&self) -> usize {
|
pub fn content_step_count(&self) -> usize {
|
||||||
self.steps[..self.length]
|
self.steps[..self.length]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| s.has_content() || s.source.is_some())
|
.filter(|s| s.has_content() || s.source.is_some())
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collection of patterns forming a bank.
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Bank {
|
pub struct Bank {
|
||||||
pub patterns: Vec<Pattern>,
|
pub patterns: Vec<Pattern>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[serde(default)]
|
|
||||||
pub prelude: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bank {
|
impl Bank {
|
||||||
/// Count patterns that contain at least one non-empty step.
|
|
||||||
pub fn content_pattern_count(&self) -> usize {
|
pub fn content_pattern_count(&self) -> usize {
|
||||||
self.patterns
|
self.patterns
|
||||||
.iter()
|
.iter()
|
||||||
@@ -562,12 +505,10 @@ impl Default for Bank {
|
|||||||
Self {
|
Self {
|
||||||
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||||
name: None,
|
name: None,
|
||||||
prelude: String::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Top-level project: banks, tempo, sample paths, and prelude script.
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub banks: Vec<Bank>,
|
pub banks: Vec<Bank>,
|
||||||
@@ -579,22 +520,12 @@ pub struct Project {
|
|||||||
pub playing_patterns: Vec<(usize, usize)>,
|
pub playing_patterns: Vec<(usize, usize)>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prelude: String,
|
pub prelude: String,
|
||||||
#[serde(default)]
|
|
||||||
pub script: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub script_speed: PatternSpeed,
|
|
||||||
#[serde(default = "default_script_length")]
|
|
||||||
pub script_length: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tempo() -> f64 {
|
fn default_tempo() -> f64 {
|
||||||
120.0
|
120.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_script_length() -> usize {
|
|
||||||
16
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Project {
|
impl Default for Project {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -603,25 +534,19 @@ impl Default for Project {
|
|||||||
tempo: default_tempo(),
|
tempo: default_tempo(),
|
||||||
playing_patterns: Vec::new(),
|
playing_patterns: Vec::new(),
|
||||||
prelude: String::new(),
|
prelude: String::new(),
|
||||||
script: String::new(),
|
|
||||||
script_speed: PatternSpeed::default(),
|
|
||||||
script_length: default_script_length(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
/// Borrow a pattern by bank and pattern index.
|
|
||||||
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
||||||
&self.banks[bank].patterns[pattern]
|
&self.banks[bank].patterns[pattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mutably borrow a pattern by bank and pattern index.
|
|
||||||
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
||||||
&mut self.banks[bank].patterns[pattern]
|
&mut self.banks[bank].patterns[pattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
|
|
||||||
pub fn normalize(&mut self) {
|
pub fn normalize(&mut self) {
|
||||||
self.banks.resize_with(MAX_BANKS, Bank::default);
|
self.banks.resize_with(MAX_BANKS, Bank::default);
|
||||||
for bank in &mut self.banks {
|
for bank in &mut self.banks {
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
//! Pattern and project sharing via compact text strings.
|
|
||||||
//!
|
|
||||||
//! Export: data → MessagePack → Brotli → base64 URL-safe → prefix
|
|
||||||
//! Import: strip prefix → base64 decode → Brotli decompress → MessagePack → data
|
|
||||||
|
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
|
||||||
use base64::Engine;
|
|
||||||
|
|
||||||
use crate::{Bank, Pattern};
|
|
||||||
|
|
||||||
const PATTERN_PREFIX: &str = "cgr:";
|
|
||||||
const BANK_PREFIX: &str = "cgrb:";
|
|
||||||
|
|
||||||
/// Error during pattern or bank import/export.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ShareError {
|
|
||||||
InvalidPrefix,
|
|
||||||
Base64(base64::DecodeError),
|
|
||||||
Decompress(std::io::Error),
|
|
||||||
Deserialize(rmp_serde::decode::Error),
|
|
||||||
Serialize(rmp_serde::encode::Error),
|
|
||||||
Compress(std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ShareError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::InvalidPrefix => write!(f, "missing cgr:/cgrb: prefix"),
|
|
||||||
Self::Base64(e) => write!(f, "base64: {e}"),
|
|
||||||
Self::Decompress(e) => write!(f, "decompress: {e}"),
|
|
||||||
Self::Deserialize(e) => write!(f, "deserialize: {e}"),
|
|
||||||
Self::Serialize(e) => write!(f, "serialize: {e}"),
|
|
||||||
Self::Compress(e) => write!(f, "compress: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
let params = brotli::enc::BrotliEncoderParams {
|
|
||||||
quality: 11,
|
|
||||||
lgwin: 22,
|
|
||||||
lgblock: 0,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
brotli::BrotliCompress(&mut &data[..], &mut output, ¶ms).map_err(ShareError::Compress)?;
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decompress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
brotli::BrotliDecompress(&mut &data[..], &mut output).map_err(ShareError::Decompress)?;
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode<T: serde::Serialize>(value: &T, prefix: &str) -> Result<String, ShareError> {
|
|
||||||
let packed = rmp_serde::to_vec_named(value).map_err(ShareError::Serialize)?;
|
|
||||||
let compressed = compress(&packed)?;
|
|
||||||
let encoded = URL_SAFE_NO_PAD.encode(&compressed);
|
|
||||||
Ok(format!("{prefix}{encoded}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T, ShareError> {
|
|
||||||
let text = text.trim();
|
|
||||||
let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?;
|
|
||||||
let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?;
|
|
||||||
let packed = decompress(&compressed)?;
|
|
||||||
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode a pattern as a shareable `cgr:` string.
|
|
||||||
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
|
|
||||||
encode(pattern, PATTERN_PREFIX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a `cgr:` string back into a pattern.
|
|
||||||
pub fn import(text: &str) -> Result<Pattern, ShareError> {
|
|
||||||
decode(text, PATTERN_PREFIX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode a bank as a shareable `cgrb:` string.
|
|
||||||
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
|
|
||||||
encode(bank, BANK_PREFIX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a `cgrb:` string back into a bank.
|
|
||||||
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
|
|
||||||
decode(text, BANK_PREFIX)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Step;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn roundtrip_empty() {
|
|
||||||
let pattern = Pattern::default();
|
|
||||||
let encoded = export(&pattern).expect("export pattern");
|
|
||||||
assert!(encoded.starts_with("cgr:"));
|
|
||||||
let decoded = import(&encoded).expect("import pattern");
|
|
||||||
assert_eq!(decoded.length, pattern.length);
|
|
||||||
assert_eq!(decoded.steps.len(), pattern.steps.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn roundtrip_with_steps() {
|
|
||||||
let mut pattern = Pattern::default();
|
|
||||||
pattern.steps[0] = Step {
|
|
||||||
active: true,
|
|
||||||
script: "kick 60 note".to_string(),
|
|
||||||
source: None,
|
|
||||||
name: Some("kick".to_string()),
|
|
||||||
};
|
|
||||||
pattern.steps[1] = Step {
|
|
||||||
active: false,
|
|
||||||
script: "snare".to_string(),
|
|
||||||
source: None,
|
|
||||||
name: None,
|
|
||||||
};
|
|
||||||
pattern.steps[3] = Step {
|
|
||||||
active: true,
|
|
||||||
script: String::new(),
|
|
||||||
source: Some(0),
|
|
||||||
name: None,
|
|
||||||
};
|
|
||||||
pattern.length = 8;
|
|
||||||
pattern.name = Some("Test".to_string());
|
|
||||||
|
|
||||||
let encoded = export(&pattern).expect("export pattern");
|
|
||||||
let decoded = import(&encoded).expect("import pattern");
|
|
||||||
|
|
||||||
assert_eq!(decoded.length, 8);
|
|
||||||
assert_eq!(decoded.name.as_deref(), Some("Test"));
|
|
||||||
assert_eq!(decoded.steps[0].script, "kick 60 note");
|
|
||||||
assert_eq!(decoded.steps[0].name.as_deref(), Some("kick"));
|
|
||||||
assert!(!decoded.steps[1].active);
|
|
||||||
assert_eq!(decoded.steps[1].script, "snare");
|
|
||||||
assert_eq!(decoded.steps[3].source, Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bad_prefix() {
|
|
||||||
assert!(matches!(import("xxx:abc"), Err(ShareError::InvalidPrefix)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bad_base64() {
|
|
||||||
assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_))));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn whitespace_trimming() {
|
|
||||||
let pattern = Pattern::default();
|
|
||||||
let encoded = export(&pattern).expect("export pattern");
|
|
||||||
let padded = format!(" {encoded} \n");
|
|
||||||
let decoded = import(&padded).expect("import padded pattern");
|
|
||||||
assert_eq!(decoded.length, pattern.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn msgpack_brotli_smaller_than_json_deflate() {
|
|
||||||
let mut pattern = Pattern::default();
|
|
||||||
for i in 0..16 {
|
|
||||||
pattern.steps[i] = Step {
|
|
||||||
active: true,
|
|
||||||
script: format!("kick {i} note 0.5 dur"),
|
|
||||||
source: None,
|
|
||||||
name: Some(format!("step_{i}")),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
pattern.length = 16;
|
|
||||||
|
|
||||||
// Current (msgpack+brotli)
|
|
||||||
let new_encoded = export(&pattern).expect("export pattern");
|
|
||||||
|
|
||||||
// Old pipeline (json+deflate) for comparison
|
|
||||||
use std::io::Write;
|
|
||||||
let json = serde_json::to_vec(&pattern).expect("serialize json");
|
|
||||||
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");
|
|
||||||
let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
new_encoded.len() < old_encoded.len(),
|
|
||||||
"msgpack+brotli ({}) should be smaller than json+deflate ({})",
|
|
||||||
new_encoded.len(),
|
|
||||||
old_encoded.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn roundtrip_bank() {
|
|
||||||
let mut bank = Bank::default();
|
|
||||||
bank.patterns[0].steps[0] = Step {
|
|
||||||
active: true,
|
|
||||||
script: "kick 60 note".to_string(),
|
|
||||||
source: None,
|
|
||||||
name: Some("kick".to_string()),
|
|
||||||
};
|
|
||||||
bank.patterns[0].length = 8;
|
|
||||||
bank.name = Some("Drums".to_string());
|
|
||||||
|
|
||||||
let encoded = export_bank(&bank).expect("export bank");
|
|
||||||
assert!(encoded.starts_with("cgrb:"));
|
|
||||||
let decoded = import_bank(&encoded).expect("import bank");
|
|
||||||
|
|
||||||
assert_eq!(decoded.name.as_deref(), Some("Drums"));
|
|
||||||
assert_eq!(decoded.patterns[0].length, 8);
|
|
||||||
assert_eq!(decoded.patterns[0].steps[0].script, "kick 60 note");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,4 +11,4 @@ description = "TUI components for cagire sequencer"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
tui-textarea = { git = "https://github.com/phsym/tui-textarea", rev = "e2ec4d3", features = ["search"] }
|
tui-textarea = { git = "https://github.com/phsym/tui-textarea", branch = "main", features = ["search"] }
|
||||||
|
|||||||
@@ -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,5 +1,3 @@
|
|||||||
//! Collapsible categorized list widget with section headers.
|
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::widgets::{Block, Borders, List, ListItem};
|
use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||||
@@ -7,20 +5,17 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
/// Entry in a category list: either a section header or a leaf item.
|
|
||||||
pub struct CategoryItem<'a> {
|
pub struct CategoryItem<'a> {
|
||||||
pub label: &'a str,
|
pub label: &'a str,
|
||||||
pub is_section: bool,
|
pub is_section: bool,
|
||||||
pub collapsed: bool,
|
pub collapsed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What is currently selected: a leaf item or a section header.
|
|
||||||
pub enum Selection {
|
pub enum Selection {
|
||||||
Item(usize),
|
Item(usize),
|
||||||
Section(usize),
|
Section(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrollable list with collapsible section headers.
|
|
||||||
pub struct CategoryList<'a> {
|
pub struct CategoryList<'a> {
|
||||||
items: &'a [CategoryItem<'a>],
|
items: &'a [CategoryItem<'a>],
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Yes/No confirmation dialog widget.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
@@ -9,7 +7,6 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use super::ModalFrame;
|
use super::ModalFrame;
|
||||||
|
|
||||||
/// Modal dialog with Yes/No buttons.
|
|
||||||
pub struct ConfirmModal<'a> {
|
pub struct ConfirmModal<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
message: &'a str,
|
message: &'a str,
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
//! Script editor widget with completion, search, and sample finder popups.
|
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@@ -13,10 +10,8 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
/// Callback that syntax-highlights a single line, returning styled spans (bool = annotation).
|
|
||||||
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
|
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
|
||||||
|
|
||||||
/// Metadata for a single autocomplete entry.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CompletionCandidate {
|
pub struct CompletionCandidate {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -26,7 +21,7 @@ pub struct CompletionCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct CompletionState {
|
struct CompletionState {
|
||||||
candidates: Arc<[CompletionCandidate]>,
|
candidates: Vec<CompletionCandidate>,
|
||||||
matches: Vec<usize>,
|
matches: Vec<usize>,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
@@ -38,7 +33,7 @@ struct CompletionState {
|
|||||||
impl CompletionState {
|
impl CompletionState {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
candidates: Arc::from([]),
|
candidates: Vec::new(),
|
||||||
matches: Vec::new(),
|
matches: Vec::new(),
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
prefix: String::new(),
|
prefix: String::new(),
|
||||||
@@ -83,7 +78,6 @@ impl SearchState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multi-line text editor backed by tui_textarea.
|
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
text: TextArea<'static>,
|
text: TextArea<'static>,
|
||||||
completion: CompletionState,
|
completion: CompletionState,
|
||||||
@@ -105,14 +99,6 @@ impl Editor {
|
|||||||
self.text.is_selecting()
|
self.text.is_selecting()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor_to(&mut self, row: u16, col: u16) {
|
|
||||||
self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scroll_offset(&self) -> u16 {
|
|
||||||
self.scroll_offset.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copy(&mut self) {
|
pub fn copy(&mut self) {
|
||||||
self.text.copy();
|
self.text.copy();
|
||||||
}
|
}
|
||||||
@@ -125,14 +111,6 @@ impl Editor {
|
|||||||
self.text.paste()
|
self.text.paste()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yank_text(&self) -> String {
|
|
||||||
self.text.yank_text()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_yank_text(&mut self, text: impl Into<String>) {
|
|
||||||
self.text.set_yank_text(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_all(&mut self) {
|
pub fn select_all(&mut self) {
|
||||||
self.text.select_all();
|
self.text.select_all();
|
||||||
}
|
}
|
||||||
@@ -160,11 +138,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_content(&mut self, lines: Vec<String>) {
|
pub fn set_content(&mut self, lines: Vec<String>) {
|
||||||
let yank = self.text.yank_text();
|
|
||||||
self.text = TextArea::new(lines);
|
self.text = TextArea::new(lines);
|
||||||
if !yank.is_empty() {
|
|
||||||
self.text.set_yank_text(yank);
|
|
||||||
}
|
|
||||||
self.completion.active = false;
|
self.completion.active = false;
|
||||||
self.sample_finder.active = false;
|
self.sample_finder.active = false;
|
||||||
self.search.query.clear();
|
self.search.query.clear();
|
||||||
@@ -172,7 +146,7 @@ impl Editor {
|
|||||||
self.scroll_offset.set(0);
|
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;
|
self.completion.candidates = candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +462,7 @@ impl Editor {
|
|||||||
if is_cursor {
|
if is_cursor {
|
||||||
cursor_style
|
cursor_style
|
||||||
} else if is_selected {
|
} else if is_selected {
|
||||||
base_style.bg(selection_style.bg.expect("selection style has bg"))
|
base_style.bg(selection_style.bg.unwrap())
|
||||||
} else {
|
} else {
|
||||||
base_style
|
base_style
|
||||||
}
|
}
|
||||||
@@ -708,7 +682,6 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Score a fuzzy match of `query` against `target`. Lower is better; `None` if no match.
|
|
||||||
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
||||||
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
||||||
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! File/directory browser modal widget.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
@@ -9,19 +7,15 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use super::ModalFrame;
|
use super::ModalFrame;
|
||||||
|
|
||||||
/// Modal listing files and directories with a filter input line.
|
|
||||||
pub struct FileBrowserModal<'a> {
|
pub struct FileBrowserModal<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
input: &'a str,
|
input: &'a str,
|
||||||
entries: &'a [(String, bool, bool)],
|
entries: &'a [(String, bool, bool)],
|
||||||
audio_counts: &'a [Option<usize>],
|
|
||||||
selected: usize,
|
selected: usize,
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
border_color: Option<Color>,
|
border_color: Option<Color>,
|
||||||
width: u16,
|
width: u16,
|
||||||
height: u16,
|
height: u16,
|
||||||
hints: Option<Line<'a>>,
|
|
||||||
color_path: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FileBrowserModal<'a> {
|
impl<'a> FileBrowserModal<'a> {
|
||||||
@@ -30,14 +24,11 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
title,
|
title,
|
||||||
input,
|
input,
|
||||||
entries,
|
entries,
|
||||||
audio_counts: &[],
|
|
||||||
selected: 0,
|
selected: 0,
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
border_color: None,
|
border_color: None,
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 16,
|
height: 16,
|
||||||
hints: None,
|
|
||||||
color_path: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,21 +57,6 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
self
|
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 {
|
pub fn render_centered(self, frame: &mut Frame, term: Rect) -> Rect {
|
||||||
let colors = theme::get();
|
let colors = theme::get();
|
||||||
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
|
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
|
||||||
@@ -91,61 +67,37 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
.border_color(border_color)
|
.border_color(border_color)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
|
|
||||||
let has_hints = self.hints.is_some();
|
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
|
||||||
let constraints = if has_hints {
|
|
||||||
vec![
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
vec![Constraint::Length(1), Constraint::Min(1)]
|
|
||||||
};
|
|
||||||
let rows = Layout::vertical(constraints).split(inner);
|
|
||||||
|
|
||||||
// Input line
|
// Input line
|
||||||
let input_spans = if self.color_path {
|
frame.render_widget(
|
||||||
let (path_part, filter_part) = match self.input.rfind('/') {
|
Paragraph::new(Line::from(vec![
|
||||||
Some(pos) => (&self.input[..=pos], &self.input[pos + 1..]),
|
|
||||||
None => ("", self.input),
|
|
||||||
};
|
|
||||||
vec![
|
|
||||||
Span::raw("> "),
|
|
||||||
Span::styled(path_part.to_string(), Style::new().fg(colors.browser.directory)),
|
|
||||||
Span::styled(filter_part.to_string(), Style::new().fg(colors.input.text)),
|
|
||||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
vec![
|
|
||||||
Span::raw("> "),
|
Span::raw("> "),
|
||||||
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
||||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||||
]
|
])),
|
||||||
};
|
rows[0],
|
||||||
frame.render_widget(Paragraph::new(Line::from(input_spans)), rows[0]);
|
);
|
||||||
|
|
||||||
// Hints bar
|
|
||||||
if let Some(hints) = self.hints {
|
|
||||||
let hint_row = rows[2];
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(hints).alignment(ratatui::layout::Alignment::Right),
|
|
||||||
hint_row,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entries list
|
// Entries list
|
||||||
let visible_height = rows[1].height as usize;
|
let visible_height = rows[1].height as usize;
|
||||||
let visible_entries = self
|
let visible_entries = self
|
||||||
.entries
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
|
||||||
.skip(self.scroll_offset)
|
.skip(self.scroll_offset)
|
||||||
.take(visible_height);
|
.take(visible_height);
|
||||||
|
|
||||||
let lines: Vec<Line> = visible_entries
|
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 is_selected = abs_idx == self.selected;
|
||||||
let prefix = if is_selected { "> " } else { " " };
|
let prefix = if is_selected { "> " } else { " " };
|
||||||
|
let display = if *is_dir {
|
||||||
|
format!("{prefix}{name}/")
|
||||||
|
} else {
|
||||||
|
format!("{prefix}{name}")
|
||||||
|
};
|
||||||
let color = if is_selected {
|
let color = if is_selected {
|
||||||
colors.browser.selected
|
colors.browser.selected
|
||||||
} else if *is_dir {
|
} else if *is_dir {
|
||||||
@@ -155,21 +107,7 @@ impl<'a> FileBrowserModal<'a> {
|
|||||||
} else {
|
} else {
|
||||||
colors.browser.file
|
colors.browser.file
|
||||||
};
|
};
|
||||||
let display = if *is_dir {
|
Line::from(Span::styled(display, Style::new().fg(color)))
|
||||||
format!("{prefix}{name}/")
|
|
||||||
} else {
|
|
||||||
format!("{prefix}{name}")
|
|
||||||
};
|
|
||||||
let mut spans = vec![Span::styled(display, Style::new().fg(color))];
|
|
||||||
if *is_dir && name != ".." {
|
|
||||||
if let Some(Some(count)) = self.audio_counts.get(abs_idx) {
|
|
||||||
spans.push(Span::styled(
|
|
||||||
format!(" ({count})"),
|
|
||||||
Style::new().fg(colors.browser.file),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Line::from(spans)
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
//! Bottom-bar keyboard hint renderer.
|
|
||||||
|
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
/// Build a styled line of key/action pairs for the hint bar.
|
|
||||||
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
|
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let key_style = Style::default().fg(theme.hint.key);
|
let key_style = Style::default().fg(theme.hint.key);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ mod confirm;
|
|||||||
mod editor;
|
mod editor;
|
||||||
mod file_browser;
|
mod file_browser;
|
||||||
mod hint_bar;
|
mod hint_bar;
|
||||||
mod lissajous;
|
|
||||||
mod list_select;
|
mod list_select;
|
||||||
mod modal;
|
mod modal;
|
||||||
mod nav_minimap;
|
mod nav_minimap;
|
||||||
@@ -27,7 +26,6 @@ pub use confirm::ConfirmModal;
|
|||||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||||
pub use file_browser::FileBrowserModal;
|
pub use file_browser::FileBrowserModal;
|
||||||
pub use hint_bar::hint_line;
|
pub use hint_bar::hint_line;
|
||||||
pub use lissajous::Lissajous;
|
|
||||||
pub use list_select::ListSelect;
|
pub use list_select::ListSelect;
|
||||||
pub use modal::ModalFrame;
|
pub use modal::ModalFrame;
|
||||||
pub use nav_minimap::{hit_test_tile, minimap_area, NavMinimap, NavTile};
|
pub use nav_minimap::{hit_test_tile, minimap_area, NavMinimap, NavTile};
|
||||||
@@ -38,7 +36,7 @@ pub use scroll_indicators::{render_scroll_indicators, IndicatorAlign};
|
|||||||
pub use search_bar::render_search_bar;
|
pub use search_bar::render_search_bar;
|
||||||
pub use section_header::render_section_header;
|
pub use section_header::render_section_header;
|
||||||
pub use sparkles::Sparkles;
|
pub use sparkles::Sparkles;
|
||||||
pub use spectrum::{Spectrum, SpectrumStyle};
|
pub use spectrum::Spectrum;
|
||||||
pub use text_input::TextInputModal;
|
pub use text_input::TextInputModal;
|
||||||
pub use vu_meter::VuMeter;
|
pub use vu_meter::VuMeter;
|
||||||
pub use waveform::Waveform;
|
pub use waveform::Waveform;
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
//! Lissajous XY oscilloscope widget using braille characters.
|
|
||||||
|
|
||||||
use crate::theme;
|
|
||||||
use ratatui::buffer::Buffer;
|
|
||||||
use ratatui::layout::Rect;
|
|
||||||
use ratatui::style::Color;
|
|
||||||
use ratatui::widgets::Widget;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
|
||||||
static TRAIL: RefCell<TrailState> = const { RefCell::new(TrailState { fine_w: 0, fine_h: 0, heat: Vec::new() }) };
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TrailState {
|
|
||||||
fine_w: usize,
|
|
||||||
fine_h: usize,
|
|
||||||
heat: Vec<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// XY oscilloscope plotting left vs right channels as a Lissajous curve.
|
|
||||||
pub struct Lissajous<'a> {
|
|
||||||
left: &'a [f32],
|
|
||||||
right: &'a [f32],
|
|
||||||
color: Option<Color>,
|
|
||||||
gain: f32,
|
|
||||||
trails: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Lissajous<'a> {
|
|
||||||
pub fn new(left: &'a [f32], right: &'a [f32]) -> Self {
|
|
||||||
Self {
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
color: None,
|
|
||||||
gain: 1.0,
|
|
||||||
trails: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trails(mut self, enabled: bool) -> Self {
|
|
||||||
self.trails = enabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn color(mut self, c: Color) -> Self {
|
|
||||||
self.color = Some(c);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gain(mut self, g: f32) -> Self {
|
|
||||||
self.gain = g;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Lissajous<'_> {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
if area.width == 0 || area.height == 0 || self.left.is_empty() || self.right.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.trails {
|
|
||||||
self.render_trails(area, buf);
|
|
||||||
} else {
|
|
||||||
self.render_normal(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Lissajous<'_> {
|
|
||||||
fn render_normal(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
|
|
||||||
let width = area.width as usize;
|
|
||||||
let height = area.height as usize;
|
|
||||||
let fine_width = width * 2;
|
|
||||||
let fine_height = height * 4;
|
|
||||||
let len = self.left.len().min(self.right.len());
|
|
||||||
|
|
||||||
PATTERNS.with(|p| {
|
|
||||||
let mut patterns = p.borrow_mut();
|
|
||||||
let size = width * height;
|
|
||||||
patterns.clear();
|
|
||||||
patterns.resize(size, 0);
|
|
||||||
|
|
||||||
for i in 0..len {
|
|
||||||
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
|
|
||||||
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
|
|
||||||
|
|
||||||
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
|
||||||
let fine_y = ((1.0 - l) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
|
||||||
let fine_x = fine_x.min(fine_width - 1);
|
|
||||||
let fine_y = fine_y.min(fine_height - 1);
|
|
||||||
|
|
||||||
let char_x = fine_x / 2;
|
|
||||||
let char_y = fine_y / 4;
|
|
||||||
let dot_x = fine_x % 2;
|
|
||||||
let dot_y = fine_y % 4;
|
|
||||||
|
|
||||||
patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y);
|
|
||||||
}
|
|
||||||
|
|
||||||
for cy in 0..height {
|
|
||||||
for cx in 0..width {
|
|
||||||
let pattern = patterns[cy * width + cx];
|
|
||||||
if pattern != 0 {
|
|
||||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
|
||||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
|
||||||
.set_char(ch)
|
|
||||||
.set_fg(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_trails(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
let theme = theme::get();
|
|
||||||
let width = area.width as usize;
|
|
||||||
let height = area.height as usize;
|
|
||||||
let fine_w = width * 2;
|
|
||||||
let fine_h = height * 4;
|
|
||||||
let len = self.left.len().min(self.right.len());
|
|
||||||
|
|
||||||
TRAIL.with(|t| {
|
|
||||||
let mut trail = t.borrow_mut();
|
|
||||||
|
|
||||||
// Reset if dimensions changed
|
|
||||||
if trail.fine_w != fine_w || trail.fine_h != fine_h {
|
|
||||||
trail.fine_w = fine_w;
|
|
||||||
trail.fine_h = fine_h;
|
|
||||||
trail.heat.clear();
|
|
||||||
trail.heat.resize(fine_w * fine_h, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decay existing heat
|
|
||||||
for h in trail.heat.iter_mut() {
|
|
||||||
*h *= 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plot new sample points
|
|
||||||
for i in 0..len {
|
|
||||||
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
|
|
||||||
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
|
|
||||||
|
|
||||||
let fx = ((r + 1.0) * 0.5 * (fine_w - 1) as f32).round() as usize;
|
|
||||||
let fy = ((1.0 - l) * 0.5 * (fine_h - 1) as f32).round() as usize;
|
|
||||||
let fx = fx.min(fine_w - 1);
|
|
||||||
let fy = fy.min(fine_h - 1);
|
|
||||||
|
|
||||||
trail.heat[fy * fine_w + fx] = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert heat map to braille
|
|
||||||
PATTERNS.with(|p| {
|
|
||||||
let mut patterns = p.borrow_mut();
|
|
||||||
patterns.clear();
|
|
||||||
patterns.resize(width * height, 0);
|
|
||||||
|
|
||||||
// Track brightest color per cell
|
|
||||||
let mut colors: Vec<Option<Color>> = vec![None; width * height];
|
|
||||||
|
|
||||||
for fy in 0..fine_h {
|
|
||||||
for fx in 0..fine_w {
|
|
||||||
let h = trail.heat[fy * fine_w + fx];
|
|
||||||
if h < 0.05 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cx = fx / 2;
|
|
||||||
let cy = fy / 4;
|
|
||||||
let dx = fx % 2;
|
|
||||||
let dy = fy % 4;
|
|
||||||
|
|
||||||
let idx = cy * width + cx;
|
|
||||||
patterns[idx] |= braille_bit(dx, dy);
|
|
||||||
|
|
||||||
let dot_color = if h > 0.7 {
|
|
||||||
theme.meter.high
|
|
||||||
} else if h > 0.25 {
|
|
||||||
theme.meter.mid
|
|
||||||
} else {
|
|
||||||
theme.meter.low
|
|
||||||
};
|
|
||||||
|
|
||||||
let replace = match colors[idx] {
|
|
||||||
None => true,
|
|
||||||
Some(cur) => {
|
|
||||||
rank_color(dot_color, &theme) > rank_color(cur, &theme)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if replace {
|
|
||||||
colors[idx] = Some(dot_color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for cy in 0..height {
|
|
||||||
for cx in 0..width {
|
|
||||||
let idx = cy * width + cx;
|
|
||||||
let pattern = patterns[idx];
|
|
||||||
if pattern != 0 {
|
|
||||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
|
||||||
let color = colors[idx].unwrap_or(theme.meter.low);
|
|
||||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
|
||||||
.set_char(ch)
|
|
||||||
.set_fg(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
|
|
||||||
match (dot_x, dot_y) {
|
|
||||||
(0, 0) => 0x01,
|
|
||||||
(0, 1) => 0x02,
|
|
||||||
(0, 2) => 0x04,
|
|
||||||
(0, 3) => 0x40,
|
|
||||||
(1, 0) => 0x08,
|
|
||||||
(1, 1) => 0x10,
|
|
||||||
(1, 2) => 0x20,
|
|
||||||
(1, 3) => 0x80,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rank_color(c: Color, theme: &crate::theme::ThemeColors) -> u8 {
|
|
||||||
if c == theme.meter.high { 2 }
|
|
||||||
else if c == theme.meter.mid { 1 }
|
|
||||||
else { 0 }
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Scrollable single-select list widget with cursor highlight.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
@@ -7,7 +5,6 @@ use ratatui::text::{Line, Span};
|
|||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
/// Scrollable list with a highlighted cursor and selected-item marker.
|
|
||||||
pub struct ListSelect<'a> {
|
pub struct ListSelect<'a> {
|
||||||
items: &'a [String],
|
items: &'a [String],
|
||||||
selected: usize,
|
selected: usize,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
//! Centered modal frame with border and title.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
/// Centered modal overlay with titled border.
|
|
||||||
pub struct ModalFrame<'a> {
|
pub struct ModalFrame<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
width: u16,
|
width: u16,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Page navigation minimap showing a 3x2 grid of tiles.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Rect};
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Vertical label/value property form renderer.
|
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
@@ -7,7 +5,6 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
/// Render a vertical list of label/value pairs with selection highlight.
|
|
||||||
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
|
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
//! Tree-view sample browser with search filtering.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
/// Node type in the sample tree.
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum TreeLineKind {
|
pub enum TreeLineKind {
|
||||||
Root { expanded: bool },
|
Root { expanded: bool },
|
||||||
@@ -15,7 +12,6 @@ pub enum TreeLineKind {
|
|||||||
File,
|
File,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single row in the sample browser tree.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TreeLine {
|
pub struct TreeLine {
|
||||||
pub depth: u8,
|
pub depth: u8,
|
||||||
@@ -23,10 +19,8 @@ pub struct TreeLine {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
pub folder: String,
|
pub folder: String,
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub child_count: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tree-view browser for navigating sample folders.
|
|
||||||
pub struct SampleBrowser<'a> {
|
pub struct SampleBrowser<'a> {
|
||||||
entries: &'a [TreeLine],
|
entries: &'a [TreeLine],
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
@@ -117,13 +111,13 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||||
let height = area.height as usize;
|
let height = area.height as usize;
|
||||||
if self.entries.is_empty() {
|
if self.entries.is_empty() {
|
||||||
if self.search_query.is_empty() {
|
let msg = if self.search_query.is_empty() {
|
||||||
self.render_empty_guide(frame, area, colors);
|
"No samples loaded"
|
||||||
} else {
|
} else {
|
||||||
let line =
|
"No matches"
|
||||||
Line::from(Span::styled("No matches", Style::new().fg(colors.browser.empty_text)));
|
};
|
||||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
|
||||||
}
|
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +131,10 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
let (icon, icon_color) = match entry.kind {
|
let (icon, icon_color) = match entry.kind {
|
||||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
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::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),
|
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,43 +158,15 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
Style::new().fg(icon_color)
|
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![
|
let mut spans = vec![
|
||||||
Span::raw(indent),
|
Span::raw(indent),
|
||||||
Span::styled(icon, icon_style),
|
Span::styled(icon, icon_style),
|
||||||
Span::styled(label, label_style),
|
Span::styled(&entry.label, label_style),
|
||||||
];
|
];
|
||||||
|
|
||||||
match entry.kind {
|
if matches!(entry.kind, TreeLineKind::File) {
|
||||||
TreeLineKind::File => {
|
let idx_style = Style::new().fg(colors.browser.empty_text);
|
||||||
let idx_style = Style::new().fg(colors.browser.empty_text);
|
spans.push(Span::styled(format!(" {}", entry.index), idx_style));
|
||||||
spans.push(Span::styled(suffix, idx_style));
|
|
||||||
}
|
|
||||||
_ if !suffix.is_empty() => {
|
|
||||||
let dim_style = Style::new().fg(colors.browser.empty_text);
|
|
||||||
spans.push(Span::styled(suffix, dim_style));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(Line::from(spans));
|
lines.push(Line::from(spans));
|
||||||
@@ -208,47 +174,4 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
frame.render_widget(Paragraph::new(lines), area);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Oscilloscope waveform widget using braille characters.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -11,14 +9,12 @@ thread_local! {
|
|||||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rendering direction for the oscilloscope.
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Orientation {
|
pub enum Orientation {
|
||||||
Horizontal,
|
Horizontal,
|
||||||
Vertical,
|
Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Single-channel oscilloscope using braille dot plotting.
|
|
||||||
pub struct Scope<'a> {
|
pub struct Scope<'a> {
|
||||||
data: &'a [f32],
|
data: &'a [f32],
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
@@ -45,11 +41,6 @@ impl<'a> Scope<'a> {
|
|||||||
self.color = Some(c);
|
self.color = Some(c);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gain(mut self, g: f32) -> Self {
|
|
||||||
self.gain = g;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scope<'_> {
|
impl Widget for Scope<'_> {
|
||||||
@@ -75,6 +66,9 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
|||||||
let fine_width = width * 2;
|
let fine_width = width * 2;
|
||||||
let fine_height = height * 4;
|
let fine_height = height * 4;
|
||||||
|
|
||||||
|
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||||
|
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||||
|
|
||||||
PATTERNS.with(|p| {
|
PATTERNS.with(|p| {
|
||||||
let mut patterns = p.borrow_mut();
|
let mut patterns = p.borrow_mut();
|
||||||
let size = width * height;
|
let size = width * height;
|
||||||
@@ -83,7 +77,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
|||||||
|
|
||||||
for fine_x in 0..fine_width {
|
for fine_x in 0..fine_width {
|
||||||
let sample_idx = (fine_x * data.len()) / fine_width;
|
let sample_idx = (fine_x * data.len()) / fine_width;
|
||||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
|
||||||
|
|
||||||
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||||
let fine_y = fine_y.min(fine_height - 1);
|
let fine_y = fine_y.min(fine_height - 1);
|
||||||
@@ -128,6 +122,9 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
|||||||
let fine_width = width * 2;
|
let fine_width = width * 2;
|
||||||
let fine_height = height * 4;
|
let fine_height = height * 4;
|
||||||
|
|
||||||
|
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||||
|
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||||
|
|
||||||
PATTERNS.with(|p| {
|
PATTERNS.with(|p| {
|
||||||
let mut patterns = p.borrow_mut();
|
let mut patterns = p.borrow_mut();
|
||||||
let size = width * height;
|
let size = width * height;
|
||||||
@@ -136,7 +133,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
|||||||
|
|
||||||
for fine_y in 0..fine_height {
|
for fine_y in 0..fine_height {
|
||||||
let sample_idx = (fine_y * data.len()) / fine_height;
|
let sample_idx = (fine_y * data.len()) / fine_height;
|
||||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
|
||||||
|
|
||||||
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||||
let fine_x = fine_x.min(fine_width - 1);
|
let fine_x = fine_x.min(fine_width - 1);
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
//! Up/down arrow scroll indicators for bounded lists.
|
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
/// Horizontal alignment for scroll indicators.
|
|
||||||
pub enum IndicatorAlign {
|
pub enum IndicatorAlign {
|
||||||
Center,
|
Center,
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render up/down scroll arrows when content overflows.
|
|
||||||
pub fn render_scroll_indicators(
|
pub fn render_scroll_indicators(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Inline search bar with active/inactive styling.
|
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
@@ -8,7 +6,6 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
/// Render a `/query` search bar.
|
|
||||||
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
|
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let style = if active {
|
let style = if active {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Section header with horizontal divider for engine-view panels.
|
|
||||||
|
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
@@ -7,7 +5,6 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
/// Render a section title with a horizontal divider below it.
|
|
||||||
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let [header_area, divider_area] =
|
let [header_area, divider_area] =
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Decorative particle effect using random Unicode glyphs.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
@@ -16,7 +14,6 @@ struct Sparkle {
|
|||||||
life: u8,
|
life: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Animated sparkle particles for visual flair.
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Sparkles {
|
pub struct Sparkles {
|
||||||
sparkles: Vec<Sparkle>,
|
sparkles: Vec<Sparkle>,
|
||||||
|
|||||||
@@ -1,58 +1,18 @@
|
|||||||
//! 32-band frequency spectrum display with optional peak hold.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
use std::cell::RefCell;
|
|
||||||
|
|
||||||
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
|
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub enum SpectrumStyle {
|
|
||||||
#[default]
|
|
||||||
Bars,
|
|
||||||
Line,
|
|
||||||
Filled,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static PEAKS: RefCell<[f32; 32]> = const { RefCell::new([0.0; 32]) };
|
|
||||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 32-band spectrum analyzer using block characters.
|
|
||||||
pub struct Spectrum<'a> {
|
pub struct Spectrum<'a> {
|
||||||
data: &'a [f32; 32],
|
data: &'a [f32; 32],
|
||||||
gain: f32,
|
|
||||||
style: SpectrumStyle,
|
|
||||||
peaks: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Spectrum<'a> {
|
impl<'a> Spectrum<'a> {
|
||||||
pub fn new(data: &'a [f32; 32]) -> Self {
|
pub fn new(data: &'a [f32; 32]) -> Self {
|
||||||
Self {
|
Self { data }
|
||||||
data,
|
|
||||||
gain: 1.0,
|
|
||||||
style: SpectrumStyle::Bars,
|
|
||||||
peaks: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gain(mut self, g: f32) -> Self {
|
|
||||||
self.gain = g;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style(mut self, s: SpectrumStyle) -> Self {
|
|
||||||
self.style = s;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn peaks(mut self, enabled: bool) -> Self {
|
|
||||||
self.peaks = enabled;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,177 +22,45 @@ impl Widget for Spectrum<'_> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update peak hold state
|
let colors = theme::get();
|
||||||
let peak_values = if self.peaks {
|
let height = area.height as f32;
|
||||||
Some(PEAKS.with(|p| {
|
let base = area.width as usize / 32;
|
||||||
let mut peaks = p.borrow_mut();
|
let remainder = area.width as usize % 32;
|
||||||
for (i, &mag) in self.data.iter().enumerate() {
|
if base == 0 && remainder == 0 {
|
||||||
let v = (mag * self.gain).min(1.0);
|
return;
|
||||||
if v >= peaks[i] {
|
|
||||||
peaks[i] = v;
|
|
||||||
} else {
|
|
||||||
peaks[i] = (peaks[i] - 0.02).max(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*peaks
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.style {
|
|
||||||
SpectrumStyle::Bars => render_bars(self.data, area, buf, self.gain, peak_values.as_ref()),
|
|
||||||
SpectrumStyle::Line => render_braille(self.data, area, buf, self.gain, false, peak_values.as_ref()),
|
|
||||||
SpectrumStyle::Filled => render_braille(self.data, area, buf, self.gain, true, peak_values.as_ref()),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn band_color(ratio: f32, colors: &theme::ThemeColors) -> Color {
|
let mut x_start = area.x;
|
||||||
if ratio < 0.33 {
|
for (band, &mag) in self.data.iter().enumerate() {
|
||||||
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
|
let w = base + if band < remainder { 1 } else { 0 };
|
||||||
} else if ratio < 0.66 {
|
if w == 0 {
|
||||||
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
|
continue;
|
||||||
} else {
|
}
|
||||||
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
|
let bar_height = mag * height;
|
||||||
}
|
let full_cells = bar_height as usize;
|
||||||
}
|
let frac = bar_height - full_cells as f32;
|
||||||
|
let frac_idx = (frac * 8.0) as usize;
|
||||||
|
|
||||||
fn render_bars(data: &[f32; 32], area: Rect, buf: &mut Buffer, gain: f32, peaks: Option<&[f32; 32]>) {
|
for row in 0..area.height as usize {
|
||||||
let colors = theme::get();
|
let y = area.y + area.height - 1 - row as u16;
|
||||||
let height = area.height as f32;
|
let ratio = row as f32 / area.height as f32;
|
||||||
let base = area.width as usize / 32;
|
let color = if ratio < 0.33 {
|
||||||
let remainder = area.width as usize % 32;
|
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
|
||||||
if base == 0 && remainder == 0 {
|
} else if ratio < 0.66 {
|
||||||
return;
|
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
|
||||||
}
|
} else {
|
||||||
|
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
|
||||||
let mut x_start = area.x;
|
};
|
||||||
for (band, &mag) in data.iter().enumerate() {
|
for dx in 0..w as u16 {
|
||||||
let w = base + if band < remainder { 1 } else { 0 };
|
let x = x_start + dx;
|
||||||
if w == 0 {
|
if row < full_cells {
|
||||||
continue;
|
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
|
||||||
}
|
} else if row == full_cells && frac_idx > 0 {
|
||||||
let bar_height = (mag * gain).min(1.0) * height;
|
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
|
||||||
let full_cells = bar_height as usize;
|
|
||||||
let frac = bar_height - full_cells as f32;
|
|
||||||
let frac_idx = (frac * 8.0) as usize;
|
|
||||||
|
|
||||||
// Peak hold row
|
|
||||||
let peak_row = peaks.map(|p| {
|
|
||||||
let ph = p[band] * height;
|
|
||||||
let row = (height - ph).max(0.0) as usize;
|
|
||||||
row.min(area.height as usize - 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
for row in 0..area.height as usize {
|
|
||||||
let y = area.y + area.height - 1 - row as u16;
|
|
||||||
let ratio = row as f32 / area.height as f32;
|
|
||||||
let color = band_color(ratio, &colors);
|
|
||||||
|
|
||||||
for dx in 0..w as u16 {
|
|
||||||
let x = x_start + dx;
|
|
||||||
if row < full_cells {
|
|
||||||
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
|
|
||||||
} else if row == full_cells && frac_idx > 0 {
|
|
||||||
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
|
|
||||||
} else if let Some(pr) = peak_row {
|
|
||||||
// peak_row is from top (0 = top), row is from bottom
|
|
||||||
let from_top = area.height as usize - 1 - row;
|
|
||||||
if from_top == pr {
|
|
||||||
buf[(x, y)].set_char('─').set_fg(colors.meter.high);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
x_start += w as u16;
|
||||||
}
|
}
|
||||||
x_start += w as u16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_braille(
|
|
||||||
data: &[f32; 32],
|
|
||||||
area: Rect,
|
|
||||||
buf: &mut Buffer,
|
|
||||||
gain: f32,
|
|
||||||
filled: bool,
|
|
||||||
peaks: Option<&[f32; 32]>,
|
|
||||||
) {
|
|
||||||
let colors = theme::get();
|
|
||||||
let width = area.width as usize;
|
|
||||||
let height = area.height as usize;
|
|
||||||
let fine_w = width * 2;
|
|
||||||
let fine_h = height * 4;
|
|
||||||
|
|
||||||
PATTERNS.with(|p| {
|
|
||||||
let mut patterns = p.borrow_mut();
|
|
||||||
patterns.clear();
|
|
||||||
patterns.resize(width * height, 0);
|
|
||||||
|
|
||||||
// Interpolate 32 bands across fine_w columns
|
|
||||||
for fx in 0..fine_w {
|
|
||||||
let band_f = fx as f32 * 31.0 / (fine_w - 1).max(1) as f32;
|
|
||||||
let lo = band_f as usize;
|
|
||||||
let hi = (lo + 1).min(31);
|
|
||||||
let t = band_f - lo as f32;
|
|
||||||
let mag = ((data[lo] * (1.0 - t) + data[hi] * t) * gain).min(1.0);
|
|
||||||
let fy = ((1.0 - mag) * (fine_h - 1) as f32).round() as usize;
|
|
||||||
let fy = fy.min(fine_h - 1);
|
|
||||||
|
|
||||||
if filled {
|
|
||||||
for y in fy..fine_h {
|
|
||||||
let cy = y / 4;
|
|
||||||
let dy = y % 4;
|
|
||||||
let cx = fx / 2;
|
|
||||||
let dx = fx % 2;
|
|
||||||
patterns[cy * width + cx] |= braille_bit(dx, dy);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let cy = fy / 4;
|
|
||||||
let dy = fy % 4;
|
|
||||||
let cx = fx / 2;
|
|
||||||
let dx = fx % 2;
|
|
||||||
patterns[cy * width + cx] |= braille_bit(dx, dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peak dots
|
|
||||||
if let Some(pk) = peaks {
|
|
||||||
let pv = (pk[lo] * (1.0 - t) + pk[hi] * t).min(1.0);
|
|
||||||
let py = ((1.0 - pv) * (fine_h - 1) as f32).round() as usize;
|
|
||||||
let py = py.min(fine_h - 1);
|
|
||||||
let cy = py / 4;
|
|
||||||
let dy = py % 4;
|
|
||||||
let cx = fx / 2;
|
|
||||||
let dx = fx % 2;
|
|
||||||
patterns[cy * width + cx] |= braille_bit(dx, dy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for cy in 0..height {
|
|
||||||
for cx in 0..width {
|
|
||||||
let pattern = patterns[cy * width + cx];
|
|
||||||
if pattern != 0 {
|
|
||||||
let ratio = 1.0 - (cy as f32 / height as f32);
|
|
||||||
let color = band_color(ratio, &colors);
|
|
||||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
|
||||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
|
||||||
.set_char(ch)
|
|
||||||
.set_fg(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
|
|
||||||
match (dot_x, dot_y) {
|
|
||||||
(0, 0) => 0x01,
|
|
||||||
(0, 1) => 0x02,
|
|
||||||
(0, 2) => 0x04,
|
|
||||||
(0, 3) => 0x40,
|
|
||||||
(1, 0) => 0x08,
|
|
||||||
(1, 1) => 0x10,
|
|
||||||
(1, 2) => 0x20,
|
|
||||||
(1, 3) => 0x80,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Single-line text input modal with optional hint.
|
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
@@ -9,7 +7,6 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use super::ModalFrame;
|
use super::ModalFrame;
|
||||||
|
|
||||||
/// Modal dialog with a single-line text input.
|
|
||||||
pub struct TextInputModal<'a> {
|
pub struct TextInputModal<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
input: &'a str,
|
input: &'a str,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
//! Derive [`ThemeColors`] from a [`Palette`].
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use super::palette::{Palette, Rgb, darken, mid, rgb, tint};
|
use super::palette::{Palette, Rgb, darken, mid, rgb, tint};
|
||||||
|
|
||||||
/// Build a complete [`ThemeColors`] from a [`Palette`].
|
|
||||||
pub fn build(p: &Palette) -> ThemeColors {
|
pub fn build(p: &Palette) -> ThemeColors {
|
||||||
let darker_bg = darken(p.bg, 0.15);
|
let darker_bg = darken(p.bg, 0.15);
|
||||||
|
|
||||||
@@ -58,7 +55,6 @@ pub fn build(p: &Palette) -> ThemeColors {
|
|||||||
header: HeaderColors {
|
header: HeaderColors {
|
||||||
tempo_bg: rgb(tint(p.bg, p.tempo_color, 0.30)),
|
tempo_bg: rgb(tint(p.bg, p.tempo_color, 0.30)),
|
||||||
tempo_fg: rgb(p.tempo_color),
|
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_bg: rgb(tint(p.bg, p.bank_color, 0.25)),
|
||||||
bank_fg: rgb(p.bank_color),
|
bank_fg: rgb(p.bank_color),
|
||||||
pattern_bg: rgb(tint(p.bg, p.pattern_color, 0.25)),
|
pattern_bg: rgb(tint(p.bg, p.pattern_color, 0.25)),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Catppuccin Latte palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Catppuccin Mocha palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Dracula palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Eden palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Ember palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
//! Everforest palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
|
||||||
Palette {
|
|
||||||
bg: (45, 53, 59),
|
|
||||||
surface: (52, 62, 68),
|
|
||||||
surface2: (68, 80, 86),
|
|
||||||
fg: (211, 198, 170),
|
|
||||||
fg_dim: (135, 131, 116),
|
|
||||||
fg_muted: (80, 80, 68),
|
|
||||||
accent: (167, 192, 128),
|
|
||||||
red: (230, 126, 128),
|
|
||||||
green: (167, 192, 128),
|
|
||||||
yellow: (219, 188, 127),
|
|
||||||
blue: (127, 187, 179),
|
|
||||||
purple: (214, 153, 182),
|
|
||||||
cyan: (131, 192, 146),
|
|
||||||
orange: (230, 152, 117),
|
|
||||||
tempo_color: (214, 153, 182),
|
|
||||||
bank_color: (127, 187, 179),
|
|
||||||
pattern_color: (131, 192, 146),
|
|
||||||
title_accent: (167, 192, 128),
|
|
||||||
title_author: (127, 187, 179),
|
|
||||||
secondary: (230, 152, 117),
|
|
||||||
link_bright: [
|
|
||||||
(167, 192, 128), (214, 153, 182), (230, 152, 117),
|
|
||||||
(127, 187, 179), (219, 188, 127),
|
|
||||||
],
|
|
||||||
link_dim: [
|
|
||||||
(56, 66, 46), (70, 52, 62), (72, 52, 42),
|
|
||||||
(44, 64, 60), (70, 62, 44),
|
|
||||||
],
|
|
||||||
sparkle: [
|
|
||||||
(167, 192, 128), (230, 152, 117), (131, 192, 146),
|
|
||||||
(214, 153, 182), (219, 188, 127),
|
|
||||||
],
|
|
||||||
meter: [(148, 172, 110), (200, 170, 108), (210, 108, 110)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Fairyfloss palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
//! Fauve palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
|
||||||
Palette {
|
|
||||||
bg: (28, 22, 18),
|
|
||||||
surface: (42, 33, 26),
|
|
||||||
surface2: (58, 46, 36),
|
|
||||||
fg: (240, 228, 210),
|
|
||||||
fg_dim: (170, 150, 130),
|
|
||||||
fg_muted: (100, 82, 66),
|
|
||||||
accent: (230, 60, 20),
|
|
||||||
red: (220, 38, 32),
|
|
||||||
green: (30, 170, 80),
|
|
||||||
yellow: (255, 210, 0),
|
|
||||||
blue: (20, 80, 200),
|
|
||||||
purple: (170, 40, 150),
|
|
||||||
cyan: (0, 150, 180),
|
|
||||||
orange: (240, 120, 0),
|
|
||||||
tempo_color: (230, 60, 20),
|
|
||||||
bank_color: (20, 80, 200),
|
|
||||||
pattern_color: (0, 150, 180),
|
|
||||||
title_accent: (230, 60, 20),
|
|
||||||
title_author: (20, 80, 200),
|
|
||||||
secondary: (170, 40, 150),
|
|
||||||
link_bright: [
|
|
||||||
(230, 60, 20), (20, 80, 200), (240, 120, 0),
|
|
||||||
(0, 150, 180), (30, 170, 80),
|
|
||||||
],
|
|
||||||
link_dim: [
|
|
||||||
(72, 24, 10), (10, 28, 65), (76, 40, 6),
|
|
||||||
(6, 48, 58), (14, 54, 28),
|
|
||||||
],
|
|
||||||
sparkle: [
|
|
||||||
(230, 60, 20), (255, 210, 0), (30, 170, 80),
|
|
||||||
(20, 80, 200), (170, 40, 150),
|
|
||||||
],
|
|
||||||
meter: [(26, 152, 72), (235, 190, 0), (200, 34, 28)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
//! Georges palette (C64 colors on black).
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
|
// C64 palette on pure black
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
Palette {
|
Palette {
|
||||||
bg: (0, 0, 0),
|
bg: (0, 0, 0),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Gruvbox Dark palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Hot Dog Stand palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
//! Iceberg palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
|
||||||
Palette {
|
|
||||||
bg: (22, 24, 33),
|
|
||||||
surface: (30, 33, 46),
|
|
||||||
surface2: (45, 48, 64),
|
|
||||||
fg: (198, 200, 209),
|
|
||||||
fg_dim: (109, 112, 126),
|
|
||||||
fg_muted: (64, 66, 78),
|
|
||||||
accent: (132, 160, 198),
|
|
||||||
red: (226, 120, 120),
|
|
||||||
green: (180, 190, 130),
|
|
||||||
yellow: (226, 164, 120),
|
|
||||||
blue: (132, 160, 198),
|
|
||||||
purple: (160, 147, 199),
|
|
||||||
cyan: (137, 184, 194),
|
|
||||||
orange: (226, 164, 120),
|
|
||||||
tempo_color: (160, 147, 199),
|
|
||||||
bank_color: (132, 160, 198),
|
|
||||||
pattern_color: (137, 184, 194),
|
|
||||||
title_accent: (132, 160, 198),
|
|
||||||
title_author: (160, 147, 199),
|
|
||||||
secondary: (226, 164, 120),
|
|
||||||
link_bright: [
|
|
||||||
(132, 160, 198), (160, 147, 199), (226, 164, 120),
|
|
||||||
(137, 184, 194), (180, 190, 130),
|
|
||||||
],
|
|
||||||
link_dim: [
|
|
||||||
(45, 55, 70), (55, 50, 68), (70, 55, 42),
|
|
||||||
(46, 62, 66), (58, 62, 44),
|
|
||||||
],
|
|
||||||
sparkle: [
|
|
||||||
(132, 160, 198), (226, 164, 120), (180, 190, 130),
|
|
||||||
(160, 147, 199), (226, 120, 120),
|
|
||||||
],
|
|
||||||
meter: [(160, 175, 115), (210, 150, 105), (200, 105, 105)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
//! Jaipur palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
|
||||||
Palette {
|
|
||||||
bg: (30, 24, 22),
|
|
||||||
surface: (44, 36, 32),
|
|
||||||
surface2: (60, 48, 42),
|
|
||||||
fg: (238, 222, 200),
|
|
||||||
fg_dim: (165, 145, 125),
|
|
||||||
fg_muted: (95, 78, 65),
|
|
||||||
accent: (210, 90, 100),
|
|
||||||
red: (200, 44, 52),
|
|
||||||
green: (30, 160, 120),
|
|
||||||
yellow: (240, 180, 20),
|
|
||||||
blue: (60, 60, 180),
|
|
||||||
purple: (150, 50, 120),
|
|
||||||
cyan: (0, 155, 155),
|
|
||||||
orange: (220, 120, 50),
|
|
||||||
tempo_color: (210, 90, 100),
|
|
||||||
bank_color: (60, 60, 180),
|
|
||||||
pattern_color: (0, 155, 155),
|
|
||||||
title_accent: (210, 90, 100),
|
|
||||||
title_author: (60, 60, 180),
|
|
||||||
secondary: (220, 120, 50),
|
|
||||||
link_bright: [
|
|
||||||
(210, 90, 100), (60, 60, 180), (220, 120, 50),
|
|
||||||
(0, 155, 155), (30, 160, 120),
|
|
||||||
],
|
|
||||||
link_dim: [
|
|
||||||
(66, 30, 34), (22, 22, 58), (70, 40, 18),
|
|
||||||
(6, 48, 48), (12, 50, 38),
|
|
||||||
],
|
|
||||||
sparkle: [
|
|
||||||
(210, 90, 100), (240, 180, 20), (30, 160, 120),
|
|
||||||
(60, 60, 180), (150, 50, 120),
|
|
||||||
],
|
|
||||||
meter: [(26, 144, 106), (222, 164, 18), (184, 40, 46)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Kanagawa palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Letz Light palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -8,38 +8,30 @@ mod catppuccin_mocha;
|
|||||||
mod dracula;
|
mod dracula;
|
||||||
mod eden;
|
mod eden;
|
||||||
mod ember;
|
mod ember;
|
||||||
mod everforest;
|
|
||||||
mod georges;
|
mod georges;
|
||||||
mod fairyfloss;
|
mod fairyfloss;
|
||||||
mod gruvbox_dark;
|
mod gruvbox_dark;
|
||||||
mod hot_dog_stand;
|
mod hot_dog_stand;
|
||||||
mod iceberg;
|
|
||||||
mod jaipur;
|
|
||||||
mod kanagawa;
|
mod kanagawa;
|
||||||
mod letz_light;
|
mod letz_light;
|
||||||
mod monochrome_black;
|
mod monochrome_black;
|
||||||
mod monochrome_white;
|
mod monochrome_white;
|
||||||
mod monokai;
|
mod monokai;
|
||||||
mod nord;
|
mod nord;
|
||||||
mod fauve;
|
|
||||||
mod pitch_black;
|
mod pitch_black;
|
||||||
mod tropicalia;
|
|
||||||
mod rose_pine;
|
mod rose_pine;
|
||||||
mod tokyo_night;
|
mod tokyo_night;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
|
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// Entry in the theme registry: id, display label, and palette constructor.
|
|
||||||
pub struct ThemeEntry {
|
pub struct ThemeEntry {
|
||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
pub palette: fn() -> palette::Palette,
|
pub palette: fn() -> palette::Palette,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All available themes.
|
|
||||||
pub const THEMES: &[ThemeEntry] = &[
|
pub const THEMES: &[ThemeEntry] = &[
|
||||||
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", palette: catppuccin_mocha::palette },
|
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", palette: catppuccin_mocha::palette },
|
||||||
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", palette: catppuccin_latte::palette },
|
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", palette: catppuccin_latte::palette },
|
||||||
@@ -59,28 +51,20 @@ pub const THEMES: &[ThemeEntry] = &[
|
|||||||
ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette },
|
ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette },
|
||||||
ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette },
|
ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette },
|
||||||
ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette },
|
ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette },
|
||||||
ThemeEntry { id: "Iceberg", label: "Iceberg", palette: iceberg::palette },
|
|
||||||
ThemeEntry { id: "Everforest", label: "Everforest", palette: everforest::palette },
|
|
||||||
ThemeEntry { id: "Fauve", label: "Fauve", palette: fauve::palette },
|
|
||||||
ThemeEntry { id: "Tropicalia", label: "Tropicalia", palette: tropicalia::palette },
|
|
||||||
ThemeEntry { id: "Jaipur", label: "Jaipur", palette: jaipur::palette },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
thread_local! {
|
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() -> ThemeColors {
|
||||||
pub fn get() -> Rc<ThemeColors> {
|
CURRENT_THEME.with(|t| t.borrow().clone())
|
||||||
CURRENT_THEME.with(|t| Rc::clone(&t.borrow()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the current thread-local theme.
|
|
||||||
pub fn set(theme: ThemeColors) {
|
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.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ThemeColors {
|
pub struct ThemeColors {
|
||||||
pub ui: UiColors,
|
pub ui: UiColors,
|
||||||
@@ -111,7 +95,6 @@ pub struct ThemeColors {
|
|||||||
pub confirm: ConfirmColors,
|
pub confirm: ConfirmColors,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core UI colors: background, text, borders.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct UiColors {
|
pub struct UiColors {
|
||||||
pub bg: Color,
|
pub bg: Color,
|
||||||
@@ -126,7 +109,6 @@ pub struct UiColors {
|
|||||||
pub surface: Color,
|
pub surface: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Playback status bar colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct StatusColors {
|
pub struct StatusColors {
|
||||||
pub playing_bg: Color,
|
pub playing_bg: Color,
|
||||||
@@ -138,7 +120,6 @@ pub struct StatusColors {
|
|||||||
pub fill_bg: Color,
|
pub fill_bg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Step grid selection and cursor colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SelectionColors {
|
pub struct SelectionColors {
|
||||||
pub cursor_bg: Color,
|
pub cursor_bg: Color,
|
||||||
@@ -152,7 +133,6 @@ pub struct SelectionColors {
|
|||||||
pub in_range: Color,
|
pub in_range: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Step tile colors for various states.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TileColors {
|
pub struct TileColors {
|
||||||
pub playing_active_bg: Color,
|
pub playing_active_bg: Color,
|
||||||
@@ -170,12 +150,10 @@ pub struct TileColors {
|
|||||||
pub link_dim: [(u8, u8, u8); 5],
|
pub link_dim: [(u8, u8, u8); 5],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Top header bar segment colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HeaderColors {
|
pub struct HeaderColors {
|
||||||
pub tempo_bg: Color,
|
pub tempo_bg: Color,
|
||||||
pub tempo_fg: Color,
|
pub tempo_fg: Color,
|
||||||
pub beat_bg: Color,
|
|
||||||
pub bank_bg: Color,
|
pub bank_bg: Color,
|
||||||
pub bank_fg: Color,
|
pub bank_fg: Color,
|
||||||
pub pattern_bg: Color,
|
pub pattern_bg: Color,
|
||||||
@@ -184,7 +162,6 @@ pub struct HeaderColors {
|
|||||||
pub stats_fg: Color,
|
pub stats_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modal dialog border colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ModalColors {
|
pub struct ModalColors {
|
||||||
pub border: Color,
|
pub border: Color,
|
||||||
@@ -198,7 +175,6 @@ pub struct ModalColors {
|
|||||||
pub preview: Color,
|
pub preview: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flash notification colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FlashColors {
|
pub struct FlashColors {
|
||||||
pub error_bg: Color,
|
pub error_bg: Color,
|
||||||
@@ -209,7 +185,6 @@ pub struct FlashColors {
|
|||||||
pub info_fg: Color,
|
pub info_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pattern list row state colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ListColors {
|
pub struct ListColors {
|
||||||
pub playing_bg: Color,
|
pub playing_bg: Color,
|
||||||
@@ -228,7 +203,6 @@ pub struct ListColors {
|
|||||||
pub soloed_fg: Color,
|
pub soloed_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ableton Link status indicator colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LinkStatusColors {
|
pub struct LinkStatusColors {
|
||||||
pub disabled: Color,
|
pub disabled: Color,
|
||||||
@@ -236,7 +210,6 @@ pub struct LinkStatusColors {
|
|||||||
pub listening: Color,
|
pub listening: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Syntax highlighting (fg, bg) pairs per token category.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SyntaxColors {
|
pub struct SyntaxColors {
|
||||||
pub gap_bg: Color,
|
pub gap_bg: Color,
|
||||||
@@ -261,35 +234,30 @@ pub struct SyntaxColors {
|
|||||||
pub default: (Color, Color),
|
pub default: (Color, Color),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alternating table row colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TableColors {
|
pub struct TableColors {
|
||||||
pub row_even: Color,
|
pub row_even: Color,
|
||||||
pub row_odd: Color,
|
pub row_odd: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Value display colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ValuesColors {
|
pub struct ValuesColors {
|
||||||
pub tempo: Color,
|
pub tempo: Color,
|
||||||
pub value: Color,
|
pub value: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keyboard hint key/text colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HintColors {
|
pub struct HintColors {
|
||||||
pub key: Color,
|
pub key: Color,
|
||||||
pub text: Color,
|
pub text: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// View badge pill colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ViewBadgeColors {
|
pub struct ViewBadgeColors {
|
||||||
pub bg: Color,
|
pub bg: Color,
|
||||||
pub fg: Color,
|
pub fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigation minimap tile colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct NavColors {
|
pub struct NavColors {
|
||||||
pub selected_bg: Color,
|
pub selected_bg: Color,
|
||||||
@@ -298,7 +266,6 @@ pub struct NavColors {
|
|||||||
pub unselected_fg: Color,
|
pub unselected_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Script editor colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EditorWidgetColors {
|
pub struct EditorWidgetColors {
|
||||||
pub cursor_bg: Color,
|
pub cursor_bg: Color,
|
||||||
@@ -310,7 +277,6 @@ pub struct EditorWidgetColors {
|
|||||||
pub completion_example: Color,
|
pub completion_example: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File and sample browser colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BrowserColors {
|
pub struct BrowserColors {
|
||||||
pub directory: Color,
|
pub directory: Color,
|
||||||
@@ -325,7 +291,6 @@ pub struct BrowserColors {
|
|||||||
pub empty_text: Color,
|
pub empty_text: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text input field colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct InputColors {
|
pub struct InputColors {
|
||||||
pub text: Color,
|
pub text: Color,
|
||||||
@@ -333,7 +298,6 @@ pub struct InputColors {
|
|||||||
pub hint: Color,
|
pub hint: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search bar and match highlight colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SearchColors {
|
pub struct SearchColors {
|
||||||
pub active: Color,
|
pub active: Color,
|
||||||
@@ -342,7 +306,6 @@ pub struct SearchColors {
|
|||||||
pub match_fg: Color,
|
pub match_fg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Markdown renderer colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MarkdownColors {
|
pub struct MarkdownColors {
|
||||||
pub h1: Color,
|
pub h1: Color,
|
||||||
@@ -357,7 +320,6 @@ pub struct MarkdownColors {
|
|||||||
pub list: Color,
|
pub list: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Engine view panel colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EngineColors {
|
pub struct EngineColors {
|
||||||
pub header: Color,
|
pub header: Color,
|
||||||
@@ -380,7 +342,6 @@ pub struct EngineColors {
|
|||||||
pub hint_inactive: Color,
|
pub hint_inactive: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dictionary view colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DictColors {
|
pub struct DictColors {
|
||||||
pub word_name: Color,
|
pub word_name: Color,
|
||||||
@@ -398,7 +359,6 @@ pub struct DictColors {
|
|||||||
pub header_desc: Color,
|
pub header_desc: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Title screen colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TitleColors {
|
pub struct TitleColors {
|
||||||
pub big_title: Color,
|
pub big_title: Color,
|
||||||
@@ -409,7 +369,6 @@ pub struct TitleColors {
|
|||||||
pub subtitle: Color,
|
pub subtitle: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VU meter and spectrum level colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MeterColors {
|
pub struct MeterColors {
|
||||||
pub low: Color,
|
pub low: Color,
|
||||||
@@ -420,13 +379,11 @@ pub struct MeterColors {
|
|||||||
pub high_rgb: (u8, u8, u8),
|
pub high_rgb: (u8, u8, u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sparkle particle colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SparkleColors {
|
pub struct SparkleColors {
|
||||||
pub colors: [(u8, u8, u8); 5],
|
pub colors: [(u8, u8, u8); 5],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Confirm dialog colors.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfirmColors {
|
pub struct ConfirmColors {
|
||||||
pub border: Color,
|
pub border: Color,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Monochrome (black background) palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Monochrome (white background) palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Monokai palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Nord palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
//! Palette definition and color mixing utilities.
|
|
||||||
|
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
|
|
||||||
/// RGB color triple.
|
|
||||||
pub type Rgb = (u8, u8, u8);
|
pub type Rgb = (u8, u8, u8);
|
||||||
|
|
||||||
/// Base color palette that themes are derived from.
|
|
||||||
pub struct Palette {
|
pub struct Palette {
|
||||||
// Core
|
// Core
|
||||||
pub bg: Rgb,
|
pub bg: Rgb,
|
||||||
@@ -37,12 +33,10 @@ pub struct Palette {
|
|||||||
pub meter: [Rgb; 3],
|
pub meter: [Rgb; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert an RGB triple to a ratatui [`Color`].
|
|
||||||
pub fn rgb(c: Rgb) -> Color {
|
pub fn rgb(c: Rgb) -> Color {
|
||||||
Color::Rgb(c.0, c.1, c.2)
|
Color::Rgb(c.0, c.1, c.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blend `bg` toward `accent` by `amount` (0.0–1.0).
|
|
||||||
pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
|
pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
|
||||||
let mix = |b: u8, a: u8| -> u8 {
|
let mix = |b: u8, a: u8| -> u8 {
|
||||||
let v = b as f32 + (a as f32 - b as f32) * amount;
|
let v = b as f32 + (a as f32 - b as f32) * amount;
|
||||||
@@ -51,12 +45,10 @@ pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
|
|||||||
(mix(bg.0, accent.0), mix(bg.1, accent.1), mix(bg.2, accent.2))
|
(mix(bg.0, accent.0), mix(bg.1, accent.1), mix(bg.2, accent.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Linearly interpolate between two colors.
|
|
||||||
pub fn mid(a: Rgb, b: Rgb, t: f32) -> Rgb {
|
pub fn mid(a: Rgb, b: Rgb, t: f32) -> Rgb {
|
||||||
tint(a, b, t)
|
tint(a, b, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Darken a color by reducing brightness.
|
|
||||||
pub fn darken(c: Rgb, amount: f32) -> Rgb {
|
pub fn darken(c: Rgb, amount: f32) -> Rgb {
|
||||||
let d = |v: u8| -> u8 { (v as f32 * (1.0 - amount)).clamp(0.0, 255.0) as u8 };
|
let d = |v: u8| -> u8 { (v as f32 * (1.0 - amount)).clamp(0.0, 255.0) as u8 };
|
||||||
(d(c.0), d(c.1), d(c.2))
|
(d(c.0), d(c.1), d(c.2))
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Pitch Black palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Rose Pine palette.
|
|
||||||
|
|
||||||
use super::palette::Palette;
|
use super::palette::Palette;
|
||||||
|
|
||||||
pub fn palette() -> Palette {
|
pub fn palette() -> Palette {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user