7 Commits

Author SHA1 Message Date
3ad82a1954 chore: Release
Some checks failed
Deploy Website / deploy (push) Has been skipped
CI / check (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Successful in 7m59s
Release / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Has been skipped
Release / build-cross (cagire-linux-aarch64, aarch64-unknown-linux-gnu) (push) Has been skipped
CI / check (macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / check (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Release / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
Release / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
Release / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Release / universal-macos (push) Has been cancelled
Release / release (push) Has been cancelled
2026-03-01 20:00:05 +01:00
4718248ee6 Introduce release.toml 2026-03-01 19:57:02 +01:00
6fd844cdf6 Update CHANGELOG.md before release 2026-03-01 19:52:33 +01:00
2d3094464f Feat: docs should be good enough 2026-03-01 19:38:52 +01:00
db44f9b98e Feat: documentation, UI/UX 2026-03-01 19:09:52 +01:00
ecb559e556 Fix: website links and photos 2026-03-01 11:30:00 +01:00
5a59937cc7 Fix: build instructions
All checks were successful
Deploy Website / deploy (push) Has been skipped
2026-03-01 11:01:10 +01:00
76 changed files with 2024 additions and 911 deletions

View File

@@ -1,6 +1,3 @@
# Uncomment to use local doux for development
paths = ["/Users/bubo/doux"]
[env]
MACOSX_DEPLOYMENT_TARGET = "12.0"

View File

@@ -3,7 +3,7 @@
## Quick Start
```bash
git clone --recursive https://github.com/Bubobubobubobubo/cagire
git clone https://github.com/Bubobubobubobubo/cagire
cd cagire
cargo build --release
```
@@ -163,7 +163,7 @@ scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop
scripts/build-all.sh --all --yes
```
Builds selected targets, producing binaries in `target/releases/`.
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`.
@@ -175,7 +175,7 @@ Linux releases ship as AppImages — self-contained executables that bundle all
After building a Linux target, produce an AppImage with:
```bash
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 target/releases
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.

View File

@@ -4,108 +4,120 @@ 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
**Bracket syntax `[ ... ]`**
- `[ v1 v2 v3 ]` pushes all items plus their count. Sugar for `v1 v2 v3 3`.
**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).
- `pbounce` — ping-pong cycle keyed by pattern iteration (vs `bounce` which is step-keyed).
- `except` — inverse of `every`: run quotation on all iterations except every nth.
- `every+` / `except+``every`/`except` with a phase offset.
- `all` / `noall` — apply current params globally to all emitted sounds; clear global params.
- `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`) — toggle recording/overdubbing master audio to a named sample.
- `orec` / `odub` toggle recording/overdubbing a single orbit to a named sample.
- `rec` / `overdub` (`dub`) — record/overdub master audio to a named sample.
- `orec` / `odub` — record/overdub a single orbit.
**Harmony and voicing words:**
- `key!` — set tonal center for scale operations.
- `triad` / `seventh` — diatonic triad/seventh from scale degree (follows a scale word).
**Harmony and voicing:**
- `key!` — set tonal center.
- `triad` / `seventh` — diatonic chord from scale degree.
- `inv` / `dinv` — chord inversion / down inversion.
- `drop2` / `drop3` — drop-2 / drop-3 voicings.
- `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`.
**Ducking compressor params:**
- `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
**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 and loads them.
- Audio stream errors surfaced as flash messages instead of printing to stderr.
- 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 / Visualization
- Lissajous XY scope: stereo phase display using Braille characters, togglable via Options.
### 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` on Patterns page, shown in pattern row and properties.
- 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.
### CLAP Plugin (experimental)
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
### Documentation
- Complete reorganization into `docs/` subdirectories.
- 10 getting-started guides, 5 interactive tutorials.
- New tutorials: Recording, Soundfonts, Sharing (import/export).
- New topics: control flow, generators, harmony, randomness, variables, timing, bracket syntax.
- Crate-level READMEs for forth, markdown, project, ratatui.
### Fixed
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
- Scope widget not drawing completely in some terminal sizes.
### Documentation
- New tutorials: Recording (`docs/tutorials/recording.md`), Soundfonts (`docs/tutorials/soundfont.md`).
### UI / UX (breaking cosmetic changes)
- **Options page**: Each option now shows a short description line below when focused, replacing the static header box.
- **Dictionary page**: Removed the Forth description box at the top. The word list now uses the full page height.
### CLAP Plugin (experimental)
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
### Forth Language
- Removed `chain` word (replaced by pattern-level Follow Up setting).
- `case/of/endof/endcase` control flow for pattern-matching dispatch.
- `bjork` / `pbjork` — euclidean rhythm gates using quotations: execute a block only on Bjorklund-distributed hits.
- `arp` — arpeggio list type that spreads notes across time positions instead of stacking them simultaneously.
- `,varname` assignment syntax (SetKeep): assign to a variable without consuming the value from the stack.
- `every` reworked to accept quotations for cleaner conditional step logic.
- All parameter words now accept varargs — over 100 words updated to consume the full stack.
- Reverb parameter words added.
### Engine
- Follow-up actions: patterns now have a configurable follow-up behavior (Loop, Stop, or Chain to another pattern). Replaces the Forth `chain` word with a declarative setting in the Pattern Properties modal (`e` key). Chain targets specify bank and pattern via UI fields.
- Delta-time MIDI scheduling for tighter, sample-accurate timing.
- Tempo and current beat exposed in sequencer snapshot.
- Spectrum analyzer rescaling.
### UI / UX
- Patterns view redesign: new layout with banks column (showing pattern counts), expandable detail rows for the focused pattern (quantization, sync mode, progress bar), and a bottom preview strip with mini step grid and pattern properties.
- Smooth playback progress: playing patterns display a real-time progress bar interpolated between steps.
- Dynamic step grid sizing: `steps_per_page` adapts to terminal height instead of using a fixed constant.
- Mouse support: click navigation on the pattern grid, panels, and modals.
- 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 topics: control flow, generators, harmony, randomness, variables, timing.
### Theme System
- Palette-based generation: all 18 themes now derived from a 14-field Palette via Oklab color space.
- Theme definitions reduced from ~300 lines each to ~20 lines.
### Codebase
- `src/app.rs` split into 10 focused modules (dispatch, clipboard, editing, navigation, persistence, scripting, sequencer, staging, undo).
- `src/app.rs` split into 10 focused modules.
- `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,6 +10,7 @@ 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

12
Cargo.lock generated
View File

@@ -846,7 +846,7 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21"
[[package]]
name = "cagire"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"arboard",
"arc-swap",
@@ -885,7 +885,7 @@ dependencies = [
[[package]]
name = "cagire-forth"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"arc-swap",
"parking_lot",
@@ -894,7 +894,7 @@ dependencies = [
[[package]]
name = "cagire-markdown"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"minimad",
"ratatui",
@@ -902,7 +902,7 @@ dependencies = [
[[package]]
name = "cagire-plugins"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"arc-swap",
"cagire",
@@ -926,7 +926,7 @@ dependencies = [
[[package]]
name = "cagire-project"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"base64",
"brotli",
@@ -938,7 +938,7 @@ dependencies = [
[[package]]
name = "cagire-ratatui"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"rand",
"ratatui",

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.0.9"
version = "0.1.0"
edition = "2021"
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
license = "AGPL-3.0"

View File

@@ -12,14 +12,18 @@
AGPL-3.0
</p>
Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that gets evaluated at the right time, producing sound and events. It ships with a self-contained audio engine — no external software needed.
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 .
saw sound
200 199 freq
400 lpf
.8 lpq .3 verb
.
```
A generative pattern using randomness, scales, and effects:
@@ -34,17 +38,17 @@ sine sound 2 fm 0.5 fmh
### Features
- **Cagire's Forth**: a stack-based DSL specialized for live coding
- Concise and light syntax, every step is a short script. Forth has almost no syntax, only words and numbers.
- Nondeterminism: randomness, probabilities, patterns as first-class operations.
- **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 the language on the fly with `:name ... ;` definitions.
- 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: kick, snare, hat, tom, rim, cowbell, cymbal with timbral morphing.
- Sampling: disk-loaded samples with slicing, 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.
- 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).

22
crates/forth/README.md Normal file
View File

@@ -0,0 +1,22 @@
# 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

@@ -65,6 +65,7 @@ pub enum Op {
NewCmd,
SetParam(&'static str),
Emit,
Print,
Get,
Set,
SetKeep,

View File

@@ -328,6 +328,16 @@ 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();
@@ -558,9 +568,12 @@ impl Forth {
Op::NewCmd => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
let values = drain_skip_quotations(stack);
if values.is_empty() {
return Err("expected sound name".into());
}
let val = if values.len() == 1 {
values.into_iter().next().expect("single value after len check")
values.into_iter().next().unwrap()
} else {
Value::CycleList(Arc::from(values))
};
@@ -568,9 +581,12 @@ impl Forth {
}
Op::SetParam(param) => {
ensure(stack, 1)?;
let values = std::mem::take(stack);
let values = drain_skip_quotations(stack);
if values.is_empty() {
return Err("expected parameter value".into());
}
let val = if values.len() == 1 {
values.into_iter().next().expect("single value after len check")
values.into_iter().next().unwrap()
} else {
Value::CycleList(Arc::from(values))
};
@@ -1866,6 +1882,21 @@ 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());

View File

@@ -13,6 +13,7 @@ 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,

View File

@@ -33,6 +33,16 @@ 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: &[],

15
crates/markdown/README.md Normal file
View File

@@ -0,0 +1,15 @@
# 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.

22
crates/project/README.md Normal file
View File

@@ -0,0 +1,22 @@
# 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`

25
crates/ratatui/README.md Normal file
View File

@@ -0,0 +1,25 @@
# 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,13 +1,15 @@
# About Forth
Forth is a _stack-based_ programming language created by Charles H. Moore in the early 1970s. It was designed with simplicity, directness, and interactive exploration in mind. Forth has been used for scientific work and embedded systems: it controlled telescopes and even ran on hardware aboard space missions. It evolved into many implementations targeting various architectures, but none of them really caught on. Nonetheless, the ideas behind Forth continue to attract people from very different, often unrelated fields. Today, Forth languages are used by hackers and artists for their unconventional nature. Forth is simple, direct, and beautiful to implement. Forth is an elegant, minimal language, easy to understand, extend, and tailor to a specific task. The Forth we use in Cagire is specialized in making live music. It is used as a DSL: a _Domain Specific Language_.
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 +
3 4 + print
```
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.
@@ -20,6 +22,7 @@ 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.
@@ -38,7 +41,7 @@ Words compose naturally on the stack. To double a number:
```forth
;; 3 3 +
3 dup +
3 dup + print
```
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:
@@ -58,13 +61,22 @@ Four basic types of values can live on the stack:
Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`.
Parentheses are ignored by the parser. You can use them freely for visual grouping without affecting execution:
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:
```forth
(c4 note) (0.5 gain) "sine" s .
( 60 note 0.3 verb ) 1 ?
```
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.
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
.
```
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.

108
docs/forth/brackets.md Normal file
View File

@@ -0,0 +1,108 @@
# 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,140 +1,143 @@
# Control Flow
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.
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.
## if / else / then
## Branching with if / else / then
The simplest branch. Push a condition, then `if`:
Push a condition, then `if`. Everything between `if` and `then` runs only when the condition is truthy:
```forth
coin if 0.8 gain then
saw s c4 note .
;; degrade sound if true
coin if
7 crush
then
sine sound
c4 note
1 decay
.
```
The gain is applied if the coin flip is true. The sound will always plays. Add `else` for a two-way split:
The crush is applied on half the hits. The sound always plays. Add `else` for a two-way split:
```forth
coin if
c4 note
c5 note
else
c3 note
then
saw s 0.6 gain .
saw sound
0.3 verb
0.5 decay
0.6 gain
.
```
These are compiled directly into branch instructions. For that reason, these words will not appear in the dictionary.
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.
## ? and !?
When you already have a quotation, `?` executes it if the condition is truthy:
```forth
( 0.4 verb ) coin ?
saw s c4 note 0.5 gain . ;; reverb on half the hits
```
`!?` is the opposite — executes when falsy:
```forth
( 0.2 gain ) coin !?
saw s c4 note . ;; quiet on half the hits
```
These pair well with `chance`, `prob`, and the other probability words:
```forth
( 0.5 verb ) 0.3 chance ? ;; occasional reverb wash
( 12 + ) fill ? ;; octave up during fills
```
## ifelse
Two quotations, one condition. The true branch comes first:
```forth
( c3 note ) ( c4 note ) coin ifelse
saw s 0.6 gain . ;; bass or lead, coin flip
```
Reads naturally: "c3 or c4, depending on the coin."
```forth
( 0.8 gain ) ( 0.3 gain ) fill ifelse
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
```
## select
Choose the nth option from a list of quotations:
```forth
( c4 ) ( e4 ) ( g4 ) ( b4 ) iter 4 mod select
note sine s 0.5 decay .
```
Four notes cycling through a major seventh chord, one per pattern iteration. The index is 0-based.
## apply
When you have a quotation and want to execute it unconditionally, use `apply`:
```forth
( dup + ) apply ;; doubles the top value
```
This is simpler than `?` when there is no condition to check. It pops the quotation and runs it.
## case / of / endof / endcase
## Matching with case
For matching a value against several options. Cleaner than a chain of `if`s when you have more than two branches:
```forth
iter 4 mod case
1 8 rand 4 mod case
0 of c3 note endof
1 of e3 note endof
2 of g3 note endof
3 of a3 note endof
endcase
saw s 0.6 gain 800 lpf .
tri s
2 fm 0.99 fmh
0.6 gain 0.2 chorus
1 decay
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 ;; default: quieter
0.4 gain
endcase
saw s c4 note .
saw s
.5 decay
c4 note
.
```
## times
Like `if/then`, `case` is compiled syntax and does not appear in the dictionary.
Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0):
## 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:
```forth
3 ( c4 @i 4 * + note ) times
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 a chord
( 0.4 verb 6 crush ) coin ?
tri sound 2 fm 0.5 fmh
c3 note 0.5 gain 2 decay
.
```
Subdivide with `at`:
Reverb on half the hits. `!?` is the opposite — executes when falsy:
```forth
4 ( @i 4 / at sine s c4 note 0.3 gain . ) times
( 0.5 delay 0.9 delayfeedback ) coin !?
saw sound
c4 note
500 lpf
0.5 decay
0.5 gain
.
```
Four evenly spaced notes within the step.
Quiet on half the hits. These pair well with `chance` and `fill` from the Randomness tutorial.
Vary intensity per iteration:
### ifelse
Two quotations, one condition. The true branch comes first:
```forth
8 (
@i 8 / at
@i 4 mod 0 = if 0.7 else 0.2 then gain
tri s c5 note 0.1 decay .
) times
( c3 note ) ( c5 note ) coin ifelse
saw sound 0.3 verb
0.5 decay 0.6 gain
.
```
Eight notes per step. Every fourth one louder.
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.

92
docs/forth/cycling.md Normal file
View File

@@ -0,0 +1,92 @@
# 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
```

View File

@@ -13,8 +13,7 @@ Use `:` to start a definition and `;` to end it:
This creates a word called `double` that duplicates the top value and adds it to itself. Now you can use it:
```forth
3 double ;; leaves 6 on the stack
5 double ;; leaves 10 on the stack
3 double print ;; leaves 6 on the stack
```
The definition is simple: everything between `:` and `;` becomes the body of the word.

View File

@@ -10,7 +10,7 @@ Classic Forth uses parentheses for comments:
( this is a comment )
```
Cagire uses double semicolons:
In Cagire, parentheses create quotations, so comments use double semicolons instead:
```forth
;; this is a comment
@@ -18,18 +18,6 @@ Cagire uses double semicolons:
Everything after `;;` until the end of the line is ignored.
## Quotations
Classic Forth has no quotations. Code is not a value you can pass around.
Cagire has first-class quotations using parentheses:
```forth
( dup + )
```
This pushes a block of code onto the stack. You can store it, pass it to other words, and execute it later. Quotations enable conditionals, probability, and cycling.
## Conditionals
Classic Forth uses `IF ... ELSE ... THEN`:
@@ -190,31 +178,6 @@ Execute a quotation on specific iterations:
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits as evenly as possible across n positions. `bjork` counts by step runs, `pbjork` counts by pattern iterations. Classic Euclidean rhythms: tresillo (3,8), cinquillo (5,8), son clave (5,16).
## Cycling
Cagire has built-in support for cycling through values. Push values onto the stack, then select one based on pattern state:
```forth
60 64 67 3 cycle note
```
Each time the step runs, a different note is selected. The `3` tells `cycle` how many values to pick from.
You can also use quotations if you need to execute code:
```forth
( c4 note ) ( e4 note ) ( g4 note ) 3 cycle
```
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.
Two cycling words exist:
- `cycle` - selects based on `runs` (how many times this step has played)
- `pcycle` - selects based on `iter` (how many times the pattern has looped)
The difference between `cycle` and `pcycle` matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
## Polyphonic Parameters
Parameter words like `note`, `freq`, and `gain` consume the entire stack. If you push multiple values before a param word, you get polyphony:

View File

@@ -89,7 +89,7 @@ The fix is simple: make sure you push enough values before calling a word. Check
* **Leftover values** are the opposite problem: values remain on the stack after your script finishes. This is less critical but indicates sloppy code. If your script leaves unused values behind, you probably made a mistake somewhere.
```forth
3 4 5 + . ;; plays a sound, but 3 is still on the stack
3 4 5 + ;; 3 is still on the stack, unconsumed
```
The `3` was never used. Either it should not be there, or you forgot a word that consumes it.

View File

@@ -6,6 +6,8 @@ Cagire organizes all your patterns and data following a strict hierarchy:
- **Banks** contain **Patterns**.
- **Patterns** contain **Steps**.
If strict organization isn't your style, don't worry, you can ignore banks entirely and just work in a single pattern. You can also escape the strict metric using sub-step timing and randomness.
## Structure
```
@@ -15,7 +17,7 @@ Project
└── 1024 Steps (per pattern)
```
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps. This means that you can create a staggering amount of things. Don't hesitate to create copies, variations, and explore the pattern system thoroughly.
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps. This means that you can create a staggering amount of music. Don't hesitate to create copies, variations, and explore the pattern system thoroughly. The more you add, the more surprising it becomes.
## Patterns
@@ -29,7 +31,7 @@ Each pattern is an independent sequence of steps with its own properties:
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
| Follow Up | What happens when the pattern finishes an iteration | `Loop` |
Press `e` in the patterns view to edit these settings.
Press `e` in the patterns view to edit these settings. After editing properties, you will have to hit the `c` key to _commit_ these changes. More about that later!
### Follow Up
@@ -49,6 +51,8 @@ Access the patterns view with `F2` (or `Ctrl+Up` from the sequencer). The view s
- `M` Muted
- `S` Soloed
It is quite essential for you to understand the stage / commit system in order to use patterns. Please read the next section carefully!
### Keybindings
| Key | Action |
@@ -63,3 +67,5 @@ Access the patterns view with `F2` (or `Ctrl+Up` from the sequencer). The view s
| `Ctrl+c` / `Ctrl+v` | Copy / Paste |
| `Delete` | Reset to empty pattern |
| `Esc` | Cancel staged changes |

View File

@@ -1,6 +1,8 @@
# Big Picture
Let's answer some basic questions: what exactly is Cagire? What purpose does it serve? Cagire is a small and simple piece of software that allows you to create music live while playing with scripts. At heart, it is really nothing more than a classic step sequencer, the kind you can buy in a music store. It is deliberately kept small and simple. Adding the Forth language to program steps allows you to create patterns and behaviors of any complexity. Forth also makes it super easy to extend and to customize Cagire while keeping the core mechanisms and the logic simple.
> **What exactly is Cagire? What purpose does it serve?**
Cagire is a small and simple software that allows you to create music live while programming short scripts. At heart, it is really nothing more than a classic step sequencer, the kind you can buy in a music store. It is deliberately kept small and simple in form, but it goes rather deep if you take the time to discover the audio engine and all its capabilities. Adding the Forth language to program steps allows you to create patterns and behaviors of any complexity. Forth also makes it easy to extend and to customize Cagire while keeping the core mechanisms and the logic simple.
Cagire is not complex, it is just very peculiar. It has been created as a hybrid between a step sequencer and a programming environment. It allows you to create music live and to extend and customize it using the power of Forth. It has been designed to be fast and responsive, low-tech in the sense that you can run it on any decent computer. You can think of it as a musical instrument. You learn it by getting into the flow and practicing. What you ultimately do with it is up to you: improvisation, composition, etc. Cagire is also made to be autonomous, self-contained, and self-sustaining: it contains all the necessary components to make music without relying on external software or hardware.
@@ -8,29 +10,40 @@ Cagire is not complex, it is just very peculiar. It has been created as a hybrid
A traditional step sequencer would offer the musician a grid where each step represents a note or a single musical event. Cagire replaces notes and/or events in favour of **Forth scripts**. When the sequencer reaches a step to play, it runs the script associated with it. A script can do whatever it is programmed to do: play a note, trigger a sample, apply effects, generate randomness, or all of the above. Scripts can share code and data with each other. Everything else works like a regular step sequencer: you can toggle, copy, paste, and rearrange steps freely.
```forth
0.0 8.0 rand at
sine sound
200 2000 rand 100 4000 rand
4 slide freq 0.6 verb 2 vib
0.125 vibmod 0.2 chorus
0.4 0.6 rand gain
.
```
## What Does a Script Look Like?
A Forth script is generally kind of small, and it solves a simple problem: playing a chord, tweaking some parameters, etc. The more focused it is, the better. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. Here is a program that plays a middle C note using a sine wave:
A Forth script is generally kind of small, and it solves a simple problem: playing a chord, tweaking some parameters, etc. The more focused it is, the better. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. Here is a program that plays a middle C note for two steps using a sine wave:
```forth
c4 note sine sound .
c4 note sine sound 2 decay .
```
Read it backwards and you will understand what it does:
- `.` — play a sound.
- `2 decay` — the sound takes two steps to die.
- `sine sound` — the sound is a sine wave.
- `c4 note` — the pitch is C4 (middle C).
Five tokens separated by spaces. There is pretty much no syntax to learn, just three rules:
There is pretty much no syntax to learn, just three rules:
- There are `words` and `numbers`.
- A `word` is anything that is not a space or a number.
- A `word` is anything that is not a space or a number (can include symbols).
- A `number` is anything that is not a space or a word.
- They are separated by spaces.
- Everything piles up on the **stack**.
The stack is what makes Forth tick. Think of it as a pile of things. `c4` puts a pitch on the pile. `note` picks it up. `sine` chooses a waveform. `sound` assembles everything into a voice. `.` plays it. Each word picks up what the previous ones left behind and leaves something for the next. Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. You will need to understand the stack, but it will take five minutes. See the **Forth** section for details.
The stack is what makes Forth tick. Think of it as a pile of things. `c4` puts a pitch on the pile. `note` picks it up. `sine` chooses a waveform. `sound` assembles everything into a voice. `.` plays it. Each word picks up what the previous ones left behind and leaves something for the next. Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. Cagire requires you to understand what the stack is. The good thing is that it will take five minutes for you to make sense of it. See the **Forth** section for details.
## The Audio Engine

View File

@@ -1,6 +1,6 @@
# Editing a Step
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the code editor. The editor appears as a modal overlay with the step number in the title bar. If the step is a linked step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the code editor. The editor appears as a modal overlay with the step number in the title bar. If the step is a mirrored step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
## Writing Scripts
@@ -18,7 +18,7 @@ Add parameters before words to modify them:
c4 note 0.75 decay sine sound .
```
Writing long lines is not recommended because it can become quite unmanageable. Instead, break them into multiple lines for clarity:
Writing long lines can become tedious. Instead, break your code into multiple lines for clarity:
```forth
;; the same sound on multiple lines
@@ -29,6 +29,12 @@ sine sound
.
```
Forth has no special rule about what a line should look like and space has no meaning.
## Adding comments to your code
You can comment a line using `;;`. This is not very common for people that are used to Forth. There are no multiline comments.
## Saving
- `Esc` — Save, compile, and close the editor.
@@ -64,27 +70,6 @@ Press `Ctrl+F` to open the search bar. Type your query, then navigate matches:
- `Enter` — Confirm and close search.
- `Esc` — Cancel search.
## Debugging
## Script preview
Press `Ctrl+S` to toggle the stack display. This shows the stack state evaluated up to the cursor line, useful for understanding how values flow through your script.
Press `Ctrl+R` to execute the script immediately as a one-shot, without waiting for the sequencer to reach the step. A green flash indicates success, red indicates an error.
## Keybindings
| Key | Action |
|-----|--------|
| `Esc` | Save and close |
| `Ctrl+E` | Evaluate (save + compile in place) |
| `Ctrl+R` | Execute script once |
| `Ctrl+S` | Toggle stack display |
| `Ctrl+B` | Open sample finder |
| `Ctrl+F` | Search |
| `Ctrl+N` | Next match / next suggestion |
| `Ctrl+P` | Previous match / previous suggestion |
| `Ctrl+A` | Select all |
| `Ctrl+C` | Copy |
| `Ctrl+X` | Cut |
| `Ctrl+V` | Paste |
| `Shift+Arrows` | Extend selection |
| `Tab` | Accept completion / sample |
Press `Ctrl+R` to execute the script immediately as a one-shot, without waiting for the sequencer to reach the step. A green flash indicates success, red indicates an error. This is super useful for sound design. It also works when hovering on a step with the editor closed.

View File

@@ -1,6 +1,6 @@
# The Audio Engine
The Engine page (`F6`) is where you configure audio hardware, adjust performance settings, and manage your sample library. The right side of the page shows a real-time oscilloscope and spectrum analyzer. The page is divided into three sections. Press `Tab` to move between them, `Shift+Tab` to go back.
The Engine page (`F6`) is where you configure audio hardware, manage MIDI connections, set up Ableton Link, and manage your sample library. The left column holds six configuration sections — press `Tab` to move between them, `Shift+Tab` to go back. The right column is a read-only monitoring panel with VU meters, status metrics, and an oscilloscope.
## Devices
@@ -17,11 +17,29 @@ Four audio parameters are adjustable with `Left`/`Right`:
| Voices | 1128 | Maximum polyphony (simultaneous sounds) |
| Nudge | -100 to +100 ms | Timing offset to compensate for latency |
The last two rows — sample rate and audio host — are read-only values reported by your system. After changing the buffer size or channel count, press `Shift+r` to restart the audio engine for changes to take effect.
After changing the buffer size or channel count, press `Shift+r` to restart the audio engine for changes to take effect.
## Link
Ableton Link synchronizes tempo across devices and applications on the same network. Three settings are adjustable with `Left`/`Right`:
- **Enabled** — Turn Link on or off. A status badge next to the header shows DISABLED, LISTENING, or CONNECTED.
- **Start/Stop Sync** — Whether play/stop commands are shared with other Link peers.
- **Quantum** — Number of beats per phrase, used for phase alignment.
Below the settings, three read-only session values update in real time: Tempo, Beat, and Phase.
## MIDI Outputs
Four output slots (03). Browse with `Up`/`Down`, cycle available devices with `Left`/`Right`. A slot shows "(not connected)" until you assign a device.
## MIDI Inputs
Same layout as outputs — four input slots (03) with the same navigation.
## Samples
This section shows how many sample directories are registered and how many files have been indexed. Press `A` to open a file browser and add a new sample directory. Press `D` to remove the last one. Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
This section shows how many sample directories are registered and how many files have been indexed. Browse existing paths with `Up`/`Down`. Press `A` to open a file browser and add a new sample directory. Press `D` to remove the selected path. Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
Sample directories must be added here before you can use the sample browser or reference samples in your scripts.
@@ -30,23 +48,17 @@ Sample directories must be added here before you can use the sample browser or r
A few keys work from anywhere on the Engine page:
- `h` — Hush. Silence all audio immediately.
- `p` — Panic. Hard stop, clears all active voices.
- `p` — Panic. Hard stop, clears all active voices, stop all patterns.
- `t` — Test tone. Plays a brief sine wave to verify audio output.
- `r` — Reset the peak voice counter.
- `Shift+r` — Restart the audio engine.
## Keybindings
## Monitoring
| Key | Action |
|-----|--------|
| `Tab` / `Shift+Tab` | Next / previous section |
| `Up` / `Down` | Navigate within section |
| `Left` / `Right` | Switch device column / adjust setting |
| `PageUp` / `PageDown` | Scroll device list |
| `Enter` | Select device |
| `D` | Refresh devices / remove last sample path |
| `A` | Add sample directory |
| `Shift+r` | Restart audio engine |
| `h` | Hush |
| `p` | Panic |
| `t` | Test tone |
| `r` | Reset peak voices |
The right column displays a live overview of the engine state. Everything here is read-only.
- **VU Meters** — Left and right channel levels with horizontal bars and dB readouts. Green below -12 dB, yellow approaching 0 dB, red above.
- **Status** — CPU load (with bar graph), active voices and peak count, scheduled events, schedule depth, nudge offset, sample rate, audio host, and Link peers (when connected).
- **Scope** — An oscilloscope showing the current audio output waveform.

View File

@@ -1,43 +1,48 @@
# The Sequencer Grid
The sequencer grid is the main view of Cagire (`F5`). This is the one you see when you open the application. On this view, you can see the step sequencer grid and edit each step using the code editor. You can optionally display the following widgets:
- **an oscilloscope**: visualize the current audio output.
- **a spectrum analyzer**: 32 bands spectrum analyze (mostly cosmetic).
- **a step preview**: visualize the content of the hovered script.
You can press `o` to cycle through layouts. It will basically rotate the sequencer around. Use it to find the view that makes the more sense for you.
The sequencer grid (`F5`) is where you spend most of your time in Cagire. It shows the step sequencer and lets you edit each step using the code editor. This is the first view you see when you open the application. Optional widgets — oscilloscope, spectrum analyzer, goniometer, prelude preview, and step preview — can be toggled on for visual feedback while you work.
## Navigation
Use arrow keys to move between steps. The grid wraps around at pattern boundaries. Press `:` to jump directly to a step by number. This keybinding is useful for very long patterns.
Use arrow keys to move between steps. `Shift+arrows` selects multiple steps, and `Esc` clears any selection. The grid wraps around at pattern boundaries. Press `:` to jump directly to a step by number.
## Preview
Press `p` to enter preview mode. A read-only code editor opens showing the script of the step under the cursor. You can still navigate the grid while previewing. Press `Esc` to exit preview mode.
## Selection
Hold `Shift` while pressing arrow keys to select multiple steps. Press `Esc` to clear the selection.
- `Alt+Up` / `Alt+Down` — Previous / next pattern
- `Alt+Left` / `Alt+Right` — Previous / next bank
## Editing Steps
- `Enter` — Open the script editor
- `t`Toggle step active/inactive
- `t`Make a step active / inactive
- `r` — Rename a step
- `Del` — Delete selected steps
## Mirrored Steps
Imagine a drum pattern where four steps play the same kick script. You tweak the sound on one of them — now you have to find and edit all four. Mirrored steps solve this: one step is the source, the others are mirrors that always reflect its script. Edit the source once, every mirror follows.
On the grid, mirrors are easy to spot. They show an arrow prefix like `→05`, meaning "I mirror step 05." Steps that share a source also share a background color, so clusters of linked steps are visible at a glance.
To create mirrors: copy a step with `Ctrl+C`, then paste with `Ctrl+B` instead of `Ctrl+V`. The pasted steps become mirrors of the original. Pressing `Enter` on a mirror jumps to its source and opens the editor there. If you want to break the link and make a mirror independent again, press `Ctrl+H` to harden it back into a regular copy.
## Copy & Paste
- `Ctrl+C` — Copy selected steps
- `Ctrl+V` — Paste as copies
- `Ctrl+B` — Paste as linked steps
- `Ctrl+V` — Paste as independent copies
- `Ctrl+B` — Paste as mirrored steps
- `Ctrl+D` — Duplicate selection
- `Ctrl+H` — Harden links (convert to independent copies)
- `Ctrl+H` — Harden mirrors (convert to independent copies)
Linked steps share the same script as their source. When you edit the source, all linked steps update automatically. This is an extremely important and powerful feature. It allows you to create complex patterns with minimal effort. `Ctrl+H` converts linked steps back to independent copies.
## Prelude
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
- `p` — Open the prelude editor
- `d` — Evaluate the prelude
## Pattern Controls
Each pattern has its own length and speed. Length sets how many steps it cycles through. Speed is a multiplier on the global tempo.
- `<` / `>` — Decrease / increase pattern length
- `[` / `]` — Decrease / increase pattern speed
- `L` — Set length directly
@@ -45,6 +50,8 @@ Linked steps share the same script as their source. When you edit the source, al
## Playback
Playback starts and stops globally across all unmuted patterns. The highlighted cell on the grid marks the currently playing step.
- `Space` — Toggle play / stop
- `+` / `-` — Adjust tempo
- `T` — Set tempo directly
@@ -52,57 +59,24 @@ Linked steps share the same script as their source. When you edit the source, al
## Mute & Solo
Mute silences a pattern; solo silences everything except it. Both work while playing.
- `m` — Mute current pattern
- `x` — Solo current pattern
- `Shift+m` — Clear all mutes
- `Shift+x` — Clear all solos
## Prelude
## Project
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
- `d`Open the prelude editor
- `Shift+d` — Evaluate the prelude
- `s` — Save project
- `l` — Load project
- `q`Quit
## Tools
A few utilities accessible from the grid.
- `e` — Euclidean rhythm distribution
- `?` — Show keybindings help
- `o` — Cycle layout
- `Tab` — Toggle sample browser panel
## Visual Indicators
- **Highlighted cell** — Currently playing step
- **Colored backgrounds** — Linked steps share colors by source
- **Arrow prefix** (`→05`) — Step is linked to step 05
## Keybindings
| Key | Action |
|-----|--------|
| `Arrows` | Navigate grid |
| `Shift+Arrows` | Extend selection |
| `:` | Jump to step |
| `Enter` | Open editor |
| `p` | Preview step |
| `t` | Toggle step active |
| `r` | Rename step |
| `Del` | Delete steps |
| `Ctrl+C` / `Ctrl+V` | Copy / Paste |
| `Ctrl+B` | Paste as links |
| `Ctrl+D` | Duplicate |
| `Ctrl+H` | Harden links |
| `<` / `>` | Pattern length |
| `[` / `]` | Pattern speed |
| `L` / `S` | Set length / speed |
| `Space` | Play / Stop |
| `+` / `-` | Tempo up / down |
| `T` | Set tempo |
| `Ctrl+R` | Execute step once |
| `m` / `x` | Mute / Solo |
| `d` | Prelude editor |
| `e` | Euclidean distribution |
| `o` | Cycle layout |
| `Tab` | Sample browser |
| `Ctrl+Z` | Undo |
| `Ctrl+Shift+Z` | Redo |
| `?` | Show keybindings |

View File

@@ -1,41 +1,6 @@
# Navigation
Cagire's interface is organized as a 3x2 grid of six views:
```
Dict Patterns Options
Help Sequencer Engine
```
- *Dict* : Forth dictionary — learn about the language.
- *Help* : Help and tutorials — learn about the tool.
- *Patterns* : Manage your current session / project.
- *Sequencer* : The main view, where you edit sequences and play music.
- *Options* : Configuration settings for the application.
- *Engine* : Configuration settings for the audio engine.
## Switching Views
Use `Ctrl+Arrow` keys to move between views. A minimap will briefly appear to show your position in the grid. You can also click on the view name at the bottom left to open the switch view panel.
- `Ctrl+Left` / `Ctrl+Right` — move horizontally (wraps around)
- `Ctrl+Up` / `Ctrl+Down` — move vertically (does not wrap)
- `Click` at bottom left — select a view
You can also jump directly to any view with the F-keys:
| Key | View |
|------|------------|
| `F1` | Dict |
| `F2` | Patterns |
| `F3` | Options |
| `F4` | Help |
| `F5` | Sequencer |
| `F6` | Engine |
## Common Keys
These shortcuts work on every view:
Press `?` on any view to see its keybindings. The most important shortcuts are always displayed in the footer bar. Press `Esc` to close the keybindings panel. These shortcuts work on every view:
| Key | Action |
|---------|---------------------------|
@@ -45,6 +10,29 @@ These shortcuts work on every view:
| `l` | Load project |
| `?` | Show keybindings for view |
## Getting Help
## Views
Press `?` on any view to see the associated keybindings. This shows all available shortcuts for the current context. The most important keybindings are displayed in the footer bar. Press `Esc` to close the keybindings panel.
Cagire's interface is organized as a 3x2 grid of six views. Jump to any view with its F-key or `Ctrl+Arrow` keys:
```
F1 Dict F2 Patterns F3 Options
F4 Help F5 Sequencer F6 Engine
```
| Key | View | Description |
|------|------------|-------------|
| `F1` | Dict | Forth dictionary — learn about the language |
| `F2` | Patterns | Manage your current session / project |
| `F3` | Options | Configuration settings for the application |
| `F4` | Help | Help and tutorials — learn about the tool |
| `F5` | Sequencer | The main view, where you edit sequences and play music |
| `F6` | Engine | Configuration settings for the audio engine |
Use `Ctrl+Arrow` keys to move between adjacent views. A minimap will briefly appear to show your position in the grid. You can also click on the view name at the bottom left or in the top left corner of the header bar to open the switch view panel.
- `Ctrl+Left` / `Ctrl+Right` — move horizontally (wraps around)
- `Ctrl+Up` / `Ctrl+Down` — move vertically (does not wrap)
## Secrets
There is a hidden seventh view: the **Periodic Script**. Press `F11` to open it. The periodic script is a free-running Forth script evaluated at every step, independent of any pattern. It is useful for drones, global effects, control logic, and experimentation. See the **Periodic Script** tutorial for details.

View File

@@ -1,45 +1,28 @@
# Options
The Options page (`F3`) gathers all configuration settings in one place: display, synchronization and MIDI. Navigate options with `Up`/`Down` or `Tab`, change values with `Left`/`Right`. All changes are saved automatically.
The Options page (`F3`) gathers display and onboarding settings in one place. Navigate with `Up`/`Down` or `Tab`, change values with `Left`/`Right`. All changes are saved automatically. A description line appears below the focused option to explain what it does.
## Display
| Option | Values | Description |
|--------|--------|-------------|
| Theme | (cycle) | Color scheme for the entire interface |
| Hue rotation | 0360° | Shift theme colors by a hue angle (±5° per step) |
| Hue rotation | 0360° | Shift all theme colors by a hue angle (±5° per step) |
| Refresh rate | 60 / 30 / 15 fps | Lower values reduce CPU usage |
| Runtime highlight | on / off | Highlight executed code spans during playback |
| Show scope | on / off | Oscilloscope on the engine page |
| Show spectrum | on / off | Spectrum analyzer on the engine page |
| Show scope | on / off | Oscilloscope on the main view |
| Show spectrum | on / off | Spectrum analyzer on the main view |
| Show lissajous | on / off | XY stereo phase scope |
| Gain boost | 1x 16x | Amplify scope and lissajous waveforms |
| Normalize | on / off | Auto-scale visualizations to fill the display |
| Completion | on / off | Word completion popup in the editor |
| Show preview | on / off | Step script preview on the sequencer grid |
| Performance mode | on / off | Hide header and footer bars |
| Font | 6x13 10x20 | Bitmap font size (plugin mode only) |
| Zoom | 0.5x 2.0x | Interface zoom factor (plugin mode only) |
| Zoom | 50% 200% | Interface zoom factor (plugin mode only) |
| Window | (presets) | Window size presets (plugin mode only) |
## Ableton Link
Cagire uses Ableton Link to synchronize tempo with other applications on the same network. Three settings control the connection:
- **Enabled** — Turn Link on or off. When enabled, Cagire listens for peers and shares its tempo.
- **Start/Stop sync** — When on, pressing play or stop in one app affects all peers.
- **Quantum** — The beat subdivision used for phase alignment.
Below these settings, a read-only session display shows the current tempo, beat position, and phase. The status line at the top shows the connection state: disabled, listening, or connected with peer count.
## MIDI
Four output slots and four input slots let you connect to MIDI devices. Cycle through available devices with `Left`/`Right`. Each slot can hold one device, and the same device cannot be assigned to multiple slots.
## Onboarding
At the bottom, you can reset the onboarding guides if you dismissed them earlier and want to see them again.
## Keybindings
| Key | Action |
|-----|--------|
| `Up` / `Down` / `Tab` | Navigate options |
| `Left` / `Right` | Change value |
- **Reset guides** — Re-enable all dismissed guide popups.
- **Demo on startup** — Load a rotating demo song on fresh startup.

View File

@@ -1,6 +1,6 @@
# The Sample Browser
Press `Tab` on the sequencer grid to open the sample browser. It appears as a side panel showing a tree of all your sample directories and files. Press `Tab` again to close it. Before using the browser, you need to register at least one sample directory on the Engine page (`F6`). Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
Press `Tab` on the sequencer grid to open the sample browser. It appears as a side panel showing a tree of all your sample directories and files. Press `Tab` again to close it. Before using the browser, you need to register at least one sample directory on the Engine page (`F6`). Cagire indexes audio files (`.wav`, `.mp3`, `.ogg`, `.flac`, `.aac`, `.m4a`) from all registered paths.
## Browsing
@@ -28,15 +28,3 @@ kick sound .
See the **Samples** section in the Audio Engine documentation for details on how sample playback works.
## Keybindings
| Key | Action |
|-----|--------|
| `Tab` | Open / close browser |
| `Up` / `Down` | Navigate |
| `Right` | Expand folder / play file |
| `Left` | Collapse folder |
| `Enter` | Play file |
| `PageUp` / `PageDown` | Fast scroll |
| `/` | Search |
| `Esc` | Clear search / close |

View File

@@ -23,15 +23,3 @@ When saving, type a filename and press `Enter`. Parent directories are created a
When loading, browse to a `.cagire` file and press `Enter`. The project replaces the current session entirely.
## Keybindings
| Key | Action |
|-----|--------|
| `s` | Save (from any view) |
| `l` | Load (from any view) |
| `Up` / `Down` | Browse entries |
| `Right` | Enter directory |
| `Left` | Parent directory |
| `Tab` | Autocomplete path |
| `Enter` | Confirm |
| `Esc` | Cancel |

View File

@@ -1,18 +1,27 @@
# Stage / Commit
Cagire requires you to `stage` changes you wish to make to the playback state and then `commit` it. It is way more simple than it seems. For instance, you mark pattern `04` and `05` to start playing, and _then_ you send the order to start the playback (`commit`). The same goes for stopping patterns. You mark which pattern to stop (`stage`) and then you give the order to stop them (`commit`). Why is staging useful? Here are some reasons why this design choice was made:
In Cagire, changes to playback happen in two steps. First you **stage**: you mark what you want to happen. Then you **commit**: you apply all staged changes at once. Nothing changes until you commit. It is simpler than it sounds.
- **To apply multiple changes**: Queue several patterns to start/stop, commit them together.
- **To get clean timing**: All changes happen on beat/bar boundaries.
- **To help with live performance**: Prepare the next section without affecting current playback.
Say you want patterns `04` and `05` to start playing together. You stage both (`p` on each), then commit (`c`). Both start at the same time. Want to stop them later? Stage them again, commit again. That's it.
This two-step process exists for good reasons:
- **Multiple changes at once**: queue several patterns to start/stop, commit them together.
- **Clean timing**: all changes land on beat or bar boundaries, never mid-step.
- **Safe preparation**: set up the next section while the current one keeps playing.
## Push changes, then apply
Staging is an essential feature to understand to be effective when doing live performances:
1. Open the **Patterns** view (`F2` or `Ctrl+Up` from sequencer)
2. Navigate to a pattern you wish to change/play
3. Press `p` to stage it. The pending change is going to be displayed:
- `+` (staged to play)
- `+` (staged to play)
- `-` (staged to stop)
- `m` (staged to mute)
- `s` (staged to solo)
- etc.
4. Repeat for other patterns you want to change
5. Press `c` to commit all changes
6. Or press `Esc` to cancel
@@ -24,7 +33,8 @@ You can also stage mute/solo changes:
- Press `Shift+m` to clear all mutes
- Press `Shift+x` to clear all solos
A pattern might not start immediately depending on the sync mode you have chosen. It might wait for the next beat/bar boundary.
A pattern might not start immediately depending on the sync mode you have chosen.
It might wait for the next beat/bar boundary.
## Status Indicators

View File

@@ -13,10 +13,17 @@ Every step has a duration. By default, sounds emit at the very start of that dur
Push multiple values before calling `at` to get multiple emits from a single `.`:
```forth
0 0.5 at kick s . ;; two kicks: one at start, one at midpoint
0 0.25 0.5 0.75 at hat s . ;; four hats, evenly spaced
0 0.5 at kick s .
```
Two kicks: one at start, one at midpoint.
```forth
0 0.25 0.5 0.75 at hat s .
```
Four hats, evenly spaced.
The deltas persist across multiple `.` calls until `clear` or a new `at`:
```forth
@@ -33,10 +40,10 @@ Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2
```forth
0 0.5 at
c4 e4 g4 note sine s . ;; 6 emits: 3 notes x 2 deltas
c4 e4 g4 note 1.5 decay sine s .
```
This is a chord played twice per step.
6 emits: 3 notes x 2 deltas. A chord played twice per step.
## 1:1 Pairing: at With arp
@@ -44,16 +51,20 @@ This is a chord played twice per step.
```forth
0 0.33 0.66 at
c4 e4 g4 arp note sine s . ;; c4 at 0, e4 at 0.33, g4 at 0.66
c4 e4 g4 arp note 0.5 decay sine s .
```
C4 at 0, E4 at 0.33, G4 at 0.66.
If the lists differ in length, the shorter one wraps around:
```forth
0 0.25 0.5 0.75 at
c4 e4 arp note sine s . ;; c4, e4, c4, e4 at 4 time points
c4 e4 arp note 0.3 decay sine s .
```
C4, E4, C4, E4 — the shorter list wraps to fill 4 time points.
This is THE key distinction. Without `arp`: every note at every time. With `arp`: one note per time slot.
## Generating Deltas
@@ -89,12 +100,18 @@ Geometric spacing via `geom..`:
Wrap `at` expressions in quotations for conditional timing:
```forth
( 0 0.25 0.5 0.75 at ) 2 every ;; 16th-note hats every other bar
( 0 0.25 0.5 0.75 at ) 2 every
hat s .
```
( 0 0.5 at ) 0.5 chance ;; 50% chance of double-hit
16th-note hats every other bar.
```forth
( 0 0.5 at ) 0.5 chance
kick s .
```
50% chance of double-hit.
When the quotation doesn't execute, no deltas are set -- you get the default single emit at beat start.

View File

@@ -1,46 +1,46 @@
# Notes & Harmony
Cagire speaks music theory. Notes, intervals, chords, and scales are all first-class words that compile to stack operations on MIDI values. This tutorial covers every pitch-related feature.
This tutorial covers everything pitch-related: notes, intervals, chords, voicings, transposition, scales, and diatonic harmony. Each section builds on the previous one.
## MIDI Notes
## Notes
Write a note name followed by an octave number. It compiles to a MIDI integer:
A note name followed by an octave number compiles to a MIDI integer:
```forth
c4 ;; 60 (middle C)
a4 ;; 69 (concert A)
e3 ;; 52
c4 note sine s .
```
Sharps use `s` or `#`. Flats use `b`:
That plays middle C (MIDI 60). `a4` is concert A (69), `e3` is 52. Sharps use `s` or `#`, flats use `b`:
```forth
fs4 ;; 66 (F sharp 4)
f#4 ;; 66 (same thing)
bb3 ;; 58 (B flat 3)
eb4 ;; 63
fs4 note 0.5 decay saw s .
```
Octave range is -1 to 9. The formula is `(octave + 1) * 12 + base + modifier`, where C=0, D=2, E=4, F=5, G=7, A=9, B=11.
Note literals push a single integer onto the stack, just like writing `60` directly. They work everywhere an integer works:
```forth
c4 note sine s . ;; play middle C as a sine
a4 note 0.5 gain modal s . ;; concert A, quieter
eb4 note 0.8 decay tri s .
```
`fs4` and `f#4` both mean F sharp 4 (MIDI 66). `bb3` is B flat 3 (58). Octave range is -1 to 9.
Notes are just integers. They work anywhere an integer works — you can do arithmetic on them, store them in variables, pass them to any word that expects a number.
## Intervals
An interval duplicates the top of the stack and adds semitones. This lets you build chords by stacking:
An interval duplicates the top of the stack and adds semitones. Stack two intervals to build a chord by hand:
```forth
c4 M3 P5 ;; stack: 60 64 67 (C major triad)
c4 m3 P5 ;; stack: 60 63 67 (C minor triad)
a3 P5 ;; stack: 57 64 (A plus a fifth)
c4 M3 P5 note 1.5 decay sine s .
```
Simple intervals (within one octave):
That builds a C major triad from scratch: C4 (60), then a major third above (64), then a perfect fifth above the root (67). Three notes on the stack, all played together.
```forth
a3 m3 P5 note 1.2 decay va s .
```
A minor triad: A3, C4, E4.
**Simple intervals** (within one octave):
| Interval | Semitones | Name |
|----------|-----------|------|
@@ -58,7 +58,7 @@ Simple intervals (within one octave):
| `M7` | 11 | Major 7th |
| `P8` | 12 | Octave |
Compound intervals (beyond one octave):
**Compound intervals** (beyond one octave):
| Interval | Semitones |
|----------|-----------|
@@ -75,108 +75,333 @@ Compound intervals (beyond one octave):
| `M14` | 23 |
| `P15` | 24 |
## Chords
Chord words take a root note and push all the chord tones. They eat the root and replace it with the full voicing:
Custom voicings with wide intervals:
```forth
c4 maj ;; stack: 60 64 67
c4 min7 ;; stack: 60 63 67 70
c4 dom9 ;; stack: 60 64 67 70 74
c3 P5 P8 M10 note 1.5 decay sine s .
```
**Triads:**
C3, G3, C4, E4 — an open-voiced C major spread across two octaves.
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `maj` | 0 4 7 | 60 64 67 |
| `m` | 0 3 7 | 60 63 67 |
| `dim` | 0 3 6 | 60 63 66 |
| `aug` | 0 4 8 | 60 64 68 |
| `sus2` | 0 2 7 | 60 62 67 |
| `sus4` | 0 5 7 | 60 65 67 |
## Chords
**Seventh chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `maj7` | 0 4 7 11 | 60 64 67 71 |
| `min7` | 0 3 7 10 | 60 63 67 70 |
| `dom7` | 0 4 7 10 | 60 64 67 70 |
| `dim7` | 0 3 6 9 | 60 63 66 69 |
| `m7b5` | 0 3 6 10 | 60 63 66 70 |
| `minmaj7` | 0 3 7 11 | 60 63 67 71 |
| `aug7` | 0 4 8 10 | 60 64 68 70 |
**Sixth chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `maj6` | 0 4 7 9 | 60 64 67 69 |
| `min6` | 0 3 7 9 | 60 63 67 69 |
**Extended chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `dom9` | 0 4 7 10 14 | 60 64 67 70 74 |
| `maj9` | 0 4 7 11 14 | 60 64 67 71 74 |
| `min9` | 0 3 7 10 14 | 60 63 67 70 74 |
| `dom11` | 0 4 7 10 14 17 | 60 64 67 70 74 77 |
| `min11` | 0 3 7 10 14 17 | 60 63 67 70 74 77 |
| `dom13` | 0 4 7 10 14 21 | 60 64 67 70 74 81 |
**Add chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `add9` | 0 4 7 14 | 60 64 67 74 |
| `add11` | 0 4 7 17 | 60 64 67 77 |
| `madd9` | 0 3 7 14 | 60 63 67 74 |
**Altered dominants:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `dom7b9` | 0 4 7 10 13 | 60 64 67 70 73 |
| `dom7s9` | 0 4 7 10 15 | 60 64 67 70 75 |
| `dom7b5` | 0 4 6 10 | 60 64 66 70 |
| `dom7s5` | 0 4 8 10 | 60 64 68 70 |
Chord tones are varargs -- they eat the entire stack. So a chord word should come right after the root note:
Chord words replace a root note with all the chord tones. They're shortcuts for what intervals do manually:
```forth
c4 maj note sine s . ;; plays all 3 notes as one chord
c4 maj note 1.5 decay sine s .
```
That's the same C major triad, but in one word instead of `M3 P5`. A few more:
```forth
d3 min7 note 1.5 decay va s .
```
```forth
e3 dom9 note 1.2 decay saw s .
```
```forth
a3 sus2 note 1.5 decay tri s .
```
Common triads:
| Word | Intervals |
|------|-----------|
| `maj` | 0 4 7 |
| `m` | 0 3 7 |
| `dim` | 0 3 6 |
| `aug` | 0 4 8 |
| `sus2` | 0 2 7 |
| `sus4` | 0 5 7 |
| `pwr` | 0 7 |
Common seventh chords:
| Word | Intervals |
|------|-----------|
| `maj7` | 0 4 7 11 |
| `min7` | 0 3 7 10 |
| `dom7` | 0 4 7 10 |
| `dim7` | 0 3 6 9 |
| `m7b5` | 0 3 6 10 |
| `minmaj7` | 0 3 7 11 |
| `aug7` | 0 4 8 10 |
| `augmaj7` | 0 4 8 11 |
| `7sus4` | 0 5 7 10 |
Extended, add, altered, and other chord types are listed in the Reference section at the end.
## Voicings
Four words reshape chord voicings without changing the harmony.
`inv` moves the bottom note up an octave (inversion):
```forth
c4 maj inv note 1.5 decay sine s .
```
The root C goes up, giving E4 G4 C5 — first inversion. Apply it twice for second inversion:
```forth
c4 maj inv inv note 1.5 decay sine s .
```
G4 C5 E5. `dinv` does the opposite — moves the top note down an octave:
```forth
c4 maj dinv note 1.5 decay sine s .
```
G3 C4 E4. The fifth drops below the root.
`drop2` and `drop3` are jazz voicing techniques for four-note chords. `drop2` takes the second-from-top note and drops it an octave:
```forth
c4 maj7 drop2 note 1.2 decay va s .
```
From C4 E4 G4 B4, the G drops to G3: G3 C4 E4 B4. `drop3` drops the third-from-top:
```forth
c4 maj7 drop3 note 1.2 decay va s .
```
E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jazz guitar and piano.
## Transposition
`tp` shifts every note on the stack by N semitones:
```forth
c4 maj 3 tp note 1.5 decay sine s .
```
C major transposed up 3 semitones becomes Eb major. Works with any number of notes:
```forth
c4 min7 -2 tp note 1.5 decay va s .
```
Shifts the whole chord down 2 semitones (Bb minor 7).
`oct` shifts a single note by octaves:
```forth
c4 1 oct note 0.3 decay sine s .
```
C5 (one octave up). Useful for bass lines:
```forth
0 2 4 5 7 5 4 2 8 cycle minor note
-2 oct 0.8 gain sine s .
```
## Scales
Scale words convert a degree index into a MIDI note. The base note is C4 (MIDI 60). Degrees wrap around with octave transposition:
Scale words convert a degree index into a MIDI note. By default the root is C4 (MIDI 60):
```forth
0 major ;; 60 (C4 -- degree 0)
4 major ;; 67 (G4 -- degree 4)
7 major ;; 72 (C5 -- degree 7, wraps to next octave)
-1 major ;; 59 (B3 -- negative degrees go down)
0 major note 0.5 decay sine s .
```
Use scales with `cycle` or `rand` to walk through pitches:
Degree 0 of the major scale: C4. Degrees wrap with octave transposition — degree 7 gives C5 (72), degree -1 gives B3 (59).
Walk through a scale with `cycle`:
```forth
0 1 2 3 4 5 6 7 8 cycle minor note sine s .
0 1 2 3 4 5 6 7 8 cycle minor note 0.5 decay sine s .
```
**Standard modes:**
Random notes from a scale:
| Word | Pattern (semitones) |
|------|-------------------|
```forth
0 7 rand pentatonic note 0.8 decay va s .
```
### Setting the key
By default scales are rooted at C4. Use `key!` to change the tonal center:
```forth
g3 key! 0 major note 0.5 decay sine s .
```
Now degree 0 is G3 (55) instead of C4. The key persists across steps until changed again:
```forth
a3 key! 0 3 5 7 3 cycle minor note 0.8 decay tri s .
```
A minor melody starting from A3.
**Common modes:**
| Word | Pattern |
|------|---------|
| `major` | 0 2 4 5 7 9 11 |
| `minor` | 0 2 3 5 7 8 10 |
| `dorian` | 0 2 3 5 7 9 10 |
| `phrygian` | 0 1 3 5 7 8 10 |
| `lydian` | 0 2 4 6 7 9 11 |
| `mixolydian` | 0 2 4 5 7 9 10 |
| `aeolian` | 0 2 3 5 7 8 10 |
| `pentatonic` | 0 2 4 7 9 |
| `minpent` | 0 3 5 7 10 |
| `blues` | 0 3 5 6 7 10 |
| `harmonicminor` | 0 2 3 5 7 8 11 |
| `melodicminor` | 0 2 3 5 7 9 11 |
Jazz, symmetric, and modal variant scales are listed in the Reference section.
## Diatonic Harmony
`triad` and `seventh` build chords from scale degrees. Instead of specifying a chord type, you get whatever chord the scale produces at that degree:
```forth
0 major triad note 1.5 decay sine s .
```
Degree 0 of the major scale, stacked in thirds: C E G — a major triad. The scale determines the chord quality automatically. Degree 1 gives D F A (minor), degree 4 gives G B D (major):
```forth
4 major triad note 1.5 decay sine s .
```
`seventh` adds a fourth note:
```forth
0 major seventh note 1.2 decay va s .
```
C E G B — Cmaj7. Degree 1 gives Dm7, degree 4 gives G7 (dominant). The diatonic context determines everything.
Combine with `key!` to play diatonic chords in any key:
```forth
g3 key! 0 major triad note 1.5 decay sine s .
```
G major triad rooted at G3.
A I-vi-IV-V chord progression using `pcycle`:
```forth
( 0 major seventh ) ( 5 major seventh )
( 3 major seventh ) ( 4 major seventh ) 4 pcycle
note 1.2 decay va s .
```
Combine with voicings for smoother voice leading:
```forth
( 0 major seventh ) ( 5 major seventh inv )
( 3 major seventh ) ( 4 major seventh drop2 ) 4 pcycle
note 1.5 decay va s .
```
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):
```forth
0 major seventh arp note 0.5 decay sine s .
```
## Frequency Conversion
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
```forth
c4 mtof freq sine s .
```
Useful when a synth parameter expects Hz rather than MIDI.
## Reference
### All Chords
**Triads:**
| Word | Intervals |
|------|-----------|
| `maj` | 0 4 7 |
| `m` | 0 3 7 |
| `dim` | 0 3 6 |
| `aug` | 0 4 8 |
| `sus2` | 0 2 7 |
| `sus4` | 0 5 7 |
| `pwr` | 0 7 |
**Seventh chords:**
| Word | Intervals |
|------|-----------|
| `maj7` | 0 4 7 11 |
| `min7` | 0 3 7 10 |
| `dom7` | 0 4 7 10 |
| `dim7` | 0 3 6 9 |
| `m7b5` | 0 3 6 10 |
| `minmaj7` | 0 3 7 11 |
| `aug7` | 0 4 8 10 |
| `augmaj7` | 0 4 8 11 |
| `7sus4` | 0 5 7 10 |
**Sixth chords:**
| Word | Intervals |
|------|-----------|
| `maj6` | 0 4 7 9 |
| `min6` | 0 3 7 9 |
| `maj69` | 0 4 7 9 14 |
| `min69` | 0 3 7 9 14 |
**Extended chords:**
| Word | Intervals |
|------|-----------|
| `dom9` | 0 4 7 10 14 |
| `maj9` | 0 4 7 11 14 |
| `min9` | 0 3 7 10 14 |
| `9sus4` | 0 5 7 10 14 |
| `dom11` | 0 4 7 10 14 17 |
| `maj11` | 0 4 7 11 14 17 |
| `min11` | 0 3 7 10 14 17 |
| `dom13` | 0 4 7 10 14 21 |
| `maj13` | 0 4 7 11 14 21 |
| `min13` | 0 3 7 10 14 21 |
**Add chords:**
| Word | Intervals |
|------|-----------|
| `add9` | 0 4 7 14 |
| `add11` | 0 4 7 17 |
| `madd9` | 0 3 7 14 |
**Altered dominants:**
| Word | Intervals |
|------|-----------|
| `dom7b9` | 0 4 7 10 13 |
| `dom7s9` | 0 4 7 10 15 |
| `dom7b5` | 0 4 6 10 |
| `dom7s5` | 0 4 8 10 |
| `dom7s11` | 0 4 7 10 18 |
### All Scales
**Modes:**
| Word | Pattern |
|------|---------|
| `major` | 0 2 4 5 7 9 11 |
| `minor` / `aeolian` | 0 2 3 5 7 8 10 |
| `dorian` | 0 2 3 5 7 9 10 |
| `phrygian` | 0 1 3 5 7 8 10 |
| `lydian` | 0 2 4 6 7 9 11 |
| `mixolydian` | 0 2 4 5 7 9 10 |
| `locrian` | 0 1 3 5 6 8 10 |
**Pentatonic and blues:**
@@ -187,13 +412,6 @@ Use scales with `cycle` or `rand` to walk through pitches:
| `minpent` | 0 3 5 7 10 |
| `blues` | 0 3 5 6 7 10 |
**Chromatic and whole tone:**
| Word | Pattern |
|------|---------|
| `chromatic` | 0 1 2 3 4 5 6 7 8 9 10 11 |
| `wholetone` | 0 2 4 6 8 10 |
**Harmonic and melodic minor:**
| Word | Pattern |
@@ -201,6 +419,13 @@ Use scales with `cycle` or `rand` to walk through pitches:
| `harmonicminor` | 0 2 3 5 7 8 11 |
| `melodicminor` | 0 2 3 5 7 9 11 |
**Chromatic and whole tone:**
| Word | Pattern |
|------|---------|
| `chromatic` | 0 1 2 3 4 5 6 7 8 9 10 11 |
| `wholetone` | 0 2 4 6 8 10 |
**Jazz / Bebop:**
| Word | Pattern |
@@ -229,74 +454,3 @@ Use scales with `cycle` or `rand` to walk through pitches:
| `lydianaug` | 0 2 4 6 8 9 11 |
| `mixb6` | 0 2 4 5 7 8 10 |
| `locrian2` | 0 2 3 5 6 8 10 |
## Octave Shifting
`oct` transposes a note by octaves:
```forth
c4 1 oct ;; 72 (C5)
c4 -1 oct ;; 48 (C3)
c4 2 oct ;; 84 (C6)
```
Stack effect: `(note shift -- transposed)`. The shift is multiplied by 12 and added to the note.
## Frequency Conversion
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
```forth
69 mtof ;; 440.0 (A4)
60 mtof ;; 261.63 (C4)
440 ftom ;; 69.0
```
Useful when a synth parameter expects Hz rather than MIDI:
```forth
c4 mtof freq sine s .
```
## Putting It Together
A chord progression cycling every pattern iteration:
```forth
( c3 maj7 ) ( f3 maj7 ) ( g3 dom7 ) ( c3 maj7 ) 4 pcycle
note sine s .
```
Arpeggiate a chord across the step's time divisions:
```forth
c4 min7 arp note 0.5 decay sine s .
```
Random notes from a scale:
```forth
0 7 rand minor note sine s .
```
A bass line walking scale degrees:
```forth
0 2 4 5 7 5 4 2 8 cycle minor note
-2 oct 0.8 gain sine s .
```
Chord voicings with random inversion:
```forth
e3 min9
( ) ( 1 oct ) 2 choose
note modal s .
```
Stacked intervals for custom voicings:
```forth
c3 P5 P8 M10 ;; C3, G3, C4, E4
note sine s .
```

View File

@@ -0,0 +1,43 @@
# The Periodic Script
The periodic script is a hidden seventh view accessible with `F11`. It is a Forth script that runs continuously alongside your patterns, evaluated at every step like a pattern would be. Think of it as a free-running pattern with no grid — just code.
## What is it for?
The periodic script is useful for things that don't belong to any specific pattern:
- **Global effects**: apply a filter sweep or reverb tail across everything.
- **Drones**: run a sustained sound that keeps going regardless of which patterns are playing.
- **Control logic**: update variables, send MIDI clock, or modulate global parameters.
- **Experimentation**: sketch ideas without touching your pattern grid.
## Opening the Script
Press `F11` from any view. The script page appears with an editor on the left and visualizations on the right (scope, spectrum, prelude preview). The script is saved with your project.
## Editing
Press `Enter` to focus the editor. Write Forth code as you would in any step. Press `Esc` to unfocus and save. Press `Ctrl+E` to evaluate without unfocusing.
```forth
;; a simple drone
saw s c2 note 0.3 gain 0.4 verb .
```
## Speed and Length
The periodic script has its own speed and length settings, independent of any pattern. Press `S` (unfocused) to set the speed and `L` to set the length (1-256 steps). Speed and length are displayed in the editor title bar.
The script loops over its length just like a pattern. Context words like `step`, `iter`, and `phase` work as expected, counting within the script's own cycle.
## Keybindings
| Key | Action |
|-----|--------|
| `F11` | Open periodic script view |
| `Enter` | Focus editor |
| `Esc` | Unfocus and save |
| `Ctrl+E` | Evaluate |
| `Ctrl+S` | Toggle stack display |
| `S` | Set speed (unfocused) |
| `L` | Set length (unfocused) |

View File

@@ -7,21 +7,25 @@ Music needs surprise. A pattern that plays identically every time gets boring fa
`coin` pushes 0 or 1 with equal probability:
```forth
coin note sine s . ;; either 0 or 1 as the note
;; sometimes, reverb
sine sound
( 0.5 verb ) coin ?
1 decay
.
```
`rand` takes a range and returns a random value. If both bounds are integers, the result is an integer. If either is a float, you get a float:
```forth
60 72 rand note sine s . ;; random MIDI note from 60 to 72
0.3 0.9 rand gain sine s . ;; random gain between 0.3 and 0.9
60 72 rand note sine s .5 decay . ;; random MIDI note from 60 to 72
0.3 0.9 rand gain sine s .5 decay . ;; random gain between 0.3 and 0.9
```
`exprand` and `logrand` give you weighted distributions. `exprand` is biased toward the low end, `logrand` toward the high end:
```forth
200.0 8000.0 exprand freq sine s . ;; mostly low frequencies
200.0 8000.0 logrand freq sine s . ;; mostly high frequencies
200.0 8000.0 exprand freq sine s .5 decay . ;; mostly low frequencies
200.0 8000.0 logrand freq sine s .5 decay . ;; mostly high frequencies
```
These are useful for parameters where perception is logarithmic, like frequency and duration.
@@ -32,7 +36,7 @@ The probability words take a quotation and execute it with some chance. `chance`
```forth
( hat s . ) 0.25 chance ;; 25% chance
( hat s . ) 75 prob ;; 75% chance
( kick s . ) 75 prob ;; 75% chance
```
Named probability words save you from remembering numbers:
@@ -67,8 +71,8 @@ Use `?` and `!?` with `coin` for quick coin-flip decisions:
`choose` picks randomly from n items on the stack:
```forth
kick snare hat 3 choose s . ;; random drum hit
60 64 67 72 4 choose note sine s . ;; random note from a set
kick snare hat 3 choose s . ;; random drum hit
60 64 67 72 4 choose note sine s .5 decay . ;; random note from a set
```
When a chosen item is a quotation, it gets executed:
@@ -94,37 +98,6 @@ Kick plays 50% of the time, snare 30%, hat 20%. Weights don't need to sum to 1 -
Combined with `note`, this gives you a random permutation of a chord every time the step runs.
## Cycling
Cycling steps through values deterministically. No randomness -- pure rotation.
`cycle` selects based on how many times this step has played (its `runs` count):
```forth
60 64 67 3 cycle note sine s . ;; 60, 64, 67, 60, 64, 67, ...
```
`pcycle` selects based on the pattern iteration count (`iter`):
```forth
kick snare 2 pcycle s . ;; kick on even iterations, snare on odd
```
The difference matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
Quotations work here too:
```forth
( c4 note ) ( e4 note ) ( g4 note ) 3 cycle
sine s .
```
`bounce` ping-pongs instead of wrapping around:
```forth
60 64 67 72 4 bounce note sine s . ;; 60, 64, 67, 72, 67, 64, 60, 64, ...
```
## Periodic Execution
`every` runs a quotation once every n pattern iterations:
@@ -139,6 +112,20 @@ sine s .
( 2 distort ) 4 except ;; distort on all iterations except every 4th
```
`every+` and `except+` take an extra offset argument to shift the phase:
```forth
( snare s . ) 4 2 every+ ;; fires at iter 2, 6, 10, 14...
( snare s . ) 4 2 except+ ;; skips at iter 2, 6, 10, 14...
```
Without the offset, `every` fires at 0, 4, 8... The offset shifts that by 2, so it fires at 2, 6, 10... This lets you interleave patterns that share the same period:
```forth
( kick s . ) 4 every ;; kick at 0, 4, 8...
( snare s . ) 4 2 every+ ;; snare at 2, 6, 10...
```
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms:
```forth
@@ -204,4 +191,4 @@ c4 0.4 e4 0.3 g4 0.2 b4 0.1 4 wchoose note
modal s .
```
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.

View File

@@ -1,25 +1,25 @@
# Welcome to Cagire
Cagire is a live-codable step sequencer. Each sequencer step is defined by a **Forth** script that gets evaluated at the right time. **Forth** is a minimal, fun and rewarding programming language. It has almost no syntax but provides infinite fun. It rewards exploration, creativity and curiosity. This documentation is both a _tutorial_ and a _reference_. All the code examples in the documentation are interactive. **You can run them!** Use `n` and `p` (next/previous) to navigate through the examples. Press `Enter` to evaluate an example! Try to evaluate the following example using `n`, `p` and `Enter`:
Cagire is a live-codable step sequencer. Each sequencer step is defined by a **Forth** script that gets evaluated at the right time. **Forth** is a minimal, fun and rewarding programming language. It has almost no syntax but provides infinite fun. It rewards exploration, creativity and curiosity.
This documentation is both a _tutorial_ and a _reference_. All the code examples in the documentation are interactive. **You can run them!** Use `n` and `p` (next/previous) to navigate through the examples. Press `Enter` to evaluate an example! Try to evaluate the following example using `n`, `p` and `Enter`:
```forth
saw sound
400 freq
1 decay
c4 note
0.5 decay
.
```
## What is live coding?
Live coding is a technique where you write code in real-time to create audiovisual performances. Most often, it is practiced in front of an audience. Live coding is a way to experiment with code, to share things and thoughts openly, to think through code. It can be technical, poetic, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media with a strong emphasis on _improvisation_. Learn more about live coding on [https://toplap.org](https://toplap.org) or [https://livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: the act of doing it is its own reward. There are no errors, only fun.
Live coding is a technique where you write code in real-time to create audiovisual performances. Most often, it is practiced in front of an audience. Live coding is a way to experiment with code, to share things and thoughts openly, to think through code. It can be technical, poetic, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media with a strong emphasis on _improvisation_. Learn more about live coding on [toplap.org](https://toplap.org) or [livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: the act of doing it is its own reward. There are no errors, only fun.
## About
Cagire is mainly developed by BuboBubo (Raphaël Maurice Forment, [https://raphaelforment.fr](https://raphaelforment.fr)). It is a free and open-source project licensed under the `AGPL-3.0 License`. You are free to contribute to the project by contributing to the codebase or by sharing feedback. Help and feedback are welcome!
Cagire is mainly developed by BuboBubo (Raphaël Maurice Forment, [raphaelforment.fr](https://raphaelforment.fr)). It is a free and open-source project licensed under the `AGPL-3.0 License`. You are free to contribute to the project by contributing to the codebase or by sharing feedback. Help and feedback are welcome!
### Credits
* **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
* **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits.
* **Author**: Oliver Rockstedt [info@sourcebox.de](info@sourcebox.de).
* **Original author**: Emilie Gillet [emilie.o.gillet@gmail.com](emilie.o.gillet@gmail.com).
* **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits (Emilie Gillet). Rust port by Oliver Rockstedt.

1
release.toml Normal file
View File

@@ -0,0 +1 @@
publish = false

26
src/README.md Normal file
View File

@@ -0,0 +1,26 @@
# cagire (main application)
Terminal UI application — ties together the Forth VM, audio engine, and project model.
## Modules
| Module | Description |
|--------|-------------|
| `app/` | `App` struct and submodules: dispatch, editing, navigation, persistence, scripting, sequencer, clipboard, staging, undo |
| `engine/` | Audio engine: `sequencer`, `audio`, `link` (Ableton Link), `dispatcher`, `realtime`, `timing` |
| `input/` | Keyboard/mouse handling: per-page handlers, modal input, `InputContext` |
| `views/` | Pure rendering functions taking `&App` |
| `state/` | UI state modules (audio, editor, modals, panels, playback, ...) |
| `services/` | Domain logic: clipboard, dict navigation, euclidean, help navigation, pattern editor, stack preview |
| `model/` | Domain models: docs, categories, onboarding, script |
| `commands` | `AppCommand` enum (~150 variants) |
| `page` | `Page` navigation enum |
| `midi` | MIDI I/O (up to 4 inputs/outputs) |
| `settings` | Confy-based persistent settings |
## Key Types
- **`App`** — Central application state, coordinates all subsystems
- **`AppCommand`** — Enum of all user actions, dispatched via `App::dispatch()`
- **`InputContext`** — Holds `&mut App` + channel senders, bridges input to commands
- **`Page`** — 3x2 page grid (Dict, Patterns, Options, Help, Main, Engine)

View File

@@ -308,12 +308,6 @@ impl App {
self.ui.show_title = false;
self.maybe_show_onboarding();
}
AppCommand::ToggleEditorStack => {
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
if self.editor_ctx.show_stack {
crate::services::stack_preview::update_cache(&self.editor_ctx);
}
}
AppCommand::SetColorScheme(scheme) => {
self.ui.color_scheme = scheme;
let palette = scheme.to_palette();
@@ -494,9 +488,6 @@ impl App {
}
AppCommand::ScriptSave => self.save_script_from_editor(),
AppCommand::ScriptEvaluate => self.evaluate_script_page(link),
AppCommand::ToggleScriptStack => {
self.script_editor.show_stack = !self.script_editor.show_stack;
}
}
}

View File

@@ -61,9 +61,6 @@ impl App {
.set_completion_enabled(self.ui.show_completion);
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
if self.editor_ctx.show_stack {
crate::services::stack_preview::update_cache(&self.editor_ctx);
}
}
}
@@ -129,20 +126,33 @@ impl App {
}
/// Evaluate a script and immediately send its audio commands.
/// Returns collected `print` output, if any.
pub fn execute_script_oneshot(
&self,
script: &str,
link: &LinkState,
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
) -> Result<(), String> {
) -> Result<Option<String>, String> {
let ctx = self.create_step_context(self.editor_ctx.step, link);
let cmds = self.script_engine.evaluate(script, &ctx)?;
let mut print_output = String::new();
for cmd in cmds {
if let Some(text) = cmd.strip_prefix("print:") {
if !print_output.is_empty() {
print_output.push(' ');
}
print_output.push_str(text);
continue;
}
let _ = audio_tx
.load()
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
}
Ok(())
Ok(if print_output.is_empty() {
None
} else {
Some(print_output)
})
}
/// Compile (evaluate) the current step's script to check for errors.

View File

@@ -221,7 +221,6 @@ pub enum AppCommand {
// UI state
ClearMinimap,
HideTitle,
ToggleEditorStack,
SetColorScheme(ColorScheme),
SetHueRotation(f32),
ToggleRuntimeHighlight,
@@ -317,5 +316,4 @@ pub enum AppCommand {
SetScriptLength(usize),
ScriptSave,
ScriptEvaluate,
ToggleScriptStack,
}

View File

@@ -176,7 +176,7 @@ impl SpectrumAnalyzer {
let avg = sum / (hi - lo) as f32;
let amplitude = avg / (FFT_SIZE as f32 / 4.0);
let db = 20.0 * amplitude.max(1e-10).log10();
*mag = ((db + 60.0) / 60.0).clamp(0.0, 1.0);
*mag = ((db + 80.0) / 80.0).clamp(0.0, 1.0);
}
output.write(&bands);

View File

@@ -171,6 +171,7 @@ pub struct SharedSequencerState {
pub tempo: f64,
pub beat: f64,
pub script_trace: Option<ExecutionTrace>,
pub print_output: Option<String>,
}
pub struct SequencerSnapshot {
@@ -180,6 +181,7 @@ pub struct SequencerSnapshot {
pub tempo: f64,
pub beat: f64,
script_trace: Option<ExecutionTrace>,
pub print_output: Option<String>,
}
impl From<&SharedSequencerState> for SequencerSnapshot {
@@ -191,6 +193,7 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
tempo: s.tempo,
beat: s.beat,
script_trace: s.script_trace.clone(),
print_output: s.print_output.clone(),
}
}
}
@@ -205,6 +208,7 @@ impl SequencerSnapshot {
tempo: 0.0,
beat: 0.0,
script_trace: None,
print_output: None,
}
}
@@ -573,6 +577,7 @@ pub struct SequencerState {
script_frontier: f64,
script_step: usize,
script_trace: Option<ExecutionTrace>,
print_output: Option<String>,
}
impl SequencerState {
@@ -610,6 +615,7 @@ impl SequencerState {
script_frontier: -1.0,
script_step: 0,
script_trace: None,
print_output: None,
}
}
@@ -817,6 +823,7 @@ impl SequencerState {
self.script_frontier = -1.0;
self.script_step = 0;
self.script_trace = None;
self.print_output = None;
self.buf_audio_commands.clear();
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
TickOutput {
@@ -922,6 +929,7 @@ impl SequencerState {
) -> StepResult {
self.buf_audio_commands.clear();
self.buf_completed_iterations.clear();
let mut print_cleared = false;
let mut result = StepResult {
any_step_fired: false,
};
@@ -1012,12 +1020,26 @@ impl SequencerState {
std::mem::take(&mut trace),
);
if !print_cleared {
self.print_output = None;
print_cleared = true;
}
for cmd in cmds {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
if let Some(text) = cmd.strip_prefix("print:") {
match &mut self.print_output {
Some(existing) => {
existing.push(' ');
existing.push_str(text);
}
None => self.print_output = Some(text.to_string()),
}
} else {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
}
}
}
}
@@ -1113,12 +1135,23 @@ impl SequencerState {
self.script_engine
.evaluate_with_trace(&self.script_text, &ctx, &mut trace)
{
self.print_output = None;
for cmd in cmds {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
if let Some(text) = cmd.strip_prefix("print:") {
match &mut self.print_output {
Some(existing) => {
existing.push(' ');
existing.push_str(text);
}
None => self.print_output = Some(text.to_string()),
}
} else {
self.event_count += 1;
self.buf_audio_commands.push(TimestampedCommand {
cmd,
time: event_time,
});
}
}
}
self.script_trace = Some(trace);
@@ -1201,6 +1234,7 @@ impl SequencerState {
tempo: self.last_tempo,
beat: self.last_beat,
script_trace: self.script_trace.clone(),
print_output: self.print_output.clone(),
}
}
}

View File

@@ -30,6 +30,7 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
}
KeyCode::Esc if ctx.app.ui.help_focused_block.is_some() => {
ctx.app.ui.help_focused_block = None;
ctx.app.ui.help_block_output = None;
}
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
KeyCode::Left if ctx.app.ui.help_focus == HelpFocus::Topics => {
@@ -106,6 +107,7 @@ fn navigate_code_block(ctx: &mut InputContext, forward: bool) {
let scroll_to = parsed.code_blocks[next].start_line.saturating_sub(2);
drop(cache);
ctx.app.ui.help_focused_block = Some(next);
ctx.app.ui.help_block_output = None;
*ctx.app.ui.help_scroll_mut() = scroll_to;
}
@@ -115,7 +117,7 @@ fn execute_focused_block(ctx: &mut InputContext) {
let Some(parsed) = cache[ctx.app.ui.help_topic].as_ref() else {
return;
};
let idx = ctx.app.ui.help_focused_block.expect("block focused in code nav");
let idx = ctx.app.ui.help_focused_block.unwrap();
let Some(block) = parsed.code_blocks.get(idx) else {
return;
};
@@ -126,11 +128,17 @@ fn execute_focused_block(ctx: &mut InputContext) {
.map(|l| l.split(" => ").next().unwrap_or(l))
.collect::<Vec<_>>()
.join("\n");
let topic = ctx.app.ui.help_topic;
let block_idx = ctx.app.ui.help_focused_block.expect("block focused in code nav");
match ctx
.app
.execute_script_oneshot(&cleaned, ctx.link, ctx.audio_tx)
{
Ok(()) => ctx.app.ui.flash("Executed", 100, FlashKind::Info),
Ok(Some(output)) => {
ctx.app.ui.flash(&output, 200, FlashKind::Info);
ctx.app.ui.help_block_output = Some((topic, block_idx, output));
}
Ok(None) => ctx.app.ui.flash("Executed", 100, FlashKind::Info),
Err(e) => ctx
.app
.ui
@@ -153,6 +161,7 @@ fn collapse_help_section(ctx: &mut InputContext) {
}
ctx.app.ui.help_on_section = Some(section);
ctx.app.ui.help_focused_block = None;
ctx.app.ui.help_block_output = None;
}
fn expand_help_section(ctx: &mut InputContext) {
@@ -167,6 +176,7 @@ fn expand_help_section(ctx: &mut InputContext) {
ctx.app.ui.help_topic = first;
}
ctx.app.ui.help_focused_block = None;
ctx.app.ui.help_block_output = None;
}
fn collapse_dict_section(ctx: &mut InputContext) {

View File

@@ -121,7 +121,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
.app
.execute_script_oneshot(script, ctx.link, ctx.audio_tx)
{
Ok(()) => ctx
Ok(_) => ctx
.app
.ui
.flash("Executed", 100, crate::state::FlashKind::Info),

View File

@@ -158,6 +158,12 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
}
// F11 — hidden Script page (no minimap flash)
if key.code == KeyCode::F(11) {
ctx.dispatch(AppCommand::GoToPage(Page::Script));
return InputResult::Continue;
}
if let Some(page) = match key.code {
KeyCode::F(1) => Some(Page::Dict),
KeyCode::F(2) => Some(Page::Patterns),
@@ -165,7 +171,6 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::F(4) => Some(Page::Help),
KeyCode::F(5) => Some(Page::Main),
KeyCode::F(6) => Some(Page::Engine),
KeyCode::F(7) => Some(Page::Script),
_ => None,
} {
ctx.app.ui.minimap = MinimapMode::Timed(Instant::now() + Duration::from_millis(1000));

View File

@@ -357,16 +357,13 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
editor.search_prev();
}
}
KeyCode::Char('s') if ctrl => {
ctx.dispatch(AppCommand::ToggleEditorStack);
}
KeyCode::Char('r') if ctrl => {
let script = ctx.app.editor_ctx.editor.lines().join("\n");
match ctx
.app
.execute_script_oneshot(&script, ctx.link, ctx.audio_tx)
{
Ok(()) => ctx
Ok(_) => ctx
.app
.ui
.flash("Executed", 100, crate::state::FlashKind::Info),
@@ -413,9 +410,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
}
}
if ctx.app.editor_ctx.show_stack {
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
}
}
Modal::PatternProps {
bank,

View File

@@ -26,9 +26,6 @@ fn handle_focused(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
ctx.dispatch(AppCommand::ScriptSave);
ctx.dispatch(AppCommand::ScriptEvaluate);
}
(true, KeyCode::Char('s')) => {
ctx.dispatch(AppCommand::ToggleScriptStack);
}
_ => {
ctx.app.script_editor.editor.input(key);
}

View File

@@ -114,6 +114,8 @@ fn main() -> io::Result<()> {
terminal.clear()?;
let mut last_frame = Instant::now();
let mut was_playing = false;
let mut last_stop_time = Instant::now();
loop {
if app.audio.restart_pending {
@@ -216,6 +218,10 @@ fn main() -> io::Result<()> {
}
app.playback.playing = playing.load(Ordering::Relaxed);
if was_playing && !app.playback.playing {
last_stop_time = Instant::now();
}
was_playing = app.playback.playing;
while let Ok(midi_cmd) = midi_rx.try_recv() {
match midi_cmd {
@@ -325,9 +331,6 @@ fn main() -> io::Result<()> {
Event::Paste(text) => {
if matches!(app.ui.modal, state::Modal::Editor) {
app.editor_ctx.editor.insert_str(&text);
if app.editor_ctx.show_stack {
services::stack_preview::update_cache(&app.editor_ctx);
}
} else if app.page == page::Page::Script && app.script_editor.focused {
app.script_editor.editor.insert_str(&text);
}
@@ -345,7 +348,9 @@ fn main() -> io::Result<()> {
|| app.ui.modal_fx.borrow().is_some()
|| app.ui.title_fx.borrow().is_some()
|| app.ui.nav_fx.borrow().is_some();
if app.playback.playing || had_event || app.ui.show_title || effects_active || app.ui.show_minimap() {
let cursor_pulse = app.page == page::Page::Main && !app.ui.performance_mode && !app.playback.playing;
let audio_cooldown = !app.playback.playing && last_stop_time.elapsed() < Duration::from_secs(1);
if app.playback.playing || had_event || app.ui.show_title || effects_active || app.ui.show_minimap() || cursor_pulse || audio_cooldown {
if app.ui.show_title {
app.ui.sparkles.tick(terminal.get_frame().area());
}

View File

@@ -68,6 +68,8 @@ pub const DOCS: &[DocEntry] = &[
"Control Flow",
include_str!("../../docs/forth/control_flow.md"),
),
Topic("Brackets", include_str!("../../docs/forth/brackets.md")),
Topic("Cycling", include_str!("../../docs/forth/cycling.md")),
Topic("The Prelude", include_str!("../../docs/forth/prelude.md")),
Topic(
"Cagire vs Classic",
@@ -131,6 +133,10 @@ pub const DOCS: &[DocEntry] = &[
"Sharing",
include_str!("../../docs/tutorials/sharing.md"),
),
Topic(
"Periodic Script",
include_str!("../../docs/tutorials/periodic_script.md"),
),
];
pub fn topic_count() -> usize {

View File

@@ -1,4 +1,4 @@
use cagire_forth::{Dictionary, ExecutionTrace, Forth, Rng, StepContext, Value, Variables};
use cagire_forth::{Dictionary, ExecutionTrace, Forth, Rng, StepContext, Variables};
pub struct ScriptEngine {
forth: Forth,
@@ -27,8 +27,4 @@ impl ScriptEngine {
pub fn clear_global_params(&self) {
self.forth.clear_global_params();
}
pub fn stack(&self) -> Vec<Value> {
self.forth.stack()
}
}

View File

@@ -103,11 +103,6 @@ impl Page {
pub fn down(&mut self) {
let (col, row) = self.grid_pos();
// From Main (1,1), going down reaches Script
if *self == Page::Main {
*self = Page::Script;
return;
}
if let Some(page) = Self::at_pos(col, row + 1) {
*self = page;
}

View File

@@ -84,6 +84,7 @@ pub fn next_topic(ui: &mut UiState, n: usize) {
let next = (cur + n) % items.len();
apply_selection(ui, items[next]);
ui.help_focused_block = None;
ui.help_block_output = None;
}
pub fn prev_topic(ui: &mut UiState, n: usize) {
@@ -95,6 +96,7 @@ pub fn prev_topic(ui: &mut UiState, n: usize) {
let next = (cur + items.len() - (n % items.len())) % items.len();
apply_selection(ui, items[next]);
ui.help_focused_block = None;
ui.help_block_output = None;
}
pub fn scroll_down(ui: &mut UiState, n: usize) {

View File

@@ -3,4 +3,3 @@ pub mod dict_nav;
pub mod euclidean;
pub mod help_nav;
pub mod pattern_editor;
pub mod stack_preview;

View File

@@ -1,102 +0,0 @@
use arc_swap::ArcSwap;
use parking_lot::Mutex;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use rand::rngs::StdRng;
use rand::SeedableRng;
use crate::model::{ScriptEngine, StepContext, Value};
use crate::state::{EditorContext, StackCache};
pub fn update_cache(editor_ctx: &EditorContext) {
let lines = editor_ctx.editor.lines();
let cursor_line = editor_ctx.editor.cursor().0;
let mut hasher = DefaultHasher::new();
for (i, line) in lines.iter().enumerate() {
if i > cursor_line {
break;
}
line.hash(&mut hasher);
}
let lines_hash = hasher.finish();
if let Some(ref c) = *editor_ctx.stack_cache.borrow() {
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
return;
}
}
let partial: Vec<&str> = lines
.iter()
.take(cursor_line + 1)
.map(|s| s.as_str())
.collect();
let script = partial.join("\n");
let result = if script.trim().is_empty() {
"Stack: []".to_string()
} else {
let vars = Arc::new(ArcSwap::from_pointee(HashMap::new()));
let dict = Arc::new(Mutex::new(HashMap::new()));
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
let engine = ScriptEngine::new(vars, dict, rng);
let ctx = StepContext {
step: 0,
beat: 0.0,
bank: 0,
pattern: 0,
tempo: 120.0,
phase: 0.0,
slot: 0,
runs: 0,
iter: 0,
speed: 1.0,
fill: false,
nudge_secs: 0.0,
cc_access: None,
speed_key: "",
mouse_x: 0.5,
mouse_y: 0.5,
mouse_down: 0.0,
};
match engine.evaluate(&script, &ctx) {
Ok(_) => {
let stack = engine.stack();
let formatted: Vec<String> = stack.iter().map(format_value).collect();
format!("Stack: [{}]", formatted.join(" "))
}
Err(e) => format!("Error: {e}"),
}
};
*editor_ctx.stack_cache.borrow_mut() = Some(StackCache {
cursor_line,
lines_hash,
result,
});
}
fn format_value(v: &Value) -> String {
match v {
Value::Int(n, _) => n.to_string(),
Value::Float(f, _) => {
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
format!("{f:.1}")
} else {
format!("{f:.4}")
}
}
Value::Str(s, _) => format!("\"{s}\""),
Value::Quotation(..) => "[...]".to_string(),
Value::CycleList(items) | Value::ArpList(items) => {
let inner: Vec<String> = items.iter().map(format_value).collect();
format!("({})", inner.join(" "))
}
}
}

View File

@@ -1,4 +1,4 @@
use std::cell::{Cell, RefCell};
use std::cell::Cell;
use std::ops::RangeInclusive;
use cagire_ratatui::Editor;
@@ -101,20 +101,11 @@ pub struct EditorContext {
pub editor: Editor,
pub selection_anchor: Option<usize>,
pub copied_steps: Option<CopiedSteps>,
pub show_stack: bool,
pub stack_cache: RefCell<Option<StackCache>>,
pub target: EditorTarget,
pub steps_per_page: Cell<usize>,
pub mouse_selecting: bool,
}
#[derive(Clone)]
pub struct StackCache {
pub cursor_line: usize,
pub lines_hash: u64,
pub result: String,
}
#[derive(Clone)]
pub struct CopiedSteps {
pub bank: usize,
@@ -153,8 +144,6 @@ impl Default for EditorContext {
editor: Editor::new(),
selection_anchor: None,
copied_steps: None,
show_stack: false,
stack_cache: RefCell::new(None),
target: EditorTarget::default(),
steps_per_page: Cell::new(32),
mouse_selecting: false,
@@ -164,8 +153,6 @@ impl Default for EditorContext {
pub struct ScriptEditorState {
pub editor: Editor,
pub show_stack: bool,
pub stack_cache: RefCell<Option<StackCache>>,
pub dirty: bool,
pub focused: bool,
pub mouse_selecting: bool,
@@ -175,8 +162,6 @@ impl Default for ScriptEditorState {
fn default() -> Self {
Self {
editor: Editor::new(),
show_stack: false,
stack_cache: RefCell::new(None),
dirty: false,
focused: true,
mouse_selecting: false,

View File

@@ -33,7 +33,7 @@ pub use audio::{AudioSettings, DeviceKind, EngineSection, LinkSetting, MainLayou
pub use color_scheme::ColorScheme;
pub use editor::{
CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField,
PatternPropsField, ScriptEditorState, ScriptField, StackCache,
PatternPropsField, ScriptEditorState, ScriptField,
};
pub use live_keys::LiveKeyState;
pub use modal::{ConfirmAction, Modal, RenameTarget};

View File

@@ -51,6 +51,7 @@ pub struct UiState {
pub help_search_active: bool,
pub help_search_query: String,
pub help_focused_block: Option<usize>,
pub help_block_output: Option<(usize, usize, String)>,
pub help_parsed: RefCell<Vec<Option<ParsedMarkdown>>>,
pub dict_focus: DictFocus,
pub dict_category: usize,
@@ -102,6 +103,7 @@ impl Default for UiState {
help_search_active: false,
help_search_query: String::new(),
help_focused_block: None,
help_block_output: None,
help_parsed: RefCell::new(
(0..crate::model::docs::topic_count())
.map(|_| None)

View File

@@ -228,8 +228,8 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let mut lines = parsed.lines.clone();
// Highlight focused code block with background tint
if let Some(block_idx) = app.ui.help_focused_block {
// Highlight focused code block with background tint and border
let border_lines_inserted = if let Some(block_idx) = app.ui.help_focused_block {
if let Some(block) = parsed.code_blocks.get(block_idx) {
let tint_bg = theme.ui.surface;
for line in lines.iter_mut().take(block.end_line).skip(block.start_line) {
@@ -242,6 +242,31 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
*span = Span::styled(span.content.clone(), style);
}
}
let border = "".repeat(content_width.saturating_sub(1));
let border_line = RLine::from(Span::styled(
format!(" {border}"),
Style::new().fg(theme.ui.accent),
));
lines.insert(block.end_line, border_line.clone());
lines.insert(block.start_line, border_line);
2
} else {
0
}
} else {
0
};
// Insert print output line after the executed code block
if let Some((topic, block_idx, ref output)) = app.ui.help_block_output {
if topic == app.ui.help_topic {
if let Some(block) = parsed.code_blocks.get(block_idx) {
let output_line = RLine::from(vec![
Span::styled("", Style::new().fg(theme.markdown.code_border)),
Span::styled(output.clone(), Style::new().fg(theme.markdown.code)),
]);
lines.insert(block.end_line + border_lines_inserted, output_line);
}
}
}

View File

@@ -1,6 +1,8 @@
//! Main page view — sequencer grid, visualizations (scope/spectrum), script previews.
use std::collections::HashSet;
use std::sync::OnceLock;
use std::time::Instant;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
@@ -361,6 +363,26 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
}
}
fn pulse_phase() -> f32 {
static EPOCH: OnceLock<Instant> = OnceLock::new();
let t = EPOCH.get_or_init(Instant::now).elapsed().as_secs_f32();
(1.0 + (t * std::f32::consts::TAU).sin()) * 0.5
}
fn pulse_color(color: Color) -> Color {
let Color::Rgb(r, g, b) = color else { return color };
let lum = 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32;
let amp = pulse_phase() * 0.12;
let shift = |c: u8| -> u8 {
if lum < 128.0 {
(c as f32 + (255.0 - c as f32) * amp) as u8
} else {
(c as f32 * (1.0 - amp)) as u8
}
};
Color::Rgb(shift(r), shift(g), shift(b))
}
fn render_tile(
frame: &mut Frame,
area: Rect,
@@ -417,6 +439,7 @@ fn render_tile(
(false, false, _, _, true) => (theme.selection.in_range, theme.selection.cursor_fg),
(false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
};
let bg = if is_selected && !is_playing { pulse_color(bg) } else { bg };
let bg_fill = Paragraph::new("").style(Style::new().bg(bg));
frame.render_widget(bg_fill, area);

View File

@@ -197,7 +197,7 @@ pub fn render(
}
if !perf {
render_footer(frame, app, footer_area);
render_footer(frame, app, snapshot, footer_area);
}
let modal_area = render_modal(frame, app, snapshot, term);
@@ -512,7 +512,7 @@ fn render_header(
);
}
fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let theme = theme::get();
let block = Block::default()
.borders(Borders::ALL)
@@ -539,6 +539,15 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
Span::raw(" "),
Span::styled(msg.clone(), Style::new().fg(theme.modal.confirm)),
])
} else if let Some(ref text) = snapshot.print_output {
Line::from(vec![
Span::styled(
page_indicator.to_string(),
Style::new().bg(theme.view_badge.bg).fg(theme.view_badge.fg),
),
Span::raw(" "),
Span::styled(text.clone(), Style::new().fg(theme.hint.text)),
])
} else {
let bindings: Vec<(&str, &str)> = match app.page {
Page::Main => vec![
@@ -593,7 +602,6 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
("Esc", "Save & Back"),
("C-e", "Eval"),
("[ ]", "Speed"),
("C-s", "Stack"),
("?", "Keys"),
],
};
@@ -1071,33 +1079,12 @@ fn render_modal_editor(
if app.editor_ctx.editor.search_active() {
let hints = hint_line(&[("Enter", "confirm"), ("Esc", "cancel")]);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
} else if app.editor_ctx.show_stack {
let stack_text = app
.editor_ctx
.stack_cache
.borrow()
.as_ref()
.map(|c| c.result.clone())
.unwrap_or_else(|| "Stack: []".to_string());
let hints = hint_line(&[("Esc", "save"), ("C-e", "eval"), ("C-s", "hide")]);
let [hint_left, stack_right] = Layout::horizontal([
Constraint::Length(hints.width() as u16),
Constraint::Fill(1),
])
.areas(hint_area);
frame.render_widget(Paragraph::new(hints), hint_left);
let dim = Style::default().fg(theme.hint.text);
frame.render_widget(
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
stack_right,
);
} else {
let hints = hint_line(&[
("Esc", "save"),
("C-e", "eval"),
("C-f", "find"),
("C-b", "samples"),
("C-s", "stack"),
("C-u", "/"),
("C-r", "undo/redo"),
]);

View File

@@ -2,7 +2,6 @@ use std::collections::HashSet;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::Style;
use ratatui::text::Span;
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
@@ -93,31 +92,10 @@ fn render_editor(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
("?", "keys"),
]);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
} else if app.script_editor.show_stack {
let stack_text = app
.script_editor
.stack_cache
.borrow()
.as_ref()
.map(|c| c.result.clone())
.unwrap_or_else(|| "Stack: []".to_string());
let hints = hint_line(&[("Esc", "unfocus"), ("C-e", "eval"), ("C-s", "hide stack")]);
let [hint_left, stack_right] = Layout::horizontal([
Constraint::Length(hints.width() as u16),
Constraint::Fill(1),
])
.areas(hint_area);
frame.render_widget(Paragraph::new(hints), hint_left);
let dim = Style::default().fg(theme.hint.text);
frame.render_widget(
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
stack_right,
);
} else {
let hints = hint_line(&[
("Esc", "unfocus"),
("C-e", "eval"),
("C-s", "stack"),
]);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
}

305
website/public/docs.css Normal file
View File

@@ -0,0 +1,305 @@
body:has(.docs-layout) {
max-width: 100vw;
padding: 1rem 2rem;
overflow: hidden;
height: 100vh;
}
.docs-layout {
display: flex;
gap: 2rem;
height: calc(100vh - 3.5rem);
}
.docs-sidebar {
width: 200px;
flex-shrink: 0;
overflow-y: auto;
}
.docs-sidebar h3 {
font-size: 0.8rem;
color: var(--text-muted);
margin: 1.25rem 0 0.25rem;
padding: 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.docs-sidebar h3:first-child {
margin-top: 0;
}
.docs-sidebar button {
display: block;
width: 100%;
text-align: left;
font-family: 'VCR OSD Mono', monospace;
font-size: 0.8rem;
background: none;
border: none;
border-left: 2px solid transparent;
color: var(--text-dim);
padding: 0.2rem 0.5rem;
cursor: pointer;
}
.docs-sidebar button:hover {
color: var(--text);
}
.docs-sidebar button.active {
color: var(--text);
border-left-color: var(--text);
}
.docs-content {
flex: 1;
min-width: 0;
max-width: none;
overflow-wrap: break-word;
overflow-y: auto;
}
.docs-content article {
display: none;
}
.docs-content article.visible {
display: block;
}
.docs-content h1 {
font-size: 1.3rem;
margin: 0 0 1rem;
border-bottom: 1px solid var(--text-muted);
padding-bottom: 0.5rem;
}
.docs-content h2,
.docs-content h3,
.docs-content h4,
.docs-content h5 {
background: var(--surface);
margin: 1.5rem 0 0.5rem;
padding: 0.3rem 0.5rem;
}
.docs-content h2 {
font-size: 1.1rem;
}
.docs-content h3 {
font-size: 1rem;
}
.docs-content h4 {
font-size: 0.95rem;
}
.docs-content h5 {
font-size: 0.9rem;
}
.docs-content p {
margin: 0.5rem 0;
text-align: left;
line-height: 1.5;
}
.docs-content pre {
background: var(--surface);
border-left: 2px solid var(--text-muted);
padding: 0.75rem 1rem;
margin: 1rem 0;
overflow-x: auto;
font-size: 0.85em;
line-height: 1.4;
}
.docs-content code {
font-family: 'VCR OSD Mono', monospace;
}
.docs-content p code,
.docs-content li code,
.docs-content td code {
background: var(--surface);
padding: 0.1rem 0.3rem;
font-size: 0.9em;
}
.docs-content table {
border-collapse: collapse;
margin: 1rem 0;
width: 100%;
table-layout: auto;
overflow-x: auto;
display: block;
}
.docs-content th,
.docs-content td {
padding: 0.25rem 0.75rem;
text-align: left;
}
.docs-content th {
color: var(--text);
}
.docs-content td:first-child {
color: var(--text-muted);
}
.docs-content tr:nth-child(even) {
background: var(--surface);
}
.docs-content ul,
.docs-content ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.docs-content ul {
list-style-type: "- ";
}
.docs-content ol {
list-style-type: decimal;
}
.docs-content ul ul {
list-style-type: none;
border-left: 1px solid var(--text-muted);
padding-left: 1rem;
margin: 0.15rem 0;
}
.docs-content li {
margin: 0.3rem 0;
line-height: 1.5;
text-align: left;
}
.docs-content ul ul li {
color: var(--text-dim);
margin: 0.15rem 0;
}
.docs-content a {
color: var(--text-dim);
}
.docs-content strong {
color: var(--text);
}
.docs-content em {
font-style: italic;
color: var(--text-dim);
}
.docs-content del {
opacity: 0.5;
}
.docs-content blockquote {
border-left: 2px solid var(--text-muted);
padding: 0.25rem 1rem;
margin: 1rem 0;
color: var(--text-dim);
}
.docs-content blockquote p {
margin: 0.25rem 0;
}
.docs-content hr {
border: none;
border-top: 1px solid var(--text-muted);
margin: 1.5rem 0;
}
.docs-topbar {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 1rem;
}
.docs-back {
font-family: 'VCR OSD Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
text-decoration: none;
white-space: nowrap;
}
.docs-back:hover {
color: var(--text);
}
.docs-notice {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.8rem;
color: var(--text-dim);
}
.docs-notice.hidden {
display: none;
}
.docs-notice button {
font-family: 'VCR OSD Mono', monospace;
font-size: 0.75rem;
background: none;
border: 1px solid var(--text-muted);
color: var(--text);
padding: 0.15rem 0.5rem;
cursor: pointer;
white-space: nowrap;
}
.docs-notice button:hover {
border-color: var(--text);
}
/* Forth syntax highlighting — monochrome via weight/opacity */
.f-emit { font-weight: bold; }
.f-com { opacity: 0.5; font-style: italic; }
.f-num { opacity: 0.6; }
.f-note { font-weight: bold; }
.f-snd { text-decoration: underline; }
.f-par { opacity: 0.6; font-style: italic; }
.f-stack { opacity: 0.6; }
.f-prob { opacity: 0.6; }
.f-ctx { opacity: 0.5; }
.f-var { opacity: 0.6; }
@media (max-width: 768px) {
body:has(.docs-layout) {
height: auto;
overflow: visible;
}
.docs-layout {
flex-direction: column;
height: auto;
}
.docs-sidebar {
width: 100%;
overflow-y: visible;
border-bottom: 1px solid var(--text-muted);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.docs-content {
overflow-y: visible;
}
}

27
website/public/docs.js Normal file
View File

@@ -0,0 +1,27 @@
function dismissNotice() {
document.getElementById('docs-notice').classList.add('hidden');
}
function showTopic(s, t) {
document.querySelectorAll('.docs-content article').forEach(a => a.classList.remove('visible'));
document.querySelectorAll('.docs-sidebar button').forEach(b => b.classList.remove('active'));
const article = document.getElementById('topic-s' + s + 't' + t);
const btn = document.querySelector('[data-s="' + s + '"][data-t="' + t + '"]');
if (article) article.classList.add('visible');
if (btn) btn.classList.add('active');
history.replaceState(null, '', '#s' + s + 't' + t);
document.querySelector('.docs-content').scrollTop = 0;
}
function parseHash() {
const m = location.hash.match(/^#s(\d+)t(\d+)$/);
return m ? [parseInt(m[1]), parseInt(m[2])] : [0, 0];
}
document.querySelectorAll('.docs-sidebar button').forEach(btn => {
btn.addEventListener('click', () => showTopic(+btn.dataset.s, +btn.dataset.t));
});
const [s, t] = parseHash();
showTopic(s, t);
window.addEventListener('hashchange', () => { const [s, t] = parseHash(); showTopic(s, t); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,338 @@
---
import fs from 'node:fs';
// --- Forth syntax highlighter ---
const EMIT = new Set(['.', '.!']);
const SOUNDS = new Set([
'sound', 's', 'saw', 'sine', 'kick', 'hat', 'snare', 'modal', 'noise',
'square', 'tri', 'pulse', 'clap', 'rim', 'crash', 'fm', 'sample', 'plaits',
'analog', 'waveshaping', 'granular', 'string', 'chord', 'speech', 'sub',
'super', 'wt', 'input', 'hh',
]);
const PARAMS = new Set([
'freq', 'note', 'gain', 'decay', 'attack', 'release', 'lpf', 'hpf', 'bpf',
'verb', 'delay', 'pan', 'orbit', 'harmonics', 'distort', 'speed', 'voice',
'dur', 'sustain', 'delaytime', 'delayfb', 'chorus', 'phaser', 'flanger',
'crush', 'fold', 'wrap', 'resonance', 'begin', 'end', 'velocity', 'chan',
'dev', 'ccnum', 'ccout', 'bend', 'pressure', 'program', 'tilt', 'slope',
'sub_gain', 'sub_oct', 'feedback', 'depth', 'sweep', 'comb', 'damping',
'detune', 'timbre', 'morph', 'color', 'model', 'cutoff',
]);
const STACK = new Set([
'dup', 'drop', 'swap', 'over', 'rot', 'nip', 'tuck', '2dup', '2drop',
]);
const PROB = new Set([
'chance', 'prob', 'choose', 'wchoose', 'cycle', 'pcycle', 'bounce',
'rand', 'exprand', 'logrand', 'coin', 'seed', 'often', 'sometimes',
'rarely', 'always', 'never', 'almostAlways', 'almostNever', 'every',
'except', 'bjork', 'pbjork', 'shuffle',
]);
const KEYWORDS = new Set([
':', ';', 'if', 'then', 'else', 'do', 'loop', '?', '!?', 'ifelse',
'select', 'apply', 'times', 'forget', 'all', 'noall', 'clear', 'm.',
]);
const CONTEXT = new Set([
'step', 'beat', 'pattern', 'tempo', 'phase', 'runs', 'iter', 'fill', 'stepdur',
]);
const NOTE_RE = /^[a-g][#sb]?\d+$/i;
const NUM_RE = /^-?\d+(\.\d+)?$/;
function classifyToken(word: string): string | null {
if (EMIT.has(word)) return 'f-emit';
if (SOUNDS.has(word)) return 'f-snd';
if (PARAMS.has(word)) return 'f-par';
if (STACK.has(word)) return 'f-stack';
if (PROB.has(word)) return 'f-prob';
if (KEYWORDS.has(word)) return 'f-kw';
if (CONTEXT.has(word)) return 'f-ctx';
if (NOTE_RE.test(word)) return 'f-note';
if (NUM_RE.test(word)) return 'f-num';
if (word.length > 1 && (word[0] === '@' || word[0] === '!' || word[0] === ',')) return 'f-var';
return null;
}
function highlightForth(code: string): string {
return code.split('\n').map(line => {
let result = '';
let i = 0;
while (i < line.length) {
if (line[i] === ' ' || line[i] === '\t') { result += line[i]; i++; continue; }
if (line[i] === ';' && line[i + 1] === ';') {
result += `<span class="f-com">${escapeHtml(line.slice(i))}</span>`;
break;
}
if (line[i] === '"') {
let end = line.indexOf('"', i + 1);
if (end === -1) end = line.length - 1;
result += escapeHtml(line.slice(i, end + 1));
i = end + 1;
continue;
}
if (line[i] === '(' || line[i] === ')') { result += line[i]; i++; continue; }
let end = i;
while (end < line.length && line[end] !== ' ' && line[end] !== '\t') end++;
const word = line.slice(i, end);
const cls = classifyToken(word);
result += cls ? `<span class="${cls}">${escapeHtml(word)}</span>` : escapeHtml(word);
i = end;
}
return result;
}).join('\n');
}
// --- Markdown renderer ---
function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function inline(text: string): string {
const codes: string[] = [];
text = text.replace(/`([^`]+)`/g, (_, code) => { codes.push(code); return `\x00${codes.length - 1}\x00`; });
text = escapeHtml(text);
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
text = text.replace(/\x00(\d+)\x00/g, (_, i) => `<code>${escapeHtml(codes[+i])}</code>`);
return text;
}
function parseTableCells(line: string, tag: string): string {
return line.split('|').slice(1, -1).map(cell => `<${tag}>${inline(cell.trim())}</${tag}>`).join('');
}
function renderMarkdown(md: string): string {
const lines = md.split('\n');
let html = '';
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (line.trim() === '') { i++; continue; }
if (line.startsWith('### ')) { html += `<h3>${inline(line.slice(4))}</h3>`; i++; continue; }
if (line.startsWith('## ')) { html += `<h2>${inline(line.slice(3))}</h2>`; i++; continue; }
if (line.startsWith('# ')) { html += `<h1>${inline(line.slice(2))}</h1>`; i++; continue; }
if (/^---+$|^\*\*\*+$/.test(line.trim())) { html += '<hr>'; i++; continue; }
if (line.startsWith('> ')) {
html += '<blockquote>';
while (i < lines.length && lines[i].startsWith('> ')) {
html += `<p>${inline(lines[i].slice(2))}</p>`;
i++;
}
html += '</blockquote>';
continue;
}
if (line.startsWith('```')) {
const lang = line.slice(3).trim();
i++;
let code = '';
while (i < lines.length && !lines[i].startsWith('```')) {
code += (code ? '\n' : '') + lines[i];
i++;
}
i++;
html += lang === 'forth'
? `<pre><code>${highlightForth(code)}</code></pre>`
: `<pre><code>${escapeHtml(code)}</code></pre>`;
continue;
}
if (line.startsWith('|')) {
html += '<table>';
html += `<tr>${parseTableCells(line, 'th')}</tr>`;
i++;
if (i < lines.length && /^\|[\s:|-]+\|$/.test(lines[i])) i++;
while (i < lines.length && lines[i].startsWith('|')) {
html += `<tr>${parseTableCells(lines[i], 'td')}</tr>`;
i++;
}
html += '</table>';
continue;
}
if (/^\d+\.\s/.test(line)) {
html += '<ol>';
while (i < lines.length && (/^\d+\.\s/.test(lines[i]) || /^\s+[*-]\s/.test(lines[i]))) {
if (/^\d+\.\s/.test(lines[i])) {
html += `<li>${inline(lines[i].replace(/^\d+\.\s/, ''))}`;
i++;
if (i < lines.length && /^\s+[*-]\s/.test(lines[i])) {
html += '<ul>';
while (i < lines.length && /^\s+[*-]\s/.test(lines[i])) {
html += `<li>${inline(lines[i].replace(/^\s*[*-]\s/, ''))}</li>`;
i++;
}
html += '</ul>';
}
html += '</li>';
} else {
i++;
}
}
html += '</ol>';
continue;
}
if (/^(\s*)[*-]\s/.test(line)) {
let depth = 0;
let first = true;
html += '<ul>';
while (i < lines.length && /^(\s*)[*-]\s/.test(lines[i])) {
const indent = lines[i].match(/^(\s*)/)![1].length;
const level = Math.floor(indent / 2);
if (level > depth) {
while (level > depth) { html += '<ul>'; depth++; }
} else if (level < depth) {
html += '</li>';
while (level < depth) { html += '</ul></li>'; depth--; }
} else if (!first) {
html += '</li>';
}
html += `<li>${inline(lines[i].replace(/^\s*[*-]\s/, ''))}`;
first = false;
i++;
}
while (depth > 0) { html += '</li></ul>'; depth--; }
html += '</li></ul>';
continue;
}
let para = '';
while (i < lines.length && lines[i].trim() !== ''
&& !lines[i].startsWith('#') && !lines[i].startsWith('```')
&& !lines[i].startsWith('|') && !/^(\s*)[*-]\s/.test(lines[i])
&& !/^\d+\.\s/.test(lines[i])) {
para += (para ? ' ' : '') + lines[i];
i++;
}
if (para) html += `<p>${inline(para)}</p>`;
}
return html;
}
// --- Section/topic manifest — mirrors src/model/docs.rs (keep in sync!) ---
const SECTIONS = [
{
title: "Getting Started",
topics: [
{ title: "Welcome", file: "welcome.md" },
{ title: "Navigation", file: "getting-started/navigation.md" },
{ title: "The Big Picture", file: "getting-started/big_picture.md" },
{ title: "Banks & Patterns", file: "getting-started/banks_patterns.md" },
{ title: "Stage / Commit", file: "getting-started/staging.md" },
{ title: "Using the Sequencer", file: "getting-started/grid.md" },
{ title: "Editing a Step", file: "getting-started/editing.md" },
{ title: "The Audio Engine", file: "getting-started/engine.md" },
{ title: "Options", file: "getting-started/options.md" },
{ title: "Saving & Loading", file: "getting-started/saving.md" },
{ title: "The Sample Browser", file: "getting-started/samples.md" },
],
},
{
title: "Cagire's Forth",
topics: [
{ title: "About Forth", file: "forth/about_forth.md" },
{ title: "The Dictionary", file: "forth/dictionary.md" },
{ title: "The Stack", file: "forth/stack.md" },
{ title: "Creating Words", file: "forth/definitions.md" },
{ title: "Control Flow", file: "forth/control_flow.md" },
{ title: "The Prelude", file: "forth/prelude.md" },
{ title: "Cagire vs Classic", file: "forth/oddities.md" },
],
},
{
title: "Building sounds",
topics: [
{ title: "Introduction", file: "engine/intro.md" },
{ title: "Settings", file: "engine/settings.md" },
{ title: "Sources", file: "engine/sources.md" },
{ title: "Samples", file: "engine/samples.md" },
{ title: "Wavetables", file: "engine/wavetable.md" },
{ title: "Filters", file: "engine/filters.md" },
{ title: "Modulation", file: "engine/modulation.md" },
{ title: "Distortion", file: "engine/distortion.md" },
{ title: "Space & Time", file: "engine/space.md" },
{ title: "Audio-Rate Mod", file: "engine/audio_modulation.md" },
{ title: "Words & Sounds", file: "engine/words.md" },
],
},
{
title: "Branching out",
topics: [
{ title: "Introduction", file: "midi/intro.md" },
{ title: "MIDI Output", file: "midi/output.md" },
{ title: "MIDI Input", file: "midi/input.md" },
],
},
{
title: "Tutorials",
topics: [
{ title: "Randomness", file: "tutorials/randomness.md" },
{ title: "Notes & Harmony", file: "tutorials/harmony.md" },
{ title: "Generators", file: "tutorials/generators.md" },
{ title: "Timing with at", file: "tutorials/at.md" },
{ title: "Using Variables", file: "tutorials/variables.md" },
{ title: "Recording", file: "tutorials/recording.md" },
{ title: "Soundfonts", file: "tutorials/soundfont.md" },
{ title: "Sharing", file: "tutorials/sharing.md" },
],
},
];
// Read all doc files at build time
for (const section of SECTIONS) {
for (const topic of section.topics) {
const raw = fs.readFileSync(`../docs/${topic.file}`, 'utf-8');
(topic as any).html = renderMarkdown(raw);
}
}
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cagire - Documentation</title>
<meta name="description" content="Cagire documentation — Forth-based live coding sequencer">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/docs.css">
</head>
<body>
<div class="docs-topbar">
<a class="docs-back" href="/">&larr; Back to Cagire</a>
<div class="docs-notice" id="docs-notice">
<span>You are reading Cagire's built-in documentation. Here it is static, but inside the app the examples are runnable and the docs are interactive.</span>
<button onclick="dismissNotice()">Got it</button>
</div>
</div>
<div class="docs-layout">
<nav class="docs-sidebar">
{SECTIONS.map((section, si) => (
<Fragment>
<h3>{section.title}</h3>
{section.topics.map((topic, ti) => (
<button data-s={si} data-t={ti}>{topic.title}</button>
))}
</Fragment>
))}
</nav>
<main class="docs-content">
{SECTIONS.map((section, si) =>
section.topics.map((topic, ti) => (
<article id={`topic-s${si}t${ti}`} set:html={(topic as any).html} />
))
)}
</main>
</div>
<script is:inline src="/docs.js"></script>
</body>
</html>

View File

@@ -56,37 +56,47 @@ const DL = 'https://dlcagire.raphaelforment.fr';
<table class="downloads-table">
<tr>
<th>Platform</th>
<th>Desktop</th>
<th>Terminal</th>
</tr>
<tr>
<td>macOS (Universal)</td>
<td colspan="2"><a href={`${DL}/cagire-macos-universal.pkg`}>.pkg</a></td>
<th>Desktop</th>
<th>Plugins</th>
</tr>
<tr>
<td>macOS (ARM)</td>
<td><a href={`${DL}/cagire-macos-aarch64-desktop.app.zip`}>.app</a></td>
<td><a href={`${DL}/cagire-macos-aarch64.tar.gz`}>.tar.gz</a></td>
<td><a href={`${DL}/cagire-macos-aarch64`}>binary</a></td>
<td><a href={`${DL}/Cagire-aarch64.dmg`}>.dmg</a></td>
<td><a href={`${DL}/cagire-plugins-macos-aarch64.clap`}>CLAP</a> · <a href={`${DL}/cagire-plugins-macos-aarch64.vst3`}>VST3</a></td>
</tr>
<tr>
<td>macOS (Intel)</td>
<td><a href={`${DL}/cagire-macos-x86_64-desktop.app.zip`}>.app</a></td>
<td><a href={`${DL}/cagire-macos-x86_64.tar.gz`}>.tar.gz</a></td>
<td><a href={`${DL}/cagire-macos-x86_64`}>binary</a></td>
<td><a href={`${DL}/Cagire-x86_64.dmg`}>.dmg</a></td>
<td><a href={`${DL}/cagire-plugins-macos-x86_64.clap`}>CLAP</a> · <a href={`${DL}/cagire-plugins-macos-x86_64.vst3`}>VST3</a></td>
</tr>
<tr>
<td>Windows</td>
<td><a href={`${DL}/cagire-windows-x86_64-desktop.exe`}>.exe</a></td>
<td><a href={`${DL}/cagire-windows-x86_64.zip`}>.zip</a></td>
<td>Linux (x86_64)</td>
<td><a href={`${DL}/cagire-linux-x86_64.AppImage`}>AppImage</a></td>
<td><a href={`${DL}/cagire-desktop-linux-x86_64.AppImage`}>AppImage</a></td>
<td><a href={`${DL}/cagire-plugins-linux-x86_64.clap`}>CLAP</a> · <a href={`${DL}/cagire-plugins-linux-x86_64.vst3`}>VST3</a></td>
</tr>
<tr>
<td>Linux</td>
<td><a href={`${DL}/cagire-linux-x86_64-desktop.deb`}>.deb</a></td>
<td><a href={`${DL}/cagire-linux-x86_64.tar.gz`}>.tar.gz</a></td>
<td>Linux (ARM)</td>
<td><a href={`${DL}/cagire-linux-aarch64.AppImage`}>AppImage</a></td>
<td><a href={`${DL}/cagire-desktop-linux-aarch64.AppImage`}>AppImage</a></td>
<td><a href={`${DL}/cagire-plugins-linux-aarch64.clap`}>CLAP</a> · <a href={`${DL}/cagire-plugins-linux-aarch64.vst3`}>VST3</a></td>
</tr>
<tr>
<td>Windows (x86_64)</td>
<td><a href={`${DL}/cagire-windows-x86_64.exe`}>.exe</a></td>
<td><a href={`${DL}/cagire-desktop-windows-x86_64.exe`}>.exe</a> · <a href={`${DL}/cagire-windows-x86_64.msi`}>.msi</a></td>
<td><a href={`${DL}/cagire-plugins-windows-x86_64.clap`}>CLAP</a> · <a href={`${DL}/cagire-plugins-windows-x86_64.vst3`}>VST3</a></td>
</tr>
</table>
<p class="note">Source code and issue tracker on <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a>. You can also compile the software yourself from source!</p>
<h2>Documentation</h2>
<p>Cagire ships with built-in interactive documentation — browse it inside the app with runnable examples, or <a href="/docs">read the static version here</a>.</p>
<h2>About</h2>
<p>Cagire is a step sequencer where each step contains a Forth script instead of typical note data. When the sequencer reaches a step, it runs the associated script. Scripts can produce sound, trigger samples, apply effects, or do nothing at all. You are free to define what your scripts will do. Cagire includes a built-in audio engine called <a href="https://doux.livecoding.fr">Doux</a>. No external software is needed to make sound. It comes with oscillators, sample players, filters, reverb, delay, distortion, and more.</p>
@@ -118,7 +128,7 @@ const DL = 'https://dlcagire.raphaelforment.fr';
<video src="/mono_cagire.mp4" autoplay muted loop playsinline></video>
<p class="colophon">
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · AGPL-3.0 </p>
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · <a href="/docs">Docs</a> · AGPL-3.0 </p>
<script is:inline src="/script.js"></script>
</body>