180 Commits

Author SHA1 Message Date
4a8396670f Feat: lissajous
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-23 22:06:09 +01:00
77b7fa1f9e Feat: fixing stderr catching and scope not drawing completely 2026-02-23 21:53:53 +01:00
979b7639ac Feat: new harmony / melodic words and demo
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-23 02:25:32 +01:00
2a2b3c5651 Feat: fixes and demo
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-23 01:18:43 +01:00
f6c7438886 Fix: revert optimizations 2026-02-23 00:51:01 +01:00
057ba5b2f3 Feat: demo songs 2026-02-22 23:50:35 +01:00
40e69b66da Feat: script execution performance optimization
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s
2026-02-22 14:16:38 +01:00
1ce5b8597a Feat: cleanup
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-22 13:28:03 +01:00
789dbb186b Feat: CHANGELOG updates 2026-02-22 12:55:58 +01:00
8ba98e8f3b Feat: introduce follow up actions
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-22 03:59:09 +01:00
003ee0518e Feat: WIP pattern view redesign
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-22 03:26:48 +01:00
52406c7374 Feat: add wave word for drum synthesis
Some checks failed
Deploy Website / deploy (push) Failing after 4m51s
2026-02-21 22:03:07 +01:00
0b78f15ef1 Feat: fixing some errors in the documentation 2026-02-21 18:23:31 +01:00
302f40c4ac Feat: better UI in the main view
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-21 16:21:29 +01:00
79a4c3b6e2 Feat: saving screen during perfs 2026-02-21 15:56:52 +01:00
12b90bc99b Feat: update CHANGELOG
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-21 15:07:03 +01:00
a1190af494 Feat: clean the codebase as much as possible
Some checks failed
Deploy Website / deploy (push) Failing after 4m51s
2026-02-21 14:46:53 +01:00
f85a20d9a7 Feat: make some stuff optional for the CLAP/VST version 2026-02-21 13:23:43 +01:00
baa2aba381 Clean plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-21 01:27:32 +01:00
75a8fd4401 Trying to clena the mess opened by plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
2026-02-21 01:03:55 +01:00
ac0ddc7fb9 WIP: rename to cagire-plugins 2026-02-20 22:31:13 +01:00
07e95d5b6f WIP: fix VST3 version 2026-02-20 22:26:35 +01:00
00d6eb2f1f WIP: clap 2026-02-20 22:14:21 +01:00
12752e0167 Cargo to github
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s
2026-02-19 16:51:39 +01:00
3b41a06d5e Feat: continue to improve documentation
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-17 00:51:56 +01:00
f258358c8f Feat: collapsible help 2026-02-16 23:43:25 +01:00
2d8abe4af9 Feat: documentation
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-16 23:19:06 +01:00
37f5f74ec1 Feat: refactoring codebase 2026-02-16 16:26:57 +01:00
58624b64cf Feat: refactoring codebase 2026-02-16 16:00:57 +01:00
5385bf675a Feat: fixing ratatui big-text and UX
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-16 15:43:22 +01:00
211e71f5a9 Feat: UI / UX 2026-02-16 01:22:40 +01:00
23c7abb145 Feat: improving MIDI 2026-02-15 19:06:49 +01:00
670ae0b6b6 Feat: lots of things, preparing for live gig
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-15 11:23:11 +01:00
10ca567ac5 Feat: early mouse support 2026-02-14 16:26:29 +01:00
b2871ac251 Feat: F1 F2 F3
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
2026-02-14 15:13:21 +01:00
8ba89f91a0 Fixes 2026-02-10 23:51:17 +01:00
7d670dacb9 Re-update cargo 2026-02-10 21:42:24 +01:00
1de8c068f6 Feat: all engine params use varargs and can eat the stack, document it as such 2026-02-10 19:41:59 +01:00
d792f011ee Feat: rescale spectrum 2026-02-10 19:32:51 +01:00
897f1a776e Feat: reverb words 2026-02-10 19:27:11 +01:00
869d3af244 Feat: entretien de la codebase 2026-02-09 21:12:49 +01:00
a5f17687f1 chore: Release 2026-02-08 13:57:52 +01:00
5b851751e5 Feat: update the CHANGELOG.md correctly 2026-02-08 13:57:25 +01:00
bc5d12e53a Feat: lots of improvements
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-08 13:52:40 +01:00
d6bbae173b Feat: improve website
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-08 02:57:41 +01:00
1f339f1503 Small corrections
Some checks failed
Deploy Website / deploy (push) Failing after 4m51s
2026-02-08 01:33:50 +01:00
8ffe2c22c7 Feat: comfort features 2026-02-08 00:46:56 +01:00
20c32ce0d8 Prepare v0.0.8 release
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-07 13:14:14 +01:00
a326d58d30 Feat: restore Cargo.toml to git version 2026-02-07 13:07:56 +01:00
c72733bac8 WIP: prepare the ground for audio rate modulation 2026-02-07 12:08:11 +01:00
5758b18d58 Feat: trying to get rid of some sequencer bugs 2026-02-07 01:24:38 +01:00
52cc890a67 Feat: website WIP and new words
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-06 16:19:09 +01:00
0f9d750069 Feat: trying to improve bundling and compilation 2026-02-06 00:46:40 +01:00
66ee2e28ff Words and universal macOS installer
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-06 00:37:08 +01:00
6ec3a86568 New themes 2026-02-06 00:19:16 +01:00
51f52be4ce Feat: optimizations 2026-02-05 23:15:46 +01:00
2c98a915fa Space on all views
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-05 18:57:09 +01:00
e42476dd4d Feat: rework audio sample library viewer 2026-02-05 18:37:32 +01:00
3e364a6622 chore: Release
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-05 15:56:52 +01:00
1248f74b25 Feat: update CHANGELOG.md 2026-02-05 15:56:27 +01:00
fc2ab0757b Feat: update CHANGELOG.md
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-05 14:36:12 +01:00
10ed5a629a Feat: background head-preload for sample libraries 2026-02-05 14:35:26 +01:00
88c2b51720 Feat: introduce Forth words for 3-OP Fm synthesis (with feedback)
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s
2026-02-05 12:00:00 +01:00
5cda1a8f95 chore: Release
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-05 01:40:51 +01:00
200832f230 Feat: update CHANGELOG.md before release 2026-02-05 01:40:06 +01:00
91bc9011b2 Feat: new euclidean words and sugar for floating point numbers
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-05 01:30:34 +01:00
de56598fca Feat: prelude and new words
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-05 00:58:53 +01:00
abafea8ddf Feat: refactoring by breaking words in multiple files
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-04 23:50:38 +01:00
e6f776bdf4 Feat: tri is now triangle (disambiguation) 2026-02-04 20:34:37 +01:00
d40d713649 Feat: really good lookahead mechanism for scheduling
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-04 20:28:42 +01:00
767575b25d Removing lookahead concept 2026-02-04 20:01:17 +01:00
82b0668bcf Some kind of refactoring 2026-02-04 19:35:30 +01:00
6cf9d2eec1 Ungoing refactoring 2026-02-04 18:47:40 +01:00
2097997372 Feat: tweak and fix from last night workshop
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-04 09:37:29 +01:00
5579708f69 Feat: add tachyonFX animations 2026-02-04 00:40:15 +01:00
1b01491e87 Fix: prevent 0 division error when loading project 2026-02-03 23:41:27 +01:00
5581ba1881 chore: Release 2026-02-03 17:03:58 +01:00
8983b3f21c Fix: dict popup in editor is less intrusive
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 17:02:07 +01:00
4a7ae83019 Fix: desktop build
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-03 16:00:26 +01:00
61a6d7aad0 Fix: simpler scheduling
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2026-02-03 15:55:43 +01:00
1b01e3b805 WIP: improve Linux audio support
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 14:42:03 +01:00
2a57cc415b Fix: JACK stuff
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 14:23:24 +01:00
7c76bdb8d6 clamp audio options
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-03 14:14:28 +01:00
1facc72a67 Fix Linux audio: enable JACK support and RT priority for audio callback
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-03 14:04:34 +01:00
726ea16e92 Wip 2026-02-03 13:52:36 +01:00
154cac6547 Again 2026-02-03 03:25:31 +01:00
3380e454df Again 2026-02-03 03:08:13 +01:00
660f48216a Still searching... 2026-02-03 02:53:34 +01:00
fb1f73ebd6 WIP: not sure 2026-02-03 02:31:55 +01:00
cd223592a7 Insane linux fixes
Some checks failed
Deploy Website / deploy (push) Failing after 4m45s
2026-02-03 01:15:07 +01:00
af81c94207 WIP: even more crazy linux optimizations
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 00:38:46 +01:00
b53e4a76ab WIP: optimizations for linux
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-03 00:16:31 +01:00
8c31ed4196 Another round of optimization
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-02 22:16:00 +01:00
8024c18bb0 Less memory allocations at runtime 2026-02-02 21:55:10 +01:00
194030d953 fixing linux stuff
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-02 19:26:01 +01:00
e4799c1f42 Merge branch 'main' of github.com:Bubobubobubobubo/cagire
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-02 19:12:37 +01:00
636129688d lookahead 2026-02-02 19:12:32 +01:00
a2ee0e5a50 Fix: Copy register handling for cagire-desktop (Linux) 2026-02-02 18:25:02 +01:00
96ed74c6fe Fix: CPAL version mismatch 2026-02-02 18:08:55 +01:00
a67d982fcd Pattern mute and so on 2026-02-02 16:27:11 +01:00
c9ab7a4f0b chore: Release 2026-02-02 13:44:47 +01:00
772d21a8ed Feat: update CHANGELOG.md
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-02 13:42:42 +01:00
4396147a8b Euclidean + hue rotation
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2026-02-02 13:25:27 +01:00
c396c39b6b Fix layout 2026-02-02 12:18:22 +01:00
f6b43cb021 Add double-stack words (2dup, 2drop, 2swap, 2over) and forget
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-02 07:46:39 +01:00
60d1d7ca74 Feat: update website to prevent ugliness
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-02 01:38:21 +01:00
9864cc6d61 Update changelog for v0.0.3 2026-02-02 01:12:49 +01:00
985ab687d7 chore: Release
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
CI / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 12m15s
CI / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
CI / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
CI / release (push) Has been cancelled
2026-02-02 01:09:13 +01:00
9b925d881e Feat: update changelog
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2026-02-02 01:08:33 +01:00
71146c7cea Feat: more predictable projet load behavior
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-02 01:01:01 +01:00
6b95f31afd Feat: polyphony + iterator reset
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-02 00:33:46 +01:00
adee8d0d57 Feat: adding some basic music theory
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-01 16:15:09 +01:00
f9c284effd Feat: adding logrand and exprand 2026-02-01 15:16:20 +01:00
57fd51be3e Fix release.toml format
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
CI / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 12m21s
CI / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
CI / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
CI / release (push) Has been cancelled
2026-02-01 14:05:55 +01:00
ce70251057 Feat: work on metadata and packaging
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-01 14:00:10 +01:00
b47c789612 Feat: continue refactoring
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-01 13:39:25 +01:00
dd853b8e1b Feat: begin slight refactoring
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-01 12:38:48 +01:00
a0585b0814 MIDI Documentation and optional mouse event support
Some checks failed
Deploy Website / deploy (push) Failing after 4m45s
2026-02-01 00:51:56 +01:00
2100b82dad More robust midi implementation
Some checks failed
Deploy Website / deploy (push) Failing after 4m58s
2026-01-31 23:58:57 +01:00
15a4300db5 better quality midi 2026-01-31 23:23:36 +01:00
fed39c01e8 Lots + MIDI implementation 2026-01-31 23:13:51 +01:00
0a4f1419eb Fix: continue to fix release build and CI 2026-01-31 19:58:21 +01:00
793c83e18c Fix: again CI breaks 2026-01-31 18:04:11 +01:00
20bc0ffcb4 Fixing builds and workflows 2026-01-31 17:52:44 +01:00
8e09fd106e Remove emit_n tests (feature not implemented) 2026-01-31 17:37:00 +01:00
73ca0ff096 Add Windows/Linux desktop bundles to CI 2026-01-31 17:24:41 +01:00
425f1c8627 CI build versions 2026-01-31 16:35:38 +01:00
730332cfb0 Work on documentation
Some checks failed
Deploy Website / deploy (push) Failing after 6s
2026-01-31 15:03:20 +01:00
1d70a83759 Work on documentation 2026-01-31 14:31:44 +01:00
0299012725 Work on documentation 2026-01-31 13:46:43 +01:00
08029ec604 Working on internal documentation
Some checks failed
Deploy Website / deploy (push) Failing after 7s
2026-01-31 02:41:05 +01:00
4f9b1f39f9 Write some amount of documentation 2026-01-31 01:46:18 +01:00
4772b02f77 Feat: fix scope / spectrum / vumeter
Some checks failed
Deploy Website / deploy (push) Failing after 6s
2026-01-30 21:50:00 +01:00
4049c7787c Feat: extend CI to cover desktop 2026-01-30 21:19:48 +01:00
4c635500dd Feat: extend CI to cover desktop 2026-01-30 20:34:34 +01:00
d0e37e13e6 Feat: README update
Some checks failed
Deploy Website / deploy (push) Failing after 6s
2026-01-30 20:28:43 +01:00
7658cf9d51 Feat: add icon and reorganize desktop.rs
Some checks failed
Deploy Website / deploy (push) Failing after 7s
2026-01-30 20:27:08 +01:00
584dbb6aad Fixing color schemes 2026-01-30 20:15:43 +01:00
2731eea037 Monster commit: native version 2026-01-30 15:03:49 +01:00
22ee5f97e6 More robust workflows for website deployment 2026-01-30 12:39:09 +01:00
5fb059ea20 Corrections
Some checks failed
Deploy Website / deploy (push) Failing after 30s
CI / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 24m12s
CI / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
CI / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
CI / release (push) Has been cancelled
2026-01-30 12:27:27 +01:00
705d93702b Deplyment 2026-01-30 12:13:38 +01:00
77a6aa9eb7 Feat: ability to rename steps 2026-01-30 11:58:16 +01:00
d25b1317fc WIP: words for wavetable synthesis 2026-01-30 01:55:40 +01:00
2851785e0d WIP: consolidate sampling 2026-01-30 00:04:25 +01:00
a72772c8cc WIP: better precision? 2026-01-29 18:50:54 +01:00
4d22bd5d2b Remi 2026-01-29 12:17:09 +01:00
495bfb3bdc Try to optimize 2026-01-29 11:53:47 +01:00
73db616139 WIP simplify 2026-01-29 09:38:41 +01:00
8efafffaff Cleaning old temporal model 2026-01-29 01:28:57 +01:00
48f5920fed Cleaning language 2026-01-29 01:10:53 +01:00
d106711708 Before going crazy 2026-01-28 18:05:50 +01:00
2be15d11f4 Mixed bag of things 2026-01-28 17:39:41 +01:00
5952807240 wip 2026-01-28 13:54:29 +01:00
0beed16c31 Help modal 2026-01-28 13:22:51 +01:00
c6860105a6 vastly improved selection system 2026-01-28 02:29:17 +01:00
f4eafdf5b2 A ton of bug fixes 2026-01-28 01:09:23 +01:00
935df84920 ok 2026-01-27 15:23:04 +01:00
a3a39ea28e Fixing subtle bugs 2026-01-27 13:40:52 +01:00
574625735b Feat: parameter duration scaling 2026-01-27 12:17:23 +01:00
40c509e295 cleaning 2026-01-27 12:00:34 +01:00
61daa9d79d big commit 2026-01-27 01:04:08 +01:00
9e597258e4 WIP 2026-01-26 12:22:44 +01:00
223679acf8 So much better 2026-01-26 02:24:04 +01:00
2235a4b0a1 Basic search mechanism in editor 2026-01-26 01:25:40 +01:00
2453b78237 Looks better now 2026-01-26 01:02:18 +01:00
fcb6adb6af ok 2026-01-26 00:24:17 +01:00
ce98acacd0 Wip: refacto 2026-01-25 22:17:08 +01:00
d2d6ef5b06 broken 2026-01-25 21:44:08 +01:00
6efcabd32d WIP: menu 2026-01-25 21:37:53 +01:00
250e359fc5 scales 2026-01-25 20:43:12 +01:00
cf5994e604 Loop word 2026-01-24 12:47:19 +01:00
e1aff189cd Flash 2026-01-24 02:16:18 +01:00
b3c56bc56c WIP: half broken 2026-01-24 01:59:51 +01:00
3bb19cbda8 chain word and better save/load UI 2026-01-23 23:36:23 +01:00
42ad77d9ae Reorganize repository 2026-01-23 20:29:44 +01:00
e853e67492 Break down forth implementation properly 2026-01-23 19:36:40 +01:00
f7e6f96cbf words definition 2026-01-23 11:15:15 +01:00
8af64fc4e2 trace 2026-01-23 10:37:48 +01:00
183dd5b516 spectrum 2026-01-23 01:42:07 +01:00
254 changed files with 2364 additions and 16652 deletions

View File

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

View File

