Compare commits
22 Commits
v0.1.3
...
dacc9bd6be
| Author | SHA1 | Date | |
|---|---|---|---|
| dacc9bd6be | |||
| bfd52c0053 | |||
| 12172ce1e8 | |||
| 1513d80a8d | |||
| 6d71c64a34 | |||
| 097104a074 | |||
| c13ddaaf37 | |||
| 001a42abfc | |||
| 0d0c2738f5 | |||
| 859629ae34 | |||
| 82e5f47933 | |||
| 9cc17d14de | |||
| 453ba62403 | |||
| 35aa97a93d | |||
| 25866f66d4 | |||
| 8b058f2bb9 | |||
| cb82337d24 | |||
| 539aa6a9f7 | |||
| b7d9436cee | |||
| 3d345d57f5 | |||
| c6b14bf508 | |||
|
|
5d755594cb |
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Build .pkg installer
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
VERSION="${GITEA_REF_NAME#v}"
|
||||
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||
cp -R Cagire.app pkg-root/Applications/
|
||||
cp cagire pkg-root/usr/local/bin/
|
||||
@@ -36,9 +36,7 @@ jobs:
|
||||
- name: Prepare plugin artifacts
|
||||
run: |
|
||||
mkdir -p target/bundled
|
||||
# CLAP: single .so renamed to .clap
|
||||
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so target/bundled/cagire-plugins.clap
|
||||
# VST3: correct directory structure
|
||||
mkdir -p "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux"
|
||||
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux/cagire-plugins.so"
|
||||
|
||||
17
.gitea/workflows/build-plugins.yml
Normal file
17
.gitea/workflows/build-plugins.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Build Plugins
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.gitea/workflows/build-plugins-linux.yml
|
||||
|
||||
macos:
|
||||
uses: ./.gitea/workflows/build-plugins-macos.yml
|
||||
|
||||
windows:
|
||||
uses: ./.gitea/workflows/build-plugins-windows.yml
|
||||
|
||||
rpi:
|
||||
uses: ./.gitea/workflows/build-plugins-rpi.yml
|
||||
@@ -54,22 +54,22 @@ jobs:
|
||||
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target x86_64-pc-windows-msvc
|
||||
run: cargo build --release --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
|
||||
run: cargo build --release --features desktop,asio --bin cagire-desktop --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Test
|
||||
if: inputs.run-tests
|
||||
run: cargo test --target x86_64-pc-windows-msvc
|
||||
run: cargo test --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Clippy
|
||||
if: inputs.run-clippy
|
||||
run: cargo clippy --target x86_64-pc-windows-msvc -- -D warnings
|
||||
run: cargo clippy --features asio --target x86_64-pc-windows-msvc -- -D warnings
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
|
||||
run: cargo xtask bundle cagire-plugins --release --features asio --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install NSIS
|
||||
if: inputs.build-packages
|
||||
23
.gitea/workflows/ci.yml
Normal file
23
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.gitea/workflows/build-linux.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
macos:
|
||||
uses: ./.gitea/workflows/build-macos.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
windows:
|
||||
uses: ./.gitea/workflows/build-windows.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
38
.gitea/workflows/deploy-website.yml
Normal file
38
.gitea/workflows/deploy-website.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
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/
|
||||
@@ -2,23 +2,15 @@ name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
uses: ./.gitea/workflows/build-linux.yml
|
||||
with:
|
||||
build-packages: true
|
||||
|
||||
macos:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-macos.yml
|
||||
uses: ./.gitea/workflows/build-macos.yml
|
||||
with:
|
||||
build-packages: true
|
||||
matrix: >-
|
||||
@@ -28,26 +20,21 @@ jobs:
|
||||
]
|
||||
|
||||
windows:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-windows.yml
|
||||
uses: ./.gitea/workflows/build-windows.yml
|
||||
with:
|
||||
build-packages: true
|
||||
|
||||
cross:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-cross.yml
|
||||
uses: ./.gitea/workflows/build-cross.yml
|
||||
|
||||
assemble-macos:
|
||||
needs: macos
|
||||
uses: ./.github/workflows/assemble-macos.yml
|
||||
uses: ./.gitea/workflows/assemble-macos.yml
|
||||
|
||||
release:
|
||||
needs: [linux, macos, windows, cross, assemble-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/v') && github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
@@ -100,8 +87,25 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release/*
|
||||
generate_release_notes: true
|
||||
- name: Create Gitea release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
TAG="${GITEA_REF_NAME:-manual-$(date +%Y%m%d-%H%M%S)}"
|
||||
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||
|
||||
RELEASE_ID=$(curl -s -X POST "$API_URL" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"draft\": true}" \
|
||||
| jq -r '.id')
|
||||
|
||||
for file in release/*; do
|
||||
filename=$(basename "$file")
|
||||
curl -s -X POST "$API_URL/$RELEASE_ID/assets?name=$filename" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$file"
|
||||
done
|
||||
|
||||
echo "Release $TAG created as draft with $(ls release | wc -l) assets"
|
||||
18
.github/workflows/build-plugins.yml
vendored
18
.github/workflows/build-plugins.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Build Plugins
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.github/workflows/build-plugins-linux.yml
|
||||
|
||||
macos:
|
||||
uses: ./.github/workflows/build-plugins-macos.yml
|
||||
|
||||
windows:
|
||||
uses: ./.github/workflows/build-plugins-windows.yml
|
||||
|
||||
rpi:
|
||||
uses: ./.github/workflows/build-plugins-rpi.yml
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
macos:
|
||||
uses: ./.github/workflows/build-macos.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
windows:
|
||||
uses: ./.github/workflows/build-windows.yml
|
||||
with:
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
59
.github/workflows/pages.yml
vendored
59
.github/workflows/pages.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Deploy Website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.server_url == 'https://github.com'
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: website/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: website
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
working-directory: website
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: website/dist
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -3,7 +3,7 @@
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Bubobubobubobubo/cagire
|
||||
git clone https://git.raphaelforment.fr/BuboBubo/cagire
|
||||
cd cagire
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -2,6 +2,56 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.4]
|
||||
|
||||
### Breaking
|
||||
- **Doux v0.0.12**: removed Mutable Instruments Plaits modes (`modal`, `va`, `analog`, `waveshape`, `grain`, `chord`, `swarm`, `pnoise`, etc.). Native percussion models retained; new models added: `tom`, `cowbell`, `cymbal`.
|
||||
- Simplified effects/filter API: removed per-filter envelope parameters in favor of the universal `env` word.
|
||||
- Recording commands simplified: removed `/sound/` path segment from `rec`, `overdub`, `orec`, `odub`.
|
||||
|
||||
### Forth Language
|
||||
- New modulation transition words: `islide` (swell), `oslide` (pluck), `pslide` (stair/stepped).
|
||||
- New `lpg` word (Low Pass Gate): pairs amplitude envelope with lowpass filter modulation.
|
||||
- New `inchan` word: select audio input channel by index.
|
||||
- New EQ frequency words: `eqlofreq`, `eqmidfreq`, `eqhifreq`.
|
||||
|
||||
### UI / UX
|
||||
- Redesigned top bar: consolidated transport, tempo, bar:beat display with visual beat segments.
|
||||
- CPU meter with color-coded fill bar (green/yellow/red).
|
||||
|
||||
### Engine
|
||||
- Audio input channel selection support.
|
||||
- Audio buffer sizing improved for multi-channel input.
|
||||
|
||||
### Packaging
|
||||
- CI migrated from GitHub Actions to Gitea Actions.
|
||||
- Removed WIX installer; Windows now distributed via zip and NSIS only.
|
||||
- Gitea Actions workflow for automatic website deployment.
|
||||
- Added LICENSE file.
|
||||
|
||||
### Documentation
|
||||
- Extensive documentation updates reflecting doux v0.0.12 API changes across sources, filters, modulation, wavetable, and audio modulation docs.
|
||||
|
||||
## [0.1.3]
|
||||
|
||||
### Forth Language
|
||||
- New `stretch` word: pitch-independent time stretching via phase vocoder (e.g., `kick sound 2 stretch .` plays at half speed, same pitch).
|
||||
- Automatic default release time on sounds when none is explicitly set.
|
||||
|
||||
### Engine
|
||||
- Sample-accurate timing: delta computation switched from float seconds to integer sample ticks, fixing precision issues.
|
||||
- Lock-free audio input buffer: replaced `Arc<Mutex<VecDeque>>` with `HeapRb` ring buffer.
|
||||
- Theme access optimized: `Rc<ThemeColors>` replaces deep cloning on every `get()`.
|
||||
- Dictionary keys cached in `App` to avoid repeated lock acquisitions during rendering.
|
||||
|
||||
### Fixed
|
||||
- Realtime priority diagnostics: dedicated `warn_no_rt()` on Linux, lookahead widened from 20ms to 40ms when RT priority unavailable.
|
||||
- Float epsilon precision in delta/nudge zero-comparisons.
|
||||
- Windows build fixes for standalone and plugin targets.
|
||||
|
||||
### Documentation
|
||||
- Time stretching usage guide added to `docs/engine/samples.md`.
|
||||
|
||||
## [0.1.2]
|
||||
|
||||
### Forth Language
|
||||
|
||||
179
Cargo.lock
generated
179
Cargo.lock
generated
@@ -372,6 +372,20 @@ dependencies = [
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asio-sys"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "826194e1612938c9be09b78b58323fbb2e326de3d491b4230186cf6e832d8ded"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"parse_cfg",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -846,7 +860,7 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21"
|
||||
|
||||
[[package]]
|
||||
name = "cagire"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"arc-swap",
|
||||
@@ -859,7 +873,7 @@ dependencies = [
|
||||
"cpal 0.17.1",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"doux",
|
||||
"doux 0.0.14",
|
||||
"eframe",
|
||||
"egui",
|
||||
"egui_ratatui",
|
||||
@@ -885,7 +899,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-forth"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"parking_lot",
|
||||
@@ -894,7 +908,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-markdown"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"minimad",
|
||||
"ratatui",
|
||||
@@ -902,7 +916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-plugins"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"cagire",
|
||||
@@ -911,7 +925,7 @@ dependencies = [
|
||||
"cagire-ratatui",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"doux",
|
||||
"doux 0.0.13",
|
||||
"egui_ratatui",
|
||||
"nih_plug",
|
||||
"nih_plug_egui",
|
||||
@@ -926,7 +940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-project"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"brotli",
|
||||
@@ -938,7 +952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cagire-ratatui"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"ratatui",
|
||||
@@ -1452,6 +1466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
||||
dependencies = [
|
||||
"alsa 0.10.0",
|
||||
"asio-sys",
|
||||
"coreaudio-rs 0.13.0",
|
||||
"dasp_sample",
|
||||
"jack 0.13.5",
|
||||
@@ -1473,7 +1488,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.62.2",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1809,14 +1824,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "doux"
|
||||
version = "0.0.8"
|
||||
source = "git+https://github.com/sova-org/doux#a916717fa54b1cc0be093c131e03668d14ee1e3b"
|
||||
version = "0.0.13"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.13#b8150d907e4cc2764e82fdaa424df41ceef9b0d2"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clap",
|
||||
"cpal 0.17.1",
|
||||
"crossbeam-channel",
|
||||
"mi-plaits-dsp",
|
||||
"ringbuf",
|
||||
"rosc",
|
||||
"rustyline",
|
||||
"soundfont",
|
||||
"symphonia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doux"
|
||||
version = "0.0.14"
|
||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.14#f0de4f4047adfced8fb2116edd3b33d260ba75c8"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clap",
|
||||
"cpal 0.17.1",
|
||||
"crossbeam-channel",
|
||||
"ringbuf",
|
||||
"rosc",
|
||||
"rustyline",
|
||||
"soundfont",
|
||||
@@ -1835,12 +1866,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.33.3"
|
||||
@@ -3238,16 +3263,6 @@ dependencies = [
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mi-plaits-dsp"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/sourcebox/mi-plaits-dsp-rs?rev=dc55bd55e73bd6f86fbbb4f8adc3b598d659fdb4#dc55bd55e73bd6f86fbbb4f8adc3b598d659fdb4"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"num-traits",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "micromath"
|
||||
version = "2.1.0"
|
||||
@@ -4118,6 +4133,15 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse_cfg"
|
||||
version = "4.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "905787a434a2c721408e7c9a252e85f3d93ca0f118a5283022636c0e05a7ea49"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
@@ -5128,15 +5152,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a7f4cb358863e55f8f1a3882f68601360cf6c42fc53ff2fe9aea41c33e24489"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
@@ -6567,23 +6582,11 @@ version = "0.61.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||
dependencies = [
|
||||
"windows-collections 0.2.0",
|
||||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future 0.2.1",
|
||||
"windows-future",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections 0.3.2",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future 0.3.2",
|
||||
"windows-numerics 0.3.1",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6595,15 +6598,6 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
@@ -6652,19 +6646,6 @@ dependencies = [
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
@@ -6673,18 +6654,7 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link 0.1.3",
|
||||
"windows-threading 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-threading 0.2.1",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6775,16 +6745,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
@@ -6812,15 +6772,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
@@ -6840,15 +6791,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -6951,15 +6893,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/Bubobubobubobubo/cagire"
|
||||
repository = "https://git.raphaelforment.fr/BuboBubo/cagire"
|
||||
homepage = "https://cagire.raphaelforment.fr"
|
||||
description = "Forth-based live coding music sequencer"
|
||||
|
||||
@@ -45,13 +45,14 @@ desktop = [
|
||||
"dep:egui_ratatui",
|
||||
"dep:image",
|
||||
]
|
||||
asio = ["doux/asio", "cpal/asio"]
|
||||
|
||||
[dependencies]
|
||||
cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cagire.raphaelforment.fr">Website</a> ·
|
||||
<a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> ·
|
||||
<a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a> ·
|
||||
AGPL-3.0
|
||||
</p>
|
||||
|
||||
@@ -45,13 +45,13 @@ sine sound 2 fm 0.5 fmh
|
||||
- User-defined words: extend (or redefine) the language on the fly with `:name ... ;` definitions.
|
||||
- Interactive documentation: built-in tutorials with runnable examples.
|
||||
- **Audio engine** (powered by [Doux](https://doux.livecoding.fr)):
|
||||
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive, FM (2-op, 3 algorithms), additive synthesis, wavetables, 7-voice spread, Mutable Instruments Plaits models: modal, granular, waveshaping, chord, swarm, etc.
|
||||
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive (up to 32 partials), FM (2-op, 3 algorithms), wavetables, 7-voice spread.
|
||||
- Drum models: seven drum models with timbral morphing.
|
||||
- Sampling: disk-loaded samples with slicing, looping, pitch tracking, wavetable mode, and live recording from engine output or line input.
|
||||
- Filters: biquad LP/HP/BP and ladder filters, each with independent envelope. Filters can be modulated, stacked, etc.
|
||||
- Filters: biquad LP/HP/BP and ladder filters. Filters can be modulated, stacked, etc.
|
||||
- Effects: phaser, flanger, chorus, smear, distortion, wavefolder, wavewrapper, bitcrusher, sample-rate reduction, 3-band EQ, tilt EQ, Haas stereo.
|
||||
- Bus effects: delay (standard, ping-pong, tape, multitap), two reverb engines (Dattorro plate, Vital Space), comb filter, feedback delay with LFO, sidechain compressor.
|
||||
- Modulation: vibrato, AM, ring mod, pitch envelope, FM envelope, glide — all with selectable LFO shapes (sine, tri, saw, square, sample & hold).
|
||||
- Modulation: vibrato, AM, ring mod, audio-rate LFO, transitions, DAHDSR envelope modulation — all applicable to any parameter.
|
||||
- **Sequencing**: probabilities, patterns, euclidean structures, sub-step timing, pattern chaining and a lot more.
|
||||
- **MIDI**: receive or send MIDI messages across up to 4 inputs and 4 outputs.
|
||||
- **Ableton Link**: tempo and phase sync with any Link-enabled software or hardware.
|
||||
@@ -77,7 +77,6 @@ Cagire includes interactive documentation with runnable code examples. Press **F
|
||||
Cagire is developed by [BuboBubo](https://raphaelforment.fr) (Raphael Forment).
|
||||
|
||||
- **[Doux](https://doux.livecoding.fr)** (audio engine) — Rust port of Dough, originally written in C by Felix Roos
|
||||
- **mi-plaits-dsp-rs** — Rust port of Mutable Instruments Plaits DSP by Oliver Rockstedt, original code by Emilie Gillet
|
||||
|
||||
### License
|
||||
|
||||
|
||||
@@ -133,6 +133,9 @@ pub enum Op {
|
||||
ModSlide(u8),
|
||||
ModRnd(u8),
|
||||
ModEnv,
|
||||
ModEnvAd,
|
||||
ModEnvAdr,
|
||||
Lpg,
|
||||
// Global params
|
||||
EmitAll,
|
||||
ClearGlobal,
|
||||
|
||||
@@ -1433,7 +1433,7 @@ impl Forth {
|
||||
let dur = pop_float(stack)? * ctx.step_duration();
|
||||
let end = pop_float(stack)?;
|
||||
let start = pop_float(stack)?;
|
||||
let suffix = match curve { 1 => "e", 2 => "s", _ => "" };
|
||||
let suffix = match curve { 1 => "e", 2 => "s", 3 => "i", 4 => "o", 5 => "p", _ => "" };
|
||||
let s = format!("{start}>{end}:{dur}{suffix}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
@@ -1446,25 +1446,57 @@ impl Forth {
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
Op::ModEnv => {
|
||||
ensure(stack, 1)?;
|
||||
let values = std::mem::take(stack);
|
||||
let mut floats = Vec::with_capacity(values.len());
|
||||
for v in &values {
|
||||
floats.push(v.as_float()?);
|
||||
}
|
||||
if floats.len() < 3 || (floats.len() - 1) % 2 != 0 {
|
||||
return Err("env expects: start target1 dur1 [target2 dur2 ...]".into());
|
||||
}
|
||||
let step_dur = ctx.step_duration();
|
||||
let release = pop_float(stack)? * ctx.step_duration();
|
||||
let sustain = pop_float(stack)?;
|
||||
let decay = pop_float(stack)? * ctx.step_duration();
|
||||
let attack = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
let _ = write!(&mut s, "{}", floats[0]);
|
||||
for pair in floats[1..].chunks(2) {
|
||||
let _ = write!(&mut s, ">{}:{}", pair[0], pair[1] * step_dur);
|
||||
}
|
||||
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:{sustain}:{release}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
|
||||
Op::ModEnvAd => {
|
||||
let decay = pop_float(stack)? * ctx.step_duration();
|
||||
let attack = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:0:0");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
|
||||
Op::ModEnvAdr => {
|
||||
let release = pop_float(stack)? * ctx.step_duration();
|
||||
let decay = pop_float(stack)? * ctx.step_duration();
|
||||
let attack = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
let _ = write!(&mut s, "{min}^{max}:{attack}:{decay}:0:{release}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
|
||||
Op::Lpg => {
|
||||
let depth = pop_float(stack)?.clamp(0.0, 1.0);
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
let effective_max = min + (max - min) * depth;
|
||||
let sd = ctx.step_duration();
|
||||
let a = cmd_param_float(cmd, "attack").unwrap_or(0.0) * sd;
|
||||
let d = cmd_param_float(cmd, "decay").unwrap_or(1.0) * sd;
|
||||
let s = cmd_param_float(cmd, "sustain").unwrap_or(0.0);
|
||||
let r = cmd_param_float(cmd, "release").unwrap_or(0.0) * sd;
|
||||
use std::fmt::Write;
|
||||
let mut mod_str = String::new();
|
||||
let _ = write!(&mut mod_str, "{min}^{effective_max}:{a}:{d}:{s}:{r}");
|
||||
cmd.set_param("lpf", Value::Str(mod_str.into(), None));
|
||||
}
|
||||
|
||||
// MIDI operations
|
||||
Op::MidiEmit => {
|
||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||
@@ -1642,21 +1674,21 @@ impl Forth {
|
||||
}
|
||||
Op::Rec => {
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/sound/{}", name.as_str()?));
|
||||
outputs.push(format!("/doux/rec/{}", name.as_str()?));
|
||||
}
|
||||
Op::Overdub => {
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?));
|
||||
outputs.push(format!("/doux/rec/{}/overdub/1", name.as_str()?));
|
||||
}
|
||||
Op::Orec => {
|
||||
let orbit = pop(stack)?.as_int()?;
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/sound/{}/orbit/{}", name.as_str()?, orbit));
|
||||
outputs.push(format!("/doux/rec/{}/orbit/{}", name.as_str()?, orbit));
|
||||
}
|
||||
Op::Odub => {
|
||||
let orbit = pop(stack)?.as_int()?;
|
||||
let name = pop(stack)?;
|
||||
outputs.push(format!("/doux/rec/sound/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
|
||||
outputs.push(format!("/doux/rec/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
|
||||
}
|
||||
Op::Forget => {
|
||||
let name = pop(stack)?;
|
||||
@@ -1710,30 +1742,18 @@ fn extract_dev_param(params: &[(&str, Value)]) -> u8 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn cmd_param_float(cmd: &CmdRegister, name: &str) -> Option<f64> {
|
||||
cmd.params()
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(k, _)| *k == name)
|
||||
.and_then(|(_, v)| v.as_float().ok())
|
||||
}
|
||||
|
||||
fn is_tempo_scaled_param(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"attack"
|
||||
| "decay"
|
||||
| "release"
|
||||
| "lpa"
|
||||
| "lpd"
|
||||
| "lpr"
|
||||
| "hpa"
|
||||
| "hpd"
|
||||
| "hpr"
|
||||
| "bpa"
|
||||
| "bpd"
|
||||
| "bpr"
|
||||
| "patt"
|
||||
| "pdec"
|
||||
| "prel"
|
||||
| "fma"
|
||||
| "fmd"
|
||||
| "fmr"
|
||||
| "glide"
|
||||
| "chorusdelay"
|
||||
| "duration"
|
||||
"attack" | "decay" | "release" | "envdelay" | "hold" | "chorusdelay"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1758,6 +1778,9 @@ fn emit_output(
|
||||
}
|
||||
|
||||
for (i, (k, v)) in params.iter().enumerate() {
|
||||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !out.ends_with('/') {
|
||||
out.push('/');
|
||||
}
|
||||
|
||||
@@ -136,10 +136,16 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"slide" => Op::ModSlide(0),
|
||||
"expslide" => Op::ModSlide(1),
|
||||
"sslide" => Op::ModSlide(2),
|
||||
"islide" => Op::ModSlide(3),
|
||||
"oslide" => Op::ModSlide(4),
|
||||
"pslide" => Op::ModSlide(5),
|
||||
"jit" => Op::ModRnd(0),
|
||||
"sjit" => Op::ModRnd(1),
|
||||
"drunk" => Op::ModRnd(2),
|
||||
"env" => Op::ModEnv,
|
||||
"ead" => Op::ModEnvAd,
|
||||
"eadr" => Op::ModEnvAdr,
|
||||
"eadsr" | "env" => Op::ModEnv,
|
||||
"lpg" => Op::Lpg,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -73,6 +73,26 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "envdelay",
|
||||
aliases: &["envdly"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set envelope delay time",
|
||||
example: "0.1 envdelay",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hold",
|
||||
aliases: &["hld"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set envelope hold time",
|
||||
example: "0.05 hold",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "adsr",
|
||||
aliases: &[],
|
||||
@@ -93,56 +113,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "penv",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch envelope",
|
||||
example: "0.5 penv",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "patt",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch attack",
|
||||
example: "0.01 patt",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pdec",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch decay",
|
||||
example: "0.1 pdec",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "psus",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch sustain",
|
||||
example: "0 psus",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "prel",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch release",
|
||||
example: "0.1 prel",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Filter
|
||||
Word {
|
||||
name: "lpf",
|
||||
@@ -164,56 +134,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpe",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass envelope",
|
||||
example: "0.5 lpe",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpa",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass attack",
|
||||
example: "0.01 lpa",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpd",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass decay",
|
||||
example: "0.1 lpd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lps",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass sustain",
|
||||
example: "0.5 lps",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpr",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass release",
|
||||
example: "0.3 lpr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpf",
|
||||
aliases: &[],
|
||||
@@ -234,56 +154,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpe",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass envelope",
|
||||
example: "0.5 hpe",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpa",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass attack",
|
||||
example: "0.01 hpa",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpd",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass decay",
|
||||
example: "0.1 hpd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hps",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass sustain",
|
||||
example: "0.5 hps",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpr",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass release",
|
||||
example: "0.3 hpr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpf",
|
||||
aliases: &[],
|
||||
@@ -304,56 +174,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpe",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass envelope",
|
||||
example: "0.5 bpe",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpa",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass attack",
|
||||
example: "0.01 bpa",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpd",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass decay",
|
||||
example: "0.1 bpd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bps",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass sustain",
|
||||
example: "0.5 bps",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpr",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass release",
|
||||
example: "0.3 bpr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "llpf",
|
||||
aliases: &[],
|
||||
@@ -454,6 +274,36 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "eqlofreq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set low shelf frequency (Hz)",
|
||||
example: "400 eqlofreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "eqmidfreq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set mid peak frequency (Hz)",
|
||||
example: "2000 eqmidfreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "eqhifreq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set high shelf frequency (Hz)",
|
||||
example: "8000 eqhifreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "tilt",
|
||||
aliases: &[],
|
||||
|
||||
@@ -126,16 +126,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "repeat",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set repeat count",
|
||||
example: "4 repeat",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dur",
|
||||
aliases: &[],
|
||||
@@ -151,7 +141,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set gate time",
|
||||
desc: "Set gate duration (total note length, 0 = infinite sustain)",
|
||||
example: "0.8 gate",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -246,6 +236,16 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "inchan",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Select input channel for live input (0-indexed)",
|
||||
example: "0 inchan",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "cut",
|
||||
aliases: &[],
|
||||
@@ -287,16 +287,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "glide",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set glide/portamento",
|
||||
example: "0.1 glide",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pw",
|
||||
aliases: &[],
|
||||
@@ -362,7 +352,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set harmonics (mutable only)",
|
||||
desc: "Set harmonics (add source)",
|
||||
example: "4 harmonics",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -372,7 +362,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set timbre (mutable only)",
|
||||
desc: "Set timbre (add source)",
|
||||
example: "0.5 timbre",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -382,7 +372,7 @@ pub(super) const WORDS: &[Word] = &[
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set morph (mutable only)",
|
||||
desc: "Set morph (add source)",
|
||||
example: "0.5 morph",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
@@ -468,36 +458,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "scanlfo",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set scan LFO rate (Hz)",
|
||||
example: "0.2 scanlfo",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "scandepth",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set scan LFO depth (0-1)",
|
||||
example: "0.4 scandepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "scanshape",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set scan LFO shape (sine/tri/saw/square/sh)",
|
||||
example: "\"tri\" scanshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// FM
|
||||
Word {
|
||||
name: "fm",
|
||||
@@ -529,56 +489,6 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fme",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM envelope",
|
||||
example: "0.5 fme",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fma",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM attack",
|
||||
example: "0.01 fma",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmd",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM decay",
|
||||
example: "0.1 fmd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fms",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM sustain",
|
||||
example: "0.5 fms",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmr",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM release",
|
||||
example: "0.1 fmr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fm2",
|
||||
aliases: &[],
|
||||
@@ -852,6 +762,36 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "islide",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start end dur -- str)",
|
||||
desc: "Swell transition (slow start, fast finish): start>end:duri",
|
||||
example: "200 4000 1 islide lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "oslide",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start end dur -- str)",
|
||||
desc: "Pluck transition (fast attack, slow settle): start>end:duro",
|
||||
example: "0 1 0.5 oslide gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "pslide",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start end dur -- str)",
|
||||
desc: "Stair transition (8 discrete steps): start>end:durp",
|
||||
example: "0 1 2 pslide gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "jit",
|
||||
aliases: &[],
|
||||
@@ -882,13 +822,53 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "ead",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max a d -- str)",
|
||||
desc: "Percussive envelope mod: min^max:a:d:0:0",
|
||||
example: "200 8000 0.01 0.1 ead lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "eadr",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max a d r -- str)",
|
||||
desc: "Percussive envelope mod with release: min^max:a:d:0:r",
|
||||
example: "200 8000 0.01 0.1 0.3 eadr lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "eadsr",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max a d s r -- str)",
|
||||
desc: "ADSR envelope mod: min^max:a:d:s:r",
|
||||
example: "200 8000 0.01 0.1 0.5 0.3 eadsr lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "env",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start t1 d1 ... -- str)",
|
||||
desc: "Multi-segment envelope: start>t1:d1>...",
|
||||
example: "0 1 0.01 0.7 0.1 0 2 env gain",
|
||||
stack: "(min max a d s r -- str)",
|
||||
desc: "DAHDSR envelope modulation: min^max:a:d:s:r",
|
||||
example: "200 8000 0.01 0.1 0.5 0.3 env lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "lpg",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max depth --)",
|
||||
desc: "Low pass gate: pairs amp envelope with lpf modulation",
|
||||
example: "0.01 0.1 ad 200 8000 1 lpg .",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
|
||||
@@ -14,4 +14,4 @@ pub const MAX_STEPS: usize = 1024;
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, load_str, save, FileError};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step};
|
||||
|
||||
@@ -206,39 +206,6 @@ impl LaunchQuantization {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a pattern synchronizes when launched: restart or phase-lock.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum SyncMode {
|
||||
#[default]
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
|
||||
impl SyncMode {
|
||||
/// Human-readable label for display.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Reset",
|
||||
Self::PhaseLock => "Phase-Lock",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Rst",
|
||||
Self::PhaseLock => "Plk",
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between Reset and PhaseLock.
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
Self::Reset => Self::PhaseLock,
|
||||
Self::PhaseLock => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum FollowUp {
|
||||
@@ -315,7 +282,7 @@ impl Default for Step {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
||||
/// Sequence of steps with playback settings (speed, quantization, follow-up).
|
||||
#[derive(Clone)]
|
||||
pub struct Pattern {
|
||||
pub steps: Vec<Step>,
|
||||
@@ -324,7 +291,6 @@ pub struct Pattern {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -361,8 +327,6 @@ struct SparsePattern {
|
||||
description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default, skip_serializing_if = "is_default_follow_up")]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
@@ -371,10 +335,6 @@ fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||
*q == LaunchQuantization::default()
|
||||
}
|
||||
|
||||
fn is_default_sync_mode(s: &SyncMode) -> bool {
|
||||
*s == SyncMode::default()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LegacyPattern {
|
||||
steps: Vec<Step>,
|
||||
@@ -388,8 +348,6 @@ struct LegacyPattern {
|
||||
#[serde(default)]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default)]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default)]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -416,7 +374,6 @@ impl Serialize for Pattern {
|
||||
name: self.name.clone(),
|
||||
description: self.description.clone(),
|
||||
quantization: self.quantization,
|
||||
sync_mode: self.sync_mode,
|
||||
follow_up: self.follow_up,
|
||||
};
|
||||
sparse.serialize(serializer)
|
||||
@@ -452,7 +409,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: sparse.name,
|
||||
description: sparse.description,
|
||||
quantization: sparse.quantization,
|
||||
sync_mode: sparse.sync_mode,
|
||||
follow_up: sparse.follow_up,
|
||||
})
|
||||
}
|
||||
@@ -463,7 +419,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
name: legacy.name,
|
||||
description: legacy.description,
|
||||
quantization: legacy.quantization,
|
||||
sync_mode: legacy.sync_mode,
|
||||
follow_up: legacy.follow_up,
|
||||
}),
|
||||
}
|
||||
@@ -479,7 +434,6 @@ impl Default for Pattern {
|
||||
name: None,
|
||||
description: None,
|
||||
quantization: LaunchQuantization::default(),
|
||||
sync_mode: SyncMode::default(),
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn build(p: &Palette) -> ThemeColors {
|
||||
header: HeaderColors {
|
||||
tempo_bg: rgb(tint(p.bg, p.tempo_color, 0.30)),
|
||||
tempo_fg: rgb(p.tempo_color),
|
||||
beat_bg: rgb(tint(p.bg, p.tempo_color, 0.45)),
|
||||
bank_bg: rgb(tint(p.bg, p.bank_color, 0.25)),
|
||||
bank_fg: rgb(p.bank_color),
|
||||
pattern_bg: rgb(tint(p.bg, p.pattern_color, 0.25)),
|
||||
|
||||
@@ -175,6 +175,7 @@ pub struct TileColors {
|
||||
pub struct HeaderColors {
|
||||
pub tempo_bg: Color,
|
||||
pub tempo_fg: Color,
|
||||
pub beat_bg: Color,
|
||||
pub bank_bg: Color,
|
||||
pub bank_fg: Color,
|
||||
pub pattern_bg: Color,
|
||||
|
||||
@@ -57,17 +57,53 @@ saw snd 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
|
||||
Stack effect: `( min max period -- str )`
|
||||
|
||||
## Envelopes
|
||||
## Envelope Modulation
|
||||
|
||||
Define a multi-segment envelope for a parameter. Provide a start value, then pairs of target and duration.
|
||||
Apply an envelope to any parameter. The `env` word is the complete form: it sweeps from `min` to `max` following a full attack, decay, sustain, release shape. All times are in steps.
|
||||
|
||||
```forth
|
||||
saw snd 0 1 0.1 0.7 0.5 0 8 env gain .
|
||||
saw snd 200 8000 0.01 0.1 0.5 0.3 env lpf .
|
||||
```
|
||||
|
||||
This creates: start at `0`, rise to `1` in `0.1` steps, drop to `0.7` in `0.5` steps, fall to `0` in `8` steps.
|
||||
Stack effect: `( min max attack decay sustain release -- str )`
|
||||
|
||||
Stack effect: `( start target1 dur1 [target2 dur2 ...] -- str )`
|
||||
This is the building block. From it, three shorthands drop the parameters you don't need:
|
||||
|
||||
| Word | Stack | What it does |
|
||||
|------|-------|-------------|
|
||||
| `env` | `( min max a d s r -- str )` | Full envelope (attack, decay, sustain, release) |
|
||||
| `eadr` | `( min max a d r -- str )` | No sustain (sustain = 0) |
|
||||
| `ead` | `( min max a d -- str )` | Percussive (sustain = 0, release = 0) |
|
||||
|
||||
`eadsr` is an alias for `env`.
|
||||
|
||||
```forth
|
||||
saw snd 200 8000 0.01 0.3 ead lpf . ( percussive filter pluck )
|
||||
saw snd 0 5 0.01 0.1 0.3 eadr fm . ( FM depth with release tail )
|
||||
saw snd 200 8000 0.01 0.1 0.5 0.3 env lpf . ( full ADSR on filter )
|
||||
```
|
||||
|
||||
These work on any parameter — `lpf`, `fm`, `gain`, `pan`, `freq`, anything that accepts a value.
|
||||
|
||||
## Low Pass Gate
|
||||
|
||||
The `lpg` word couples the amplitude envelope with a lowpass filter. Set your amp envelope first with `ad` or `adsr`, then `lpg` mirrors it to `lpf`.
|
||||
|
||||
```forth
|
||||
saw snd 0.01 0.1 ad 200 8000 1 lpg . ( percussive LPG )
|
||||
saw snd 0.01 0.1 0.5 0.3 adsr 200 4000 1 lpg . ( sustained LPG )
|
||||
```
|
||||
|
||||
Stack effect: `( min max depth -- )`
|
||||
|
||||
- `min`/`max` — filter frequency range in Hz
|
||||
- `depth` — 0 to 1, scales the filter range (1 = full, 0.5 = halfway)
|
||||
|
||||
```forth
|
||||
saw snd 0.01 0.5 ad 200 8000 0.3 lpg . ( subtle LPG, filter barely opens )
|
||||
```
|
||||
|
||||
`lpg` reads `attack`, `decay`, `sustain`, and `release` from the current sound. If none are set, it defaults to a short percussive shape.
|
||||
|
||||
## Combining
|
||||
|
||||
@@ -77,6 +113,6 @@ Modulation words return strings, so they compose naturally with the rest of the
|
||||
saw snd
|
||||
200 4000 4 lfo lpf
|
||||
0.3 0.7 8 tlfo pan
|
||||
0 1 0.1 0.7 0.5 0 8 env gain
|
||||
0 1 0.01 0.1 ead gain
|
||||
.
|
||||
```
|
||||
|
||||
@@ -57,29 +57,15 @@ The `ftype` parameter sets the filter slope (rolloff steepness).
|
||||
saw 800 lpf 3 ftype . ( 48 dB/oct lowpass )
|
||||
```
|
||||
|
||||
## Filter Envelope
|
||||
## Filter Envelope Modulation
|
||||
|
||||
Filters can be modulated by an ADSR envelope. The envelope multiplies the base cutoff:
|
||||
|
||||
```
|
||||
final_cutoff = lpf + (lpe × envelope × lpf)
|
||||
```
|
||||
|
||||
When the envelope is at 1.0 and `lpe` is 1.0, the cutoff doubles. When the envelope is at 0, the cutoff equals `lpf`.
|
||||
Use the `env` word to apply a DAHDSR envelope to any filter cutoff:
|
||||
|
||||
```forth
|
||||
saw 200 lpf 2 lpe 0.01 lpa 0.3 lpd . ( cutoff sweeps from 600 Hz down to 200 Hz )
|
||||
saw 200 8000 0.01 0.3 0.5 0.3 env lpf . ( cutoff sweeps from 200 to 8000 Hz )
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `lpe` | Envelope depth (multiplier, 1.0 = double cutoff at peak) |
|
||||
| `lpa` | Attack time in seconds |
|
||||
| `lpd` | Decay time in seconds |
|
||||
| `lps` | Sustain level (0-1) |
|
||||
| `lpr` | Release time in seconds |
|
||||
|
||||
The same pattern works for highpass (`hpe`, `hpa`, etc.) and bandpass (`bpe`, `bpa`, etc.).
|
||||
The same works for highpass and bandpass: `env hpf`, `env bpf`.
|
||||
|
||||
## Ladder Filters
|
||||
|
||||
@@ -100,7 +86,7 @@ saw 1000 lbpf 0.8 lbpq . ( ladder bandpass )
|
||||
| `lbpf` | Hz | Ladder bandpass cutoff |
|
||||
| `lbpq` | 0-1 | Ladder bandpass resonance |
|
||||
|
||||
Ladder filters share the lowpass envelope parameters (`lpe`, `lpa`, etc.).
|
||||
Ladder filter cutoffs can also be modulated with `env`, `lfo`, `slide`, etc.
|
||||
|
||||
## EQ
|
||||
|
||||
|
||||
@@ -16,34 +16,6 @@ saw 5 vib 0.5 vibmod . ( 5 Hz, 0.5 semitone depth )
|
||||
| `vibmod` | semitones | Modulation depth |
|
||||
| `vibshape` | shape | LFO waveform (sine, tri, saw, square) |
|
||||
|
||||
## Pitch Envelope
|
||||
|
||||
The pitch envelope applies an ADSR to the oscillator frequency.
|
||||
|
||||
```forth
|
||||
sine 100 freq 24 penv 0.001 patt 0.1 pdec .
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `penv` | Envelope depth in semitones |
|
||||
| `patt` | Attack time in seconds |
|
||||
| `pdec` | Decay time in seconds |
|
||||
| `psus` | Sustain level (0-1) |
|
||||
| `prel` | Release time in seconds |
|
||||
|
||||
## Glide
|
||||
|
||||
Glide interpolates between pitch changes over time.
|
||||
|
||||
```forth
|
||||
saw c4 0.1 glide . ( 100ms glide )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `glide` | seconds | Glide time |
|
||||
|
||||
## FM Synthesis
|
||||
|
||||
FM modulates the carrier frequency with a modulator oscillator.
|
||||
@@ -58,7 +30,7 @@ sine 440 freq 2 fm 2 fmh . ( modulator at 2× carrier frequency )
|
||||
| `fmh` | ratio | Harmonic ratio (modulator / carrier) |
|
||||
| `fmshape` | shape | Modulator waveform |
|
||||
|
||||
FM has its own envelope (`fme`, `fma`, `fmd`, `fms`, `fmr`).
|
||||
Use `env` to apply a DAHDSR envelope to FM depth: `0 5 0.01 0.1 0.3 0.5 env fm`.
|
||||
|
||||
## Amplitude Modulation
|
||||
|
||||
|
||||
@@ -56,37 +56,29 @@ Noise sources ignore pitch. Use filters to shape the spectrum.
|
||||
|
||||
All filter and effect parameters apply to the input signal.
|
||||
|
||||
## Plaits Engines
|
||||
## Additive
|
||||
|
||||
The Plaits engines come from Mutable Instruments and provide a range of synthesis methods. Beware, these sources can be quite CPU hungry. All share three control parameters (`0.0`-`1.0`):
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `add` | Stacks 1-32 sine partials with spectral tilt, even/odd morph, harmonic stretching, phase shaping. |
|
||||
|
||||
| Parameter | Controls |
|
||||
|-----------|----------|
|
||||
| `harmonics` | Harmonic content, structure, detuning. |
|
||||
| `timbre` | Brightness, tonal color. |
|
||||
| `harmonics` | Harmonic content / structure. |
|
||||
| `timbre` | Brightness / tonal color. |
|
||||
| `morph` | Smooth transitions between variations. |
|
||||
| `partials` | Number of active harmonics (1-32). |
|
||||
|
||||
### Pitched
|
||||
## Percussion
|
||||
|
||||
Native drum synthesis with timbral morphing. All share `wave` (waveform: 0=sine, 0.5=tri, 1=saw), `morph`, `harmonics`, and `timbre` parameters.
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `modal` | Struck/plucked resonant bodies (strings, plates, tubes). |
|
||||
| `va`, `analog` | Virtual analog with waveform sync and crossfading. |
|
||||
| `ws`, `waveshape` | Waveshaper and wavefolder. |
|
||||
| `fm2` | Two-operator FM synthesis with feedback. |
|
||||
| `grain` | Granular formant oscillator (vowel-like). |
|
||||
| `additive` | Harmonic additive synthesis. |
|
||||
| `wavetable` | Built-in Plaits wavetables (four 8x8 banks). |
|
||||
| `chord` | Four-note chord generator. |
|
||||
| `swarm` | Granular cloud of enveloped sawtooths. |
|
||||
| `pnoise` | Clocked noise through multimode filter. |
|
||||
|
||||
### Percussion
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `kick`, `bass` | 808-style bass drum. |
|
||||
| `snare` | Analog snare drum with tone/noise balance. |
|
||||
| `hihat`, `hat` | Metallic 808-style hi-hat. |
|
||||
|
||||
Percussions are super hard to use correctly, because you need to tweak their envelope correctly.
|
||||
| `kick` | Bass drum. |
|
||||
| `snare` | Snare drum with tone/noise balance. |
|
||||
| `hat` | Hi-hat. |
|
||||
| `tom` | Tom drum. |
|
||||
| `rim` | Rimshot. |
|
||||
| `cowbell` | Cowbell. |
|
||||
| `cymbal` | Cymbal. |
|
||||
|
||||
@@ -32,9 +32,6 @@ Without `scan`, the sample plays normally. With `scan`, it becomes a looping wav
|
||||
|-----------|-------|-------------|
|
||||
| `scan` | 0-1 | Position in wavetable (0 = first cycle, 1 = last) |
|
||||
| `wtlen` | samples | Cycle length in samples (0 = entire sample) |
|
||||
| `scanlfo` | Hz | LFO rate for scan modulation |
|
||||
| `scandepth` | 0-1 | LFO modulation depth |
|
||||
| `scanshape` | shape | LFO waveform |
|
||||
|
||||
## Cycle Length
|
||||
|
||||
@@ -57,24 +54,16 @@ pad 0.5 scan . ( blend between middle cycles )
|
||||
pad 1 scan . ( last cycle only )
|
||||
```
|
||||
|
||||
## LFO Modulation
|
||||
## Scan Modulation
|
||||
|
||||
Automate the scan position with a built-in LFO:
|
||||
Use audio-rate modulation words to automate the scan position:
|
||||
|
||||
```forth
|
||||
pad 0 scan 2 scanlfo 0.3 scandepth . ( 2 Hz modulation, 30% depth )
|
||||
pad 0 1 2 lfo scan . ( sine LFO, full range, 2 Hz )
|
||||
pad 0 0.5 1 tlfo scan . ( triangle LFO, half range, 1 Hz )
|
||||
pad 0 1 0.5 jit scan . ( random scan every 0.5 steps )
|
||||
```
|
||||
|
||||
Available LFO shapes:
|
||||
|
||||
| Shape | Description |
|
||||
|-------|-------------|
|
||||
| `sine` | Smooth oscillation (default) |
|
||||
| `tri` | Triangle wave |
|
||||
| `saw` | Sawtooth, ramps up |
|
||||
| `square` | Alternates between extremes |
|
||||
| `sh` | Sample and hold, random steps |
|
||||
|
||||
## Creating Wavetables
|
||||
|
||||
A proper wavetable file:
|
||||
|
||||
@@ -9,7 +9,7 @@ Each bank can carry its own prelude script. Press `p` to open the current bank's
|
||||
Bank preludes make banks self-contained. When you share a bank, its prelude travels with it — recipients get all the definitions they need without merging anything into their own project.
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
|
||||
: pad sine sound 0.5 gain 2 spread 1.5 attack 0.4 verb . ;
|
||||
```
|
||||
|
||||
@@ -48,13 +48,13 @@ Only non-empty bank preludes are evaluated. Last-evaluated wins for name collisi
|
||||
The most common use of a bank prelude is to define words for your instruments. Without a prelude, every step that plays a bass has to spell out the full sound design:
|
||||
|
||||
```forth
|
||||
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
pulse sound c2 note 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width .
|
||||
```
|
||||
|
||||
In the bank prelude, define it once:
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
|
||||
```
|
||||
|
||||
Now every step just writes `c2 note bass`. Change the sound in one place, every step follows.
|
||||
@@ -63,7 +63,7 @@ Now every step just writes `c2 note bass`. Change the sound in one place, every
|
||||
|
||||
```forth
|
||||
;; instruments
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
: bass pulse sound 0.8 gain 400 8000 0.01 0.3 0.5 0.3 env lpf 0.6 width . ;
|
||||
: pad sine sound 0.5 gain 2 spread 1.5 attack 0.4 verb . ;
|
||||
: lead tri sound 0.6 gain 5000 lpf 2 decay . ;
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ Each pattern is an independent sequence of steps with its own properties:
|
||||
| Length | Steps before the pattern loops (`1`-`1024`) | `16` |
|
||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||
| Quantization | When the pattern launches | `Bar` |
|
||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||
| Follow Up | What happens when the pattern finishes an iteration | `Loop` |
|
||||
|
||||
Press `e` in the patterns view to edit these settings. After editing properties, you will have to hit the `c` key to _launch_ these changes. More about that later!
|
||||
|
||||
@@ -51,7 +51,7 @@ Cagire includes a complete synthesis and sampling engine. No external software i
|
||||
|
||||
```forth
|
||||
;; sawtooth wave + lowpass filter with envelope + chorus + reverb
|
||||
100 199 freq saw sound 250 lpf 8 lpe 1 lpd 0.2 chorus 0.8 verb 2 dur .
|
||||
100 199 freq saw sound 250 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 dur .
|
||||
```
|
||||
|
||||
```forth
|
||||
@@ -61,12 +61,12 @@ Cagire includes a complete synthesis and sampling engine. No external software i
|
||||
|
||||
```forth
|
||||
;; white noise + sine wave + envelope = percussion
|
||||
white sine sound 100 freq 0.5 decay 24 penv 0.5 pdec 2 dur .
|
||||
white sine sound 100 freq 0.5 decay 2 dur .
|
||||
```
|
||||
|
||||
```forth
|
||||
;; random robot noises: sine + randomized freq + ring modulation
|
||||
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmddepth .
|
||||
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmdepth .
|
||||
```
|
||||
|
||||
By _creating words_, registering synth definitions and effects, you will form a vocabulary that can be used to create complex sounds and music. The audio engine is quite capable, and you won't ever run out of new things to try!
|
||||
|
||||
@@ -33,7 +33,7 @@ You can also arm mute/solo changes:
|
||||
- Press `Shift+m` to clear all mutes
|
||||
- Press `Shift+x` to clear all solos
|
||||
|
||||
A pattern might not start immediately depending on the sync mode you have chosen.
|
||||
A pattern might not start immediately depending on its quantization setting.
|
||||
It might wait for the next beat/bar boundary.
|
||||
|
||||
## Status Indicators
|
||||
@@ -63,9 +63,4 @@ Launched changes don't execute immediately. They wait for a quantization boundar
|
||||
|
||||
Edit quantization in pattern properties (press `e` on a pattern).
|
||||
|
||||
## Sync Mode
|
||||
|
||||
When a pattern starts, its playback position depends on sync mode:
|
||||
|
||||
- **Reset**: Always start at step 0
|
||||
- **Phase-Lock**: Start at the current beat-aligned position (stays in sync with other patterns)
|
||||
Patterns always start at a beat-aligned position (phase-lock), staying in sync with other running patterns.
|
||||
|
||||
@@ -35,7 +35,7 @@ c4 M3 P5 note 1.5 decay sine snd .
|
||||
That builds a C major triad from scratch: C4 (60), then a major third above (64), then a perfect fifth above the root (67). Three notes on the stack, all played together.
|
||||
|
||||
```forth
|
||||
a3 m3 P5 note 1.2 decay va snd .
|
||||
a3 m3 P5 note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
A minor triad: A3, C4, E4.
|
||||
@@ -94,7 +94,7 @@ c4 maj note 1.5 decay sine snd .
|
||||
That's the same C major triad, but in one word instead of `M3 P5`. A few more:
|
||||
|
||||
```forth
|
||||
d3 min7 note 1.5 decay va snd .
|
||||
d3 min7 note 1.5 decay saw snd .
|
||||
```
|
||||
|
||||
```forth
|
||||
@@ -160,13 +160,13 @@ G3 C4 E4. The fifth drops below the root.
|
||||
`drop2` and `drop3` are jazz voicing techniques for four-note chords. `drop2` takes the second-from-top note and drops it an octave:
|
||||
|
||||
```forth
|
||||
c4 maj7 drop2 note 1.2 decay va snd .
|
||||
c4 maj7 drop2 note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
From C4 E4 G4 B4, the G drops to G3: G3 C4 E4 B4. `drop3` drops the third-from-top:
|
||||
|
||||
```forth
|
||||
c4 maj7 drop3 note 1.2 decay va snd .
|
||||
c4 maj7 drop3 note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jazz guitar and piano.
|
||||
@@ -182,7 +182,7 @@ c4 maj 3 tp note 1.5 decay sine snd .
|
||||
C major transposed up 3 semitones becomes Eb major. Works with any number of notes:
|
||||
|
||||
```forth
|
||||
c4 min7 -2 tp note 1.5 decay va snd .
|
||||
c4 min7 -2 tp note 1.5 decay saw snd .
|
||||
```
|
||||
|
||||
Shifts the whole chord down 2 semitones (Bb minor 7).
|
||||
@@ -219,7 +219,7 @@ Walk through a scale with `cycle`:
|
||||
Random notes from a scale:
|
||||
|
||||
```forth
|
||||
0 7 rand pentatonic note 0.8 decay va snd .
|
||||
0 7 rand pentatonic note 0.8 decay saw snd .
|
||||
```
|
||||
|
||||
### Setting the key
|
||||
@@ -273,7 +273,7 @@ Degree 0 of the major scale, stacked in thirds: C E G — a major triad. The sca
|
||||
`seventh` adds a fourth note:
|
||||
|
||||
```forth
|
||||
0 major seventh note 1.2 decay va snd .
|
||||
0 major seventh note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
C E G B — Cmaj7. Degree 1 gives Dm7, degree 4 gives G7 (dominant). The diatonic context determines everything.
|
||||
@@ -291,7 +291,7 @@ A I-vi-IV-V chord progression using `pcycle`:
|
||||
```forth
|
||||
( 0 major seventh ) ( 5 major seventh )
|
||||
( 3 major seventh ) ( 4 major seventh ) 4 pcycle
|
||||
note 1.2 decay va snd .
|
||||
note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
Combine with voicings for smoother voice leading:
|
||||
@@ -299,7 +299,7 @@ Combine with voicings for smoother voice leading:
|
||||
```forth
|
||||
( 0 major seventh ) ( 5 major seventh inv )
|
||||
( 3 major seventh ) ( 4 major seventh drop2 ) 4 pcycle
|
||||
note 1.5 decay va snd .
|
||||
note 1.5 decay saw snd .
|
||||
```
|
||||
|
||||
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):
|
||||
|
||||
@@ -188,7 +188,7 @@ A melodic step with weighted note selection and random timbre:
|
||||
c4 0.4 e4 0.3 g4 0.2 b4 0.1 4 wchoose note
|
||||
0.3 0.7 rand decay
|
||||
1.0 4.0 exprand harmonics
|
||||
modal snd .
|
||||
add snd .
|
||||
```
|
||||
|
||||
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.
|
||||
|
||||
@@ -22,4 +22,3 @@ Cagire is mainly developed by BuboBubo (Raphaël Maurice Forment, [raphaelformen
|
||||
### Credits
|
||||
|
||||
* **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
|
||||
* **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits (Emilie Gillet). Rust port by Oliver Rockstedt.
|
||||
|
||||
@@ -51,7 +51,7 @@ Section "Cagire (required)" SecCore
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "Publisher" "Raphael Forment"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayIcon" '"$INSTDIR\cagire-desktop.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://github.com/Bubobubobubobubo/cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://git.raphaelforment.fr/BuboBubo/cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "HelpLink" "https://cagire.raphaelforment.fr"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoRepair" 1
|
||||
|
||||
@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
|
||||
cagire-forth = { path = "../../crates/forth" }
|
||||
cagire-project = { path = "../../crates/project" }
|
||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
||||
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.13", features = ["native", "soundfont"] }
|
||||
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||
egui_ratatui = "2.1"
|
||||
|
||||
@@ -234,6 +234,7 @@ pub fn create_editor(
|
||||
// Read live snapshot from the audio thread
|
||||
let shared = editor.bridge.shared_state.load();
|
||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
||||
editor.app.playback.playing = editor.snapshot.playing;
|
||||
|
||||
// Sync host tempo into LinkState so title bar shows real tempo
|
||||
if shared.tempo > 0.0 {
|
||||
@@ -298,6 +299,11 @@ pub fn create_editor(
|
||||
let elapsed = editor.last_frame.elapsed();
|
||||
editor.last_frame = Instant::now();
|
||||
|
||||
if editor.app.playback.has_armed() {
|
||||
let rate = std::f32::consts::TAU;
|
||||
editor.app.ui.pulse_phase = (editor.app.ui.pulse_phase + elapsed.as_secs_f32() * rate) % std::f32::consts::TAU;
|
||||
}
|
||||
|
||||
let link = &editor.link;
|
||||
let app = &editor.app;
|
||||
let snapshot = &editor.snapshot;
|
||||
|
||||
@@ -219,7 +219,6 @@ impl Plugin for CagirePlugin {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
|
||||
15
plugins/nih-plug-egui/LICENSE
Normal file
15
plugins/nih-plug-egui/LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) Robbert van der Helm <mail@robbertvanderhelm.nl>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -199,7 +199,6 @@ impl App {
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
self.playback.staged_prop_changes.insert(
|
||||
@@ -210,7 +209,6 @@ impl App {
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -203,7 +203,6 @@ impl App {
|
||||
length: pat.length.to_string(),
|
||||
speed: pat.speed,
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,7 +138,6 @@ impl App {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ impl App {
|
||||
bank,
|
||||
pattern,
|
||||
quantization: staged.quantization,
|
||||
sync_mode: staged.sync_mode,
|
||||
});
|
||||
}
|
||||
PatternChange::Stop { bank, pattern } => {
|
||||
@@ -68,7 +67,6 @@ impl App {
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: pat.sync_mode,
|
||||
follow_up: pat.follow_up,
|
||||
};
|
||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
|
||||
@@ -29,7 +29,6 @@ impl App {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Stop { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui
|
||||
.set_status(format!("{} armed to stop", bp_label(bank, pattern)));
|
||||
@@ -37,7 +36,6 @@ impl App {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui
|
||||
.set_status(format!("{} armed to play", bp_label(bank, pattern)));
|
||||
@@ -84,7 +82,6 @@ impl App {
|
||||
}
|
||||
pat.speed = props.speed;
|
||||
pat.quantization = props.quantization;
|
||||
pat.sync_mode = props.sync_mode;
|
||||
pat.follow_up = props.follow_up;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
|
||||
@@ -496,6 +496,11 @@ impl eframe::App for CagireDesktop {
|
||||
let elapsed = self.last_frame.elapsed();
|
||||
self.last_frame = std::time::Instant::now();
|
||||
|
||||
if self.app.playback.has_armed() {
|
||||
let rate = std::f32::consts::TAU;
|
||||
self.app.ui.pulse_phase = (self.app.ui.pulse_phase + elapsed.as_secs_f32() * rate) % std::f32::consts::TAU;
|
||||
}
|
||||
|
||||
let link = &self.link;
|
||||
let app = &self.app;
|
||||
self.terminal
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use crate::page::Page;
|
||||
use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
|
||||
|
||||
@@ -169,7 +169,6 @@ pub enum AppCommand {
|
||||
length: Option<usize>,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
|
||||
|
||||
@@ -339,8 +339,9 @@ pub fn build_stream(
|
||||
let channels = channels as usize;
|
||||
let max_voices = config.max_voices;
|
||||
|
||||
let block_size = if config.buffer_size > 0 { config.buffer_size as usize } else { 512 };
|
||||
let mut engine =
|
||||
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics));
|
||||
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics), block_size);
|
||||
engine.sample_index = initial_samples;
|
||||
|
||||
for path in sample_paths {
|
||||
@@ -355,9 +356,6 @@ pub fn build_stream(
|
||||
|
||||
let registry = Arc::clone(&engine.sample_registry);
|
||||
|
||||
const INPUT_BUFFER_SIZE: usize = 8192;
|
||||
let (input_producer, input_consumer) = HeapRb::<f32>::new(INPUT_BUFFER_SIZE).split();
|
||||
|
||||
let input_device = config
|
||||
.input_device
|
||||
.as_ref()
|
||||
@@ -374,6 +372,12 @@ pub fn build_stream(
|
||||
.and_then(|dev| dev.default_input_config().ok())
|
||||
.map_or(0, |cfg| cfg.channels() as usize);
|
||||
|
||||
engine.input_channels = input_channels;
|
||||
|
||||
const INPUT_BUFFER_BASE: usize = 8192;
|
||||
let input_buffer_size = INPUT_BUFFER_BASE * (input_channels.max(2) / 2);
|
||||
let (input_producer, input_consumer) = HeapRb::<f32>::new(input_buffer_size).split();
|
||||
|
||||
let input_stream = input_device.and_then(|dev| {
|
||||
let input_cfg = match dev.default_input_config() {
|
||||
Ok(cfg) => cfg,
|
||||
@@ -476,47 +480,16 @@ pub fn build_stream(
|
||||
}
|
||||
}
|
||||
|
||||
// doux expects stereo interleaved live_input (CHANNELS=2)
|
||||
let stereo_len = buffer_samples * 2;
|
||||
if live_scratch.len() < stereo_len {
|
||||
live_scratch.resize(stereo_len, 0.0);
|
||||
}
|
||||
match input_channels {
|
||||
0 => {
|
||||
live_scratch[..stereo_len].fill(0.0);
|
||||
}
|
||||
1 => {
|
||||
for i in 0..buffer_samples {
|
||||
let s = input_consumer.try_pop().unwrap_or(0.0);
|
||||
live_scratch[i * 2] = s;
|
||||
live_scratch[i * 2 + 1] = s;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
for sample in &mut live_scratch[..stereo_len] {
|
||||
*sample = input_consumer.try_pop().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for i in 0..buffer_samples {
|
||||
let l = input_consumer.try_pop().unwrap_or(0.0);
|
||||
let r = input_consumer.try_pop().unwrap_or(0.0);
|
||||
for _ in 2..input_channels {
|
||||
input_consumer.try_pop();
|
||||
}
|
||||
live_scratch[i * 2] = l;
|
||||
live_scratch[i * 2 + 1] = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Discard excess if input produced more than we consumed
|
||||
let excess = input_consumer.occupied_len().saturating_sub(INPUT_BUFFER_SIZE / 2);
|
||||
for _ in 0..excess {
|
||||
input_consumer.try_pop();
|
||||
let nch_in = input_channels.max(1);
|
||||
let raw_len = buffer_samples * nch_in;
|
||||
if live_scratch.len() < raw_len {
|
||||
live_scratch.resize(raw_len, 0.0);
|
||||
}
|
||||
live_scratch[..raw_len].fill(0.0);
|
||||
input_consumer.pop_slice(&mut live_scratch[..raw_len]);
|
||||
|
||||
engine.metrics.load.set_buffer_time(buffer_time_ns);
|
||||
engine.process_block(data, &[], &live_scratch[..stereo_len]);
|
||||
engine.process_block(data, &[], &live_scratch[..raw_len]);
|
||||
scope_buffer.write(data);
|
||||
|
||||
// Feed mono mix to analysis thread via ring buffer (non-blocking)
|
||||
|
||||
@@ -81,6 +81,20 @@ impl LinkState {
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn start_playing(&self, beat: f64, time: i64, quantum: f64) {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
state.set_is_playing_and_request_beat_at_time(true, time, beat, quantum);
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn stop_playing(&self, time: i64) {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
state.set_is_playing(false, time);
|
||||
self.link.commit_app_session_state(&state);
|
||||
}
|
||||
|
||||
pub fn capture_app_state(&self) -> SessionState {
|
||||
let mut state = SessionState::new();
|
||||
self.link.capture_app_session_state(&mut state);
|
||||
|
||||
@@ -5,7 +5,7 @@ pub mod realtime;
|
||||
pub mod sequencer;
|
||||
mod timing;
|
||||
|
||||
pub use timing::{substeps_in_window, StepTiming, SyncTime};
|
||||
pub use timing::{next_boundary, substeps_in_window, SyncTime};
|
||||
|
||||
pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer};
|
||||
|
||||
|
||||
@@ -14,11 +14,17 @@ use std::thread::{self, JoinHandle};
|
||||
|
||||
use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
|
||||
use super::realtime::set_realtime_priority;
|
||||
use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
|
||||
use super::{next_boundary, substeps_in_window, LinkState, SyncTime};
|
||||
use crate::model::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
|
||||
};
|
||||
use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::model::{FollowUp, LaunchQuantization, MAX_BANKS, MAX_PATTERNS};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SyncMode {
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
use crate::state::LiveKeyState;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -114,7 +120,6 @@ pub enum SeqCommand {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
},
|
||||
PatternStop {
|
||||
bank: usize,
|
||||
@@ -141,7 +146,6 @@ pub struct PatternSnapshot {
|
||||
pub speed: crate::model::PatternSpeed,
|
||||
pub length: usize,
|
||||
pub steps: Vec<StepSnapshot>,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
@@ -170,6 +174,7 @@ pub struct SharedSequencerState {
|
||||
pub event_count: usize,
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
pub playing: bool,
|
||||
pub script_trace: Option<ExecutionTrace>,
|
||||
pub print_output: Option<String>,
|
||||
}
|
||||
@@ -180,6 +185,7 @@ pub struct SequencerSnapshot {
|
||||
pub event_count: usize,
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
pub playing: bool,
|
||||
script_trace: Option<ExecutionTrace>,
|
||||
pub print_output: Option<String>,
|
||||
}
|
||||
@@ -192,6 +198,7 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||
event_count: s.event_count,
|
||||
tempo: s.tempo,
|
||||
beat: s.beat,
|
||||
playing: s.playing,
|
||||
script_trace: s.script_trace.clone(),
|
||||
print_output: s.print_output.clone(),
|
||||
}
|
||||
@@ -207,6 +214,7 @@ impl SequencerSnapshot {
|
||||
event_count: 0,
|
||||
tempo: 0.0,
|
||||
beat: 0.0,
|
||||
playing: false,
|
||||
script_trace: None,
|
||||
print_output: None,
|
||||
}
|
||||
@@ -295,17 +303,24 @@ struct ActivePattern {
|
||||
step_index: usize,
|
||||
iter: usize,
|
||||
last_step_beat: f64,
|
||||
origin_beat: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PendingPattern {
|
||||
id: PatternId,
|
||||
quantization: LaunchQuantization,
|
||||
target_beat: Option<f64>,
|
||||
sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum PlayState {
|
||||
Idle { pause_beat: Option<f64> },
|
||||
Playing { frontier: f64 },
|
||||
}
|
||||
|
||||
struct AudioState {
|
||||
prev_beat: f64,
|
||||
play_state: PlayState,
|
||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||
pending_starts: Vec<PendingPattern>,
|
||||
pending_stops: Vec<PendingPattern>,
|
||||
@@ -315,7 +330,7 @@ struct AudioState {
|
||||
impl AudioState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
prev_beat: -1.0,
|
||||
play_state: PlayState::Idle { pause_beat: None },
|
||||
active_patterns: HashMap::new(),
|
||||
pending_starts: Vec::new(),
|
||||
pending_stops: Vec::new(),
|
||||
@@ -468,22 +483,6 @@ impl PatternSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_quantization_boundary(
|
||||
quantization: LaunchQuantization,
|
||||
beat: f64,
|
||||
prev_beat: f64,
|
||||
quantum: f64,
|
||||
) -> bool {
|
||||
match quantization {
|
||||
LaunchQuantization::Immediate => prev_beat >= 0.0,
|
||||
LaunchQuantization::Beat => StepTiming::NextBeat.crossed(prev_beat, beat, quantum),
|
||||
LaunchQuantization::Bar => StepTiming::NextBar.crossed(prev_beat, beat, quantum),
|
||||
LaunchQuantization::Bars2 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 2.0),
|
||||
LaunchQuantization::Bars4 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 4.0),
|
||||
LaunchQuantization::Bars8 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 8.0),
|
||||
}
|
||||
}
|
||||
|
||||
type StepKey = (usize, usize, usize);
|
||||
|
||||
struct RunsCounter {
|
||||
@@ -572,10 +571,11 @@ pub struct SequencerState {
|
||||
soloed: std::collections::HashSet<(usize, usize)>,
|
||||
last_tempo: f64,
|
||||
last_beat: f64,
|
||||
last_playing: bool,
|
||||
script_text: String,
|
||||
script_speed: crate::model::PatternSpeed,
|
||||
script_length: usize,
|
||||
script_frontier: f64,
|
||||
script_frontier: Option<f64>,
|
||||
script_step: usize,
|
||||
script_trace: Option<ExecutionTrace>,
|
||||
print_output: Option<String>,
|
||||
@@ -610,10 +610,11 @@ impl SequencerState {
|
||||
soloed: std::collections::HashSet::new(),
|
||||
last_tempo: 0.0,
|
||||
last_beat: 0.0,
|
||||
last_playing: false,
|
||||
script_text: String::new(),
|
||||
script_speed: crate::model::PatternSpeed::default(),
|
||||
script_length: 16,
|
||||
script_frontier: -1.0,
|
||||
script_frontier: None,
|
||||
script_step: 0,
|
||||
script_trace: None,
|
||||
print_output: None,
|
||||
@@ -631,7 +632,7 @@ impl SequencerState {
|
||||
false
|
||||
}
|
||||
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>) {
|
||||
fn process_commands(&mut self, commands: Vec<SeqCommand>, quantum: f64) {
|
||||
for cmd in commands {
|
||||
match cmd {
|
||||
SeqCommand::PatternUpdate {
|
||||
@@ -652,15 +653,15 @@ impl SequencerState {
|
||||
bank,
|
||||
pattern,
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
let id = PatternId { bank, pattern };
|
||||
self.audio_state.pending_stops.retain(|p| p.id != id);
|
||||
if !self.audio_state.pending_starts.iter().any(|p| p.id == id) {
|
||||
let target_beat = next_boundary(self.last_beat, quantization, quantum);
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id,
|
||||
quantization,
|
||||
sync_mode,
|
||||
target_beat,
|
||||
sync_mode: SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -672,9 +673,10 @@ impl SequencerState {
|
||||
let id = PatternId { bank, pattern };
|
||||
self.audio_state.pending_starts.retain(|p| p.id != id);
|
||||
if !self.audio_state.pending_stops.iter().any(|p| p.id == id) {
|
||||
let target_beat = next_boundary(self.last_beat, quantization, quantum);
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id,
|
||||
quantization,
|
||||
target_beat,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
@@ -714,6 +716,8 @@ impl SequencerState {
|
||||
self.audio_state.active_patterns.clear();
|
||||
self.audio_state.pending_starts.clear();
|
||||
self.audio_state.pending_stops.clear();
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat: None };
|
||||
self.script_frontier = None;
|
||||
self.step_traces = Arc::new(HashMap::new());
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
@@ -723,8 +727,8 @@ impl SequencerState {
|
||||
active.step_index = 0;
|
||||
active.iter = 0;
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.script_frontier = -1.0;
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat: None };
|
||||
self.script_frontier = None;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
@@ -748,23 +752,27 @@ impl SequencerState {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, input: TickInput) -> TickOutput {
|
||||
self.process_commands(input.commands);
|
||||
self.last_tempo = input.tempo;
|
||||
self.last_beat = input.beat;
|
||||
self.last_playing = input.playing;
|
||||
self.process_commands(input.commands, input.quantum);
|
||||
|
||||
if !input.playing {
|
||||
return self.tick_paused();
|
||||
}
|
||||
|
||||
let frontier = self.audio_state.prev_beat;
|
||||
let (frontier, resuming) = match self.audio_state.play_state {
|
||||
PlayState::Playing { frontier } => (frontier, false),
|
||||
PlayState::Idle { pause_beat } => (pause_beat.unwrap_or(input.beat), true),
|
||||
};
|
||||
let lookahead_end = input.lookahead_end;
|
||||
|
||||
if frontier < 0.0 {
|
||||
self.realign_phaselock_patterns(lookahead_end);
|
||||
}
|
||||
self.activate_pending(frontier, lookahead_end);
|
||||
self.deactivate_pending(frontier, lookahead_end);
|
||||
|
||||
self.activate_pending(lookahead_end, frontier, input.quantum);
|
||||
self.deactivate_pending(lookahead_end, frontier, input.quantum);
|
||||
if resuming {
|
||||
self.reset_origins_on_resume(lookahead_end);
|
||||
}
|
||||
|
||||
let steps = self.execute_steps(
|
||||
input.beat,
|
||||
@@ -800,7 +808,7 @@ impl SequencerState {
|
||||
let new_tempo = self.read_tempo_variable(steps.any_step_fired);
|
||||
self.apply_follow_ups();
|
||||
|
||||
self.audio_state.prev_beat = lookahead_end;
|
||||
self.audio_state.play_state = PlayState::Playing { frontier: lookahead_end };
|
||||
|
||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||
TickOutput {
|
||||
@@ -822,8 +830,12 @@ impl SequencerState {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
}
|
||||
}
|
||||
self.audio_state.prev_beat = -1.0;
|
||||
self.script_frontier = -1.0;
|
||||
let pause_beat = match self.audio_state.play_state {
|
||||
PlayState::Playing { frontier } => Some(frontier),
|
||||
PlayState::Idle { pause_beat } => pause_beat,
|
||||
};
|
||||
self.audio_state.play_state = PlayState::Idle { pause_beat };
|
||||
self.script_frontier = None;
|
||||
self.script_step = 0;
|
||||
self.script_trace = None;
|
||||
self.print_output = None;
|
||||
@@ -837,35 +849,46 @@ impl SequencerState {
|
||||
}
|
||||
}
|
||||
|
||||
fn realign_phaselock_patterns(&mut self, beat: f64) {
|
||||
for (id, active) in &mut self.audio_state.active_patterns {
|
||||
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
|
||||
continue;
|
||||
};
|
||||
if pattern.sync_mode != SyncMode::PhaseLock {
|
||||
continue;
|
||||
}
|
||||
let speed_mult = pattern.speed.multiplier();
|
||||
let subs_per_beat = 4.0 * speed_mult;
|
||||
let step = (beat * subs_per_beat).floor() as usize + 1;
|
||||
active.step_index = step % pattern.length;
|
||||
fn pause_beat(&self) -> Option<f64> {
|
||||
match self.audio_state.play_state {
|
||||
PlayState::Idle { pause_beat } => pause_beat,
|
||||
PlayState::Playing { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||
fn reset_origins_on_resume(&mut self, lookahead_end: f64) {
|
||||
for (id, active) in &mut self.audio_state.active_patterns {
|
||||
active.origin_beat = lookahead_end;
|
||||
let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
|
||||
continue;
|
||||
};
|
||||
let subs_per_beat = 4.0 * pattern.speed.multiplier();
|
||||
let step = (lookahead_end * subs_per_beat).floor() as usize + 1;
|
||||
active.step_index = step % pattern.length;
|
||||
}
|
||||
self.script_frontier = Some(lookahead_end);
|
||||
}
|
||||
|
||||
fn activate_pending(&mut self, frontier: f64, lookahead_end: f64) {
|
||||
self.buf_activated.clear();
|
||||
for pending in &self.audio_state.pending_starts {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
let should_activate = match pending.target_beat {
|
||||
None => true,
|
||||
Some(target) => target <= lookahead_end,
|
||||
};
|
||||
if should_activate {
|
||||
let origin_beat = match pending.target_beat {
|
||||
Some(t) if t > frontier => t - 1e-9,
|
||||
_ => frontier,
|
||||
};
|
||||
let start_step = match pending.sync_mode {
|
||||
SyncMode::Reset => 0,
|
||||
SyncMode::PhaseLock => {
|
||||
if let Some(pat) =
|
||||
self.pattern_cache.get(pending.id.bank, pending.id.pattern)
|
||||
{
|
||||
let speed_mult = pat.speed.multiplier();
|
||||
let subs_per_beat = 4.0 * speed_mult;
|
||||
let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1;
|
||||
first_sub % pat.length
|
||||
let subs_per_beat = 4.0 * pat.speed.multiplier();
|
||||
(origin_beat * subs_per_beat).floor() as usize % pat.length
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -880,7 +903,8 @@ impl SequencerState {
|
||||
pattern: pending.id.pattern,
|
||||
step_index: start_step,
|
||||
iter: 0,
|
||||
last_step_beat: beat,
|
||||
last_step_beat: lookahead_end,
|
||||
origin_beat,
|
||||
},
|
||||
);
|
||||
self.buf_activated.push(pending.id);
|
||||
@@ -892,15 +916,18 @@ impl SequencerState {
|
||||
.retain(|p| !activated.contains(&p.id));
|
||||
}
|
||||
|
||||
fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||
fn deactivate_pending(&mut self, _frontier: f64, lookahead_end: f64) {
|
||||
self.buf_stopped.clear();
|
||||
for pending in &self.audio_state.pending_stops {
|
||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||
let should_deactivate = match pending.target_beat {
|
||||
None => true,
|
||||
Some(target) => target <= lookahead_end,
|
||||
};
|
||||
if should_deactivate {
|
||||
self.audio_state.active_patterns.remove(&pending.id);
|
||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||
bank != pending.id.bank || pattern != pending.id.pattern
|
||||
});
|
||||
// Flush pending update so cache stays current for future launches
|
||||
let key = (pending.id.bank, pending.id.pattern);
|
||||
if let Some(snapshot) = self.pending_updates.remove(&key) {
|
||||
self.pattern_cache.set(key.0, key.1, snapshot);
|
||||
@@ -960,7 +987,8 @@ impl SequencerState {
|
||||
.copied()
|
||||
.unwrap_or_else(|| pattern.speed.multiplier());
|
||||
|
||||
let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult);
|
||||
let pattern_frontier = frontier.max(active.origin_beat);
|
||||
let step_beats = substeps_in_window(pattern_frontier, lookahead_end, speed_mult);
|
||||
|
||||
for step_beat in step_beats {
|
||||
result.any_step_fired = true;
|
||||
@@ -1095,11 +1123,7 @@ impl SequencerState {
|
||||
return;
|
||||
}
|
||||
|
||||
let script_frontier = if self.script_frontier < 0.0 {
|
||||
frontier
|
||||
} else {
|
||||
self.script_frontier
|
||||
};
|
||||
let script_frontier = self.script_frontier.unwrap_or(frontier);
|
||||
|
||||
let speed_mult = self.script_speed.multiplier();
|
||||
let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult);
|
||||
@@ -1166,7 +1190,7 @@ impl SequencerState {
|
||||
self.script_step += 1;
|
||||
}
|
||||
|
||||
self.script_frontier = lookahead_end;
|
||||
self.script_frontier = Some(lookahead_end);
|
||||
}
|
||||
|
||||
fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
|
||||
@@ -1199,21 +1223,21 @@ impl SequencerState {
|
||||
FollowUp::Stop => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
FollowUp::Chain { bank, pattern } => {
|
||||
self.audio_state.pending_stops.push(PendingPattern {
|
||||
id: *completed_id,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
let target = PatternId { bank, pattern };
|
||||
if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
|
||||
self.audio_state.pending_starts.push(PendingPattern {
|
||||
id: target,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
target_beat: None,
|
||||
sync_mode: SyncMode::Reset,
|
||||
});
|
||||
}
|
||||
@@ -1240,6 +1264,7 @@ impl SequencerState {
|
||||
event_count: self.event_count,
|
||||
tempo: self.last_tempo,
|
||||
beat: self.last_beat,
|
||||
playing: self.last_playing,
|
||||
script_trace: self.script_trace.clone(),
|
||||
print_output: self.print_output.clone(),
|
||||
}
|
||||
@@ -1303,9 +1328,19 @@ fn sequencer_loop(
|
||||
|
||||
let state = link.capture_app_state();
|
||||
let current_time_us = link.clock_micros() as SyncTime;
|
||||
let beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
let mut beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
let tempo = state.tempo();
|
||||
|
||||
let is_playing = playing.load(Ordering::Relaxed);
|
||||
if is_playing && !seq_state.last_playing {
|
||||
let anchor_beat = seq_state.pause_beat().unwrap_or(0.0);
|
||||
link.start_playing(anchor_beat, current_time_us as i64, quantum);
|
||||
let state = link.capture_app_state();
|
||||
beat = state.beat_at_time(current_time_us as i64, quantum);
|
||||
} else if !is_playing && seq_state.last_playing {
|
||||
link.stop_playing(current_time_us as i64);
|
||||
}
|
||||
|
||||
let lookahead_beats = if tempo > 0.0 {
|
||||
lookahead_secs * tempo / 60.0
|
||||
} else {
|
||||
@@ -1317,7 +1352,7 @@ fn sequencer_loop(
|
||||
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing: playing.load(Ordering::Relaxed),
|
||||
playing: is_playing,
|
||||
beat,
|
||||
lookahead_end,
|
||||
tempo,
|
||||
@@ -1528,7 +1563,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -1599,7 +1633,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
1.0,
|
||||
@@ -1627,7 +1660,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
@@ -1675,7 +1707,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1684,43 +1715,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quantization_boundaries() {
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Immediate,
|
||||
1.5,
|
||||
1.0,
|
||||
4.0
|
||||
));
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Beat,
|
||||
2.0,
|
||||
1.9,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Beat,
|
||||
1.5,
|
||||
1.2,
|
||||
4.0
|
||||
));
|
||||
assert!(check_quantization_boundary(
|
||||
LaunchQuantization::Bar,
|
||||
4.0,
|
||||
3.9,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Bar,
|
||||
3.5,
|
||||
3.2,
|
||||
4.0
|
||||
));
|
||||
assert!(!check_quantization_boundary(
|
||||
LaunchQuantization::Immediate,
|
||||
1.0,
|
||||
-1.0,
|
||||
4.0
|
||||
));
|
||||
fn test_next_boundary() {
|
||||
use super::super::next_boundary;
|
||||
|
||||
// Immediate returns None
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
|
||||
|
||||
// Beat: next integer beat
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
assert_eq!(next_boundary(1.9, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
// On exact beat boundary, targets next beat
|
||||
assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
|
||||
|
||||
// Bar (quantum=4): next multiple of 4
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
assert_eq!(next_boundary(3.9, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
// On exact bar boundary, targets next bar
|
||||
assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
|
||||
|
||||
// Bars2 (quantum=4): next multiple of 8
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
|
||||
|
||||
// Bars4 (quantum=4): next multiple of 16
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
|
||||
|
||||
// Bars8 (quantum=4): next multiple of 32
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1741,7 +1761,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1786,7 +1805,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1822,7 +1840,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStop {
|
||||
bank: 0,
|
||||
@@ -1857,13 +1874,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Beat,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
@@ -1899,7 +1914,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -1968,17 +1982,14 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
..tick_at(1.0, false)
|
||||
});
|
||||
|
||||
assert!(!state.audio_state.pending_starts.is_empty());
|
||||
|
||||
// Resume playing — first tick resets prev_beat from -1 to 2.0
|
||||
// Resume playing — Immediate fires on first tick
|
||||
state.tick(tick_at(2.0, true));
|
||||
// Second tick: prev_beat is now >= 0, so Immediate fires
|
||||
state.tick(tick_at(2.25, true));
|
||||
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
}
|
||||
|
||||
@@ -1999,7 +2010,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
@@ -2029,13 +2039,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
@@ -2068,7 +2076,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -2095,7 +2102,6 @@ mod tests {
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
sync_mode: SyncMode::Reset,
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
@@ -2118,7 +2124,6 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
}],
|
||||
0.5,
|
||||
));
|
||||
@@ -2166,13 +2171,11 @@ mod tests {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
sync_mode: SyncMode::Reset,
|
||||
},
|
||||
],
|
||||
0.5,
|
||||
@@ -2187,4 +2190,215 @@ mod tests {
|
||||
// Should have commands from both patterns (2 patterns * 1 command each)
|
||||
assert!(output.audio_commands.len() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_boundary_crossed_during_pause() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue Bar-quantized start at beat 3.5 (before bar at beat 4.0)
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
|
||||
// Pause — saves prev_beat=3.5 as pause_beat
|
||||
state.tick(tick_at(3.75, false));
|
||||
|
||||
// Resume after bar boundary (beat 4.0 crossed)
|
||||
state.tick(tick_at(4.5, true));
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"Bar-quantized pattern should activate on resume after bar boundary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_immediate_activates_on_first_resume_tick() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Pause
|
||||
state.tick(tick_at(1.0, false));
|
||||
|
||||
// Queue Immediate start while paused
|
||||
state.tick(TickInput {
|
||||
commands: vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Immediate,
|
||||
}],
|
||||
..tick_at(2.0, false)
|
||||
});
|
||||
|
||||
// Resume — Immediate should fire on this single tick
|
||||
state.tick(tick_at(3.0, true));
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"Immediate should activate on first resume tick"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_patterns_sync_after_pause() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
},
|
||||
SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
data: simple_pattern(8),
|
||||
},
|
||||
],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue two Bar-quantized starts
|
||||
state.tick(tick_with(
|
||||
vec![
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
},
|
||||
SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 1,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
},
|
||||
],
|
||||
3.5,
|
||||
));
|
||||
|
||||
// Pause before bar
|
||||
state.tick(tick_at(3.75, false));
|
||||
|
||||
// Resume after bar boundary
|
||||
state.tick(tick_at(4.5, true));
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"First pattern should activate"
|
||||
);
|
||||
assert!(
|
||||
state.audio_state.active_patterns.contains_key(&pid(0, 1)),
|
||||
"Second pattern should activate together"
|
||||
);
|
||||
}
|
||||
|
||||
fn phaselock_pattern(length: usize) -> PatternSnapshot {
|
||||
PatternSnapshot {
|
||||
speed: Default::default(),
|
||||
length,
|
||||
steps: (0..length)
|
||||
.map(|_| StepSnapshot {
|
||||
active: true,
|
||||
script: "test".into(),
|
||||
source: None,
|
||||
})
|
||||
.collect(),
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_phaselock_position_correct_after_resume() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: phaselock_pattern(16),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue PhaseLock pattern
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
}],
|
||||
3.5,
|
||||
));
|
||||
|
||||
// Pause before bar
|
||||
state.tick(tick_at(3.75, false));
|
||||
|
||||
// Resume at beat 5.0 (after bar boundary at 4.0)
|
||||
let resume_beat = 5.0;
|
||||
state.tick(tick_at(resume_beat, true));
|
||||
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||
|
||||
// realign_phaselock_patterns uses: (beat * 4.0).floor() + 1 % length
|
||||
// At beat 5.0: (5.0 * 4.0).floor() = 20, +1 = 21, 21 % 16 = 5
|
||||
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
|
||||
let expected = ((resume_beat * 4.0).floor() as usize + 1) % 16;
|
||||
assert_eq!(
|
||||
ap.step_index, expected,
|
||||
"PhaseLock step should be based on resume beat, not pause beat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_false_boundary_after_pause_within_same_bar() {
|
||||
let mut state = make_state();
|
||||
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternUpdate {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
data: simple_pattern(4),
|
||||
}],
|
||||
0.0,
|
||||
));
|
||||
|
||||
// Queue Bar-quantized start at beat 1.0
|
||||
state.tick(tick_with(
|
||||
vec![SeqCommand::PatternStart {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
quantization: LaunchQuantization::Bar,
|
||||
}],
|
||||
1.0,
|
||||
));
|
||||
|
||||
// Pause at beat 1.5 (within same bar)
|
||||
state.tick(tick_at(1.5, false));
|
||||
|
||||
// Resume at beat 2.5 (still within same bar, quantum=4)
|
||||
state.tick(tick_at(2.5, true));
|
||||
assert!(
|
||||
!state.audio_state.active_patterns.contains_key(&pid(0, 0)),
|
||||
"Bar-quantized pattern should NOT activate when no bar boundary was crossed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
use crate::model::LaunchQuantization;
|
||||
|
||||
/// Microsecond-precision timestamp for audio synchronization.
|
||||
pub type SyncTime = u64;
|
||||
|
||||
/// Timing boundary types for step and pattern scheduling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepTiming {
|
||||
/// Fire when a beat boundary is crossed.
|
||||
NextBeat,
|
||||
/// Fire when a bar/quantum boundary is crossed.
|
||||
NextBar,
|
||||
}
|
||||
|
||||
impl StepTiming {
|
||||
/// Returns true if the boundary was crossed between prev_beat and curr_beat.
|
||||
pub fn crossed(&self, prev_beat: f64, curr_beat: f64, quantum: f64) -> bool {
|
||||
if prev_beat < 0.0 {
|
||||
return false;
|
||||
/// Compute the exact next quantization boundary beat after `current_beat`.
|
||||
/// Returns `None` for Immediate (activate now), `Some(beat)` for all others.
|
||||
pub fn next_boundary(current_beat: f64, quantization: LaunchQuantization, quantum: f64) -> Option<f64> {
|
||||
match quantization {
|
||||
LaunchQuantization::Immediate => None,
|
||||
LaunchQuantization::Beat => Some(current_beat.floor() + 1.0),
|
||||
LaunchQuantization::Bar => {
|
||||
Some((current_beat / quantum).floor() * quantum + quantum)
|
||||
}
|
||||
match self {
|
||||
Self::NextBeat => prev_beat.floor() as i64 != curr_beat.floor() as i64,
|
||||
Self::NextBar => {
|
||||
(prev_beat / quantum).floor() as i64 != (curr_beat / quantum).floor() as i64
|
||||
}
|
||||
LaunchQuantization::Bars2 => {
|
||||
let p = quantum * 2.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
LaunchQuantization::Bars4 => {
|
||||
let p = quantum * 4.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
LaunchQuantization::Bars8 => {
|
||||
let p = quantum * 8.0;
|
||||
Some((current_beat / p).floor() * p + p)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +31,7 @@ impl StepTiming {
|
||||
/// Each entry is the exact beat at which that substep fires.
|
||||
/// Clamped to 64 results max to prevent runaway.
|
||||
pub fn substeps_in_window(frontier: f64, end: f64, speed: f64) -> Vec<f64> {
|
||||
if frontier < 0.0 || end <= frontier || speed <= 0.0 {
|
||||
if end <= frontier || speed <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let substeps_per_beat = 4.0 * speed;
|
||||
@@ -55,9 +57,6 @@ mod tests {
|
||||
}
|
||||
|
||||
fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
|
||||
if prev_beat < 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let prev_substep = (prev_beat * 4.0 * speed).floor() as i64;
|
||||
let curr_substep = (curr_beat * 4.0 * speed).floor() as i64;
|
||||
(curr_substep - prev_substep).clamp(0, 16) as usize
|
||||
@@ -88,23 +87,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_beat_crossed() {
|
||||
// Crossing from beat 0 to beat 1
|
||||
assert!(StepTiming::NextBeat.crossed(0.9, 1.1, 4.0));
|
||||
// Not crossing (both in same beat)
|
||||
assert!(!StepTiming::NextBeat.crossed(0.5, 0.9, 4.0));
|
||||
// Negative prev_beat returns false
|
||||
assert!(!StepTiming::NextBeat.crossed(-1.0, 1.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_step_timing_bar_crossed() {
|
||||
// Crossing from bar 0 to bar 1 (quantum=4)
|
||||
assert!(StepTiming::NextBar.crossed(3.9, 4.1, 4.0));
|
||||
// Not crossing (both in same bar)
|
||||
assert!(!StepTiming::NextBar.crossed(2.0, 3.0, 4.0));
|
||||
// Crossing with different quantum
|
||||
assert!(StepTiming::NextBar.crossed(7.9, 8.1, 8.0));
|
||||
fn test_next_boundary() {
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Immediate, 4.0), None);
|
||||
assert_eq!(next_boundary(1.5, LaunchQuantization::Beat, 4.0), Some(2.0));
|
||||
assert_eq!(next_boundary(2.0, LaunchQuantization::Beat, 4.0), Some(3.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bar, 4.0), Some(4.0));
|
||||
assert_eq!(next_boundary(4.0, LaunchQuantization::Bar, 4.0), Some(8.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars2, 4.0), Some(8.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars4, 4.0), Some(16.0));
|
||||
assert_eq!(next_boundary(3.5, LaunchQuantization::Bars8, 4.0), Some(32.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,9 +117,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_crossed_negative_prev() {
|
||||
// Negative prev_beat returns 0
|
||||
assert_eq!(substeps_crossed(-1.0, 0.5, 1.0), 0);
|
||||
fn test_substeps_crossed_same_position() {
|
||||
// Same position returns 0
|
||||
assert_eq!(substeps_crossed(0.5, 0.5, 1.0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -205,8 +196,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_substeps_in_window_negative_frontier() {
|
||||
let result = substeps_in_window(-1.0, 0.5, 1.0);
|
||||
fn test_substeps_in_window_reversed() {
|
||||
// end <= frontier returns empty
|
||||
let result = substeps_in_window(0.5, 0.3, 1.0);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
for (bank, pattern) in playing {
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
quantization: model::LaunchQuantization::Bar,
|
||||
});
|
||||
}
|
||||
app.ui.set_status(format!("Demo: {}", demo.name));
|
||||
@@ -96,8 +95,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::PhaseLock,
|
||||
quantization: model::LaunchQuantization::Bar,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -461,7 +461,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
@@ -472,7 +471,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Left => match field {
|
||||
PatternPropsField::Speed => *speed = speed.prev(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(),
|
||||
PatternPropsField::ChainBank => {
|
||||
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||
@@ -489,7 +487,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Right => match field {
|
||||
PatternPropsField::Speed => *speed = speed.next(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(),
|
||||
PatternPropsField::ChainBank => {
|
||||
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||
@@ -535,7 +532,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
let length_val = length.parse().ok();
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
let follow_up_val = *follow_up;
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
@@ -545,7 +541,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
length: length_val,
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
sync_mode: sync_val,
|
||||
follow_up: follow_up_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
|
||||
@@ -11,7 +11,7 @@ pub use cagire_forth::{
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, load_str, save, share, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed,
|
||||
Project, SyncMode, MAX_BANKS, MAX_PATTERNS,
|
||||
Project, MAX_BANKS, MAX_PATTERNS,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ pub enum PatternPropsField {
|
||||
Length,
|
||||
Speed,
|
||||
Quantization,
|
||||
SyncMode,
|
||||
FollowUp,
|
||||
ChainBank,
|
||||
ChainPattern,
|
||||
@@ -44,8 +43,7 @@ impl PatternPropsField {
|
||||
Self::Description => Self::Length,
|
||||
Self::Length => Self::Speed,
|
||||
Self::Speed => Self::Quantization,
|
||||
Self::Quantization => Self::SyncMode,
|
||||
Self::SyncMode => Self::FollowUp,
|
||||
Self::Quantization => Self::FollowUp,
|
||||
Self::FollowUp if follow_up_is_chain => Self::ChainBank,
|
||||
Self::FollowUp => Self::FollowUp,
|
||||
Self::ChainBank => Self::ChainPattern,
|
||||
@@ -60,8 +58,7 @@ impl PatternPropsField {
|
||||
Self::Length => Self::Description,
|
||||
Self::Speed => Self::Length,
|
||||
Self::Quantization => Self::Speed,
|
||||
Self::SyncMode => Self::Quantization,
|
||||
Self::FollowUp => Self::SyncMode,
|
||||
Self::FollowUp => Self::Quantization,
|
||||
Self::ChainBank => Self::FollowUp,
|
||||
Self::ChainPattern if follow_up_is_chain => Self::ChainBank,
|
||||
Self::ChainPattern => Self::FollowUp,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField, ScriptField};
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
|
||||
@@ -85,7 +85,6 @@ pub enum Modal {
|
||||
length: String,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
sync_mode: SyncMode,
|
||||
follow_up: FollowUp,
|
||||
},
|
||||
KeybindingsHelp {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use crate::engine::PatternChange;
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StagedChange {
|
||||
pub change: PatternChange,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -21,7 +20,6 @@ pub struct StagedPropChange {
|
||||
pub length: Option<usize>,
|
||||
pub speed: PatternSpeed,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
|
||||
@@ -507,11 +507,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
};
|
||||
let props_indicator = if has_staged_props { "~" } else { "" };
|
||||
let quant_sync = if is_selected {
|
||||
format!(
|
||||
"{}:{} ",
|
||||
pattern.quantization.short_label(),
|
||||
pattern.sync_mode.short_label()
|
||||
)
|
||||
format!("{} ", pattern.quantization.short_label())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -755,8 +751,6 @@ fn render_properties(
|
||||
let steps_label = format!("{}/{}", content_count, pattern.length);
|
||||
let speed_label = pattern.speed.label();
|
||||
let quant_label = pattern.quantization.label();
|
||||
let sync_label = pattern.sync_mode.label();
|
||||
|
||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||
let value_style = Style::new().fg(theme.ui.text_primary);
|
||||
|
||||
@@ -781,10 +775,6 @@ fn render_properties(
|
||||
Span::styled(" Quant ", label_style),
|
||||
Span::styled(quant_label, value_style),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Sync ", label_style),
|
||||
Span::styled(sync_label, value_style),
|
||||
]),
|
||||
];
|
||||
|
||||
if pattern.follow_up != FollowUp::Loop {
|
||||
|
||||
@@ -293,15 +293,14 @@ fn render_header(
|
||||
|
||||
let pad = Padding::vertical(1);
|
||||
|
||||
let [logo_area, transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] =
|
||||
let [logo_area, transport_area, tempo_area, bank_area, pattern_area, stats_area] =
|
||||
Layout::horizontal([
|
||||
Constraint::Length(5),
|
||||
Constraint::Min(12),
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(14),
|
||||
Constraint::Min(20),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(2),
|
||||
Constraint::Min(20),
|
||||
Constraint::Min(24),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
@@ -317,43 +316,76 @@ fn render_header(
|
||||
logo_area,
|
||||
);
|
||||
|
||||
// Transport block
|
||||
let (transport_bg, transport_text) = if app.playback.playing {
|
||||
// Transport block (with fill indicator)
|
||||
let fill = app.live_keys.fill();
|
||||
let (transport_bg, transport_label) = if app.playback.playing {
|
||||
(theme.status.playing_bg, " ▶ PLAYING ")
|
||||
} else {
|
||||
(theme.status.stopped_bg, " ■ STOPPED ")
|
||||
};
|
||||
let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary);
|
||||
let fill_span = if fill {
|
||||
Span::styled("F", Style::new().fg(theme.status.fill_on).bg(transport_bg))
|
||||
} else {
|
||||
Span::styled(" ", Style::new().bg(transport_bg))
|
||||
};
|
||||
let transport_line = Line::from(vec![
|
||||
Span::styled(transport_label, Style::new().fg(theme.ui.text_primary).bg(transport_bg)),
|
||||
fill_span,
|
||||
Span::styled(" ", Style::new().bg(transport_bg)),
|
||||
]);
|
||||
frame.render_widget(
|
||||
Paragraph::new(transport_text)
|
||||
.block(Block::default().padding(pad).style(transport_style))
|
||||
Paragraph::new(transport_line)
|
||||
.block(Block::default().padding(pad).style(Style::new().bg(transport_bg)))
|
||||
.alignment(Alignment::Center),
|
||||
transport_area,
|
||||
);
|
||||
|
||||
// Fill indicator
|
||||
let fill = app.live_keys.fill();
|
||||
let fill_fg = if fill {
|
||||
theme.status.fill_on
|
||||
} else {
|
||||
theme.status.fill_off
|
||||
};
|
||||
let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
|
||||
// Tempo + bar:beat position block (beat segments as background fills)
|
||||
let tempo_bg = theme.header.tempo_bg;
|
||||
let tempo_fg = theme.ui.text_primary;
|
||||
let quantum = link.quantum();
|
||||
let quantum_int = quantum.max(1.0) as usize;
|
||||
|
||||
// Base background
|
||||
frame.render_widget(
|
||||
Paragraph::new(if fill { "F" } else { "·" })
|
||||
.block(Block::default().padding(pad).style(fill_style))
|
||||
.alignment(Alignment::Center),
|
||||
live_area,
|
||||
Block::default().style(Style::new().bg(tempo_bg)),
|
||||
tempo_area,
|
||||
);
|
||||
|
||||
// Tempo block
|
||||
let tempo_style = Style::new()
|
||||
.bg(theme.header.tempo_bg)
|
||||
.fg(theme.ui.text_primary)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
// Beat segment highlight (like CPU meter but divided into quantum segments)
|
||||
if app.playback.playing && quantum_int <= 16 {
|
||||
let phase = link.phase();
|
||||
let beat_in_bar = phase.floor() as usize;
|
||||
let seg_w = tempo_area.width / quantum_int as u16;
|
||||
let seg_x = tempo_area.x + seg_w * beat_in_bar as u16;
|
||||
let seg_width = if beat_in_bar == quantum_int - 1 {
|
||||
tempo_area.width - seg_w * beat_in_bar as u16
|
||||
} else {
|
||||
seg_w
|
||||
};
|
||||
frame.render_widget(
|
||||
Block::default().style(Style::new().bg(theme.header.beat_bg)),
|
||||
Rect {
|
||||
x: seg_x,
|
||||
width: seg_width,
|
||||
..tempo_area
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Text overlay
|
||||
let tempo_text = if app.playback.playing {
|
||||
let phase = link.phase();
|
||||
let beat_in_bar = phase.floor() as usize + 1;
|
||||
let bar = (link.beat() / quantum).floor() as usize + 1;
|
||||
format!(" {:.1} BPM {bar}:{beat_in_bar} ", link.tempo())
|
||||
} else {
|
||||
format!(" {:.1} BPM ─:─ ", link.tempo())
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!(" {:.1} BPM ", link.tempo()))
|
||||
.block(Block::default().padding(pad).style(tempo_style))
|
||||
Paragraph::new(tempo_text)
|
||||
.block(Block::default().padding(pad))
|
||||
.style(Style::new().fg(tempo_fg).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center),
|
||||
tempo_area,
|
||||
);
|
||||
@@ -393,42 +425,61 @@ fn render_header(
|
||||
.get_iter(app.editor_ctx.bank, app.editor_ctx.pattern)
|
||||
.map(|iter| format!(" · #{}", iter + 1))
|
||||
.unwrap_or_default();
|
||||
let pattern_text = format!(
|
||||
" {} · {} steps{}{}{} ",
|
||||
pattern_name, pattern.length, speed_info, page_info, iter_info
|
||||
);
|
||||
let pattern_style = Style::new()
|
||||
.bg(theme.header.pattern_bg)
|
||||
.fg(theme.ui.text_primary);
|
||||
let pattern_bg = theme.header.pattern_bg;
|
||||
let active_count = snapshot.active_patterns.len();
|
||||
let active_info = format!(" · ▶{active_count}");
|
||||
let active_style = if active_count > 0 {
|
||||
Style::new().bg(pattern_bg).fg(theme.ui.text_primary)
|
||||
} else {
|
||||
Style::new().bg(pattern_bg).fg(theme.ui.text_muted)
|
||||
};
|
||||
let pattern_line = Line::from(vec![
|
||||
Span::styled(
|
||||
format!(
|
||||
" {} · {} steps{}{}{} ",
|
||||
pattern_name, pattern.length, speed_info, page_info, iter_info
|
||||
),
|
||||
Style::new().bg(pattern_bg).fg(theme.ui.text_primary),
|
||||
),
|
||||
Span::styled(active_info, active_style),
|
||||
]);
|
||||
frame.render_widget(
|
||||
Paragraph::new(pattern_text)
|
||||
.block(Block::default().padding(pad).style(pattern_style))
|
||||
Paragraph::new(pattern_line)
|
||||
.block(Block::default().padding(pad).style(Style::new().bg(pattern_bg)))
|
||||
.alignment(Alignment::Center),
|
||||
pattern_area,
|
||||
);
|
||||
|
||||
// Stats block
|
||||
// Stats block — CPU bar filling the area, text overlaid
|
||||
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||
let peers = link.peers();
|
||||
let voices = app.metrics.active_voices;
|
||||
let cpu_color = if cpu_pct >= 80.0 {
|
||||
let cpu_bar_color = if cpu_pct >= 80.0 {
|
||||
theme.flash.error_fg
|
||||
} else if cpu_pct >= 50.0 {
|
||||
theme.ui.accent
|
||||
} else {
|
||||
theme.header.stats_fg
|
||||
theme.meter.low
|
||||
};
|
||||
let dim = Style::new()
|
||||
.bg(theme.header.stats_bg)
|
||||
.fg(theme.header.stats_fg);
|
||||
let stats_line = Line::from(vec![
|
||||
Span::styled(format!(" CPU {cpu_pct:.0}%"), dim.fg(cpu_color)),
|
||||
Span::styled(format!(" V:{voices} L:{peers} "), dim),
|
||||
]);
|
||||
let block_style = Style::new().bg(theme.header.stats_bg);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(stats_line)
|
||||
.block(Block::default().padding(pad).style(block_style))
|
||||
Block::default().style(Style::new().bg(theme.header.stats_bg)),
|
||||
stats_area,
|
||||
);
|
||||
let filled_w = (cpu_pct / 100.0 * stats_area.width as f32).round() as u16;
|
||||
if filled_w > 0 {
|
||||
frame.render_widget(
|
||||
Block::default().style(Style::new().bg(cpu_bar_color)),
|
||||
Rect {
|
||||
width: filled_w.min(stats_area.width),
|
||||
..stats_area
|
||||
},
|
||||
);
|
||||
}
|
||||
let stats_text = format!("CPU {cpu_pct:.0}% V:{voices}");
|
||||
frame.render_widget(
|
||||
Paragraph::new(stats_text)
|
||||
.block(Block::default().padding(pad))
|
||||
.style(Style::new().fg(theme.ui.text_primary))
|
||||
.alignment(Alignment::Center),
|
||||
stats_area,
|
||||
);
|
||||
@@ -686,7 +737,6 @@ fn render_modal(
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
follow_up,
|
||||
} => {
|
||||
use crate::model::FollowUp;
|
||||
@@ -715,7 +765,6 @@ fn render_modal(
|
||||
("Length", length.clone(), *field == PatternPropsField::Length),
|
||||
("Speed", speed_label, *field == PatternPropsField::Speed),
|
||||
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
||||
("Sync Mode", sync_mode.label().to_string(), *field == PatternPropsField::SyncMode),
|
||||
("Follow Up", follow_up_label, *field == PatternPropsField::FollowUp),
|
||||
];
|
||||
if is_chain {
|
||||
|
||||
@@ -230,19 +230,19 @@ fn noall_clears_across_evaluations() {
|
||||
#[test]
|
||||
fn rec() {
|
||||
let outputs = expect_outputs(r#""loop1" rec"#, 1);
|
||||
assert_eq!(outputs[0], "/doux/rec/sound/loop1");
|
||||
assert_eq!(outputs[0], "/doux/rec/loop1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overdub() {
|
||||
let outputs = expect_outputs(r#""loop1" overdub"#, 1);
|
||||
assert_eq!(outputs[0], "/doux/rec/sound/loop1/overdub/1");
|
||||
assert_eq!(outputs[0], "/doux/rec/loop1/overdub/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overdub_alias_dub() {
|
||||
let outputs = expect_outputs(r#""loop1" dub"#, 1);
|
||||
assert_eq!(outputs[0], "/doux/rec/sound/loop1/overdub/1");
|
||||
assert_eq!(outputs[0], "/doux/rec/loop1/overdub/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,9 +5,9 @@ import fs from 'node:fs';
|
||||
|
||||
const EMIT = new Set(['.', '.!']);
|
||||
const SOUNDS = new Set([
|
||||
'sound', 's', 'saw', 'sine', 'kick', 'hat', 'snare', 'modal', 'noise',
|
||||
'square', 'tri', 'pulse', 'clap', 'rim', 'crash', 'fm', 'sample', 'plaits',
|
||||
'analog', 'waveshaping', 'granular', 'string', 'chord', 'speech', 'sub',
|
||||
'sound', 's', 'saw', 'sine', 'kick', 'hat', 'snare', 'noise', 'add',
|
||||
'square', 'tri', 'pulse', 'clap', 'rim', 'crash', 'fm', 'sample',
|
||||
'tom', 'cowbell', 'cymbal', 'white', 'pink', 'brown', 'live', 'sub',
|
||||
'super', 'wt', 'input', 'hh',
|
||||
]);
|
||||
const PARAMS = new Set([
|
||||
|
||||
@@ -87,11 +87,11 @@ const DL = 'https://dlcagire.raphaelforment.fr';
|
||||
<tr>
|
||||
<td>Windows (x86_64)</td>
|
||||
<td><a href={`${DL}/cagire-windows-x86_64.zip`}>zip</a></td>
|
||||
<td><a href={`${DL}/cagire-desktop-windows-x86_64.zip`}>zip</a> · <s>.msi</s></td>
|
||||
<td><a href={`${DL}/plugins-windows-x86_64-clap.zip`}>CLAP</a> · <a href={`${DL}/plugins-windows-x86_64-vst3.zip`}>VST3</a></td>
|
||||
<td><a href={`${DL}/cagire-windows-x86_64-desktop.zip`}>zip</a> · <a href={`${DL}/cagire-windows-x86_64-installer.zip`}>installer</a></td>
|
||||
<td><a href={`${DL}/cagire-windows-x86_64-clap.zip`}>CLAP</a> · <a href={`${DL}/cagire-windows-x86_64-vst3.zip`}>VST3</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="note">Source code and issue tracker on <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a>. You can also compile the software yourself from source!</p>
|
||||
<p class="note">Source code and issue tracker on <a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a>. You can also compile the software yourself from source!</p>
|
||||
|
||||
|
||||
<h2>Documentation</h2>
|
||||
@@ -103,9 +103,9 @@ const DL = 'https://dlcagire.raphaelforment.fr';
|
||||
<h2>Features (click to learn more!)</h2>
|
||||
<div class="features">
|
||||
<div class="feature-tags">
|
||||
<button data-desc="Powered by an audio engine crafted specifically for live coding. Classic waveforms (aliased and non-aliased). Noise generators. Mutable Instruments Plaits modes (modal, virtual analog, waveshaping, FM, granular, additive, etc). Small but fun FM synth with 2 operators, multiple algorithms and feedback control. Wavetable scanning with LFO control: bring your own wavetables. Sub-oscillators with independent waveform and octave. Sample playback with slicing, time-stretching and gating. Live microphone input. Configurable polyphony (default 32 voices). 8 independent effect buses (orbits).">Synthesis</button>
|
||||
<button data-desc="Powered by an audio engine crafted specifically for live coding. Classic waveforms (aliased and non-aliased). Noise generators. Additive synthesis (up to 32 partials). 7 native drum models with timbral morphing. FM synth with 2 operators, multiple algorithms and feedback control. Wavetable scanning: bring your own wavetables. Sub-oscillators with independent waveform and octave. Sample playback with slicing, time-stretching and gating. Live microphone input. Configurable polyphony (default 32 voices). 8 independent effect buses (orbits).">Synthesis</button>
|
||||
<button data-desc="Two reverb algorithms, Four delay types: standard, ping-pong, tape and multitap. Feedback, chorus, phaser and flanger with configurable depth, sweep and feedback. Distortion, bitcrusher, wave folding and wave wrapping. Comb filter with tunable frequency, feedback and damping. Audio-rate modulation for the final touch. ">Effects</button>
|
||||
<button data-desc="Multimode filter with lowpass, highpass and bandpass modes, each with its own ADSR envelope, frequency and resonance. Selectable filter slope (12, 24 or 48 dB/oct). Ladder filter variants (lowpass, highpass, bandpass) if you like that! 3-band parametric EQ (lo/mid/hi) and a tilt EQ for broad tonal shaping.">Filters</button>
|
||||
<button data-desc="Multimode filter with lowpass, highpass and bandpass modes, modulatable via universal envelope modulation, frequency and resonance. Selectable filter slope (12, 24 or 48 dB/oct). Ladder filter variants (lowpass, highpass, bandpass) if you like that! 3-band parametric EQ (lo/mid/hi) and a tilt EQ for broad tonal shaping.">Filters</button>
|
||||
<button data-desc="4 MIDI inputs and 4 MIDI outputs. Send and receive CC messages, pitch bend, channel aftertouch and program changes. MIDI clock output (clock, start, stop, continue). Read incoming CC values in real-time from any device. Full channel selection per voice.">MIDI</button>
|
||||
<button data-desc="Write notes by name (c4, d#5), build chords and scales from a rich built-in library. Convert between MIDI and frequency on the fly. Express musical ideas directly in code.">Theory</button>
|
||||
<button data-desc="Conditional execution and probability branching built into the language. Weighted random choices, coin flips, euclidean rhythms with rotation. Multiple random distributions, Perlin noise, seeded randomness. Cagire is designed to be generative and fun.">Probability</button>
|
||||
@@ -128,7 +128,7 @@ const DL = 'https://dlcagire.raphaelforment.fr';
|
||||
<video src="/mono_cagire.mp4" autoplay muted loop playsinline></video>
|
||||
|
||||
<p class="colophon">
|
||||
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · <a href="/docs">Docs</a> · AGPL-3.0 </p>
|
||||
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://git.raphaelforment.fr/BuboBubo/cagire">Gitea</a> · <a href="/docs">Docs</a> · AGPL-3.0 </p>
|
||||
|
||||
<script is:inline src="/script.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{\rtf1\ansi\deff0\nouicompat{\fonttbl{\f0\fswiss\fcharset0 Helvetica;}}
|
||||
{\*\generator Msftedit 5.41.21.2510;}\viewkind4\uc1
|
||||
\pard\sa200\sl276\slmult1\f0\fs20\lang9
|
||||
|
||||
CAGIRE - Forth-based Music Sequencer\par
|
||||
Copyright (c) 2025 Rapha\"el Forment\par
|
||||
\par
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.\par
|
||||
\par
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.\par
|
||||
\par
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see https://www.gnu.org/licenses/.\par
|
||||
}
|
||||
146
wix/main.wxs
146
wix/main.wxs
@@ -1,146 +0,0 @@
|
||||
<?xml version='1.0' encoding='windows-1252'?>
|
||||
|
||||
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = intel64 ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif ?>
|
||||
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||
|
||||
<Product
|
||||
Id='*'
|
||||
Name='Cagire'
|
||||
UpgradeCode='F2A3D4E5-6B7C-8D9E-0F1A-2B3C4D5E6F7A'
|
||||
Manufacturer='Raphael Forment'
|
||||
Language='1033'
|
||||
Codepage='1252'
|
||||
Version='$(var.Version)'>
|
||||
|
||||
<Package Id='*'
|
||||
Keywords='Installer'
|
||||
Description='Cagire - Forth-based music sequencer'
|
||||
Manufacturer='Raphael Forment'
|
||||
InstallerVersion='450'
|
||||
Languages='1033'
|
||||
Compressed='yes'
|
||||
InstallScope='perMachine'
|
||||
SummaryCodepage='1252'
|
||||
/>
|
||||
|
||||
<MajorUpgrade
|
||||
Schedule='afterInstallInitialize'
|
||||
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
|
||||
|
||||
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
|
||||
<Property Id='DiskPrompt' Value='Cagire Installation'/>
|
||||
|
||||
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
|
||||
<Directory Id='APPLICATIONFOLDER' Name='Cagire'>
|
||||
|
||||
<Component Id='CagireCLI' Guid='A1B2C3D4-E5F6-7890-ABCD-EF1234567890' Win64='yes'>
|
||||
<File
|
||||
Id='CagireEXE'
|
||||
Name='cagire.exe'
|
||||
DiskId='1'
|
||||
Source='$(var.CargoTargetBinDir)\cagire.exe'
|
||||
KeyPath='yes'/>
|
||||
</Component>
|
||||
|
||||
<Component Id='CagireDesktop' Guid='B2C3D4E5-F6A7-8901-BCDE-F12345678901' Win64='yes'>
|
||||
<File
|
||||
Id='CagireDesktopEXE'
|
||||
Name='cagire-desktop.exe'
|
||||
DiskId='1'
|
||||
Source='$(var.CargoTargetBinDir)\cagire-desktop.exe'
|
||||
KeyPath='yes'/>
|
||||
</Component>
|
||||
|
||||
<Component Id='PathEntry' Guid='C3D4E5F6-A7B8-9012-CDEF-123456789012' Win64='yes' KeyPath='yes'>
|
||||
<Environment
|
||||
Id='PATH'
|
||||
Name='PATH'
|
||||
Value='[APPLICATIONFOLDER]'
|
||||
Permanent='no'
|
||||
Part='last'
|
||||
Action='set'
|
||||
System='yes'/>
|
||||
</Component>
|
||||
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Directory Id='ProgramMenuFolder'>
|
||||
<Directory Id='ApplicationProgramsFolder' Name='Cagire'>
|
||||
<Component Id='StartMenuShortcut' Guid='D4E5F6A7-B8C9-0123-DEFA-234567890123' Win64='yes'>
|
||||
<Shortcut
|
||||
Id='CagireDesktopShortcut'
|
||||
Name='Cagire'
|
||||
Description='Forth-based music sequencer'
|
||||
Target='[APPLICATIONFOLDER]cagire-desktop.exe'
|
||||
WorkingDirectory='APPLICATIONFOLDER'
|
||||
Icon='CagireIcon.exe'/>
|
||||
<RemoveFolder Id='CleanUpShortcutFolder' On='uninstall'/>
|
||||
<RegistryValue
|
||||
Root='HKCU'
|
||||
Key='Software\Cagire'
|
||||
Name='installed'
|
||||
Type='integer'
|
||||
Value='1'
|
||||
KeyPath='yes'/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Feature
|
||||
Id='Binaries'
|
||||
Title='Application'
|
||||
Description='Installs Cagire CLI and Desktop binaries.'
|
||||
Level='1'
|
||||
ConfigurableDirectory='APPLICATIONFOLDER'
|
||||
AllowAdvertise='no'
|
||||
Display='expand'
|
||||
Absent='disallow'>
|
||||
|
||||
<ComponentRef Id='CagireCLI'/>
|
||||
<ComponentRef Id='CagireDesktop'/>
|
||||
|
||||
<Feature
|
||||
Id='Environment'
|
||||
Title='PATH Environment Variable'
|
||||
Description='Add the install location to the PATH system environment variable. This allows the cagire CLI to be called from any location.'
|
||||
Level='1'
|
||||
Absent='allow'>
|
||||
<ComponentRef Id='PathEntry'/>
|
||||
</Feature>
|
||||
</Feature>
|
||||
|
||||
<Feature
|
||||
Id='StartMenu'
|
||||
Title='Start Menu Shortcut'
|
||||
Description='Add a Cagire shortcut to the Start Menu.'
|
||||
Level='1'
|
||||
Absent='allow'>
|
||||
<ComponentRef Id='StartMenuShortcut'/>
|
||||
</Feature>
|
||||
|
||||
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||
|
||||
<Icon Id='CagireIcon.exe' SourceFile='assets\Cagire.ico'/>
|
||||
<Property Id='ARPPRODUCTICON' Value='CagireIcon.exe'/>
|
||||
<Property Id='ARPHELPLINK' Value='https://cagire.raphaelforment.fr'/>
|
||||
<Property Id='ARPURLINFOABOUT' Value='https://github.com/Bubobubobubobubo/cagire'/>
|
||||
|
||||
<UI>
|
||||
<UIRef Id='WixUI_FeatureTree'/>
|
||||
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||
</UI>
|
||||
|
||||
<WixVariable Id='WixUILicenseRtf' Value='wix\License.rtf'/>
|
||||
|
||||
</Product>
|
||||
|
||||
</Wix>
|
||||
Reference in New Issue
Block a user