@@ -1,8 +1,11 @@
name: CI
on:
workflow_dispatch:
push:
tags: ['v*']
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
@@ -12,20 +15,26 @@ concurrency:
cancel-in-progress: true
jobs:
check:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: cagire-linux-x86_64
- os: macos-15-intel
target: x86_64-apple-darwin
artifact: cagire-macos-x86_64
- os: macos-14
target: aarch64-apple-darwin
artifact: cagire-macos-aarch64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: cagire-windows-x86_64
runs-on: ${{ matrix.os }}
timeout-minutes: 20
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
@@ -36,7 +45,6 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
@@ -49,10 +57,13 @@ jobs:
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev
cargo install cargo-bundle
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew list cmake &>/dev/null || brew install cmake
run: |
brew list cmake &>/dev/null || brew install cmake
cargo install cargo-bundle
- name: Install dependencies (Windows)
if: runner.os == 'Windows'
@@ -66,8 +77,229 @@ jobs:
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Test
run: cargo test --target ${{ matrix.target }}
- name: Bundle desktop app
if: runner.os != 'Windows'
run: cargo bundle --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Clippy
run: cargo clippy --target ${{ matrix.target }} -- -D warnings
- name: Bundle CLAP plugin
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Zip macOS app bundle
if: runner.os == 'macOS'
run: |
cd target/${{ matrix.target }}/release/bundle/osx
zip -r Cagire.app.zip Cagire.app
- name: Upload artifact (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire.exe
- name: Upload desktop artifact (Linux deb)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
- name: Upload desktop artifact (macOS app bundle)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
- name: Upload desktop artifact (Windows exe)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/cagire-desktop.exe
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: target/bundled/cagire-plugins.clap
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: target/bundled/cagire-plugins.vst3
universal-macos:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
timeout-minutes: 10
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
pattern: cagire-macos-*
path: artifacts
- name: Create universal CLI binary
run: |
lipo -create \
artifacts/cagire-macos-x86_64/cagire \
artifacts/cagire-macos-aarch64/cagire \
-output cagire
chmod +x cagire
lipo -info cagire
- name: Create universal app bundle
run: |
cd artifacts/cagire-macos-aarch64-desktop
unzip Cagire.app.zip
cd ../cagire-macos-x86_64-desktop
unzip Cagire.app.zip
cd ../..
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
lipo -create \
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
-output Cagire.app/Contents/MacOS/cagire-desktop
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
zip -r Cagire.app.zip Cagire.app
- name: Create universal CLAP plugin
run: |
mkdir -p cagire-plugins.clap/Contents/MacOS
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
cagire-plugins.clap/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
cagire-plugins.clap/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
- name: Create universal VST3 plugin
run: |
mkdir -p cagire-plugins.vst3/Contents/MacOS
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
- name: Build .pkg installer
run: |
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
cp -R Cagire.app pkg-root/Applications/
cp cagire pkg-root/usr/local/bin/
pkgbuild --analyze --root pkg-root component.plist
plutil -replace BundleIsRelocatable -bool NO component.plist
pkgbuild --root pkg-root --identifier com.sova.cagire \
--version "$VERSION" --install-location / \
--component-plist component.plist \
"Cagire-${VERSION}-universal.pkg"
- name: Upload universal CLI
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal
path: cagire
- name: Upload universal app bundle
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-desktop
path: Cagire.app.zip
- name: Upload universal CLAP plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-clap
path: cagire-plugins.clap
- name: Upload universal VST3 plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-vst3
path: cagire-plugins.vst3
- name: Upload .pkg installer
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-pkg
path: Cagire-*-universal.pkg
release:
needs: [build, universal-macos]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
for dir in artifacts/*/; do
name=$(basename "$dir")
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
cp "$dir"/*.pkg release/
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
elif [[ "$name" == "cagire-macos-universal" ]]; then
cp "$dir/cagire" "release/cagire-macos-universal"
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-clap ]]; then
base="${name%-clap}"
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == *-vst3 ]]; then
base="${name%-vst3}"
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-desktop ]]; then
base="${name%-desktop}"
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
cp "$dir"/*.deb "release/${base}-desktop.deb"
elif [ -f "$dir/Cagire.app.zip" ]; then
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
elif [ -f "$dir/cagire-desktop.exe" ]; then
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
fi
else
if [ -f "$dir/cagire.exe" ]; then
cp "$dir/cagire.exe" "release/${name}.exe"
elif [ -f "$dir/cagire" ]; then
cp "$dir/cagire" "release/${name}"
fi
fi
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true

View File

@@ -16,7 +16,6 @@ concurrency:
jobs:
deploy:
if: github.server_url == 'https://github.com'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

View File

@@ -1,409 +0,0 @@
name: Release
on:
workflow_dispatch:
push:
tags: ['v*']
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "12.0"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.server_url == 'https://github.com'
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: cagire-linux-x86_64
- os: macos-15-intel
target: x86_64-apple-darwin
artifact: cagire-macos-x86_64
- os: macos-14
target: aarch64-apple-darwin
artifact: cagire-macos-aarch64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: cagire-windows-x86_64
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install cargo-binstall
uses: cargo-bins/cargo-binstall@main
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev
cargo binstall -y cargo-bundle
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew list cmake &>/dev/null || brew install cmake
cargo binstall -y cargo-bundle
- name: Install dependencies (Windows)
if: runner.os == 'Windows'
run: |
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Bundle desktop app
if: runner.os != 'Windows'
run: cargo bundle --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Build AppImages (Linux)
if: runner.os == 'Linux'
run: |
mkdir -p target/releases
scripts/make-appimage.sh target/${{ matrix.target }}/release/cagire x86_64 target/releases
scripts/make-appimage.sh target/${{ matrix.target }}/release/cagire-desktop x86_64 target/releases
- name: Upload AppImage artifacts (Linux)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-appimage
path: target/releases/*.AppImage
- name: Bundle CLAP plugin
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Zip macOS app bundle
if: runner.os == 'macOS'
run: |
cd target/${{ matrix.target }}/release/bundle/osx
zip -r Cagire.app.zip Cagire.app
- name: Upload artifact (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire.exe
- name: Upload desktop artifact (Linux deb)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
- name: Upload desktop artifact (macOS app bundle)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
- name: Upload desktop artifact (Windows exe)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/cagire-desktop.exe
- name: Install cargo-wix (Windows)
if: runner.os == 'Windows'
run: cargo install cargo-wix
- name: Build MSI installer (Windows)
if: runner.os == 'Windows'
run: cargo wix --no-build --nocapture -C -p -C x64
- name: Upload MSI installer (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-msi
path: target/wix/*.msi
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: target/bundled/cagire-plugins.clap
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: target/bundled/cagire-plugins.vst3
build-cross:
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-unknown-linux-gnu
artifact: cagire-linux-aarch64
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build
run: cross build --release --target ${{ matrix.target }}
- name: Build desktop
run: cross build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire
- name: Upload desktop artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/cagire-desktop
universal-macos:
if: github.server_url == 'https://github.com'
needs: build
runs-on: macos-14
timeout-minutes: 10
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
pattern: cagire-macos-*
path: artifacts
- name: Create universal CLI binary
run: |
lipo -create \
artifacts/cagire-macos-x86_64/cagire \
artifacts/cagire-macos-aarch64/cagire \
-output cagire
chmod +x cagire
lipo -info cagire
- name: Create universal app bundle
run: |
cd artifacts/cagire-macos-aarch64-desktop
unzip Cagire.app.zip
cd ../cagire-macos-x86_64-desktop
unzip Cagire.app.zip
cd ../..
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
lipo -create \
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
-output Cagire.app/Contents/MacOS/cagire-desktop
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
zip -r Cagire.app.zip Cagire.app
- name: Create universal CLAP plugin
run: |
mkdir -p cagire-plugins.clap/Contents/MacOS
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
cagire-plugins.clap/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
cagire-plugins.clap/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
- name: Create universal VST3 plugin
run: |
mkdir -p cagire-plugins.vst3/Contents/MacOS
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
- uses: actions/checkout@v4
with:
sparse-checkout: |
assets/DMG-README.txt
scripts/make-dmg.sh
clean: false
- name: Create DMG
run: |
chmod +x scripts/make-dmg.sh
scripts/make-dmg.sh Cagire.app .
- name: Build .pkg installer
run: |
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
cp -R Cagire.app pkg-root/Applications/
cp cagire pkg-root/usr/local/bin/
pkgbuild --analyze --root pkg-root component.plist
plutil -replace BundleIsRelocatable -bool NO component.plist
pkgbuild --root pkg-root --identifier com.sova.cagire \
--version "$VERSION" --install-location / \
--component-plist component.plist \
"Cagire-${VERSION}-universal.pkg"
- name: Upload universal CLI
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal
path: cagire
- name: Upload universal app bundle
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-desktop
path: Cagire.app.zip
- name: Upload universal CLAP plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-clap
path: cagire-plugins.clap
- name: Upload universal VST3 plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-vst3
path: cagire-plugins.vst3
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-dmg
path: Cagire-*.dmg
- name: Upload .pkg installer
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-pkg
path: Cagire-*-universal.pkg
release:
needs: [build, build-cross, universal-macos]
if: startsWith(github.ref, 'refs/tags/v') && github.server_url == 'https://github.com'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
for dir in artifacts/*/; do
name=$(basename "$dir")
if [[ "$name" == "cagire-macos-universal-dmg" ]]; then
cp "$dir"/*.dmg release/
elif [[ "$name" == "cagire-macos-universal-pkg" ]]; then
cp "$dir"/*.pkg release/
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
elif [[ "$name" == "cagire-macos-universal" ]]; then
cp "$dir/cagire" "release/cagire-macos-universal"
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-clap ]]; then
base="${name%-clap}"
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == *-vst3 ]]; then
base="${name%-vst3}"
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-msi ]]; then
cp "$dir"/*.msi release/
elif [[ "$name" == *-appimage ]]; then
cp "$dir"/*.AppImage release/
elif [[ "$name" == *-desktop ]]; then
base="${name%-desktop}"
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
cp "$dir"/*.deb "release/${base}-desktop.deb"
elif [ -f "$dir/Cagire.app.zip" ]; then
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
elif [ -f "$dir/cagire-desktop.exe" ]; then
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
fi
else
if [ -f "$dir/cagire.exe" ]; then
cp "$dir/cagire.exe" "release/${name}.exe"
elif [ -f "$dir/cagire" ]; then
cp "$dir/cagire" "release/${name}"
fi
fi
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true

7
.gitignore vendored
View File

@@ -1,11 +1,10 @@
/target
/.cache
Cargo.lock
*.prof
.DS_Store
releases/
# Local cargo overrides (doux path patch)
.cargo/config.local.toml
# Cargo config
.cargo/config.toml
# Claude
.claude/

View File

@@ -1,187 +0,0 @@
# Building Cagire
## Quick Start
```bash
git clone https://github.com/Bubobubobubobubo/cagire
cd cagire
cargo build --release
```
The `doux` audio engine is fetched automatically from git. No local path setup needed.
## Prerequisites
**Rust** (stable toolchain): https://rustup.rs
## System Dependencies
### macOS
```bash
brew install cmake
```
cmake is required by `rusty_link` (Ableton Link C++ bindings). Xcode Command Line Tools provide the C++ compiler. CoreAudio and CoreMIDI are built-in. The desktop build needs no additional dependencies on macOS (Cocoa/Metal are provided by the system).
### Linux (Debian/Ubuntu)
```bash
sudo apt install cmake g++ pkg-config libasound2-dev libjack-jackd2-dev
```
For the desktop build (egui/eframe), also install:
```bash
sudo apt install libgl-dev libxkbcommon-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
```
### Linux (Arch)
```bash
sudo pacman -S cmake gcc pkgconf alsa-lib jack2
```
For the desktop build:
```bash
sudo pacman -S libxkbcommon libx11 libxcursor libxrandr libxi wayland mesa
```
### Linux (Fedora)
```bash
sudo dnf install cmake gcc-c++ pkgconf-pkg-config alsa-lib-devel jack-audio-connection-kit-devel
```
For the desktop build:
```bash
sudo dnf install libxkbcommon-devel libX11-devel libXcursor-devel libXrandr-devel libXi-devel wayland-devel mesa-libGL-devel
```
### Windows
Install Visual Studio Build Tools (MSVC) and CMake. Everything else is provided by the Windows SDK.
## Build
Terminal (default):
```bash
cargo build --release
```
Desktop (egui window):
```bash
cargo build --release --features desktop --bin cagire-desktop
```
Plugins (CLAP/VST3):
```bash
cargo xtask bundle cagire-plugins --release
```
The xtask alias is defined in `.cargo/config.toml` (committed). Plugin bundles are output to `target/bundled/`.
## Run
Terminal (default):
```bash
cargo run --release -- [OPTIONS]
```
Desktop (egui window):
```bash
cargo run --release --features desktop --bin cagire-desktop
```
| Flag | Description |
|------|-------------|
| `-s, --samples <path>` | Sample directory (repeatable) |
| `-o, --output <device>` | Output audio device |
| `-i, --input <device>` | Input audio device |
| `-c, --channels <n>` | Output channel count |
| `-b, --buffer <size>` | Audio buffer size |
## Cross-Compilation
[cross](https://github.com/cross-rs/cross) uses Docker to build for other platforms without installing their toolchains locally. It works on any OS that runs Docker.
### Targets
| Target | Method | Binaries |
|--------|--------|----------|
| aarch64-apple-darwin | Native (macOS ARM only) | `cagire`, `cagire-desktop` |
| x86_64-apple-darwin | Native (macOS only) | `cagire`, `cagire-desktop` |
| x86_64-unknown-linux-gnu | `cross build` | `cagire`, `cagire-desktop` |
| aarch64-unknown-linux-gnu (RPi 64-bit) | `cross build` | `cagire`, `cagire-desktop` |
| x86_64-pc-windows-gnu | `cross build` | `cagire`, `cagire-desktop` |
macOS targets can only be built on macOS — Apple does not support cross-compilation to macOS from other platforms. Linux and Windows targets can be cross-compiled from any OS. The aarch64-unknown-linux-gnu target covers Raspberry Pi (64-bit OS).
### Windows ABI
CI produces `x86_64-pc-windows-msvc` binaries (native Windows build, better compatibility). Local cross-compilation from non-Windows hosts produces `x86_64-pc-windows-gnu` binaries (MinGW via Docker). Both work; MSVC is preferred for releases.
### Prerequisites
1. **Docker**: https://docs.docker.com/get-docker/
2. **cross**: `cargo install cross --git https://github.com/cross-rs/cross`
3. On macOS, add the Intel target: `rustup target add x86_64-apple-darwin`
Docker must be running before invoking `cross` or `scripts/build-all.sh`.
### Building Individual Targets
```bash
# Linux x86_64
cross build --release --target x86_64-unknown-linux-gnu
cross build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
# Linux aarch64
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
# Windows x86_64
cross build --release --target x86_64-pc-windows-gnu
cross build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-gnu
```
### Building All Targets (macOS only)
```bash
# Interactive (prompts for platform/target selection):
scripts/build-all.sh
# Non-interactive:
scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop --yes
scripts/build-all.sh --all --yes
```
Builds selected targets, producing binaries in `releases/`.
Platform aliases: `macos-arm64`, `macos-x86_64`, `linux-x86_64`, `linux-aarch64`, `windows-x86_64`.
Target aliases: `cli`, `desktop`, `plugins`.
### Linux AppImage Packaging
Linux releases ship as AppImages — self-contained executables that bundle all shared library dependencies (ALSA, JACK, X11, OpenGL). No runtime dependencies required.
After building a Linux target, produce an AppImage with:
```bash
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 releases
```
`scripts/build-all.sh` does this automatically for every Linux target selected. The CI pipeline produces AppImages for the x86_64 Linux build. Cross-arch AppImage building (e.g. aarch64 on x86_64) is not supported — run on a matching host or in CI.
### Notes
- Custom Dockerfiles in `cross/` install the native libraries Cagire depends on (ALSA, JACK, X11, cmake, libclang, etc.). `Cross.toml` maps each target to its Dockerfile.
- The first build per target downloads Docker base images and installs packages. Subsequent builds use cached layers.
- Cross-architecture Docker builds (e.g. aarch64 on x86_64 or vice versa) run under QEMU emulation and are significantly slower.

View File

@@ -4,120 +4,54 @@ All notable changes to this project will be documented in this file.
## [0.1.0]
### Breaking
- **Quotation syntax changed from `{ }` to `( )`** — all deferred code blocks now use parentheses.
### Forth Language
**Syntax:**
- `[ v1 v2 v3 ]` bracket lists with implicit count.
- `( ... )` quotation syntax (replaces `{ }`).
- `,varname` assignment syntax (SetKeep): assign without consuming.
- `case/of/endof/endcase` control flow.
- `print` — debug word, outputs top-of-stack as text.
- Arithmetic and unary ops now lift over ArpList and CycleList element-wise.
**New words:**
- `index` — select item at explicit index (wraps with modulo).
- `slice` / `pick` — sample slicing: divide a sample into N equal parts and select which slice to play.
- `wave` / `waveform` — set drum synthesis waveform (0=sine, 0.5=triangle, 1=saw).
- `pbounce` — ping-pong cycle keyed by pattern iteration.
- `except` — inverse of `every`.
- `every+` / `except+` — phase-offset variants.
- `bjork` / `pbjork` — euclidean rhythm gates using quotations.
- `arp` — arpeggio list type (spreads notes across time).
- `all` / `noall` — apply params globally to all emitted sounds.
- `linmap` / `expmap` — linear and exponential range mapping.
- `rec` / `overdub` (`dub`) — record/overdub master audio to a named sample.
- `orec` / `odub` — record/overdub a single orbit.
**Harmony and voicing:**
- `key!` — set tonal center.
- `triad` / `seventh` — diatonic chord from scale degree.
- `inv` / `dinv` — chord inversion / down inversion.
- `drop2` / `drop3` — drop voicings.
- `tp` — transpose all ints on stack by N semitones.
**New chord types:**
- `pwr`, `augmaj7`, `7sus4`, `9sus4`, `maj69`, `min69`, `maj11`, `maj13`, `min13`, `dom7s11`.
**Effect parameters:**
- Ducking compressor: `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
- Smear effect: `smear`, `smearfreq`, `smearfb`.
- Reverb: `verbtype`, `verbchorus`, `verbchorusfreq`, `verbprelow`, `verbprehigh`, `verblowcut`, `verbhighcut`, `verblowgain`.
**Behavior changes:**
- All parameter words now accept varargs (100+ words updated to consume the full stack).
- `every` reworked to accept quotations.
- Removed `chain` word (replaced by pattern-level Follow Up setting).
### Engine
- SF2 soundfont support: auto-scans sample directories for `.sf2` files.
- Follow-up actions: patterns have configurable follow-up (Loop, Stop, Chain). Replaces the `chain` word with a declarative UI setting (`e` key).
- Delta-time MIDI scheduling for tighter timing.
- Audio stream errors surfaced as flash messages.
- Prelude script evaluated on application startup (not only on play).
- Global periodic script: a hidden script page runs alongside all patterns at its own speed/length.
- RestartAll command: reset all active patterns to step 0 and clear state.
- Tempo and current beat exposed in sequencer snapshot.
- Spectrum analyzer rescaling.
### UI / UX
- **Engine page redesign**: responsive narrow/wide layout, Link/MIDI/device settings moved here from Options.
- **Patterns view redesign**: banks column with pattern counts, expandable detail rows, bottom preview strip with mini step grid.
- **Mouse support**: click navigation on header/grid/panels/modals, text selection in code editor (click+drag), double-click on scope/spectrum/lissajous to cycle display modes.
- Smooth playback progress bar interpolated between steps.
- Dynamic step grid sizing adapts to terminal height.
- Lissajous XY scope with Braille rendering and thermal trail mode.
- Gain boost (1x16x) and normalize toggle for scope/lissajous/spectrum.
- Pattern description field: editable via `d`, shown in pattern list and properties.
- Bank/pattern import and export via clipboard (base64 serialization for sharing).
- Mute/solo on main page now apply immediately (no staging).
- Step name automatically cleared when deleting a step.
- F1F6 page navigation across the 3×2 page grid.
- Collapsible help sections with code block copy.
- Onboarding system for first-time users.
- Show/hide preview pane toggle and zoom factor setting.
- Reduced UI lag: sequencer snapshot moved after render call.
- 10 bundled demo projects loaded on fresh startup (togglable in Options).
- Options page: each option shows a description line below when focused.
- Dictionary page: word list uses full page height (removed description box).
### Themes
- 5 new themes: Iceberg, Everforest, Fauve, Tropicalia, Jaipur.
- Palette-based generation: all 18 themes derived from a 14-field Palette via Oklab color space (definitions reduced from ~300 to ~20 lines each).
### Desktop (egui)
- Fixed Alt/Option key on macOS (dead-key composition now works).
- Fixed multi-character text paste.
- Extended function key support (F13F20).
- No console window on Windows desktop build.
### Packaging
- macOS: `.dmg` disk image with `.app` bundle (Intel + Apple Silicon fat binaries via `lipo`).
- Windows: `.msi` installer via WiX.
- Linux: improved AppImage build scripts and Docker cross-compilation.
### UI / UX (breaking cosmetic changes)
- **Options page**: Each option now shows a short description line below when focused, replacing the static header box.
- **Dictionary page**: Removed the Forth description box at the top. The word list now uses the full page height.
### CLAP Plugin (experimental)
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
### Forth Language
- Removed `chain` word (replaced by pattern-level Follow Up setting).
- `case/of/endof/endcase` control flow for pattern-matching dispatch.
- `bjork` / `pbjork` — euclidean rhythm gates using quotations: execute a block only on Bjorklund-distributed hits.
- `arp` — arpeggio list type that spreads notes across time positions instead of stacking them simultaneously.
- `,varname` assignment syntax (SetKeep): assign to a variable without consuming the value from the stack.
- `every` reworked to accept quotations for cleaner conditional step logic.
- All parameter words now accept varargs — over 100 words updated to consume the full stack.
- Reverb parameter words added.
### Engine
- Follow-up actions: patterns now have a configurable follow-up behavior (Loop, Stop, or Chain to another pattern). Replaces the Forth `chain` word with a declarative setting in the Pattern Properties modal (`e` key). Chain targets specify bank and pattern via UI fields.
- Delta-time MIDI scheduling for tighter, sample-accurate timing.
- Tempo and current beat exposed in sequencer snapshot.
- Spectrum analyzer rescaling.
### UI / UX
- Patterns view redesign: new layout with banks column (showing pattern counts), expandable detail rows for the focused pattern (quantization, sync mode, progress bar), and a bottom preview strip with mini step grid and pattern properties.
- Smooth playback progress: playing patterns display a real-time progress bar interpolated between steps.
- Dynamic step grid sizing: `steps_per_page` adapts to terminal height instead of using a fixed constant.
- Mouse support: click navigation on the pattern grid, panels, and modals.
- F1F6 page navigation across the 3×2 page grid.
- Collapsible help sections with code block copy.
- Onboarding system for first-time users.
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
- Show/hide preview pane toggle and zoom factor setting.
### Documentation
- Complete reorganization into `docs/` subdirectories.
- 10 getting-started guides, 5 interactive tutorials.
- New tutorials: Recording, Soundfonts, Sharing (import/export).
- New topics: control flow, generators, harmony, randomness, variables, timing, bracket syntax.
- Crate-level READMEs for forth, markdown, project, ratatui.
- New topics: control flow, generators, harmony, randomness, variables, timing.
### Fixed
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
- Scope widget not drawing completely in some terminal sizes.
### Theme System
- Palette-based generation: all 18 themes now derived from a 14-field Palette via Oklab color space.
- Theme definitions reduced from ~300 lines each to ~20 lines.
### Codebase
- `src/app.rs` split into 10 focused modules.
- `src/app.rs` split into 10 focused modules (dispatch, clipboard, editing, navigation, persistence, scripting, sequencer, staging, undo).
- `src/input.rs` split into 8 page-specific handlers.
- Undo/redo system with scope-based tracking.
- Feature-gated CLI vs plugin builds.
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
## [0.0.9]

View File

@@ -10,7 +10,6 @@ Contributions are welcome. There are many ways to contribute beyond code:
## Prerequisites
- **Rust** (stable toolchain) - [rustup.rs](https://rustup.rs/)
- **System libraries** - See [BUILDING.md](BUILDING.md) for platform-specific packages (cmake, ALSA, etc.)
## Quick start

7363
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
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.0"
version = "0.0.9"
edition = "2021"
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
license = "AGPL-3.0"
@@ -51,11 +51,11 @@ 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 = { path = "/Users/bubo/doux", features = ["native"] }
rusty_link = "0.4"
ratatui = "0.30"
crossterm = "0.29"
cpal = { version = "0.17", optional = true }
cpal = { version = "0.17", features = ["jack"], optional = true }
clap = { version = "4", features = ["derive"], optional = true }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
@@ -83,9 +83,6 @@ rustc-hash = { version = "2", optional = true }
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
cpal = { version = "0.17", optional = true, features = ["jack"] }
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
@@ -112,4 +109,3 @@ icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
copyright = "Copyright (c) 2025 Raphaël Forment"
category = "Music"
short_description = "Forth-based music sequencer"
minimum_system_version = "12.0"

View File

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

107
README.md
View File

@@ -1,84 +1,37 @@
<h1 align="center">Cagire</h1>
<p align="center"><em>A Forth-based live coding sequencer</em></p>
<p align="center"><em>A Forth Music Sequencer</em></p>
<p align="center">
<img src="assets/Cagire.png" alt="Cagire" width="256">
<img src="cagire_pixel.png" alt="Cagire" width="256">
</p>
<p align="center">
<a href="https://cagire.raphaelforment.fr">Website</a> &middot;
<a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> &middot;
Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events.
## Build
Terminal version:
```
cargo build --release
```
Desktop version (with egui window):
```
cargo build --release --features desktop --bin cagire-desktop
```
## Run
Terminal version:
```
cargo run --release
```
Desktop version:
```
cargo run --release --features desktop --bin cagire-desktop
```
## License
AGPL-3.0
</p>
Cagire is a terminal based step sequencer and live coding platform. Each step in a sequence is represented by a **Forth** script. It ships with a self-contained audio engine. No external software is needed, Cagire is a fully autonomous musical instrument that provides everything you need to perform.
### Examples
A filtered sawtooth with reverb:
```forth
saw sound
200 199 freq
400 lpf
.8 lpq .3 verb
.
```
A generative pattern using randomness, scales, and effects:
```forth
sine sound 2 fm 0.5 fmh
0 7 rand minor 50 + note
.1 .8 rrand cutoff
1 4 irand 10 * delay .5 delayfb
.
```
### Features
- **Cagire's Forth**: a stack-based language made for live coding
- Forth has almost no syntax, only words, numbers and spaces. Very easy to learn for beginners, quite deep for experienced programmers.
- Nondeterminism and generative: randomness, probabilities, patterns thought as first-class features.
- Quotations: code blocks `( ... )` that compose with probability, cycling, euclidean, and conditional words.
- User-defined words: extend (or redefine) the language on the fly with `:name ... ;` definitions.
- Interactive documentation: built-in tutorials with runnable examples.
- **Audio engine** (powered by [Doux](https://doux.livecoding.fr)):
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive, FM (2-op, 3 algorithms), additive synthesis, wavetables, 7-voice spread, Mutable Instruments Plaits models: modal, granular, waveshaping, chord, swarm, etc.
- 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.
- 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).
- **Sequencing**: probabilities, patterns, euclidean structures, sub-step timing, pattern chaining and a lot more.
- **MIDI**: receive or send MIDI messages across up to 4 inputs and 4 outputs.
- **Ableton Link**: tempo and phase sync with any Link-enabled software or hardware.
- **Cross-platform**: terminal and desktop interfaces on macOS, Linux, and Windows.
- **Plugins**: run Cagire as a CLAP or VST3 plugin inside your DAW (separate version).
### Getting started
Download the latest release for your platform from the [website](https://cagire.raphaelforment.fr).
To build from source instead, see [BUILDING.md](BUILDING.md).
### Documentation
Cagire includes interactive documentation with runnable code examples. Press **F1** in the application to open it.
- [Website](https://cagire.raphaelforment.fr)
- [BUILDING.md](BUILDING.md) — build instructions and CLI flags
- [CHANGELOG.md](CHANGELOG.md)
### Credits
Cagire is developed by [BuboBubo](https://raphaelforment.fr) (Raphael Forment).
- **[Doux](https://doux.livecoding.fr)** (audio engine) — Rust port of Dough, originally written in C by Felix Roos
- **mi-plaits-dsp-rs** — Rust port of Mutable Instruments Plaits DSP by Oliver Rockstedt, original code by Emilie Gillet
### License
[AGPL-3.0](LICENSE)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,18 +0,0 @@
# Cagire - A Forth-based music sequencer
## Installation
Drag Cagire.app into the Applications folder.
## Unquarantine
Since this app is not signed with an Apple Developer certificate,
macOS will block it from running. Thanks Apple! To fix this, open
Terminal and run:
xattr -cr /Applications/Cagire.app
## Support
If you enjoy this software, consider supporting development:
https://ko-fi.com/raphaelbubo

View File

@@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=Cagire
Comment=Forth-based music sequencer
Exec=cagire
Icon=cagire
Categories=Audio;Music;AudioVideo;

View File

@@ -1,18 +1,4 @@
//! Build script — embeds Windows application resources (icon, metadata).
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "windows" {
// C++ runtime (stdc++, gcc, gcc_eh, pthread) linked statically via .cargo/config.toml
// using -Wl,-Bstatic. Only Windows system DLLs go here.
println!("cargo:rustc-link-lib=ws2_32");
println!("cargo:rustc-link-lib=iphlpapi");
println!("cargo:rustc-link-lib=winmm");
println!("cargo:rustc-link-lib=ole32");
println!("cargo:rustc-link-lib=oleaut32");
}
#[cfg(windows)]
{
let mut res = winres::WindowsResource::new();

View File

@@ -1,22 +0,0 @@
# cagire-forth
Stack-based Forth VM for the Cagire sequencer. Tokenizes, compiles, and executes step scripts to produce audio and MIDI commands.
## Modules
| Module | Description |
|--------|-------------|
| `vm` | Interpreter loop, `Forth::evaluate()` entry point |
| `compiler` | Tokenization (with source spans) and single-pass compilation to ops |
| `ops` | `Op` enum (~90 variants) |
| `types` | `Value`, `StepContext`, shared state types |
| `words/` | Built-in word definitions: `core`, `sound`, `music`, `midi`, `effects`, `sequencing`, `compile` |
| `theory/` | Music theory lookups: `scales` (~200 patterns), `chords` (interval arrays) |
## Key Types
- **`Forth`** — VM instance, holds stacks and compilation state
- **`Value`** — Stack value (int, float, string, list, quotation, ...)
- **`StepContext`** — Per-step evaluation context (step index, tempo, variables, ...)
- **`Op`** — Compiled operation; nondeterministic variants carry `Option<SourceSpan>` for tracing
- **`ExecutionTrace`** — Records executed/selected spans and resolved values during evaluation

View File

@@ -15,7 +15,6 @@ enum Token {
Word(String, SourceSpan),
}
/// Compile Forth source text into an executable Op sequence.
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
let tokens = tokenize(input);
compile(&tokens, dict)
@@ -31,7 +30,7 @@ fn tokenize(input: &str) -> Vec<Token> {
continue;
}
if c == '{' || c == '}' {
if c == '(' || c == ')' {
chars.next();
continue;
}
@@ -133,7 +132,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
Token::Word(w, span) => {
let word = w.as_str();
if word == "(" {
if word == "{" {
let (quote_ops, consumed, end_span) =
compile_quotation(&tokens[i + 1..], dict)?;
i += consumed;
@@ -142,21 +141,8 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
end: end_span.end,
};
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
} else if word == ")" {
return Err("unexpected )".into());
} else if word == "[" {
let (bracket_ops, consumed, end_span) =
compile_bracket(&tokens[i + 1..], dict)?;
i += consumed;
ops.push(Op::Mark);
ops.extend(bracket_ops);
let count_span = SourceSpan {
start: span.start,
end: end_span.end,
};
ops.push(Op::Count(Some(count_span)));
} else if word == "]" {
return Err("unexpected ]".into());
} else if word == "}" {
return Err("unexpected }".into());
} else if word == ":" {
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
i += consumed;
@@ -203,8 +189,8 @@ fn compile_quotation(
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"(" => depth += 1,
")" => {
"{" => depth += 1,
"}" => {
depth -= 1;
if depth == 0 {
end_idx = Some(i);
@@ -216,7 +202,7 @@ fn compile_quotation(
}
}
let end_idx = end_idx.ok_or("missing )")?;
let end_idx = end_idx.ok_or("missing }")?;
let end_span = match &tokens[end_idx] {
Token::Word(_, span) => *span,
_ => unreachable!(),
@@ -225,38 +211,6 @@ fn compile_quotation(
Ok((quote_ops, end_idx + 1, end_span))
}
fn compile_bracket(
tokens: &[Token],
dict: &Dictionary,
) -> Result<(Vec<Op>, usize, SourceSpan), String> {
let mut depth = 1;
let mut end_idx = None;
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"[" => depth += 1,
"]" => {
depth -= 1;
if depth == 0 {
end_idx = Some(i);
break;
}
}
_ => {}
}
}
}
let end_idx = end_idx.ok_or("missing ]")?;
let end_span = match &tokens[end_idx] {
Token::Word(_, span) => *span,
_ => unreachable!(),
};
let body_ops = compile(&tokens[..end_idx], dict)?;
Ok((body_ops, end_idx + 1, end_span))
}
fn token_span(tok: &Token) -> Option<SourceSpan> {
match tok {
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),

View File

@@ -4,7 +4,6 @@ use std::sync::Arc;
use super::types::SourceSpan;
/// Single VM instruction produced by the compiler.
#[derive(Clone, Debug, PartialEq)]
pub enum Op {
PushInt(i64, Option<SourceSpan>),
@@ -65,7 +64,6 @@ pub enum Op {
NewCmd,
SetParam(&'static str),
Emit,
Print,
Get,
Set,
SetKeep,
@@ -78,7 +76,6 @@ pub enum Op {
PCycle(Option<SourceSpan>),
Choose(Option<SourceSpan>),
Bounce(Option<SourceSpan>),
PBounce(Option<SourceSpan>),
WChoose(Option<SourceSpan>),
ChanceExec(Option<SourceSpan>),
ProbExec(Option<SourceSpan>),
@@ -87,9 +84,6 @@ pub enum Op {
Ftom,
SetTempo,
Every(Option<SourceSpan>),
Except(Option<SourceSpan>),
EveryOffset(Option<SourceSpan>),
ExceptOffset(Option<SourceSpan>),
Bjork(Option<SourceSpan>),
PBjork(Option<SourceSpan>),
Quotation(Arc<[Op]>, Option<SourceSpan>),
@@ -132,9 +126,6 @@ pub enum Op {
ModSlide(u8),
ModRnd(u8),
ModEnv,
// Global params
EmitAll,
ClearGlobal,
// MIDI
MidiEmit,
GetMidiCC,
@@ -142,13 +133,4 @@ pub enum Op {
MidiStart,
MidiStop,
MidiContinue,
// Recording
Rec,
Overdub,
Orec,
Odub,
// Bracket syntax (mark/count for auto-counting)
Mark,
Count(Option<SourceSpan>),
Index(Option<SourceSpan>),
}

View File

@@ -1,12 +1,8 @@
//! Chord definitions as semitone interval arrays.
/// Named chord with its interval pattern.
pub struct Chord {
pub name: &'static str,
pub intervals: &'static [i64],
}
/// All built-in chord types.
pub static CHORDS: &[Chord] = &[
// Triads
Chord {
@@ -173,7 +169,6 @@ pub static CHORDS: &[Chord] = &[
},
];
/// Find a chord's intervals by name.
pub fn lookup(name: &str) -> Option<&'static [i64]> {
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
}

View File

@@ -1,5 +1,3 @@
//! Music theory data — chord and scale lookup tables.
pub mod chords;
mod scales;

View File

@@ -1,12 +1,8 @@
//! Scale definitions as semitone offset arrays.
/// Named scale with its semitone pattern.
pub struct Scale {
pub name: &'static str,
pub pattern: &'static [i64],
}
/// All built-in scale types.
pub static SCALES: &[Scale] = &[
Scale {
name: "major",
@@ -129,7 +125,6 @@ pub static SCALES: &[Scale] = &[
},
];
/// Find a scale's pattern by name.
pub fn lookup(name: &str) -> Option<&'static [i64]> {
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
}

View File

@@ -14,14 +14,12 @@ pub trait CcAccess: Send + Sync {
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
}
/// Byte range in source text.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SourceSpan {
pub start: u32,
pub end: u32,
}
/// Concrete value resolved from a nondeterministic op, used for trace annotations.
#[derive(Clone, Debug)]
pub enum ResolvedValue {
Int(i64),
@@ -41,7 +39,6 @@ impl ResolvedValue {
}
}
/// Spans and resolved values collected during a single evaluation, used for UI highlighting.
#[derive(Clone, Debug, Default)]
pub struct ExecutionTrace {
pub executed_spans: Vec<SourceSpan>,
@@ -49,7 +46,6 @@ pub struct ExecutionTrace {
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
}
/// Per-step sequencer state passed into the VM.
pub struct StepContext<'a> {
pub step: usize,
pub beat: f64,
@@ -76,18 +72,13 @@ impl StepContext<'_> {
}
}
/// Underlying map for user-defined variables.
pub type VariablesMap = HashMap<String, Value>;
/// Shared variable store, swapped atomically after each step.
pub type Variables = Arc<ArcSwap<VariablesMap>>;
/// Shared user-defined word dictionary.
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
/// Shared random number generator.
pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Mutex<Vec<Value>>;
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
/// Stack value in the Forth VM.
#[derive(Clone, Debug)]
pub enum Value {
Int(i64, Option<SourceSpan>),
@@ -169,7 +160,6 @@ pub(super) struct CmdRegister {
sound: Option<Value>,
params: Vec<(&'static str, Value)>,
deltas: Vec<Value>,
global_params: Vec<(&'static str, Value)>,
}
impl CmdRegister {
@@ -178,7 +168,6 @@ impl CmdRegister {
sound: None,
params: Vec::with_capacity(16),
deltas: Vec::with_capacity(4),
global_params: Vec::new(),
}
}
@@ -214,28 +203,6 @@ impl CmdRegister {
}
}
pub(super) fn global_params(&self) -> &[(&'static str, Value)] {
&self.global_params
}
pub(super) fn commit_global(&mut self) {
self.global_params.append(&mut self.params);
self.sound = None;
self.deltas.clear();
}
pub(super) fn clear_global(&mut self) {
self.global_params.clear();
}
pub fn set_global(&mut self, params: Vec<(&'static str, Value)>) {
self.global_params = params;
}
pub fn take_global(&mut self) -> Vec<(&'static str, Value)> {
std::mem::take(&mut self.global_params)
}
pub(super) fn clear(&mut self) {
self.sound = None;
self.params.clear();

View File

@@ -14,13 +14,11 @@ use super::types::{
Value, Variables, VariablesMap,
};
/// Forth VM instance. Holds the stack, variables, dictionary, and RNG.
pub struct Forth {
stack: Stack,
vars: Variables,
dict: Dictionary,
rng: Rng,
global_params: Mutex<Vec<(&'static str, Value)>>,
}
impl Forth {
@@ -30,7 +28,6 @@ impl Forth {
vars,
dict,
rng,
global_params: Mutex::new(Vec::new()),
}
}
@@ -42,18 +39,12 @@ impl Forth {
self.stack.lock().clear();
}
pub fn clear_global_params(&self) {
self.global_params.lock().clear();
}
/// Evaluate a Forth script and return audio command strings.
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
self.apply_var_writes(var_writes);
Ok(outputs)
}
/// Evaluate and collect an execution trace for UI highlighting.
pub fn evaluate_with_trace(
&self,
script: &str,
@@ -65,7 +56,6 @@ impl Forth {
Ok(outputs)
}
/// Evaluate and return both outputs and pending variable writes (without applying them).
pub fn evaluate_raw(
&self,
script: &str,
@@ -112,8 +102,6 @@ impl Forth {
let vars_snapshot = self.vars.load_full();
let mut var_writes: HashMap<String, Value> = HashMap::new();
cmd.set_global(self.global_params.lock().clone());
self.execute_ops(
ops,
ctx,
@@ -125,8 +113,6 @@ impl Forth {
&mut var_writes,
)?;
*self.global_params.lock() = cmd.take_global();
Ok((outputs, var_writes))
}
@@ -144,7 +130,6 @@ impl Forth {
var_writes: &mut HashMap<String, Value>,
) -> Result<(), String> {
let mut pc = 0;
let mut marks: Vec<usize> = Vec::new();
let trace_cell = std::cell::RefCell::new(trace);
let var_writes_cell = std::cell::RefCell::new(Some(var_writes));
@@ -229,9 +214,8 @@ impl Forth {
_ => 1,
};
let param_max = cmd
.global_params()
.params()
.iter()
.chain(cmd.params().iter())
.map(|(_, v)| match v {
Value::CycleList(items) => items.len(),
_ => 1,
@@ -243,8 +227,7 @@ impl Forth {
let has_arp_list = |cmd: &CmdRegister| -> bool {
matches!(cmd.sound(), Some(Value::ArpList(_)))
|| cmd.global_params().iter().chain(cmd.params().iter())
.any(|(_, v)| matches!(v, Value::ArpList(_)))
|| cmd.params().iter().any(|(_, v)| matches!(v, Value::ArpList(_)))
};
let compute_arp_count = |cmd: &CmdRegister| -> usize {
@@ -270,21 +253,15 @@ impl Forth {
delta_secs: f64,
outputs: &mut Vec<String>|
-> Result<Option<Value>, String> {
let has_sound = cmd.sound().is_some();
let has_params = !cmd.params().is_empty();
let has_global = !cmd.global_params().is_empty();
if !has_sound && !has_params && !has_global {
return Err("nothing to emit".into());
}
let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?;
let resolved_sound_val =
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
sound_opt.map(|sv| resolve_value(sv, arp_idx, poly_idx));
let sound_str = match &resolved_sound_val {
Some(v) => Some(v.as_str()?.to_string()),
None => None,
};
let resolved_params: Vec<(&str, String)> = cmd.global_params()
let resolved_params: Vec<(&str, String)> = params
.iter()
.chain(cmd.params().iter())
.map(|(k, v)| {
let resolved = resolve_value(v, arp_idx, poly_idx);
if let Value::CycleList(_) | Value::ArpList(_) = v {
@@ -315,7 +292,7 @@ impl Forth {
Op::Dup => {
ensure(stack, 1)?;
let v = stack.last().expect("stack non-empty after ensure").clone();
let v = stack.last().unwrap().clone();
stack.push(v);
}
Op::Dupn => {
@@ -328,16 +305,6 @@ impl Forth {
Op::Drop => {
pop(stack)?;
}
Op::Print => {
let val = pop(stack)?;
let text = match &val {
Value::Int(n, _) => n.to_string(),
Value::Float(f, _) => format!("{f}"),
Value::Str(s, _) => s.to_string(),
_ => format!("{val:?}"),
};
outputs.push(format!("print:{text}"));
}
Op::Swap => {
ensure(stack, 2)?;
let len = stack.len();
@@ -568,10 +535,7 @@ impl Forth {
Op::NewCmd => {
ensure(stack, 1)?;
let values = drain_skip_quotations(stack);
if values.is_empty() {
return Err("expected sound name".into());
}
let values = std::mem::take(stack);
let val = if values.len() == 1 {
values.into_iter().next().unwrap()
} else {
@@ -581,10 +545,7 @@ impl Forth {
}
Op::SetParam(param) => {
ensure(stack, 1)?;
let values = drain_skip_quotations(stack);
if values.is_empty() {
return Err("expected parameter value".into());
}
let values = std::mem::take(stack);
let val = if values.len() == 1 {
values.into_iter().next().unwrap()
} else {
@@ -819,20 +780,16 @@ impl Forth {
drain_select_run(count, idx, stack, outputs, cmd)?;
}
Op::Bounce(word_span) | Op::PBounce(word_span) => {
Op::Bounce(word_span) => {
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("bounce count must be > 0".into());
}
let counter = match &ops[pc] {
Op::Bounce(_) => ctx.runs,
_ => ctx.iter,
};
let idx = if count == 1 {
0
} else {
let period = 2 * (count - 1);
let raw = counter % period;
let raw = ctx.runs % period;
if raw < count { raw } else { period - raw }
};
if let Some(span) = word_span {
@@ -919,47 +876,6 @@ impl Forth {
}
}
Op::Except(word_span) => {
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("except count must be > 0".into());
}
let result = ctx.iter as i64 % n != 0;
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::EveryOffset(word_span) => {
let offset = pop_int(stack)?;
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("every+ count must be > 0".into());
}
let result = ctx.iter as i64 % n == offset.rem_euclid(n);
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::ExceptOffset(word_span) => {
let offset = pop_int(stack)?;
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("except+ count must be > 0".into());
}
let result = ctx.iter as i64 % n != offset.rem_euclid(n);
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::Bjork(word_span) | Op::PBjork(word_span) => {
let n = pop_int(stack)?;
let k = pop_int(stack)?;
@@ -1278,37 +1194,6 @@ impl Forth {
cmd.clear();
}
Op::EmitAll => {
// Retroactive: patch existing sound outputs with current params
if !cmd.params().is_empty() {
let step_duration = ctx.step_duration();
for output in outputs.iter_mut() {
if output.starts_with("/sound/") {
use std::fmt::Write;
for (k, v) in cmd.params() {
let val_str = v.to_param_string();
if !output.ends_with('/') {
output.push('/');
}
if is_tempo_scaled_param(k) {
if let Ok(val) = val_str.parse::<f64>() {
let _ = write!(output, "{k}/{}", val * step_duration);
continue;
}
}
let _ = write!(output, "{k}/{val_str}");
}
}
}
}
// Prospective: store for future emits
cmd.commit_global();
}
Op::ClearGlobal => {
cmd.clear_global();
}
Op::IntRange => {
let end = pop_int(stack)?;
let start = pop_int(stack)?;
@@ -1607,47 +1492,6 @@ impl Forth {
.unwrap_or(0);
stack.push(Value::Int(val as i64, None));
}
Op::Mark => {
marks.push(stack.len());
}
Op::Count(span) => {
let mark = marks.pop().ok_or("count without mark")?;
stack.push(Value::Int((stack.len() - mark) as i64, *span));
}
Op::Index(word_span) => {
let idx = pop_int(stack)?;
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("index count must be > 0".into());
}
let resolved_idx = ((idx % count as i64 + count as i64) % count as i64) as usize;
if let Some(span) = word_span {
if stack.len() >= count {
let start = stack.len() - count;
let selected = &stack[start + resolved_idx];
record_resolved_from_value(&trace_cell, Some(*span), selected);
}
}
drain_select_run(count, resolved_idx, stack, outputs, cmd)?;
}
Op::Rec => {
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}", name.as_str()?));
}
Op::Overdub => {
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?));
}
Op::Orec => {
let orbit = pop(stack)?.as_int()?;
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/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));
}
Op::Forget => {
let name = pop(stack)?;
self.dict.lock().remove(name.as_str()?);
@@ -1820,8 +1664,8 @@ fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
groups.into_iter().partition(|g| g[0]);
for _ in 0..min_count {
let mut one = ones.pop().expect("ones sufficient for min_count");
one.extend(zeros.pop().expect("zeros sufficient for min_count"));
let mut one = ones.pop().unwrap();
one.extend(zeros.pop().unwrap());
new_groups.push(one);
}
new_groups.extend(ones);
@@ -1882,21 +1726,6 @@ fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, String> {
Ok(pop(stack)?.is_truthy())
}
/// Drain the stack, returning non-quotation values.
/// Quotations are pushed back onto the stack (transparent).
fn drain_skip_quotations(stack: &mut Vec<Value>) -> Vec<Value> {
let values = std::mem::take(stack);
let mut result = Vec::new();
for v in values {
if matches!(v, Value::Quotation(..)) {
stack.push(v);
} else {
result.push(v);
}
}
result
}
fn ensure(stack: &[Value], n: usize) -> Result<(), String> {
if stack.len() < n {
return Err("stack underflow".into());
@@ -1914,67 +1743,23 @@ fn float_to_value(result: f64) -> Value {
fn lift_unary<F>(val: Value, f: F) -> Result<Value, String>
where
F: Fn(f64) -> f64 + Copy,
F: Fn(f64) -> f64,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
v => Ok(float_to_value(f(v.as_float()?))),
}
Ok(float_to_value(f(val.as_float()?)))
}
fn lift_unary_int<F>(val: Value, f: F) -> Result<Value, String>
where
F: Fn(i64) -> i64 + Copy,
F: Fn(i64) -> i64,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
v => Ok(Value::Int(f(v.as_int()?), None)),
}
Ok(Value::Int(f(val.as_int()?), None))
}
fn lift_binary<F>(a: Value, b: Value, f: F) -> Result<Value, String>
where
F: Fn(f64, f64) -> f64 + Copy,
F: Fn(f64, f64) -> f64,
{
match (a, b) {
(Value::ArpList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(a, Value::ArpList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(Value::CycleList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
(a, Value::CycleList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
(a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))),
}
Ok(float_to_value(f(a.as_float()?, b.as_float()?)))
}
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>

View File

@@ -1,5 +1,3 @@
//! Word-to-Op translation: maps Forth word names to compiled instructions.
use std::sync::Arc;
use crate::ops::Op;
@@ -13,7 +11,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"dup" => Op::Dup,
"dupn" => Op::Dupn,
"drop" => Op::Drop,
"print" => Op::Print,
"swap" => Op::Swap,
"over" => Op::Over,
"rot" => Op::Rot,
@@ -59,7 +56,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"nand" => Op::Nand,
"nor" => Op::Nor,
"ifelse" => Op::IfElse,
"select" => Op::Pick,
"pick" => Op::Pick,
"sound" => Op::NewCmd,
"." => Op::Emit,
"rand" => Op::Rand(None),
@@ -70,12 +67,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"pcycle" => Op::PCycle(None),
"choose" => Op::Choose(None),
"bounce" => Op::Bounce(None),
"pbounce" => Op::PBounce(None),
"wchoose" => Op::WChoose(None),
"every" => Op::Every(None),
"except" => Op::Except(None),
"every+" => Op::EveryOffset(None),
"except+" => Op::ExceptOffset(None),
"bjork" => Op::Bjork(None),
"pbjork" => Op::PBjork(None),
"chance" => Op::ChanceExec(None),
@@ -101,8 +94,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"loop" => Op::Loop,
"oct" => Op::Oct,
"clear" => Op::ClearCmd,
"all" => Op::EmitAll,
"noall" => Op::ClearGlobal,
".." => Op::IntRange,
".," => Op::StepRange,
"gen" => Op::Generate,
@@ -116,12 +107,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"mstart" => Op::MidiStart,
"mstop" => Op::MidiStop,
"mcont" => Op::MidiContinue,
"rec" => Op::Rec,
"overdub" | "dub" => Op::Overdub,
"orec" => Op::Orec,
"odub" => Op::Odub,
"forget" => Op::Forget,
"index" => Op::Index(None),
"key!" => Op::SetKey,
"tp" => Op::Transpose,
"inv" => Op::Invert,
@@ -215,10 +201,9 @@ fn attach_span(op: &mut Op, span: SourceSpan) {
match op {
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
| Op::Bounce(s) | Op::PBounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
| Op::Every(s) | Op::Except(s) | Op::EveryOffset(s) | Op::ExceptOffset(s)
| Op::Bjork(s) | Op::PBjork(s)
| Op::Count(s) | Op::Index(s) => *s = Some(span),
| Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
| Op::Every(s)
| Op::Bjork(s) | Op::PBjork(s) => *s = Some(span),
_ => {}
}
}

View File

@@ -1,6 +1,6 @@
//! Word metadata for core language primitives (stack, arithmetic, logic, variables, definitions).
use super::{Word, WordCompile::*};
// Stack, Arithmetic, Comparison, Logic, Control, Variables, Definitions
pub(super) const WORDS: &[Word] = &[
// Stack manipulation
Word {
@@ -33,16 +33,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "print",
aliases: &[],
category: "Stack",
stack: "(x --)",
desc: "Print top of stack to footer bar",
example: "42 print",
compile: Simple,
varargs: false,
},
Word {
name: "swap",
aliases: &[],
@@ -512,17 +502,17 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(true-quot false-quot bool --)",
desc: "Execute true-quot if true, else false-quot",
example: "( 1 ) ( 2 ) coin ifelse",
example: "{ 1 } { 2 } coin ifelse",
compile: Simple,
varargs: false,
},
Word {
name: "select",
name: "pick",
aliases: &[],
category: "Logic",
stack: "(..quots n --)",
desc: "Execute nth quotation (0-indexed)",
example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
example: "{ 1 } { 2 } { 3 } 2 pick => 3",
compile: Simple,
varargs: true,
},
@@ -532,7 +522,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(quot bool --)",
desc: "Execute quotation if true",
example: "( 2 distort ) 0.5 chance ?",
example: "{ 2 distort } 0.5 chance ?",
compile: Simple,
varargs: false,
},
@@ -542,7 +532,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(quot bool --)",
desc: "Execute quotation if false",
example: "( 1 distort ) 0.5 chance !?",
example: "{ 1 distort } 0.5 chance !?",
compile: Simple,
varargs: false,
},
@@ -552,7 +542,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(quot --)",
desc: "Execute quotation unconditionally",
example: "( 2 * ) apply",
example: "{ 2 * } apply",
compile: Simple,
varargs: false,
},
@@ -563,7 +553,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Control",
stack: "(n quot --)",
desc: "Execute quotation n times, @i holds current index",
example: "4 ( @i . ) times => 0 1 2 3",
example: "4 { @i . } times => 0 1 2 3",
compile: Simple,
varargs: false,
},

View File

@@ -1,6 +1,6 @@
//! Word metadata for audio effect parameters (filter, envelope, reverb, delay, lo-fi, stereo, mod FX).
use super::{Word, WordCompile::*};
// Filter, Envelope, Reverb, Delay, Lo-fi, Stereo, Mod FX
pub(super) const WORDS: &[Word] = &[
// Envelope
Word {
@@ -959,45 +959,4 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
// Compressor
Word {
name: "comp",
aliases: &[],
category: "Compressor",
stack: "(v.. --)",
desc: "Set sidechain duck amount (0-1)",
example: "0.8 comp",
compile: Param,
varargs: true,
},
Word {
name: "compattack",
aliases: &["cattack"],
category: "Compressor",
stack: "(v.. --)",
desc: "Set compressor attack time in seconds",
example: "0.01 compattack",
compile: Param,
varargs: true,
},
Word {
name: "comprelease",
aliases: &["crelease"],
category: "Compressor",
stack: "(v.. --)",
desc: "Set compressor release time in seconds",
example: "0.15 comprelease",
compile: Param,
varargs: true,
},
Word {
name: "comporbit",
aliases: &["corbit"],
category: "Compressor",
stack: "(v.. --)",
desc: "Set sidechain source orbit",
example: "0 comporbit",
compile: Param,
varargs: true,
},
];

View File

@@ -1,7 +1,6 @@
//! MIDI word definitions: channel, CC, pitch bend, transport, and device routing.
use super::{Word, WordCompile::*};
// MIDI
pub(super) const WORDS: &[Word] = &[
Word {
name: "chan",

View File

@@ -1,5 +1,3 @@
//! Built-in word definitions and lookup for the Forth VM.
mod compile;
mod core;
mod effects;
@@ -13,7 +11,6 @@ use std::sync::LazyLock;
pub(crate) use compile::compile_word;
/// How a word is compiled into ops.
#[derive(Clone, Copy)]
pub enum WordCompile {
Simple,
@@ -22,7 +19,6 @@ pub enum WordCompile {
Probability(f64),
}
/// Metadata for a built-in Forth word.
#[derive(Clone, Copy)]
pub struct Word {
pub name: &'static str,
@@ -35,7 +31,6 @@ pub struct Word {
pub varargs: bool,
}
/// All built-in words, aggregated from every category module.
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
let mut words = Vec::new();
words.extend_from_slice(self::core::WORDS);
@@ -47,7 +42,6 @@ pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
words
});
/// Index mapping word names and aliases to their definitions.
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
let mut map = HashMap::with_capacity(WORDS.len() * 2);
for word in WORDS.iter() {
@@ -59,7 +53,6 @@ static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(
map
});
/// Find a word by name or alias.
pub fn lookup_word(name: &str) -> Option<&'static Word> {
WORD_MAP.get(name).copied()
}

View File

@@ -1,7 +1,6 @@
//! Word definitions for music theory, harmony, and chord construction.
use super::{Word, WordCompile::*};
// Music, Chord
pub(super) const WORDS: &[Word] = &[
// Music
Word {

View File

@@ -1,7 +1,6 @@
//! Word metadata for sequencing: probability, timing, context queries, generators.
use super::{Word, WordCompile::*};
// Time, Context, Probability, Generator, Desktop
pub(super) const WORDS: &[Word] = &[
// Probability
Word {
@@ -60,7 +59,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot prob --)",
desc: "Execute quotation with probability (0.0-1.0)",
example: "( 2 distort ) 0.75 chance",
example: "{ 2 distort } 0.75 chance",
compile: Simple,
varargs: false,
},
@@ -70,7 +69,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot pct --)",
desc: "Execute quotation with probability (0-100)",
example: "( 2 distort ) 75 prob",
example: "{ 2 distort } 75 prob",
compile: Simple,
varargs: false,
},
@@ -114,26 +113,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: true,
},
Word {
name: "pbounce",
aliases: &[],
category: "Probability",
stack: "(v1..vn n -- selected)",
desc: "Ping-pong cycle through n items by pattern iteration",
example: "60 64 67 72 4 pbounce",
compile: Simple,
varargs: true,
},
Word {
name: "index",
aliases: &[],
category: "Probability",
stack: "(v1..vn n idx -- selected)",
desc: "Select item at explicit index",
example: "[ c4 e4 g4 ] step index",
compile: Simple,
varargs: true,
},
Word {
name: "wchoose",
aliases: &[],
@@ -150,7 +129,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Always execute quotation",
example: "( 2 distort ) always",
example: "{ 2 distort } always",
compile: Probability(1.0),
varargs: false,
},
@@ -160,7 +139,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Never execute quotation",
example: "( 2 distort ) never",
example: "{ 2 distort } never",
compile: Probability(0.0),
varargs: false,
},
@@ -170,7 +149,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 75% of the time",
example: "( 2 distort ) often",
example: "{ 2 distort } often",
compile: Probability(0.75),
varargs: false,
},
@@ -180,7 +159,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 50% of the time",
example: "( 2 distort ) sometimes",
example: "{ 2 distort } sometimes",
compile: Probability(0.5),
varargs: false,
},
@@ -190,7 +169,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 25% of the time",
example: "( 2 distort ) rarely",
example: "{ 2 distort } rarely",
compile: Probability(0.25),
varargs: false,
},
@@ -200,7 +179,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 10% of the time",
example: "( 2 distort ) almostNever",
example: "{ 2 distort } almostNever",
compile: Probability(0.1),
varargs: false,
},
@@ -210,7 +189,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 90% of the time",
example: "( 2 distort ) almostAlways",
example: "{ 2 distort } almostAlways",
compile: Probability(0.9),
varargs: false,
},
@@ -221,37 +200,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time",
stack: "(quot n --)",
desc: "Execute quotation every nth iteration",
example: "( 2 distort ) 4 every",
compile: Simple,
varargs: false,
},
Word {
name: "except",
aliases: &[],
category: "Time",
stack: "(quot n --)",
desc: "Execute quotation on all iterations except every nth",
example: "( 2 distort ) 4 except",
compile: Simple,
varargs: false,
},
Word {
name: "every+",
aliases: &[],
category: "Time",
stack: "(quot n offset --)",
desc: "Execute quotation every nth iteration with phase offset",
example: "( snare ) 4 2 every+ => fires at iter 2, 6, 10...",
compile: Simple,
varargs: false,
},
Word {
name: "except+",
aliases: &[],
category: "Time",
stack: "(quot n offset --)",
desc: "Skip quotation every nth iteration with phase offset",
example: "( snare ) 4 2 except+ => skips at iter 2, 6, 10...",
example: "{ 2 distort } 4 every",
compile: Simple,
varargs: false,
},
@@ -261,7 +210,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time",
stack: "(quot k n --)",
desc: "Execute quotation using Euclidean distribution over step runs",
example: "( 2 distort ) 3 8 bjork",
example: "{ 2 distort } 3 8 bjork",
compile: Simple,
varargs: false,
},
@@ -271,7 +220,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time",
stack: "(quot k n --)",
desc: "Execute quotation using Euclidean distribution over pattern iterations",
example: "( 2 distort ) 3 8 pbjork",
example: "{ 2 distort } 3 8 pbjork",
compile: Simple,
varargs: false,
},
@@ -456,7 +405,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Desktop",
stack: "(-- bool)",
desc: "1 when mouse button held, 0 otherwise",
example: "mdown ( \"crash\" s . ) ?",
example: "mdown { \"crash\" s . } ?",
compile: Context("mdown"),
varargs: false,
},
@@ -487,7 +436,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Generator",
stack: "(quot n -- results...)",
desc: "Execute quotation n times, push all results",
example: "( 1 6 rand ) 4 gen => 4 random values",
example: "{ 1 6 rand } 4 gen => 4 random values",
compile: Simple,
varargs: true,
},

View File

@@ -1,7 +1,6 @@
//! Word metadata for sound commands, sample/oscillator params, FM, modulation, and LFO.
use super::{Word, WordCompile::*};
// Sound, Oscillator, Sample, Wavetable, FM, Modulation, LFO
pub(super) const WORDS: &[Word] = &[
// Sound
Word {
@@ -44,67 +43,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "all",
aliases: &[],
category: "Sound",
stack: "(--)",
desc: "Apply current params to all sounds",
example: "500 lpf 0.5 verb all",
compile: Simple,
varargs: false,
},
Word {
name: "noall",
aliases: &[],
category: "Sound",
stack: "(--)",
desc: "Clear global params",
example: "noall",
compile: Simple,
varargs: false,
},
// Recording
Word {
name: "rec",
aliases: &[],
category: "Sound",
stack: "(name --)",
desc: "Toggle recording audio output to named sample",
example: "\"loop1\" rec",
compile: Simple,
varargs: false,
},
Word {
name: "overdub",
aliases: &["dub"],
category: "Sound",
stack: "(name --)",
desc: "Toggle overdub recording onto existing named sample",
example: "\"loop1\" overdub",
compile: Simple,
varargs: false,
},
Word {
name: "orec",
aliases: &[],
category: "Sound",
stack: "(name orbit --)",
desc: "Toggle recording a single orbit into named sample",
example: "\"drums\" 0 orec",
compile: Simple,
varargs: false,
},
Word {
name: "odub",
aliases: &[],
category: "Sound",
stack: "(name orbit --)",
desc: "Toggle overdub recording a single orbit onto named sample",
example: "\"drums\" 0 odub",
compile: Simple,
varargs: false,
},
// Sample
Word {
name: "bank",
@@ -186,26 +124,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "slice",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Divide sample into N equal slices",
example: r#""break" s 8 slice 3 pick ."#,
compile: Param,
varargs: true,
},
Word {
name: "pick",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Select which slice to play (0-indexed, wraps)",
example: r#""break" s 8 slice 3 pick ."#,
compile: Param,
varargs: true,
},
Word {
name: "voice",
aliases: &[],

View File

@@ -1,15 +0,0 @@
# cagire-markdown
Markdown parser and renderer that produces ratatui-styled lines. Used for the built-in help/documentation views.
## Modules
| Module | Description |
|--------|-------------|
| `parser` | Markdown-to-styled-lines conversion |
| `highlighter` | `CodeHighlighter` trait for syntax highlighting in fenced code blocks |
| `theme` | Color mappings for markdown elements |
## Key Trait
- **`CodeHighlighter`** — Implement to provide language-specific syntax highlighting. Returns `Vec<(Style, String)>` per line.

View File

@@ -1,13 +1,9 @@
//! Syntax highlighting trait for fenced code blocks in markdown.
use ratatui::style::Style;
/// Produce styled spans from a single line of source code.
pub trait CodeHighlighter {
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
}
/// Pass-through highlighter that applies no styling.
pub struct NoHighlight;
impl CodeHighlighter for NoHighlight {

View File

@@ -1,5 +1,3 @@
//! Parse markdown into styled ratatui lines with pluggable syntax highlighting.
mod highlighter;
mod parser;
mod theme;

View File

@@ -1,5 +1,3 @@
//! Parse markdown text into styled ratatui lines with syntax-highlighted code blocks.
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line as RLine, Span};
@@ -7,20 +5,17 @@ use ratatui::text::{Line as RLine, Span};
use crate::highlighter::CodeHighlighter;
use crate::theme::MarkdownTheme;
/// Span of lines within a parsed document that form a fenced code block.
pub struct CodeBlock {
pub start_line: usize,
pub end_line: usize,
pub source: String,
}
/// Result of parsing a markdown string: styled lines and extracted code blocks.
pub struct ParsedMarkdown {
pub lines: Vec<RLine<'static>>,
pub code_blocks: Vec<CodeBlock>,
}
/// Parse markdown text into themed, syntax-highlighted ratatui lines.
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
md: &str,
theme: &T,
@@ -49,7 +44,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
let close_block = |start: Option<usize>,
source: &mut Vec<String>,
blocks: &mut Vec<CodeBlock>,
lines: &[RLine<'static>]| {
lines: &Vec<RLine<'static>>| {
if let Some(start) = start {
blocks.push(CodeBlock {
start_line: start,
@@ -123,7 +118,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
ParsedMarkdown { lines, code_blocks }
}
fn preprocess_markdown(md: &str) -> String {
pub fn preprocess_markdown(md: &str) -> String {
let mut out = String::with_capacity(md.len());
for line in md.lines() {
let line = convert_dash_lists(line);
@@ -167,7 +162,7 @@ fn preprocess_markdown(md: &str) -> String {
out
}
fn convert_dash_lists(line: &str) -> String {
pub fn convert_dash_lists(line: &str) -> String {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("- ") {
let indent = line.len() - trimmed.len();

View File

@@ -1,8 +1,5 @@
//! Style provider trait for markdown rendering.
use ratatui::style::{Color, Modifier, Style};
/// Style provider for each markdown element type.
pub trait MarkdownTheme {
fn h1(&self) -> Style;
fn h2(&self) -> Style;
@@ -19,7 +16,6 @@ pub trait MarkdownTheme {
fn table_row_odd(&self) -> Color;
}
/// Fallback theme with hardcoded terminal colors, used in tests.
pub struct DefaultTheme;
impl MarkdownTheme for DefaultTheme {

View File

@@ -10,9 +10,3 @@ description = "Project data structures for cagire sequencer"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rmp-serde = "1"
brotli = "7"
base64 = "0.22"
[dev-dependencies]
flate2 = "1"

View File

@@ -1,22 +0,0 @@
# cagire-project
Project data model and persistence for Cagire.
## Modules
| Module | Description |
|--------|-------------|
| `project` | `Project`, `Bank`, `Pattern`, `Step` structs and constants |
| `file` | File I/O (save/load) |
| `share` | Project sharing/export |
## Key Types
- **`Project`** — Top-level container: banks of patterns
- **`Bank`** — Collection of patterns
- **`Pattern`** — Sequence of steps with metadata
- **`Step`** — Single step holding a Forth script
## Constants
`MAX_BANKS=32`, `MAX_PATTERNS=32`, `MAX_STEPS=1024`

View File

@@ -1,17 +1,15 @@
//! JSON-based project file persistence with versioned format.
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::project::{Bank, PatternSpeed, Project};
use crate::project::{Bank, Project};
const VERSION: u8 = 1;
const EXTENSION: &str = "cagire";
pub const EXTENSION: &str = "cagire";
fn ensure_extension(path: &Path) -> PathBuf {
pub fn ensure_extension(path: &Path) -> PathBuf {
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
path.to_path_buf()
} else {
@@ -31,24 +29,6 @@ struct ProjectFile {
playing_patterns: Vec<(usize, usize)>,
#[serde(default, skip_serializing_if = "String::is_empty")]
prelude: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
script: String,
#[serde(default, skip_serializing_if = "is_default_speed")]
script_speed: PatternSpeed,
#[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")]
script_length: usize,
}
fn is_default_speed(s: &PatternSpeed) -> bool {
*s == PatternSpeed::default()
}
fn default_script_length() -> usize {
16
}
fn is_default_script_length(n: &usize) -> bool {
*n == default_script_length()
}
fn default_tempo() -> f64 {
@@ -64,9 +44,6 @@ impl From<&Project> for ProjectFile {
tempo: project.tempo,
playing_patterns: project.playing_patterns.clone(),
prelude: project.prelude.clone(),
script: project.script.clone(),
script_speed: project.script_speed,
script_length: project.script_length,
}
}
}
@@ -79,16 +56,12 @@ impl From<ProjectFile> for Project {
tempo: file.tempo,
playing_patterns: file.playing_patterns,
prelude: file.prelude,
script: file.script,
script_speed: file.script_speed,
script_length: file.script_length,
};
project.normalize();
project
}
}
/// Error returned by project save/load operations.
#[derive(Debug)]
pub enum FileError {
Io(io::Error),
@@ -118,7 +91,6 @@ impl From<serde_json::Error> for FileError {
}
}
/// Write a project to disk as pretty-printed JSON, returning the final path.
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
let path = ensure_extension(path);
let file = ProjectFile::from(project);
@@ -127,13 +99,11 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
Ok(path)
}
/// Read a project from a `.cagire` file on disk.
pub fn load(path: &Path) -> Result<Project, FileError> {
let json = fs::read_to_string(path)?;
load_str(&json)
}
/// Parse a project from a JSON string.
pub fn load_str(json: &str) -> Result<Project, FileError> {
let file: ProjectFile = serde_json::from_str(json)?;
if file.version > VERSION {

View File

@@ -2,15 +2,10 @@
mod file;
mod project;
pub mod share;
/// Maximum number of banks in a project.
pub const MAX_BANKS: usize = 32;
/// Maximum number of patterns per bank.
pub const MAX_PATTERNS: usize = 32;
/// Maximum number of steps per pattern.
pub const MAX_STEPS: usize = 1024;
/// Default pattern length in steps.
pub const DEFAULT_LENGTH: usize = 16;
pub use file::{load, load_str, save, FileError};

View File

@@ -6,7 +6,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
/// Speed multiplier for a pattern, expressed as a rational fraction.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct PatternSpeed {
pub num: u8,
@@ -38,12 +37,10 @@ impl PatternSpeed {
Self::OCTO,
];
/// Return the speed as a floating-point multiplier.
pub fn multiplier(&self) -> f64 {
self.num as f64 / self.denom as f64
}
/// Format as a human-readable label (e.g. "2x", "1/4x").
pub fn label(&self) -> String {
if self.denom == 1 {
format!("{}x", self.num)
@@ -52,7 +49,6 @@ impl PatternSpeed {
}
}
/// Return the next faster preset, or self if already at maximum.
pub fn next(&self) -> Self {
let current = self.multiplier();
Self::PRESETS
@@ -62,7 +58,6 @@ impl PatternSpeed {
.unwrap_or(*self)
}
/// Return the next slower preset, or self if already at minimum.
pub fn prev(&self) -> Self {
let current = self.multiplier();
Self::PRESETS
@@ -73,7 +68,6 @@ impl PatternSpeed {
.unwrap_or(*self)
}
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
pub fn from_label(s: &str) -> Option<Self> {
let s = s.trim().trim_end_matches('x');
if let Some((num, denom)) = s.split_once('/') {
@@ -145,7 +139,6 @@ impl<'de> Deserialize<'de> for PatternSpeed {
}
}
/// Quantization grid for launching patterns.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum LaunchQuantization {
Immediate,
@@ -158,7 +151,6 @@ pub enum LaunchQuantization {
}
impl LaunchQuantization {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Immediate => "Immediate",
@@ -170,7 +162,6 @@ impl LaunchQuantization {
}
}
/// Cycle to the next longer quantization, clamped at `Bars8`.
pub fn next(&self) -> Self {
match self {
Self::Immediate => Self::Beat,
@@ -182,7 +173,6 @@ impl LaunchQuantization {
}
}
/// Cycle to the next shorter quantization, clamped at `Immediate`.
pub fn prev(&self) -> Self {
match self {
Self::Immediate => Self::Immediate,
@@ -195,7 +185,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]
@@ -204,7 +193,6 @@ pub enum SyncMode {
}
impl SyncMode {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Reset => "Reset",
@@ -212,7 +200,6 @@ impl SyncMode {
}
}
/// Toggle between Reset and PhaseLock.
pub fn toggle(&self) -> Self {
match self {
Self::Reset => Self::PhaseLock,
@@ -221,7 +208,6 @@ impl SyncMode {
}
}
/// What happens when a pattern finishes: loop, stop, or chain to another.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum FollowUp {
#[default]
@@ -231,7 +217,6 @@ pub enum FollowUp {
}
impl FollowUp {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Loop => "Loop",
@@ -240,7 +225,6 @@ impl FollowUp {
}
}
/// Cycle forward through follow-up modes.
pub fn next_mode(&self) -> Self {
match self {
Self::Loop => Self::Stop,
@@ -249,7 +233,6 @@ impl FollowUp {
}
}
/// Cycle backward through follow-up modes.
pub fn prev_mode(&self) -> Self {
match self {
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
@@ -263,7 +246,6 @@ fn is_default_follow_up(f: &FollowUp) -> bool {
*f == FollowUp::default()
}
/// Single step in a pattern, holding a Forth script and optional metadata.
#[derive(Clone, Serialize, Deserialize)]
pub struct Step {
pub active: bool,
@@ -275,12 +257,10 @@ pub struct Step {
}
impl Step {
/// True if all fields are at their default values.
pub fn is_default(&self) -> bool {
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
}
/// True if the script is non-empty.
pub fn has_content(&self) -> bool {
!self.script.is_empty()
}
@@ -297,14 +277,12 @@ impl Default for Step {
}
}
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
#[derive(Clone)]
pub struct Pattern {
pub steps: Vec<Step>,
pub length: usize,
pub speed: PatternSpeed,
pub name: Option<String>,
pub description: Option<String>,
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp,
@@ -339,8 +317,6 @@ struct SparsePattern {
speed: PatternSpeed,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "is_default_quantization")]
quantization: LaunchQuantization,
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
@@ -366,8 +342,6 @@ struct LegacyPattern {
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
quantization: LaunchQuantization,
#[serde(default)]
sync_mode: SyncMode,
@@ -396,7 +370,6 @@ impl Serialize for Pattern {
length: self.length,
speed: self.speed,
name: self.name.clone(),
description: self.description.clone(),
quantization: self.quantization,
sync_mode: self.sync_mode,
follow_up: self.follow_up,
@@ -432,7 +405,6 @@ impl<'de> Deserialize<'de> for Pattern {
length: sparse.length,
speed: sparse.speed,
name: sparse.name,
description: sparse.description,
quantization: sparse.quantization,
sync_mode: sparse.sync_mode,
follow_up: sparse.follow_up,
@@ -443,7 +415,6 @@ impl<'de> Deserialize<'de> for Pattern {
length: legacy.length,
speed: legacy.speed,
name: legacy.name,
description: legacy.description,
quantization: legacy.quantization,
sync_mode: legacy.sync_mode,
follow_up: legacy.follow_up,
@@ -459,7 +430,6 @@ impl Default for Pattern {
length: DEFAULT_LENGTH,
speed: PatternSpeed::default(),
name: None,
description: None,
quantization: LaunchQuantization::default(),
sync_mode: SyncMode::default(),
follow_up: FollowUp::default(),
@@ -468,17 +438,14 @@ impl Default for Pattern {
}
impl Pattern {
/// Borrow a step by index.
pub fn step(&self, index: usize) -> Option<&Step> {
self.steps.get(index)
}
/// Mutably borrow a step by index.
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
self.steps.get_mut(index)
}
/// Set the active length, clamped to `[1, MAX_STEPS]`.
pub fn set_length(&mut self, length: usize) {
let length = length.clamp(1, MAX_STEPS);
while self.steps.len() < length {
@@ -487,7 +454,6 @@ impl Pattern {
self.length = length;
}
/// Follow the source chain from `index` to find the originating step.
pub fn resolve_source(&self, index: usize) -> usize {
let mut current = index;
for _ in 0..self.steps.len() {
@@ -504,22 +470,20 @@ impl Pattern {
index
}
/// Return the script at the resolved source of `index`.
pub fn resolve_script(&self, index: usize) -> Option<&str> {
let source_idx = self.resolve_source(index);
self.steps.get(source_idx).map(|s| s.script.as_str())
}
/// Count active-length steps that have a script or a source reference.
pub fn content_step_count(&self) -> usize {
self.steps[..self.length]
.iter()
.filter(|s| s.has_content() || s.source.is_some())
.count()
}
}
/// Collection of patterns forming a bank.
#[derive(Clone, Serialize, Deserialize)]
pub struct Bank {
pub patterns: Vec<Pattern>,
@@ -528,7 +492,6 @@ pub struct Bank {
}
impl Bank {
/// Count patterns that contain at least one non-empty step.
pub fn content_pattern_count(&self) -> usize {
self.patterns
.iter()
@@ -546,7 +509,6 @@ impl Default for Bank {
}
}
/// Top-level project: banks, tempo, sample paths, and prelude script.
#[derive(Clone, Serialize, Deserialize)]
pub struct Project {
pub banks: Vec<Bank>,
@@ -558,22 +520,12 @@ pub struct Project {
pub playing_patterns: Vec<(usize, usize)>,
#[serde(default)]
pub prelude: String,
#[serde(default)]
pub script: String,
#[serde(default)]
pub script_speed: PatternSpeed,
#[serde(default = "default_script_length")]
pub script_length: usize,
}
fn default_tempo() -> f64 {
120.0
}
fn default_script_length() -> usize {
16
}
impl Default for Project {
fn default() -> Self {
Self {
@@ -582,25 +534,19 @@ impl Default for Project {
tempo: default_tempo(),
playing_patterns: Vec::new(),
prelude: String::new(),
script: String::new(),
script_speed: PatternSpeed::default(),
script_length: default_script_length(),
}
}
}
impl Project {
/// Borrow a pattern by bank and pattern index.
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
&self.banks[bank].patterns[pattern]
}
/// Mutably borrow a pattern by bank and pattern index.
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
&mut self.banks[bank].patterns[pattern]
}
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
pub fn normalize(&mut self) {
self.banks.resize_with(MAX_BANKS, Bank::default);
for bank in &mut self.banks {

View File

@@ -1,214 +0,0 @@
//! Pattern and project sharing via compact text strings.
//!
//! Export: data → MessagePack → Brotli → base64 URL-safe → prefix
//! Import: strip prefix → base64 decode → Brotli decompress → MessagePack → data
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use crate::{Bank, Pattern};
const PATTERN_PREFIX: &str = "cgr:";
const BANK_PREFIX: &str = "cgrb:";
/// Error during pattern or bank import/export.
#[derive(Debug)]
pub enum ShareError {
InvalidPrefix,
Base64(base64::DecodeError),
Decompress(std::io::Error),
Deserialize(rmp_serde::decode::Error),
Serialize(rmp_serde::encode::Error),
Compress(std::io::Error),
}
impl std::fmt::Display for ShareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidPrefix => write!(f, "missing cgr:/cgrb: prefix"),
Self::Base64(e) => write!(f, "base64: {e}"),
Self::Decompress(e) => write!(f, "decompress: {e}"),
Self::Deserialize(e) => write!(f, "deserialize: {e}"),
Self::Serialize(e) => write!(f, "serialize: {e}"),
Self::Compress(e) => write!(f, "compress: {e}"),
}
}
}
fn compress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
let mut output = Vec::new();
let params = brotli::enc::BrotliEncoderParams {
quality: 11,
lgwin: 22,
lgblock: 0,
..Default::default()
};
brotli::BrotliCompress(&mut &data[..], &mut output, &params).map_err(ShareError::Compress)?;
Ok(output)
}
fn decompress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
let mut output = Vec::new();
brotli::BrotliDecompress(&mut &data[..], &mut output).map_err(ShareError::Decompress)?;
Ok(output)
}
fn encode<T: serde::Serialize>(value: &T, prefix: &str) -> Result<String, ShareError> {
let packed = rmp_serde::to_vec_named(value).map_err(ShareError::Serialize)?;
let compressed = compress(&packed)?;
let encoded = URL_SAFE_NO_PAD.encode(&compressed);
Ok(format!("{prefix}{encoded}"))
}
fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T, ShareError> {
let text = text.trim();
let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?;
let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?;
let packed = decompress(&compressed)?;
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
}
/// Encode a pattern as a shareable `cgr:` string.
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
encode(pattern, PATTERN_PREFIX)
}
/// Decode a `cgr:` string back into a pattern.
pub fn import(text: &str) -> Result<Pattern, ShareError> {
decode(text, PATTERN_PREFIX)
}
/// Encode a bank as a shareable `cgrb:` string.
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
encode(bank, BANK_PREFIX)
}
/// Decode a `cgrb:` string back into a bank.
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
decode(text, BANK_PREFIX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Step;
#[test]
fn roundtrip_empty() {
let pattern = Pattern::default();
let encoded = export(&pattern).expect("export pattern");
assert!(encoded.starts_with("cgr:"));
let decoded = import(&encoded).expect("import pattern");
assert_eq!(decoded.length, pattern.length);
assert_eq!(decoded.steps.len(), pattern.steps.len());
}
#[test]
fn roundtrip_with_steps() {
let mut pattern = Pattern::default();
pattern.steps[0] = Step {
active: true,
script: "kick 60 note".to_string(),
source: None,
name: Some("kick".to_string()),
};
pattern.steps[1] = Step {
active: false,
script: "snare".to_string(),
source: None,
name: None,
};
pattern.steps[3] = Step {
active: true,
script: String::new(),
source: Some(0),
name: None,
};
pattern.length = 8;
pattern.name = Some("Test".to_string());
let encoded = export(&pattern).expect("export pattern");
let decoded = import(&encoded).expect("import pattern");
assert_eq!(decoded.length, 8);
assert_eq!(decoded.name.as_deref(), Some("Test"));
assert_eq!(decoded.steps[0].script, "kick 60 note");
assert_eq!(decoded.steps[0].name.as_deref(), Some("kick"));
assert!(!decoded.steps[1].active);
assert_eq!(decoded.steps[1].script, "snare");
assert_eq!(decoded.steps[3].source, Some(0));
}
#[test]
fn bad_prefix() {
assert!(matches!(import("xxx:abc"), Err(ShareError::InvalidPrefix)));
}
#[test]
fn bad_base64() {
assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_))));
}
#[test]
fn whitespace_trimming() {
let pattern = Pattern::default();
let encoded = export(&pattern).expect("export pattern");
let padded = format!(" {encoded} \n");
let decoded = import(&padded).expect("import padded pattern");
assert_eq!(decoded.length, pattern.length);
}
#[test]
fn msgpack_brotli_smaller_than_json_deflate() {
let mut pattern = Pattern::default();
for i in 0..16 {
pattern.steps[i] = Step {
active: true,
script: format!("kick {i} note 0.5 dur"),
source: None,
name: Some(format!("step_{i}")),
};
}
pattern.length = 16;
// Current (msgpack+brotli)
let new_encoded = export(&pattern).expect("export pattern");
// Old pipeline (json+deflate) for comparison
use std::io::Write;
let json = serde_json::to_vec(&pattern).expect("serialize json");
let mut encoder =
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
encoder.write_all(&json).expect("write to encoder");
let old_compressed = encoder.finish().expect("finish encoder");
let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));
assert!(
new_encoded.len() < old_encoded.len(),
"msgpack+brotli ({}) should be smaller than json+deflate ({})",
new_encoded.len(),
old_encoded.len()
);
}
#[test]
fn roundtrip_bank() {
let mut bank = Bank::default();
bank.patterns[0].steps[0] = Step {
active: true,
script: "kick 60 note".to_string(),
source: None,
name: Some("kick".to_string()),
};
bank.patterns[0].length = 8;
bank.name = Some("Drums".to_string());
let encoded = export_bank(&bank).expect("export bank");
assert!(encoded.starts_with("cgrb:"));
let decoded = import_bank(&encoded).expect("import bank");
assert_eq!(decoded.name.as_deref(), Some("Drums"));
assert_eq!(decoded.patterns[0].length, 8);
assert_eq!(decoded.patterns[0].steps[0].script, "kick 60 note");
}
}

View File

@@ -11,4 +11,4 @@ description = "TUI components for cagire sequencer"
rand = "0.8"
ratatui = "0.30"
regex = "1"
tui-textarea = { git = "https://github.com/phsym/tui-textarea", rev = "e2ec4d3", features = ["search"] }
tui-textarea = { git = "https://github.com/phsym/tui-textarea", branch = "main", features = ["search"] }

View File

@@ -1,25 +0,0 @@
# cagire-ratatui
TUI widget library and theme system for Cagire.
## Widgets
`category_list`, `confirm`, `editor`, `file_browser`, `hint_bar`, `lissajous`, `list_select`, `modal`, `nav_minimap`, `props_form`, `sample_browser`, `scope`, `scroll_indicators`, `search_bar`, `section_header`, `sparkles`, `spectrum`, `text_input`, `vu_meter`, `waveform`
## Theme System
The `theme/` module provides a palette-based theming system using Oklab color space.
| Module | Description |
|--------|-------------|
| `mod` | `THEMES` array, `CURRENT_THEME` thread-local, `get()`/`set()` |
| `palette` | `Palette` (14 fields), color manipulation helpers (`shift`, `mix`, `tint_bg`, ...) |
| `build` | Derives ~190 `ThemeColors` fields from a `Palette` |
| `transform` | HSV-based hue rotation for generated palettes |
25 built-in themes.
## Key Types
- **`Palette`** — 14-field color definition, input to theme generation
- **`ThemeColors`** — ~190 derived semantic colors used throughout the UI

View File

@@ -1,5 +1,3 @@
//! Collapsible categorized list widget with section headers.
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, List, ListItem};
@@ -7,20 +5,17 @@ use ratatui::Frame;
use crate::theme;
/// Entry in a category list: either a section header or a leaf item.
pub struct CategoryItem<'a> {
pub label: &'a str,
pub is_section: bool,
pub collapsed: bool,
}
/// What is currently selected: a leaf item or a section header.
pub enum Selection {
Item(usize),
Section(usize),
}
/// Scrollable list with collapsible section headers.
pub struct CategoryList<'a> {
items: &'a [CategoryItem<'a>],
selection: Selection,

View File

@@ -1,5 +1,3 @@
//! Yes/No confirmation dialog widget.
use crate::theme;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::Style;
@@ -9,7 +7,6 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal dialog with Yes/No buttons.
pub struct ConfirmModal<'a> {
title: &'a str,
message: &'a str,

View File

@@ -1,5 +1,3 @@
//! Script editor widget with completion, search, and sample finder popups.
use std::cell::Cell;
use crate::theme;
@@ -12,10 +10,8 @@ use ratatui::{
};
use tui_textarea::TextArea;
/// Callback that syntax-highlights a single line, returning styled spans (bool = annotation).
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
/// Metadata for a single autocomplete entry.
#[derive(Clone)]
pub struct CompletionCandidate {
pub name: String,
@@ -82,7 +78,6 @@ impl SearchState {
}
}
/// Multi-line text editor backed by tui_textarea.
pub struct Editor {
text: TextArea<'static>,
completion: CompletionState,
@@ -104,14 +99,6 @@ impl Editor {
self.text.is_selecting()
}
pub fn move_cursor_to(&mut self, row: u16, col: u16) {
self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col));
}
pub fn scroll_offset(&self) -> u16 {
self.scroll_offset.get()
}
pub fn copy(&mut self) {
self.text.copy();
}
@@ -124,14 +111,6 @@ impl Editor {
self.text.paste()
}
pub fn yank_text(&self) -> String {
self.text.yank_text()
}
pub fn set_yank_text(&mut self, text: impl Into<String>) {
self.text.set_yank_text(text);
}
pub fn select_all(&mut self) {
self.text.select_all();
}
@@ -159,11 +138,7 @@ impl Editor {
}
pub fn set_content(&mut self, lines: Vec<String>) {
let yank = self.text.yank_text();
self.text = TextArea::new(lines);
if !yank.is_empty() {
self.text.set_yank_text(yank);
}
self.completion.active = false;
self.sample_finder.active = false;
self.search.query.clear();
@@ -487,7 +462,7 @@ impl Editor {
if is_cursor {
cursor_style
} else if is_selected {
base_style.bg(selection_style.bg.expect("selection style has bg"))
base_style.bg(selection_style.bg.unwrap())
} else {
base_style
}
@@ -707,7 +682,6 @@ impl Editor {
}
}
/// Score a fuzzy match of `query` against `target`. Lower is better; `None` if no match.
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
let query_lower: Vec<char> = query.to_lowercase().chars().collect();

View File

@@ -1,5 +1,3 @@
//! File/directory browser modal widget.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
@@ -9,7 +7,6 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal listing files and directories with a filter input line.
pub struct FileBrowserModal<'a> {
title: &'a str,
input: &'a str,

View File

@@ -1,11 +1,8 @@
//! Bottom-bar keyboard hint renderer.
use ratatui::text::{Line, Span};
use ratatui::style::Style;
use crate::theme;
/// Build a styled line of key/action pairs for the hint bar.
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
let theme = theme::get();
let key_style = Style::default().fg(theme.hint.key);

View File

@@ -38,7 +38,7 @@ pub use scroll_indicators::{render_scroll_indicators, IndicatorAlign};
pub use search_bar::render_search_bar;
pub use section_header::render_section_header;
pub use sparkles::Sparkles;
pub use spectrum::{Spectrum, SpectrumStyle};
pub use spectrum::Spectrum;
pub use text_input::TextInputModal;
pub use vu_meter::VuMeter;
pub use waveform::Waveform;

View File

@@ -1,5 +1,3 @@
//! Lissajous XY oscilloscope widget using braille characters.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -9,22 +7,12 @@ use std::cell::RefCell;
thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
static TRAIL: RefCell<TrailState> = const { RefCell::new(TrailState { fine_w: 0, fine_h: 0, heat: Vec::new() }) };
}
struct TrailState {
fine_w: usize,
fine_h: usize,
heat: Vec<f32>,
}
/// XY oscilloscope plotting left vs right channels as a Lissajous curve.
pub struct Lissajous<'a> {
left: &'a [f32],
right: &'a [f32],
color: Option<Color>,
gain: f32,
trails: bool,
}
impl<'a> Lissajous<'a> {
@@ -33,25 +21,13 @@ impl<'a> Lissajous<'a> {
left,
right,
color: None,
gain: 1.0,
trails: false,
}
}
pub fn trails(mut self, enabled: bool) -> Self {
self.trails = enabled;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
}
impl Widget for Lissajous<'_> {
@@ -60,16 +36,6 @@ impl Widget for Lissajous<'_> {
return;
}
if self.trails {
self.render_trails(area, buf);
} else {
self.render_normal(area, buf);
}
}
}
impl Lissajous<'_> {
fn render_normal(self, area: Rect, buf: &mut Buffer) {
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
let width = area.width as usize;
let height = area.height as usize;
@@ -77,6 +43,14 @@ impl Lissajous<'_> {
let fine_height = height * 4;
let len = self.left.len().min(self.right.len());
let peak = self
.left
.iter()
.chain(self.right.iter())
.map(|s| s.abs())
.fold(0.0f32, f32::max);
let gain = if peak > 0.001 { 1.0 / peak } else { 1.0 };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
@@ -84,9 +58,10 @@ impl Lissajous<'_> {
patterns.resize(size, 0);
for i in 0..len {
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
let l = (self.left[i] * gain).clamp(-1.0, 1.0);
let r = (self.right[i] * gain).clamp(-1.0, 1.0);
// X = right channel, Y = left channel (inverted so up = positive)
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
let fine_y = ((1.0 - l) * 0.5 * (fine_height - 1) as f32).round() as usize;
let fine_x = fine_x.min(fine_width - 1);
@@ -97,7 +72,19 @@ impl Lissajous<'_> {
let dot_x = fine_x % 2;
let dot_y = fine_y % 4;
patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y);
let bit = match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
};
patterns[char_y * width + char_x] |= bit;
}
for cy in 0..height {
@@ -113,122 +100,4 @@ impl Lissajous<'_> {
}
});
}
fn render_trails(self, area: Rect, buf: &mut Buffer) {
let theme = theme::get();
let width = area.width as usize;
let height = area.height as usize;
let fine_w = width * 2;
let fine_h = height * 4;
let len = self.left.len().min(self.right.len());
TRAIL.with(|t| {
let mut trail = t.borrow_mut();
// Reset if dimensions changed
if trail.fine_w != fine_w || trail.fine_h != fine_h {
trail.fine_w = fine_w;
trail.fine_h = fine_h;
trail.heat.clear();
trail.heat.resize(fine_w * fine_h, 0.0);
}
// Decay existing heat
for h in trail.heat.iter_mut() {
*h *= 0.85;
}
// Plot new sample points
for i in 0..len {
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
let fx = ((r + 1.0) * 0.5 * (fine_w - 1) as f32).round() as usize;
let fy = ((1.0 - l) * 0.5 * (fine_h - 1) as f32).round() as usize;
let fx = fx.min(fine_w - 1);
let fy = fy.min(fine_h - 1);
trail.heat[fy * fine_w + fx] = 1.0;
}
// Convert heat map to braille
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
patterns.clear();
patterns.resize(width * height, 0);
// Track brightest color per cell
let mut colors: Vec<Option<Color>> = vec![None; width * height];
for fy in 0..fine_h {
for fx in 0..fine_w {
let h = trail.heat[fy * fine_w + fx];
if h < 0.05 {
continue;
}
let cx = fx / 2;
let cy = fy / 4;
let dx = fx % 2;
let dy = fy % 4;
let idx = cy * width + cx;
patterns[idx] |= braille_bit(dx, dy);
let dot_color = if h > 0.7 {
theme.meter.high
} else if h > 0.25 {
theme.meter.mid
} else {
theme.meter.low
};
let replace = match colors[idx] {
None => true,
Some(cur) => {
rank_color(dot_color, &theme) > rank_color(cur, &theme)
}
};
if replace {
colors[idx] = Some(dot_color);
}
}
}
for cy in 0..height {
for cx in 0..width {
let idx = cy * width + cx;
let pattern = patterns[idx];
if pattern != 0 {
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
let color = colors[idx].unwrap_or(theme.meter.low);
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
});
});
}
}
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
}
}
fn rank_color(c: Color, theme: &crate::theme::ThemeColors) -> u8 {
if c == theme.meter.high { 2 }
else if c == theme.meter.mid { 1 }
else { 0 }
}

View File

@@ -1,5 +1,3 @@
//! Scrollable single-select list widget with cursor highlight.
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
@@ -7,7 +5,6 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
/// Scrollable list with a highlighted cursor and selected-item marker.
pub struct ListSelect<'a> {
items: &'a [String],
selected: usize,

View File

@@ -1,12 +1,9 @@
//! Centered modal frame with border and title.
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Frame;
/// Centered modal overlay with titled border.
pub struct ModalFrame<'a> {
title: &'a str,
width: u16,

View File

@@ -1,5 +1,3 @@
//! Page navigation minimap showing a 3x2 grid of tiles.
use crate::theme;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::Style;

View File

@@ -1,5 +1,3 @@
//! Vertical label/value property form renderer.
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
@@ -7,7 +5,6 @@ use ratatui::Frame;
use crate::theme;
/// Render a vertical list of label/value pairs with selection highlight.
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
let theme = theme::get();

View File

@@ -1,13 +1,10 @@
//! Tree-view sample browser with search filtering.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
/// Node type in the sample tree.
#[derive(Clone, Copy)]
pub enum TreeLineKind {
Root { expanded: bool },
@@ -15,7 +12,6 @@ pub enum TreeLineKind {
File,
}
/// A single row in the sample browser tree.
#[derive(Clone)]
pub struct TreeLine {
pub depth: u8,
@@ -25,7 +21,6 @@ pub struct TreeLine {
pub index: usize,
}
/// Tree-view browser for navigating sample folders.
pub struct SampleBrowser<'a> {
entries: &'a [TreeLine],
cursor: usize,
@@ -116,13 +111,13 @@ impl<'a> SampleBrowser<'a> {
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
let height = area.height as usize;
if self.entries.is_empty() {
if self.search_query.is_empty() {
self.render_empty_guide(frame, area, colors);
let msg = if self.search_query.is_empty() {
"No samples loaded"
} else {
let line =
Line::from(Span::styled("No matches", Style::new().fg(colors.browser.empty_text)));
"No matches"
};
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
frame.render_widget(Paragraph::new(vec![line]), area);
}
return;
}
@@ -179,47 +174,4 @@ impl<'a> SampleBrowser<'a> {
frame.render_widget(Paragraph::new(lines), area);
}
fn render_empty_guide(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
let muted = Style::new().fg(colors.browser.empty_text);
let heading = Style::new().fg(colors.ui.text_primary);
let key = Style::new().fg(colors.hint.key);
let desc = Style::new().fg(colors.hint.text);
let code = Style::new().fg(colors.ui.accent);
let lines = vec![
Line::from(Span::styled(" No samples loaded.", muted)),
Line::from(""),
Line::from(Span::styled(" Load from the Engine page:", heading)),
Line::from(""),
Line::from(vec![
Span::styled(" F6 ", key),
Span::styled("Go to Engine page", desc),
]),
Line::from(vec![
Span::styled(" A ", key),
Span::styled("Add a sample folder", desc),
]),
Line::from(""),
Line::from(Span::styled(" Organize samples like this:", heading)),
Line::from(""),
Line::from(Span::styled(" samples/", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} kick/", code)),
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} kick.wav", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} snare/", code)),
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} snare.wav", code)),
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} hats/", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} closed.wav", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} open.wav", code)),
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} pedal.wav", code)),
Line::from(""),
Line::from(Span::styled(" Folders become Forth words:", heading)),
Line::from(""),
Line::from(Span::styled(" kick sound .", code)),
Line::from(Span::styled(" hats sound 2 n .", code)),
Line::from(Span::styled(" snare sound 0.5 speed .", code)),
];
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
}
}

View File

@@ -1,5 +1,3 @@
//! Oscilloscope waveform widget using braille characters.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -11,14 +9,12 @@ thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// Rendering direction for the oscilloscope.
#[derive(Clone, Copy)]
pub enum Orientation {
Horizontal,
Vertical,
}
/// Single-channel oscilloscope using braille dot plotting.
pub struct Scope<'a> {
data: &'a [f32],
orientation: Orientation,
@@ -45,11 +41,6 @@ impl<'a> Scope<'a> {
self.color = Some(c);
self
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
}
impl Widget for Scope<'_> {
@@ -75,6 +66,9 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
let fine_width = width * 2;
let fine_height = height * 4;
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
@@ -83,7 +77,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
for fine_x in 0..fine_width {
let sample_idx = (fine_x * data.len()) / fine_width;
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
let fine_y = fine_y.min(fine_height - 1);
@@ -128,6 +122,9 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
let fine_width = width * 2;
let fine_height = height * 4;
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
@@ -136,7 +133,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
for fine_y in 0..fine_height {
let sample_idx = (fine_y * data.len()) / fine_height;
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
let fine_x = fine_x.min(fine_width - 1);

View File

@@ -1,17 +1,13 @@
//! Up/down arrow scroll indicators for bounded lists.
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
/// Horizontal alignment for scroll indicators.
pub enum IndicatorAlign {
Center,
Right,
}
/// Render up/down scroll arrows when content overflows.
pub fn render_scroll_indicators(
frame: &mut Frame,
area: Rect,

View File

@@ -1,5 +1,3 @@
//! Inline search bar with active/inactive styling.
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
@@ -8,7 +6,6 @@ use ratatui::Frame;
use crate::theme;
/// Render a `/query` search bar.
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
let theme = theme::get();
let style = if active {

View File

@@ -1,5 +1,3 @@
//! Section header with horizontal divider for engine-view panels.
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
@@ -7,7 +5,6 @@ use ratatui::Frame;
use crate::theme;
/// Render a section title with a horizontal divider below it.
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
let theme = theme::get();
let [header_area, divider_area] =

View File

@@ -1,5 +1,3 @@
//! Decorative particle effect using random Unicode glyphs.
use crate::theme;
use rand::Rng;
use ratatui::buffer::Buffer;
@@ -16,7 +14,6 @@ struct Sparkle {
life: u8,
}
/// Animated sparkle particles for visual flair.
#[derive(Default)]
pub struct Sparkles {
sparkles: Vec<Sparkle>,

View File

@@ -1,58 +1,18 @@
//! 32-band frequency spectrum display with optional peak hold.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
use std::cell::RefCell;
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum SpectrumStyle {
#[default]
Bars,
Line,
Filled,
}
thread_local! {
static PEAKS: RefCell<[f32; 32]> = const { RefCell::new([0.0; 32]) };
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// 32-band spectrum analyzer using block characters.
pub struct Spectrum<'a> {
data: &'a [f32; 32],
gain: f32,
style: SpectrumStyle,
peaks: bool,
}
impl<'a> Spectrum<'a> {
pub fn new(data: &'a [f32; 32]) -> Self {
Self {
data,
gain: 1.0,
style: SpectrumStyle::Bars,
peaks: false,
}
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
pub fn style(mut self, s: SpectrumStyle) -> Self {
self.style = s;
self
}
pub fn peaks(mut self, enabled: bool) -> Self {
self.peaks = enabled;
self
Self { data }
}
}
@@ -62,43 +22,6 @@ impl Widget for Spectrum<'_> {
return;
}
// Update peak hold state
let peak_values = if self.peaks {
Some(PEAKS.with(|p| {
let mut peaks = p.borrow_mut();
for (i, &mag) in self.data.iter().enumerate() {
let v = (mag * self.gain).min(1.0);
if v >= peaks[i] {
peaks[i] = v;
} else {
peaks[i] = (peaks[i] - 0.02).max(v);
}
}
*peaks
}))
} else {
None
};
match self.style {
SpectrumStyle::Bars => render_bars(self.data, area, buf, self.gain, peak_values.as_ref()),
SpectrumStyle::Line => render_braille(self.data, area, buf, self.gain, false, peak_values.as_ref()),
SpectrumStyle::Filled => render_braille(self.data, area, buf, self.gain, true, peak_values.as_ref()),
}
}
}
fn band_color(ratio: f32, colors: &theme::ThemeColors) -> Color {
if ratio < 0.33 {
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
} else if ratio < 0.66 {
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
} else {
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
}
}
fn render_bars(data: &[f32; 32], area: Rect, buf: &mut Buffer, gain: f32, peaks: Option<&[f32; 32]>) {
let colors = theme::get();
let height = area.height as f32;
let base = area.width as usize / 32;
@@ -108,131 +31,36 @@ fn render_bars(data: &[f32; 32], area: Rect, buf: &mut Buffer, gain: f32, peaks:
}
let mut x_start = area.x;
for (band, &mag) in data.iter().enumerate() {
for (band, &mag) in self.data.iter().enumerate() {
let w = base + if band < remainder { 1 } else { 0 };
if w == 0 {
continue;
}
let bar_height = (mag * gain).min(1.0) * height;
let bar_height = mag * height;
let full_cells = bar_height as usize;
let frac = bar_height - full_cells as f32;
let frac_idx = (frac * 8.0) as usize;
// Peak hold row
let peak_row = peaks.map(|p| {
let ph = p[band] * height;
let row = (height - ph).max(0.0) as usize;
row.min(area.height as usize - 1)
});
for row in 0..area.height as usize {
let y = area.y + area.height - 1 - row as u16;
let ratio = row as f32 / area.height as f32;
let color = band_color(ratio, &colors);
let color = if ratio < 0.33 {
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
} else if ratio < 0.66 {
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
} else {
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
};
for dx in 0..w as u16 {
let x = x_start + dx;
if row < full_cells {
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
} else if row == full_cells && frac_idx > 0 {
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
} else if let Some(pr) = peak_row {
// peak_row is from top (0 = top), row is from bottom
let from_top = area.height as usize - 1 - row;
if from_top == pr {
buf[(x, y)].set_char('─').set_fg(colors.meter.high);
}
}
}
}
x_start += w as u16;
}
}
fn render_braille(
data: &[f32; 32],
area: Rect,
buf: &mut Buffer,
gain: f32,
filled: bool,
peaks: Option<&[f32; 32]>,
) {
let colors = theme::get();
let width = area.width as usize;
let height = area.height as usize;
let fine_w = width * 2;
let fine_h = height * 4;
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
patterns.clear();
patterns.resize(width * height, 0);
// Interpolate 32 bands across fine_w columns
for fx in 0..fine_w {
let band_f = fx as f32 * 31.0 / (fine_w - 1).max(1) as f32;
let lo = band_f as usize;
let hi = (lo + 1).min(31);
let t = band_f - lo as f32;
let mag = ((data[lo] * (1.0 - t) + data[hi] * t) * gain).min(1.0);
let fy = ((1.0 - mag) * (fine_h - 1) as f32).round() as usize;
let fy = fy.min(fine_h - 1);
if filled {
for y in fy..fine_h {
let cy = y / 4;
let dy = y % 4;
let cx = fx / 2;
let dx = fx % 2;
patterns[cy * width + cx] |= braille_bit(dx, dy);
}
} else {
let cy = fy / 4;
let dy = fy % 4;
let cx = fx / 2;
let dx = fx % 2;
patterns[cy * width + cx] |= braille_bit(dx, dy);
}
// Peak dots
if let Some(pk) = peaks {
let pv = (pk[lo] * (1.0 - t) + pk[hi] * t).min(1.0);
let py = ((1.0 - pv) * (fine_h - 1) as f32).round() as usize;
let py = py.min(fine_h - 1);
let cy = py / 4;
let dy = py % 4;
let cx = fx / 2;
let dx = fx % 2;
patterns[cy * width + cx] |= braille_bit(dx, dy);
}
}
for cy in 0..height {
for cx in 0..width {
let pattern = patterns[cy * width + cx];
if pattern != 0 {
let ratio = 1.0 - (cy as f32 / height as f32);
let color = band_color(ratio, &colors);
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
});
}
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
}
}

View File

@@ -1,5 +1,3 @@
//! Single-line text input modal with optional hint.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
@@ -9,7 +7,6 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal dialog with a single-line text input.
pub struct TextInputModal<'a> {
title: &'a str,
input: &'a str,

View File

@@ -1,9 +1,6 @@
//! Derive [`ThemeColors`] from a [`Palette`].
use super::*;
use super::palette::{Palette, Rgb, darken, mid, rgb, tint};
/// Build a complete [`ThemeColors`] from a [`Palette`].
pub fn build(p: &Palette) -> ThemeColors {
let darker_bg = darken(p.bg, 0.15);

View File

@@ -1,5 +1,3 @@
//! Catppuccin Latte palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Catppuccin Mocha palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Dracula palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Eden palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Ember palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,41 +0,0 @@
//! Everforest palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (45, 53, 59),
surface: (52, 62, 68),
surface2: (68, 80, 86),
fg: (211, 198, 170),
fg_dim: (135, 131, 116),
fg_muted: (80, 80, 68),
accent: (167, 192, 128),
red: (230, 126, 128),
green: (167, 192, 128),
yellow: (219, 188, 127),
blue: (127, 187, 179),
purple: (214, 153, 182),
cyan: (131, 192, 146),
orange: (230, 152, 117),
tempo_color: (214, 153, 182),
bank_color: (127, 187, 179),
pattern_color: (131, 192, 146),
title_accent: (167, 192, 128),
title_author: (127, 187, 179),
secondary: (230, 152, 117),
link_bright: [
(167, 192, 128), (214, 153, 182), (230, 152, 117),
(127, 187, 179), (219, 188, 127),
],
link_dim: [
(56, 66, 46), (70, 52, 62), (72, 52, 42),
(44, 64, 60), (70, 62, 44),
],
sparkle: [
(167, 192, 128), (230, 152, 117), (131, 192, 146),
(214, 153, 182), (219, 188, 127),
],
meter: [(148, 172, 110), (200, 170, 108), (210, 108, 110)],
}
}

View File

@@ -1,5 +1,3 @@
//! Fairyfloss palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,41 +0,0 @@
//! Fauve palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (28, 22, 18),
surface: (42, 33, 26),
surface2: (58, 46, 36),
fg: (240, 228, 210),
fg_dim: (170, 150, 130),
fg_muted: (100, 82, 66),
accent: (230, 60, 20),
red: (220, 38, 32),
green: (30, 170, 80),
yellow: (255, 210, 0),
blue: (20, 80, 200),
purple: (170, 40, 150),
cyan: (0, 150, 180),
orange: (240, 120, 0),
tempo_color: (230, 60, 20),
bank_color: (20, 80, 200),
pattern_color: (0, 150, 180),
title_accent: (230, 60, 20),
title_author: (20, 80, 200),
secondary: (170, 40, 150),
link_bright: [
(230, 60, 20), (20, 80, 200), (240, 120, 0),
(0, 150, 180), (30, 170, 80),
],
link_dim: [
(72, 24, 10), (10, 28, 65), (76, 40, 6),
(6, 48, 58), (14, 54, 28),
],
sparkle: [
(230, 60, 20), (255, 210, 0), (30, 170, 80),
(20, 80, 200), (170, 40, 150),
],
meter: [(26, 152, 72), (235, 190, 0), (200, 34, 28)],
}
}

View File

@@ -1,7 +1,6 @@
//! Georges palette (C64 colors on black).
use super::palette::Palette;
// C64 palette on pure black
pub fn palette() -> Palette {
Palette {
bg: (0, 0, 0),

View File

@@ -1,5 +1,3 @@
//! Gruvbox Dark palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Hot Dog Stand palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,41 +0,0 @@
//! Iceberg palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (22, 24, 33),
surface: (30, 33, 46),
surface2: (45, 48, 64),
fg: (198, 200, 209),
fg_dim: (109, 112, 126),
fg_muted: (64, 66, 78),
accent: (132, 160, 198),
red: (226, 120, 120),
green: (180, 190, 130),
yellow: (226, 164, 120),
blue: (132, 160, 198),
purple: (160, 147, 199),
cyan: (137, 184, 194),
orange: (226, 164, 120),
tempo_color: (160, 147, 199),
bank_color: (132, 160, 198),
pattern_color: (137, 184, 194),
title_accent: (132, 160, 198),
title_author: (160, 147, 199),
secondary: (226, 164, 120),
link_bright: [
(132, 160, 198), (160, 147, 199), (226, 164, 120),
(137, 184, 194), (180, 190, 130),
],
link_dim: [
(45, 55, 70), (55, 50, 68), (70, 55, 42),
(46, 62, 66), (58, 62, 44),
],
sparkle: [
(132, 160, 198), (226, 164, 120), (180, 190, 130),
(160, 147, 199), (226, 120, 120),
],
meter: [(160, 175, 115), (210, 150, 105), (200, 105, 105)],
}
}

View File

@@ -1,41 +0,0 @@
//! Jaipur palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (30, 24, 22),
surface: (44, 36, 32),
surface2: (60, 48, 42),
fg: (238, 222, 200),
fg_dim: (165, 145, 125),
fg_muted: (95, 78, 65),
accent: (210, 90, 100),
red: (200, 44, 52),
green: (30, 160, 120),
yellow: (240, 180, 20),
blue: (60, 60, 180),
purple: (150, 50, 120),
cyan: (0, 155, 155),
orange: (220, 120, 50),
tempo_color: (210, 90, 100),
bank_color: (60, 60, 180),
pattern_color: (0, 155, 155),
title_accent: (210, 90, 100),
title_author: (60, 60, 180),
secondary: (220, 120, 50),
link_bright: [
(210, 90, 100), (60, 60, 180), (220, 120, 50),
(0, 155, 155), (30, 160, 120),
],
link_dim: [
(66, 30, 34), (22, 22, 58), (70, 40, 18),
(6, 48, 48), (12, 50, 38),
],
sparkle: [
(210, 90, 100), (240, 180, 20), (30, 160, 120),
(60, 60, 180), (150, 50, 120),
],
meter: [(26, 144, 106), (222, 164, 18), (184, 40, 46)],
}
}

View File

@@ -1,5 +1,3 @@
//! Kanagawa palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Letz Light palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -8,22 +8,17 @@ mod catppuccin_mocha;
mod dracula;
mod eden;
mod ember;
mod everforest;
mod georges;
mod fairyfloss;
mod gruvbox_dark;
mod hot_dog_stand;
mod iceberg;
mod jaipur;
mod kanagawa;
mod letz_light;
mod monochrome_black;
mod monochrome_white;
mod monokai;
mod nord;
mod fauve;
mod pitch_black;
mod tropicalia;
mod rose_pine;
mod tokyo_night;
pub mod transform;
@@ -31,14 +26,12 @@ pub mod transform;
use ratatui::style::Color;
use std::cell::RefCell;
/// Entry in the theme registry: id, display label, and palette constructor.
pub struct ThemeEntry {
pub id: &'static str,
pub label: &'static str,
pub palette: fn() -> palette::Palette,
}
/// All available themes.
pub const THEMES: &[ThemeEntry] = &[
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", palette: catppuccin_mocha::palette },
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", palette: catppuccin_latte::palette },
@@ -58,28 +51,20 @@ pub const THEMES: &[ThemeEntry] = &[
ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette },
ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette },
ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette },
ThemeEntry { id: "Iceberg", label: "Iceberg", palette: iceberg::palette },
ThemeEntry { id: "Everforest", label: "Everforest", palette: everforest::palette },
ThemeEntry { id: "Fauve", label: "Fauve", palette: fauve::palette },
ThemeEntry { id: "Tropicalia", label: "Tropicalia", palette: tropicalia::palette },
ThemeEntry { id: "Jaipur", label: "Jaipur", palette: jaipur::palette },
];
thread_local! {
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new(build::build(&(THEMES[0].palette)()));
}
/// Return the current thread-local theme.
pub fn get() -> ThemeColors {
CURRENT_THEME.with(|t| t.borrow().clone())
}
/// Set the current thread-local theme.
pub fn set(theme: ThemeColors) {
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
}
/// Complete set of resolved colors for all UI components.
#[derive(Clone)]
pub struct ThemeColors {
pub ui: UiColors,
@@ -110,7 +95,6 @@ pub struct ThemeColors {
pub confirm: ConfirmColors,
}
/// Core UI colors: background, text, borders.
#[derive(Clone)]
pub struct UiColors {
pub bg: Color,
@@ -125,7 +109,6 @@ pub struct UiColors {
pub surface: Color,
}
/// Playback status bar colors.
#[derive(Clone)]
pub struct StatusColors {
pub playing_bg: Color,
@@ -137,7 +120,6 @@ pub struct StatusColors {
pub fill_bg: Color,
}
/// Step grid selection and cursor colors.
#[derive(Clone)]
pub struct SelectionColors {
pub cursor_bg: Color,
@@ -151,7 +133,6 @@ pub struct SelectionColors {
pub in_range: Color,
}
/// Step tile colors for various states.
#[derive(Clone)]
pub struct TileColors {
pub playing_active_bg: Color,
@@ -169,7 +150,6 @@ pub struct TileColors {
pub link_dim: [(u8, u8, u8); 5],
}
/// Top header bar segment colors.
#[derive(Clone)]
pub struct HeaderColors {
pub tempo_bg: Color,
@@ -182,7 +162,6 @@ pub struct HeaderColors {
pub stats_fg: Color,
}
/// Modal dialog border colors.
#[derive(Clone)]
pub struct ModalColors {
pub border: Color,
@@ -196,7 +175,6 @@ pub struct ModalColors {
pub preview: Color,
}
/// Flash notification colors.
#[derive(Clone)]
pub struct FlashColors {
pub error_bg: Color,
@@ -207,7 +185,6 @@ pub struct FlashColors {
pub info_fg: Color,
}
/// Pattern list row state colors.
#[derive(Clone)]
pub struct ListColors {
pub playing_bg: Color,
@@ -226,7 +203,6 @@ pub struct ListColors {
pub soloed_fg: Color,
}
/// Ableton Link status indicator colors.
#[derive(Clone)]
pub struct LinkStatusColors {
pub disabled: Color,
@@ -234,7 +210,6 @@ pub struct LinkStatusColors {
pub listening: Color,
}
/// Syntax highlighting (fg, bg) pairs per token category.
#[derive(Clone)]
pub struct SyntaxColors {
pub gap_bg: Color,
@@ -259,35 +234,30 @@ pub struct SyntaxColors {
pub default: (Color, Color),
}
/// Alternating table row colors.
#[derive(Clone)]
pub struct TableColors {
pub row_even: Color,
pub row_odd: Color,
}
/// Value display colors.
#[derive(Clone)]
pub struct ValuesColors {
pub tempo: Color,
pub value: Color,
}
/// Keyboard hint key/text colors.
#[derive(Clone)]
pub struct HintColors {
pub key: Color,
pub text: Color,
}
/// View badge pill colors.
#[derive(Clone)]
pub struct ViewBadgeColors {
pub bg: Color,
pub fg: Color,
}
/// Navigation minimap tile colors.
#[derive(Clone)]
pub struct NavColors {
pub selected_bg: Color,
@@ -296,7 +266,6 @@ pub struct NavColors {
pub unselected_fg: Color,
}
/// Script editor colors.
#[derive(Clone)]
pub struct EditorWidgetColors {
pub cursor_bg: Color,
@@ -308,7 +277,6 @@ pub struct EditorWidgetColors {
pub completion_example: Color,
}
/// File and sample browser colors.
#[derive(Clone)]
pub struct BrowserColors {
pub directory: Color,
@@ -323,7 +291,6 @@ pub struct BrowserColors {
pub empty_text: Color,
}
/// Text input field colors.
#[derive(Clone)]
pub struct InputColors {
pub text: Color,
@@ -331,7 +298,6 @@ pub struct InputColors {
pub hint: Color,
}
/// Search bar and match highlight colors.
#[derive(Clone)]
pub struct SearchColors {
pub active: Color,
@@ -340,7 +306,6 @@ pub struct SearchColors {
pub match_fg: Color,
}
/// Markdown renderer colors.
#[derive(Clone)]
pub struct MarkdownColors {
pub h1: Color,
@@ -355,7 +320,6 @@ pub struct MarkdownColors {
pub list: Color,
}
/// Engine view panel colors.
#[derive(Clone)]
pub struct EngineColors {
pub header: Color,
@@ -378,7 +342,6 @@ pub struct EngineColors {
pub hint_inactive: Color,
}
/// Dictionary view colors.
#[derive(Clone)]
pub struct DictColors {
pub word_name: Color,
@@ -396,7 +359,6 @@ pub struct DictColors {
pub header_desc: Color,
}
/// Title screen colors.
#[derive(Clone)]
pub struct TitleColors {
pub big_title: Color,
@@ -407,7 +369,6 @@ pub struct TitleColors {
pub subtitle: Color,
}
/// VU meter and spectrum level colors.
#[derive(Clone)]
pub struct MeterColors {
pub low: Color,
@@ -418,13 +379,11 @@ pub struct MeterColors {
pub high_rgb: (u8, u8, u8),
}
/// Sparkle particle colors.
#[derive(Clone)]
pub struct SparkleColors {
pub colors: [(u8, u8, u8); 5],
}
/// Confirm dialog colors.
#[derive(Clone)]
pub struct ConfirmColors {
pub border: Color,

View File

@@ -1,5 +1,3 @@
//! Monochrome (black background) palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Monochrome (white background) palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Monokai palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Nord palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,11 +1,7 @@
//! Palette definition and color mixing utilities.
use ratatui::style::Color;
/// RGB color triple.
pub type Rgb = (u8, u8, u8);
/// Base color palette that themes are derived from.
pub struct Palette {
// Core
pub bg: Rgb,
@@ -37,12 +33,10 @@ pub struct Palette {
pub meter: [Rgb; 3],
}
/// Convert an RGB triple to a ratatui [`Color`].
pub fn rgb(c: Rgb) -> Color {
Color::Rgb(c.0, c.1, c.2)
}
/// Blend `bg` toward `accent` by `amount` (0.01.0).
pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
let mix = |b: u8, a: u8| -> u8 {
let v = b as f32 + (a as f32 - b as f32) * amount;
@@ -51,12 +45,10 @@ pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
(mix(bg.0, accent.0), mix(bg.1, accent.1), mix(bg.2, accent.2))
}
/// Linearly interpolate between two colors.
pub fn mid(a: Rgb, b: Rgb, t: f32) -> Rgb {
tint(a, b, t)
}
/// Darken a color by reducing brightness.
pub fn darken(c: Rgb, amount: f32) -> Rgb {
let d = |v: u8| -> u8 { (v as f32 * (1.0 - amount)).clamp(0.0, 255.0) as u8 };
(d(c.0), d(c.1), d(c.2))

View File

@@ -1,5 +1,3 @@
//! Pitch Black palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Rose Pine palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Tokyo Night palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Hue rotation for palette-wide color transforms.
use super::palette::{Palette, Rgb};
use super::build::build;
use super::ThemeColors;
@@ -64,7 +62,6 @@ fn rotate3(arr: [Rgb; 3], d: f32) -> [Rgb; 3] {
[rotate(arr[0], d), rotate(arr[1], d), rotate(arr[2], d)]
}
/// Build a [`ThemeColors`] with all palette hues rotated by `degrees`.
pub fn rotate_palette(palette: &Palette, degrees: f32) -> ThemeColors {
if degrees == 0.0 {
return build(palette);

View File

@@ -1,41 +0,0 @@
//! Tropicalia palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (20, 26, 22),
surface: (30, 40, 34),
surface2: (44, 56, 48),
fg: (235, 225, 200),
fg_dim: (155, 145, 120),
fg_muted: (85, 80, 62),
accent: (230, 50, 120),
red: (240, 70, 70),
green: (80, 200, 50),
yellow: (255, 195, 0),
blue: (0, 160, 200),
purple: (180, 60, 180),
cyan: (0, 200, 170),
orange: (255, 140, 30),
tempo_color: (230, 50, 120),
bank_color: (0, 160, 200),
pattern_color: (0, 200, 170),
title_accent: (230, 50, 120),
title_author: (0, 160, 200),
secondary: (255, 140, 30),
link_bright: [
(230, 50, 120), (0, 160, 200), (255, 140, 30),
(0, 200, 170), (80, 200, 50),
],
link_dim: [
(72, 20, 40), (6, 50, 64), (80, 44, 12),
(6, 62, 54), (26, 62, 18),
],
sparkle: [
(230, 50, 120), (255, 195, 0), (80, 200, 50),
(0, 160, 200), (180, 60, 180),
],
meter: [(70, 182, 44), (236, 178, 0), (220, 62, 62)],
}
}

View File

@@ -1,5 +1,3 @@
//! Stereo VU meter with dB-scaled level display.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -10,7 +8,6 @@ const DB_MIN: f32 = -48.0;
const DB_MAX: f32 = 3.0;
const DB_RANGE: f32 = DB_MAX - DB_MIN;
/// Stereo VU meter displaying left/right levels in dB.
pub struct VuMeter {
left: f32,
right: f32,

View File

@@ -1,5 +1,3 @@
//! Filled waveform display using braille characters.
use crate::scope::Orientation;
use crate::theme;
use ratatui::buffer::Buffer;
@@ -12,7 +10,6 @@ thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// Filled waveform renderer using braille dot plotting.
pub struct Waveform<'a> {
data: &'a [f32],
orientation: Orientation,
@@ -84,6 +81,9 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
let fine_height = height * 4;
let len = data.len();
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
patterns.clear();
@@ -97,7 +97,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
let mut min_s = f32::MAX;
let mut max_s = f32::MIN;
for &s in slice {
let s = (s * gain).clamp(-1.0, 1.0);
let s = (s * auto_gain).clamp(-1.0, 1.0);
if s < min_s {
min_s = s;
}
@@ -142,6 +142,9 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
let fine_height = height * 4;
let len = data.len();
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
patterns.clear();
@@ -155,7 +158,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
let mut min_s = f32::MAX;
let mut max_s = f32::MIN;
for &s in slice {
let s = (s * gain).clamp(-1.0, 1.0);
let s = (s * auto_gain).clamp(-1.0, 1.0);
if s < min_s {
min_s = s;
}

View File

@@ -7,11 +7,11 @@
"steps": [
{
"i": 0,
"script": "0 8 12 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n( inv ) rarely\n( inv ) sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n2 release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand \n0.0 1.0 rand timbre\n0.5 gain\n0.8 sustain\n2 8 rand release\n."
"script": "0 8 12 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n{ inv } rarely\n{ inv } sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n2 release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand \n0.0 1.0 rand timbre\n0.5 gain\n0.8 sustain\n2 8 rand release\n."
},
{
"i": 4,
"script": "0 12 20 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n( inv ) rarely\n( inv ) sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n10 16 rand release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand 0.0 1.0 rand timbre\n0.5 gain\n( . ) 2 every"
"script": "0 12 20 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n{ inv } rarely\n{ inv } sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n10 16 rand release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand 0.0 1.0 rand timbre\n0.5 gain\n{ . } 2 every"
}
],
"length": 16,

View File

@@ -35,41 +35,6 @@ saw s
Parameters can appear in any order. They accumulate until you emit. You can clear the register using the `clear` word.
## Global Parameters
Use `all` to apply parameters globally. Global parameters persist across all patterns and steps until cleared with `noall`. They work both prospectively (before sounds) and retroactively (after sounds):
```forth
;; Prospective: set params before emitting
500 lpf 0.5 verb all
kick s 60 note . ;; gets lpf=500 verb=0.5
hat s 70 note . ;; gets lpf=500 verb=0.5
```
```forth
;; Retroactive: patch already-emitted sounds
kick s 60 note .
hat s 70 note .
500 lpf 0.5 verb all ;; both outputs get lpf and verb
```
Per-sound parameters override global ones:
```forth
500 lpf all
kick s 2000 lpf . ;; lpf=2000 (per-sound wins)
hat s . ;; lpf=500 (global)
```
Use `noall` to clear global parameters:
```forth
500 lpf all
kick s . ;; gets lpf
noall
hat s . ;; no lpf
```
## Controlling Existing Voices
You can emit without a sound name. In this case, no new voice is created. Instead, the parameters are sent to control an existing voice. Use `voice` with an ID to target a specific voice:

View File

@@ -20,17 +20,15 @@ The engine scans these directories and builds a registry of available samples. S
```
samples/
├── kick/ → "kick"
│ └── kick.wav
├── snare/ → "snare"
│ └── snare.wav
├── kick.wav → "kick"
├── snare.wav → "snare"
└── hats/
├── closed.wav → "hats" n 0
├── open.wav → "hats" n 1
└── pedal.wav → "hats" n 2
```
Folders at the root of your sample directory become sample banks named after the folder. Each file within a folder gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
Folders at the root of your directory are used as the name of a sample bank. Folders create sample banks where each file gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
## Playing Samples
@@ -47,8 +45,6 @@ snare sound 0.5 speed . ( play snare at half speed )
| `n` | 0+ | Sample index within a folder (wraps around) |
| `begin` | 0-1 | Playback start position |
| `end` | 0-1 | Playback end position |
| `slice` | 1+ | Divide sample into N equal slices |
| `pick` | 0+ | Select which slice to play (0-indexed, wraps) |
| `speed` | any | Playback speed multiplier |
| `freq` | Hz | Base frequency for pitch tracking |
| `fit` | seconds | Stretch/compress sample to fit duration |
@@ -66,21 +62,6 @@ kick sound 0.5 end . ( play first half )
If begin is greater than end, they swap automatically.
## Slice and Pick
For evenly-spaced slicing, `slice` divides the sample into N equal parts and `pick` selects which one (0-indexed, wraps around).
```forth
break sound 8 slice 3 pick . ( play the 4th eighth of the sample )
break sound 16 slice step pick . ( scan through 16 slices by step )
```
Combine with `fit` to time-stretch each slice to a target duration. `fit` accounts for the sliced range automatically.
```forth
break sound 4 slice 2 pick 1 loop . ( quarter of the sample, fitted to 1 beat )
```
## Speed and Pitch
The `speed` parameter affects both tempo and pitch. A speed of 2 plays twice as fast and an octave higher.

View File

@@ -2,14 +2,12 @@
Forth is a _stack-based_ programming language created by Charles H. Moore in the early 1970s. It was designed with simplicity, directness, and interactive exploration in mind. Forth has been used for scientific work and embedded systems: it controlled telescopes and even ran on hardware aboard space missions. It evolved into many implementations targeting various architectures, but none of them really caught on. Nonetheless, the ideas behind Forth continue to attract people from very different, often unrelated fields. Today, Forth languages are used by hackers and artists for their unconventional nature. Forth is simple, direct, and beautiful to implement. Forth is an elegant, minimal language, easy to understand, extend, and tailor to a specific task. The Forth we use in Cagire is specialized in making live music. It is used as a DSL: a _Domain Specific Language_.
**TLDR:** Forth is a really nice language to play music with.
## Why Forth?
Most programming languages rely on a complex syntax of `variables`, `expressions` and `statements` like `x = 3 + 4` or `do_something(()=>bob(4))`. Forth works differently. It has almost no syntax at all. Instead, you push values onto a `stack` and apply `words` that transform them:
```forth
3 4 + print
3 4 +
```
The program above leaves the number `7` on the stack. There are no variables, no parentheses, no syntax to remember. You just end up with words and numbers separated by spaces. For live coding music, this directness is quite exciting. All you do is think in terms of transformations and add things to the stack: take a note, shift it up, add reverb, play it.
@@ -22,7 +20,6 @@ The stack is where values live. When you type a number, it goes on the stack. Wh
3 ;; stack: 3
4 ;; stack: 3 4
+ ;; stack: 7
print
```
The stack is `last-in, first-out`. The most recent value is always on top. This means that it's often better to read Forth programs from right to left, bottom to top.
@@ -41,7 +38,7 @@ Words compose naturally on the stack. To double a number:
```forth
;; 3 3 +
3 dup + print
3 dup +
```
Forth has a large vocabulary, so Cagire includes a `Dictionary` directly in the application. You can also create your own words. They will work just like existing words. The only difference is that these words will not be included in the dictionary. There are good reasons to create new words on-the-fly:
@@ -57,26 +54,17 @@ Four basic types of values can live on the stack:
- **Integers**: `42`, `-7`, `0`
- **Floats**: `0.5`, `3.14`, `-1.0`
- **Strings**: `"kick"`, `"hello"`
- **Quotations**: `( dup + )` (code as data)
- **Quotations**: `{ dup + }` (code as data)
Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`.
Parentheses are used to "quote" a section of a program. The code inside does not run immediately — it is pushed onto the stack as a value. A quotation only runs when a consuming word decides to execute it. This is how conditionals and loops work:
Parentheses are ignored by the parser. You can use them freely for visual grouping without affecting execution:
```forth
( 60 note 0.3 verb ) 1 ?
(c4 note) (0.5 gain) "sine" s .
```
Here `?` pops the quotation and the condition. The code inside runs only when the condition is truthy. Words like `?`, `!?`, `times`, `cycle`, `choose`, `ifelse`, `every`, `chance`, and `apply` all consume quotations this way.
Because parentheses defer execution, wrapping code in `( ... )` without a consuming word means it never runs. Quotations are transparent to sound and parameter words — they stay on the stack untouched. This is a useful trick for temporarily disabling part of a step:
```forth
( 0.5 gain ) ;; this quotation is ignored
"kick" sound
0.3 decay
.
```
Quotations are special. They let you pass code around as a value. This is how conditionals and loops work. Don't worry about them for now — you'll learn how to use them later.
Any word that is not recognized as a built-in or a user definition becomes a string on the stack. This means `kick s` and `"kick" s` are equivalent. You only need quotes when the string contains spaces or when it conflicts with an existing word name.

View File

@@ -1,108 +0,0 @@
# Brackets
Cagire uses three bracket forms. Each one behaves differently.
## ( ... ) — Quotations
Parentheses create quotations: deferred code. The contents are not executed immediately — they are pushed onto the stack as a single value.
```forth
( dup + )
```
This pushes a block of code. You can store it in a variable, pass it to other words, or execute it later. Quotations are what make Cagire's control flow work.
### Words that consume quotations
Many built-in words expect a quotation on the stack:
| Word | Effect |
|------|--------|
| `?` | Execute if condition is truthy |
| `!?` | Execute if condition is falsy |
| `ifelse` | Choose between two quotations |
| `select` | Pick the nth quotation from a list |
| `apply` | Execute unconditionally |
| `times` | Loop n times |
| `cycle` / `pcycle` | Rotate through quotations |
| `choose` | Pick one at random |
| `every` | Execute on every nth iteration |
| `chance` / `prob` | Execute with probability |
| `bjork` / `pbjork` | Euclidean rhythm gate |
When a word like `cycle` or `choose` selects a quotation, it executes it. When it selects a plain value, it pushes it.
### Nesting
Quotations nest freely:
```forth
( ( c4 note ) ( e4 note ) coin ifelse ) 4 every
```
The outer quotation runs every 4th iteration. Inside, a coin flip picks the note.
### The mute trick
Wrapping code in a quotation without consuming it is a quick way to disable it:
```forth
( kick s . )
```
Nothing will execute this quotation — it just sits on the stack and gets discarded. Useful for temporarily silencing a line while editing.
## [ ... ] — Square Brackets
Square brackets execute their contents immediately, then push a count of how many values were produced. The values themselves stay on the stack.
```forth
[ 60 64 67 ]
```
After this runs, the stack holds `60 64 67 3` — three values plus the count `3`. This is useful with words that need to know how many items precede them:
```forth
[ 60 64 67 ] cycle note sine s .
```
The `cycle` word reads the count to know how many values to rotate through. Without brackets you would write `60 64 67 3 cycle` — the brackets save you from counting manually.
Square brackets work with any word that takes a count:
```forth
[ c4 e4 g4 ] choose note saw s . ;; random note from the list
[ 60 64 67 ] note sine s . ;; 3-note chord (note consumes all)
```
### Nesting
Square brackets can nest. Each pair produces its own count:
```forth
[ [ 60 64 67 ] cycle [ 0.3 0.5 0.8 ] cycle ] choose
```
### Expressions inside brackets
The contents are compiled and executed normally, so you can use any Forth code:
```forth
[ c4 c4 3 + c4 7 + ] note sine s . ;; root, minor third, fifth
```
## { ... } — Curly Braces
Curly braces are ignored by the compiler. They do nothing. Use them as a visual aid to group related code:
```forth
{ kick s } { 0.5 gain } { 0.3 verb } .
```
This compiles to exactly the same thing as:
```forth
kick s 0.5 gain 0.3 verb .
```
They can help readability in dense one-liners but have no semantic meaning.

View File

@@ -1,143 +1,140 @@
# Control Flow
Control flow in Cagire's Forth comes in two families. The first is compiled syntax — `if/then` and `case` — which the compiler handles directly as branch instructions. The second is quotation words — `?`, `!?`, `ifelse`, `select`, `apply` — which pop `( ... )` quotations from the stack and decide whether to run them. Probability and periodic execution (`chance`, `every`, `bjork`) are covered in the Randomness tutorial.
Sometimes a step should behave differently depending on context — a coin flip, a fill, which iteration of the pattern is playing. Control flow words let you branch, choose, and repeat inside a step's script. Control structures are essential for programming and allow you to create complex and dynamic patterns.
## Branching with if / else / then
## if / else / then
Push a condition, then `if`. Everything between `if` and `then` runs only when the condition is truthy:
The simplest branch. Push a condition, then `if`:
```forth
;; degrade sound if true
coin if
7 crush
then
sine sound
c4 note
1 decay
.
coin if 0.8 gain then
saw s c4 note .
```
The crush is applied on half the hits. The sound always plays. Add `else` for a two-way split:
The gain is applied if the coin flip is true. The sound will always plays. Add `else` for a two-way split:
```forth
coin if
c5 note
c4 note
else
c3 note
then
saw sound
0.3 verb
0.5 decay
0.6 gain
.
saw s 0.6 gain .
```
These are compiled directly into branch instructions — they will not appear in the dictionary. This is a "low level" way to use conditionals in Cagire.
These are compiled directly into branch instructions. For that reason, these words will not appear in the dictionary.
## Matching with case
## ? and !?
When you already have a quotation, `?` executes it if the condition is truthy:
```forth
{ 0.4 verb } coin ?
saw s c4 note 0.5 gain . ;; reverb on half the hits
```
`!?` is the opposite — executes when falsy:
```forth
{ 0.2 gain } coin !?
saw s c4 note . ;; quiet on half the hits
```
These pair well with `chance`, `prob`, and the other probability words:
```forth
{ 0.5 verb } 0.3 chance ? ;; occasional reverb wash
{ 12 + } fill ? ;; octave up during fills
```
## ifelse
Two quotations, one condition. The true branch comes first:
```forth
{ c3 note } { c4 note } coin ifelse
saw s 0.6 gain . ;; bass or lead, coin flip
```
Reads naturally: "c3 or c4, depending on the coin."
```forth
{ 0.8 gain } { 0.3 gain } fill ifelse
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
```
## pick
Choose the nth option from a list of quotations:
```forth
{ c4 } { e4 } { g4 } { b4 } iter 4 mod pick
note sine s 0.5 decay .
```
Four notes cycling through a major seventh chord, one per pattern iteration. The index is 0-based.
## apply
When you have a quotation and want to execute it unconditionally, use `apply`:
```forth
{ dup + } apply ;; doubles the top value
```
This is simpler than `?` when there is no condition to check. It pops the quotation and runs it.
## case / of / endof / endcase
For matching a value against several options. Cleaner than a chain of `if`s when you have more than two branches:
```forth
1 8 rand 4 mod case
iter 4 mod case
0 of c3 note endof
1 of e3 note endof
2 of g3 note endof
3 of a3 note endof
endcase
tri s
2 fm 0.99 fmh
0.6 gain 0.2 chorus
1 decay
800 lpf
.
saw s 0.6 gain 800 lpf .
```
A different root note each time the pattern loops. The last line before `endcase` is the default — it runs when no `of` matched:
A different root note each time the pattern loops.
The last line before `endcase` is the default — it runs when no `of` matched:
```forth
iter 3 mod case
0 of 0.9 gain endof
0.4 gain
0.4 gain ;; default: quieter
endcase
saw s
.5 decay
c4 note
.
saw s c4 note .
```
Like `if/then`, `case` is compiled syntax and does not appear in the dictionary.
## times
## Quotation Words
The remaining control flow words operate on quotations — `( ... )` blocks sitting on the stack. Each word pops one or more quotations and decides whether or how to execute them.
### ? and !?
`?` executes a quotation if the condition is truthy:
Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0):
```forth
( 0.4 verb 6 crush ) coin ?
tri sound 2 fm 0.5 fmh
c3 note 0.5 gain 2 decay
.
3 { c4 @i 4 * + note } times
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 a chord
```
Reverb on half the hits. `!?` is the opposite — executes when falsy:
Subdivide with `at`:
```forth
( 0.5 delay 0.9 delayfeedback ) coin !?
saw sound
c4 note
500 lpf
0.5 decay
0.5 gain
.
4 { @i 4 / at sine s c4 note 0.3 gain . } times
```
Quiet on half the hits. These pair well with `chance` and `fill` from the Randomness tutorial.
Four evenly spaced notes within the step.
### ifelse
Two quotations, one condition. The true branch comes first:
Vary intensity per iteration:
```forth
( c3 note ) ( c5 note ) coin ifelse
saw sound 0.3 verb
0.5 decay 0.6 gain
.
8 {
@i 8 / at
@i 4 mod 0 = if 0.7 else 0.2 then gain
tri s c5 note 0.1 decay .
} times
```
Reads naturally: "c3 or c5, depending on the coin."
```forth
( 0.8 gain ) ( 0.3 gain ) fill ifelse
tri s c4 note 0.2 decay .
```
Loud during fills, quiet otherwise.
### select
Choose the nth quotation from a list. The index is 0-based:
```forth
( c4 ) ( e4 ) ( g4 ) ( b4 ) 0 3 rand select
note sine s 0.5 decay .
```
Four notes of a major seventh chord picked randomly. Note that this is unnecessarily complex :)
### apply
When you have a quotation and want to execute it unconditionally:
```forth
( dup + ) apply
```
Pops the quotation and runs it. Simpler than `?` when there is no condition to check.
## More!
For probability gates, periodic execution, and euclidean rhythms, see the Randomness tutorial. For generators and ranges, see the Generators tutorial.
Eight notes per step. Every fourth one louder.

View File

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

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