Compare commits
6 Commits
5a72e4cef4
...
fed7781bae
| Author | SHA1 | Date | |
|---|---|---|---|
| fed7781bae | |||
| d055d2bfc6 | |||
| f273470eaf | |||
| b2a089fb0c | |||
| 04b68850d0 | |||
| 77364dddae |
25
.github/workflows/build-windows.yml
vendored
25
.github/workflows/build-windows.yml
vendored
@@ -71,13 +71,24 @@ jobs:
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install cargo-wix
|
||||
- name: Install NSIS
|
||||
if: inputs.build-packages
|
||||
run: cargo install cargo-wix
|
||||
run: choco install nsis
|
||||
|
||||
- name: Build MSI installer
|
||||
- name: Build NSIS installer
|
||||
if: inputs.build-packages
|
||||
run: cargo wix --no-build --nocapture --package cagire -C -arch -C x64
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"(.+)"' | Select-Object -First 1).Matches.Groups[1].Value
|
||||
$root = (Get-Location).Path
|
||||
$target = "x86_64-pc-windows-msvc"
|
||||
& "C:\Program Files (x86)\NSIS\makensis.exe" `
|
||||
"-DVERSION=$version" `
|
||||
"-DCLI_EXE=$root\target\$target\release\cagire.exe" `
|
||||
"-DDESKTOP_EXE=$root\target\$target\release\cagire-desktop.exe" `
|
||||
"-DICON=$root\assets\Cagire.ico" `
|
||||
"-DOUTDIR=$root\target" `
|
||||
nsis/cagire.nsi
|
||||
|
||||
- name: Upload CLI artifact
|
||||
if: inputs.build-packages
|
||||
@@ -93,12 +104,12 @@ jobs:
|
||||
name: cagire-windows-x86_64-desktop
|
||||
path: target/x86_64-pc-windows-msvc/release/cagire-desktop.exe
|
||||
|
||||
- name: Upload MSI artifact
|
||||
- name: Upload installer artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-msi
|
||||
path: target/wix/*.msi
|
||||
name: cagire-windows-x86_64-installer
|
||||
path: target/cagire-*-setup.exe
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
if: inputs.build-packages
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -78,8 +78,8 @@ jobs:
|
||||
elif [[ "$name" == *-vst3 ]]; then
|
||||
base="${name%-vst3}"
|
||||
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
|
||||
elif [[ "$name" == *-msi ]]; then
|
||||
cp "$dir"/*.msi release/
|
||||
elif [[ "$name" == *-installer ]]; then
|
||||
cp "$dir"/*-setup.exe release/
|
||||
elif [[ "$name" == *-appimage ]]; then
|
||||
cp "$dir"/*.AppImage release/
|
||||
elif [[ "$name" == *-desktop ]]; then
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -2,6 +2,34 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.1.2]
|
||||
|
||||
### Forth Language
|
||||
- Single-letter envelope aliases: `a` (attack), `d` (decay), `s` (sustain), `r` (release).
|
||||
- `sound` alias changed from `s` to `snd` (frees `s` for sustain).
|
||||
- New `partials` word: set number of active harmonics for additive oscillator.
|
||||
- Velocity parameter normalized to 0–1 float range (was 0–127 integer).
|
||||
|
||||
### UI / UX
|
||||
- **Sample Explorer as dedicated page**: the side panel is now a full page (Tab key), with keyboard navigation (j/k, search with `/`, preview with Enter), replacing the old collapsible side panel.
|
||||
- **Pulsing armed-changes bar** on patterns page: staged play/stop/mute/solo changes shown in a launch bar with animated feedback ("c to launch").
|
||||
- Pulsing highlight on banks and patterns with staged changes.
|
||||
- Sample browser shows child count on collapsed folders and uses `+`/`-` tree icons.
|
||||
- File browser modal: shows audio file counts per directory, colored path segments, and hint bar.
|
||||
- Audio devices refreshed automatically when entering the Engine page.
|
||||
- Bank prelude field added to data model (foundation for bank-level Forth scripts).
|
||||
|
||||
### Engine
|
||||
- Audio timing switched from float seconds to integer tick-based scheduling, improving timing precision.
|
||||
- Stream error handling refined: only `DeviceNotAvailable` and `StreamInvalidated` trigger device-lost recovery (non-fatal errors no longer restart the stream).
|
||||
- Step traces use `Arc` for cheaper cloning between threads.
|
||||
|
||||
### Packaging
|
||||
- **Windows: NSIS installer** replaces cargo-wix MSI. Includes optional PATH registration, Start Menu shortcut, and proper Add/Remove Programs entry with uninstaller.
|
||||
- Improved Windows cross-compilation from Unix hosts (MinGW toolchain detection).
|
||||
- CI build timeouts increased to 60 minutes across all platforms.
|
||||
- Website download matrix updated.
|
||||
|
||||
## [0.1.1]
|
||||
|
||||
### Forth Language
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1809,8 +1809,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "doux"
|
||||
version = "0.0.6"
|
||||
source = "git+https://github.com/sova-org/doux#14ccf68c0ac626718664f32ede20f13087df8de6"
|
||||
version = "0.0.7"
|
||||
source = "git+https://github.com/sova-org/doux#b2acd4d2737e0a981635266bf22926215453380e"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"clap",
|
||||
|
||||
@@ -86,7 +86,7 @@ image = { version = "0.25", default-features = false, features = ["png"], option
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
cpal = { version = "0.17", optional = true, features = ["jack"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
|
||||
19
build.rs
19
build.rs
@@ -13,13 +13,26 @@ fn main() {
|
||||
println!("cargo:rustc-link-lib=oleaut32");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if target_os == "windows" {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let icon = format!("{manifest_dir}/assets/Cagire.ico");
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("assets/Cagire.ico")
|
||||
// Cross-compiling from Unix: use prefixed MinGW tools
|
||||
if cfg!(unix) {
|
||||
res.set_windres_path("x86_64-w64-mingw32-windres");
|
||||
res.set_ar_path("x86_64-w64-mingw32-ar");
|
||||
res.set_toolkit_path("/");
|
||||
}
|
||||
res.set_icon(&icon)
|
||||
.set("ProductName", "Cagire")
|
||||
.set("FileDescription", "Forth-based music sequencer")
|
||||
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment");
|
||||
res.compile().expect("Failed to compile Windows resources");
|
||||
// GNU ld discards unreferenced sections from static archives,
|
||||
// so link the resource object directly to ensure .rsrc is kept.
|
||||
if cfg!(unix) {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
println!("cargo:rustc-link-arg-bins={out_dir}/resource.o");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +543,8 @@ pub struct Bank {
|
||||
pub patterns: Vec<Pattern>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
@@ -560,6 +562,7 @@ impl Default for Bank {
|
||||
Self {
|
||||
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||
name: None,
|
||||
prelude: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8376
demos/02.cagire
8376
demos/02.cagire
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ All time values are in **steps**, just like `attack`, `decay`, and `release`. At
|
||||
Oscillate a parameter between two values.
|
||||
|
||||
```forth
|
||||
saw s 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
|
||||
saw s 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
|
||||
saw snd 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
|
||||
saw snd 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
|
||||
```
|
||||
|
||||
| Word | Shape | Output |
|
||||
@@ -27,8 +27,8 @@ Stack effect: `( min max period -- str )`
|
||||
Transition from one value to another over a duration.
|
||||
|
||||
```forth
|
||||
saw s 0 1 0.5 slide gain . ( fade in over half a step )
|
||||
saw s 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
|
||||
saw snd 0 1 0.5 slide gain . ( fade in over half a step )
|
||||
saw snd 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
|
||||
```
|
||||
|
||||
| Word | Curve | Output |
|
||||
@@ -44,9 +44,9 @@ Stack effect: `( start end dur -- str )`
|
||||
Randomize a parameter within a range, retriggering at a given period.
|
||||
|
||||
```forth
|
||||
saw s 200 4000 2 jit lpf . ( new random value every 2 steps )
|
||||
saw s 200 4000 2 sjit lpf . ( same but smoothly interpolated )
|
||||
saw s 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
saw snd 200 4000 2 jit lpf . ( new random value every 2 steps )
|
||||
saw snd 200 4000 2 sjit lpf . ( same but smoothly interpolated )
|
||||
saw snd 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
```
|
||||
|
||||
| Word | Behavior | Output |
|
||||
@@ -62,7 +62,7 @@ Stack effect: `( min max period -- str )`
|
||||
Define a multi-segment envelope for a parameter. Provide a start value, then pairs of target and duration.
|
||||
|
||||
```forth
|
||||
saw s 0 1 0.1 0.7 0.5 0 8 env gain .
|
||||
saw snd 0 1 0.1 0.7 0.5 0 8 env gain .
|
||||
```
|
||||
|
||||
This creates: start at `0`, rise to `1` in `0.1` steps, drop to `0.7` in `0.5` steps, fall to `0` in `8` steps.
|
||||
@@ -74,7 +74,7 @@ Stack effect: `( start target1 dur1 [target2 dur2 ...] -- str )`
|
||||
Modulation words return strings, so they compose naturally with the rest of the language. Use them anywhere a parameter value is expected.
|
||||
|
||||
```forth
|
||||
saw s
|
||||
saw snd
|
||||
200 4000 4 lfo lpf
|
||||
0.3 0.7 8 tlfo pan
|
||||
0 1 0.1 0.7 0.5 0 8 env gain
|
||||
|
||||
@@ -7,7 +7,7 @@ Cagire includes an audio engine called `Doux`. No external software is needed to
|
||||
When you write a Forth script and emit (`.`), the script produces a command string. This command travels to the audio engine, which interprets it and creates a voice. The voice plays until its envelope finishes or until it is killed by another voice. You can also spawn infinite voices, but you will need to manage their lifecycle manually, otherwise they will never stop.
|
||||
|
||||
```forth
|
||||
saw s c4 note 0.8 gain 0.3 verb .
|
||||
saw snd c4 note 0.8 gain 0.3 verb .
|
||||
```
|
||||
|
||||
## Voices
|
||||
@@ -24,7 +24,7 @@ Press `r` on the Engine page to reset the peak counter.
|
||||
After selecting a sound source, you add parameters. Each parameter word takes a value from the stack and stores it in the command register:
|
||||
|
||||
```forth
|
||||
saw s
|
||||
saw snd
|
||||
c4 note ;; pitch
|
||||
0.5 gain ;; volume
|
||||
0.1 attack ;; envelope attack time
|
||||
@@ -42,14 +42,14 @@ Use `all` to apply parameters globally. Global parameters persist across all pat
|
||||
```forth
|
||||
;; Prospective: set params before emitting
|
||||
500 lpf 0.5 verb all
|
||||
kick s 60 note . ;; gets lpf=500 verb=0.5
|
||||
hat s 70 note . ;; gets lpf=500 verb=0.5
|
||||
kick snd 60 note . ;; gets lpf=500 verb=0.5
|
||||
hat snd 70 note . ;; gets lpf=500 verb=0.5
|
||||
```
|
||||
|
||||
```forth
|
||||
;; Retroactive: patch already-emitted sounds
|
||||
kick s 60 note .
|
||||
hat s 70 note .
|
||||
kick snd 60 note .
|
||||
hat snd 70 note .
|
||||
500 lpf 0.5 verb all ;; both outputs get lpf and verb
|
||||
```
|
||||
|
||||
@@ -57,17 +57,17 @@ Per-sound parameters override global ones:
|
||||
|
||||
```forth
|
||||
500 lpf all
|
||||
kick s 2000 lpf . ;; lpf=2000 (per-sound wins)
|
||||
hat s . ;; lpf=500 (global)
|
||||
kick snd 2000 lpf . ;; lpf=2000 (per-sound wins)
|
||||
hat snd . ;; lpf=500 (global)
|
||||
```
|
||||
|
||||
Use `noall` to clear global parameters:
|
||||
|
||||
```forth
|
||||
500 lpf all
|
||||
kick s . ;; gets lpf
|
||||
kick snd . ;; gets lpf
|
||||
noall
|
||||
hat s . ;; no lpf
|
||||
hat snd . ;; no lpf
|
||||
```
|
||||
|
||||
## Controlling Existing Voices
|
||||
|
||||
@@ -79,16 +79,16 @@ Top-level files are named by their filename (without extension). Files inside fo
|
||||
Reference samples by name:
|
||||
|
||||
```forth
|
||||
kick s . ;; play kick.wav
|
||||
snare s 0.5 gain . ;; play snare at half volume
|
||||
kick snd . ;; play kick.wav
|
||||
snare snd 0.5 gain . ;; play snare at half volume
|
||||
```
|
||||
|
||||
For samples in folders, use `n` to select which one:
|
||||
|
||||
```forth
|
||||
hats s 0 n . ;; play hats/closed.wav (index 0)
|
||||
hats s 1 n . ;; play hats/open.wav (index 1)
|
||||
hats s 2 n . ;; play hats/pedal.wav (index 2)
|
||||
hats snd 0 n . ;; play hats/closed.wav (index 0)
|
||||
hats snd 1 n . ;; play hats/open.wav (index 1)
|
||||
hats snd 2 n . ;; play hats/pedal.wav (index 2)
|
||||
```
|
||||
|
||||
The index wraps around. If you have 3 samples and request `5 n`, you get index 2 (because 5 % 3 = 2).
|
||||
@@ -106,9 +106,9 @@ samples/
|
||||
```
|
||||
|
||||
```forth
|
||||
kick s . ;; plays kick.wav
|
||||
kick s a bank . ;; plays kick_a.wav
|
||||
kick s hard bank . ;; plays kick_hard.wav
|
||||
kick snd . ;; plays kick.wav
|
||||
kick snd a bank . ;; plays kick_a.wav
|
||||
kick snd hard bank . ;; plays kick_hard.wav
|
||||
```
|
||||
|
||||
If the banked version does not exist, it falls back to the default.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sources
|
||||
|
||||
The audio engine provides a variety of sound sources. Use the `sound` word (or `s` for short) to select one.
|
||||
The audio engine provides a variety of sound sources. Use the `sound` word (or `snd` for short) to select one.
|
||||
|
||||
## Basic Oscillators
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Word definitions let you abstract sound design into reusable units.
|
||||
## Defining Sounds
|
||||
|
||||
```forth
|
||||
: lead "saw" s 0.3 gain 1200 lpf ;
|
||||
: lead "saw" snd 0.3 gain 1200 lpf ;
|
||||
```
|
||||
|
||||
Use it with different notes:
|
||||
@@ -20,8 +20,8 @@ e4 note lead .
|
||||
Include the emit to make the word play directly:
|
||||
|
||||
```forth
|
||||
: kk "kick" s 1 decay . ;
|
||||
: hh "hihat" s 0.5 gain 0.5 decay . ;
|
||||
: kk "kick" snd 1 decay . ;
|
||||
: hh "hihat" snd 0.5 gain 0.5 decay . ;
|
||||
```
|
||||
|
||||
Steps become simple:
|
||||
@@ -39,5 +39,5 @@ kk
|
||||
```
|
||||
|
||||
```forth
|
||||
c4 note saw s dark wet .
|
||||
c4 note saw snd dark wet .
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ Because parentheses defer execution, wrapping code in `( ... )` without a consum
|
||||
.
|
||||
```
|
||||
|
||||
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.
|
||||
Any word that is not recognized as a built-in or a user definition becomes a string on the stack. This means `kick snd` and `"kick" snd` are equivalent. You only need quotes when the string contains spaces or when it conflicts with an existing word name.
|
||||
|
||||
## The Command Register
|
||||
|
||||
@@ -94,7 +94,7 @@ kick sound ;; sets the sound name
|
||||
. ;; emits the command and clears the register
|
||||
```
|
||||
|
||||
The word `sound` (or its shorthand `s`) sets what sound to play. Parameter words like `gain`, `freq`, `decay`, or `verb` add key-value pairs to the register. Nothing happens until you emit with `.` (dot). At that moment, the register is packaged into a command and sent to the audio engine.
|
||||
The word `sound` (or its shorthand `snd`) sets what sound to play. Parameter words like `gain`, `freq`, `decay`, or `verb` add key-value pairs to the register. Nothing happens until you emit with `.` (dot). At that moment, the register is packaged into a command and sent to the audio engine.
|
||||
|
||||
This design lets you build sounds incrementally:
|
||||
|
||||
@@ -110,14 +110,14 @@ c4 note
|
||||
Each line adds something to the register. The final `.` triggers the sound. You can also write it all on one line:
|
||||
|
||||
```forth
|
||||
"sine" s c4 note 0.5 gain 0.3 decay 0.4 verb .
|
||||
"sine" snd c4 note 0.5 gain 0.3 decay 0.4 verb .
|
||||
```
|
||||
|
||||
The order of parameters does not matter. You can even emit multiple times in a single step. If you need to discard the register without emitting, use `clear`:
|
||||
|
||||
```forth
|
||||
"kick" s 0.5 gain clear ;; nothing plays, register is emptied
|
||||
"hat" s . ;; only the hat plays
|
||||
"kick" snd 0.5 gain clear ;; nothing plays, register is emptied
|
||||
"hat" snd . ;; only the hat plays
|
||||
```
|
||||
|
||||
This is useful when conditionals might cancel a sound before it emits.
|
||||
|
||||
@@ -47,7 +47,7 @@ The outer quotation runs every 4th iteration. Inside, a coin flip picks the note
|
||||
Wrapping code in a quotation without consuming it is a quick way to disable it:
|
||||
|
||||
```forth
|
||||
( kick s . )
|
||||
( kick snd . )
|
||||
```
|
||||
|
||||
Nothing will execute this quotation — it just sits on the stack and gets discarded. Useful for temporarily silencing a line while editing.
|
||||
@@ -63,7 +63,7 @@ Square brackets execute their contents immediately, then push a count of how man
|
||||
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 .
|
||||
[ 60 64 67 ] cycle note sine snd .
|
||||
```
|
||||
|
||||
The `cycle` word reads the count to know how many values to rotate through. Without brackets you would write `60 64 67 3 cycle` — the brackets save you from counting manually.
|
||||
@@ -71,8 +71,8 @@ The `cycle` word reads the count to know how many values to rotate through. With
|
||||
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)
|
||||
[ c4 e4 g4 ] choose note saw snd . ;; random note from the list
|
||||
[ 60 64 67 ] note sine snd . ;; 3-note chord (note consumes all)
|
||||
```
|
||||
|
||||
### Nesting
|
||||
@@ -88,7 +88,7 @@ Square brackets can nest. Each pair produces its own count:
|
||||
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
|
||||
[ c4 c4 3 + c4 7 + ] note sine snd . ;; root, minor third, fifth
|
||||
```
|
||||
|
||||
## { ... } — Curly Braces
|
||||
@@ -96,13 +96,13 @@ The contents are compiled and executed normally, so you can use any Forth code:
|
||||
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 } .
|
||||
{ kick snd } { 0.5 gain } { 0.3 verb } .
|
||||
```
|
||||
|
||||
This compiles to exactly the same thing as:
|
||||
|
||||
```forth
|
||||
kick s 0.5 gain 0.3 verb .
|
||||
kick snd 0.5 gain 0.3 verb .
|
||||
```
|
||||
|
||||
They can help readability in dense one-liners but have no semantic meaning.
|
||||
|
||||
@@ -112,7 +112,7 @@ 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 .
|
||||
tri snd c4 note 0.2 decay .
|
||||
```
|
||||
|
||||
Loud during fills, quiet otherwise.
|
||||
@@ -123,7 +123,7 @@ 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 .
|
||||
note sine snd 0.5 decay .
|
||||
```
|
||||
|
||||
Four notes of a major seventh chord picked randomly. Note that this is unnecessarily complex :)
|
||||
|
||||
@@ -9,13 +9,13 @@ 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, ...
|
||||
60 64 67 3 cycle note sine snd . ;; 60, 64, 67, 60, 64, 67, ...
|
||||
```
|
||||
|
||||
`pcycle` advances based on `iter` — the pattern iteration count:
|
||||
|
||||
```forth
|
||||
kick snare 2 pcycle s . ;; kick on even iterations, snare on odd
|
||||
kick snare 2 pcycle snd . ;; kick on even iterations, snare on odd
|
||||
```
|
||||
|
||||
The distinction matters when patterns have different lengths or when multiple steps share the same script. `cycle` gives each step its own independent counter. `pcycle` ties all steps to the same global pattern position.
|
||||
@@ -25,8 +25,8 @@ The distinction matters when patterns have different lengths or when multiple st
|
||||
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
|
||||
60 64 67 72 4 bounce note sine snd . ;; ping-pong by step runs
|
||||
60 64 67 72 4 pbounce note sine snd . ;; ping-pong by pattern iteration
|
||||
```
|
||||
|
||||
Same `runs` vs `iter` split as `cycle` / `pcycle`.
|
||||
@@ -36,7 +36,7 @@ Same `runs` vs `iter` split as `cycle` / `pcycle`.
|
||||
Uniform random selection:
|
||||
|
||||
```forth
|
||||
kick snare hat 3 choose s . ;; random drum hit each time
|
||||
kick snare hat 3 choose snd . ;; random drum hit each time
|
||||
```
|
||||
|
||||
Unlike the cycling words, `choose` is nondeterministic — every evaluation picks independently.
|
||||
@@ -46,7 +46,7 @@ Unlike the cycling words, `choose` is nondeterministic — every evaluation pick
|
||||
Weighted random. Push value/weight pairs, then the count:
|
||||
|
||||
```forth
|
||||
kick 0.5 snare 0.3 hat 0.2 3 wchoose s .
|
||||
kick 0.5 snare 0.3 hat 0.2 3 wchoose snd .
|
||||
```
|
||||
|
||||
Kick plays 50% of the time, snare 30%, hat 20%. Weights are normalized automatically — they don't need to sum to 1.
|
||||
@@ -56,8 +56,8 @@ Kick plays 50% of the time, snare 30%, hat 20%. Weights are normalized automatic
|
||||
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
|
||||
[ c4 e4 g4 ] step index note sine snd . ;; step number picks the note
|
||||
[ c4 e4 g4 ] iter index note sine snd . ;; pattern iteration picks the note
|
||||
```
|
||||
|
||||
This is useful when you want full control over which value is selected, driven by any expression you like.
|
||||
@@ -67,9 +67,9 @@ This is useful when you want full control over which value is selected, driven b
|
||||
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 .
|
||||
[ 60 64 67 ] cycle note sine snd . ;; no need to write "3"
|
||||
[ kick snare hat ] choose snd .
|
||||
[ c4 e4 g4 b4 ] bounce note sine snd .
|
||||
```
|
||||
|
||||
Without brackets: `60 64 67 3 cycle`. With brackets: `[ 60 64 67 ] cycle`. Same result, less counting.
|
||||
@@ -80,7 +80,7 @@ When any of these words selects a quotation, it executes it instead of pushing i
|
||||
|
||||
```forth
|
||||
[ ( c4 note ) ( e4 note ) ( g4 note ) ] cycle
|
||||
sine s .
|
||||
sine snd .
|
||||
```
|
||||
|
||||
On the first run the quotation `( c4 note )` executes, setting the note to C4. Next run, E4. Then G4. Then back to C4.
|
||||
@@ -88,5 +88,5 @@ On the first run the quotation `( c4 note )` executes, setting the note to C4. N
|
||||
This works with all selection words. Mix plain values and quotations freely:
|
||||
|
||||
```forth
|
||||
[ ( hat s 0.3 gain . ) ( snare s . ) ( kick s . ) ] choose
|
||||
[ ( hat snd 0.3 gain . ) ( snare snd . ) ( kick snd . ) ] choose
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ When you define a word in one step, it becomes available to all other steps. Thi
|
||||
|
||||
Step 0:
|
||||
```forth
|
||||
: bass "saw" s 0.8 gain 800 lpf ;
|
||||
: bass "saw" snd 0.8 gain 800 lpf ;
|
||||
```
|
||||
|
||||
Step 4:
|
||||
@@ -75,7 +75,7 @@ This only affects words you defined with `:` ... `;`. Built-in words cannot be f
|
||||
**Synth definitions** save you from repeating sound design:
|
||||
|
||||
```forth
|
||||
: pad "sine" s 0.3 gain 2 attack 0.5 verb ;
|
||||
: pad "sine" snd 0.3 gain 2 attack 0.5 verb ;
|
||||
```
|
||||
|
||||
**Transpositions** and musical helpers:
|
||||
@@ -90,8 +90,8 @@ This only affects words you defined with `:` ... `;`. Built-in words cannot be f
|
||||
A word can contain `.` to emit sounds directly:
|
||||
|
||||
```forth
|
||||
: kick "kick" s . ;
|
||||
: hat "hat" s 0.4 gain . ;
|
||||
: kick "kick" snd . ;
|
||||
: hat "hat" snd 0.4 gain . ;
|
||||
```
|
||||
|
||||
Then a step becomes trivial:
|
||||
|
||||
@@ -33,4 +33,4 @@ Each word entry shows:
|
||||
- **Description**: What the word does
|
||||
- **Example**: How to use it
|
||||
|
||||
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing. Use the dictionary while writing scripts to check stack effects and study their behavior. Some words also come with shorter aliases (e.g., `sound` → `s`). You will learn aliases quite naturally, because aliases are usually reserved for very common words.
|
||||
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing. Use the dictionary while writing scripts to check stack effects and study their behavior. Some words also come with shorter aliases (e.g., `sound` → `snd`). You will learn aliases quite naturally, because aliases are usually reserved for very common words.
|
||||
|
||||
@@ -35,8 +35,8 @@ Cagire supports this syntax but also provides quotation-based conditionals:
|
||||
The words `?` and `!?` execute a quotation based on a condition:
|
||||
|
||||
```forth
|
||||
( "kick" s . ) coin ? ;; execute if coin is 1
|
||||
( "snare" s . ) coin !? ;; execute if coin is 0
|
||||
( "kick" snd . ) coin ? ;; execute if coin is 1
|
||||
( "snare" snd . ) coin !? ;; execute if coin is 0
|
||||
```
|
||||
|
||||
## Strings
|
||||
@@ -56,7 +56,7 @@ Cagire has first-class strings:
|
||||
This pushes a string value onto the stack. Strings are used for sound names, sample names, and variable keys. You often do not need quotes at all. Any unrecognized word becomes a string automatically:
|
||||
|
||||
```forth
|
||||
kick s . ;; "kick" is not a word, so it becomes the string "kick"
|
||||
kick snd . ;; "kick" is not a word, so it becomes the string "kick"
|
||||
myweirdname ;; pushes "myweirdname" onto the stack
|
||||
```
|
||||
|
||||
@@ -110,8 +110,8 @@ Cagire uses a quotation-based loop with `times`:
|
||||
The loop counter is stored in the variable `i`, accessed with `@i`. This fits Cagire's style where control flow uses quotations.
|
||||
|
||||
```forth
|
||||
4 ( @i 4 / at hat s . ) times ;; hat at 0, 0.25, 0.5, 0.75
|
||||
4 ( c4 @i + note sine s . ) times ;; ascending notes
|
||||
4 ( @i 4 / at hat snd . ) times ;; hat at 0, 0.25, 0.5, 0.75
|
||||
4 ( c4 @i + note sine snd . ) times ;; ascending notes
|
||||
```
|
||||
|
||||
For generating sequences without side effects, use `..` or `gen`:
|
||||
@@ -155,11 +155,11 @@ These have no equivalent in classic Forth. They connect your script to the seque
|
||||
Classic Forth is deterministic. Cagire has built-in randomness:
|
||||
|
||||
```forth
|
||||
( "snare" s . ) 50 prob ;; 50% chance
|
||||
( "clap" s . ) 0.25 chance ;; 25% chance
|
||||
( "hat" s . ) often ;; 75% chance
|
||||
( "rim" s . ) sometimes ;; 50% chance
|
||||
( "tom" s . ) rarely ;; 25% chance
|
||||
( "snare" snd . ) 50 prob ;; 50% chance
|
||||
( "clap" snd . ) 0.25 chance ;; 25% chance
|
||||
( "hat" snd . ) often ;; 75% chance
|
||||
( "rim" snd . ) sometimes ;; 50% chance
|
||||
( "tom" snd . ) rarely ;; 25% chance
|
||||
```
|
||||
|
||||
These words take a quotation and execute it probabilistically.
|
||||
@@ -169,9 +169,9 @@ These words take a quotation and execute it probabilistically.
|
||||
Execute a quotation on specific iterations:
|
||||
|
||||
```forth
|
||||
( "snare" s . ) 4 every ;; every 4th pattern iteration
|
||||
( "hat" s . ) 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
|
||||
( "hat" s . ) 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations
|
||||
( "snare" snd . ) 4 every ;; every 4th pattern iteration
|
||||
( "hat" snd . ) 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
|
||||
( "hat" snd . ) 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations
|
||||
```
|
||||
|
||||
`every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped.
|
||||
@@ -183,13 +183,13 @@ Execute a quotation on specific iterations:
|
||||
Parameter words like `note`, `freq`, and `gain` consume the entire stack. If you push multiple values before a param word, you get polyphony:
|
||||
|
||||
```forth
|
||||
60 64 67 note sine s . ;; emits 3 voices with notes 60, 64, 67
|
||||
60 64 67 note sine snd . ;; emits 3 voices with notes 60, 64, 67
|
||||
```
|
||||
|
||||
This works for any param and for the sound word itself:
|
||||
|
||||
```forth
|
||||
440 880 freq sine tri s . ;; 2 voices: sine at 440, tri at 880
|
||||
440 880 freq sine tri snd . ;; 2 voices: sine at 440, tri at 880
|
||||
```
|
||||
|
||||
When params have different lengths, shorter lists cycle:
|
||||
@@ -197,7 +197,7 @@ When params have different lengths, shorter lists cycle:
|
||||
```forth
|
||||
60 64 67 note ;; 3 notes
|
||||
0.5 1.0 gain ;; 2 gains (cycles: 0.5, 1.0, 0.5)
|
||||
sine s . ;; emits 3 voices
|
||||
sine snd . ;; emits 3 voices
|
||||
```
|
||||
|
||||
Polyphony multiplies with `at` deltas:
|
||||
@@ -205,7 +205,7 @@ Polyphony multiplies with `at` deltas:
|
||||
```forth
|
||||
0 0.5 at ;; 2 time points
|
||||
60 64 note ;; 2 notes
|
||||
sine s . ;; emits 4 voices (2 notes × 2 times)
|
||||
sine snd . ;; emits 4 voices (2 notes × 2 times)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
# The Prelude
|
||||
# Preludes
|
||||
|
||||
You can define words in any step and they become available to all other steps. But as a project grows, definitions get scattered across steps and become hard to find and maintain. The **prelude** is a dedicated place for this. It is a project-wide Forth script that runs once before the first step plays. Definitions, variables, settings — everything in one place. Press `d` to open the prelude editor. Press `Esc` to save and close. Press `D` (Shift+d) to re-evaluate it without opening the editor.
|
||||
Cagire has two levels of prelude: a **project prelude** shared by all banks, and **bank preludes** that travel with each bank.
|
||||
|
||||
## Bank Prelude
|
||||
|
||||
Each bank can carry its own prelude script. Press `p` to open the current bank's prelude editor. Press `Esc` to save, evaluate, and close.
|
||||
|
||||
Bank preludes make banks self-contained. When you share a bank, its prelude travels with it — recipients get all the definitions they need without merging anything into their own project.
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
: pad sine sound 0.5 gain 2 spread 1.5 attack 0.4 verb . ;
|
||||
```
|
||||
|
||||
Every step in that bank can now use `bass` and `pad`. Share the bank and the recipient gets these definitions automatically.
|
||||
|
||||
## Project Prelude
|
||||
|
||||
The project prelude is a global script shared across all banks. Press `P` (Shift+p) to open it. Use it for truly project-wide definitions, variables, and settings that every bank should see.
|
||||
|
||||
```forth
|
||||
c2 !root
|
||||
0 !mode
|
||||
42 seed
|
||||
```
|
||||
|
||||
## Evaluation Order
|
||||
|
||||
When preludes are evaluated (on playback start, project load, or pressing `d`):
|
||||
|
||||
1. **Project prelude** runs first
|
||||
2. **Bank 0 prelude** runs next (if non-empty)
|
||||
3. **Bank 1 prelude**, then **Bank 2**, ... up to **Bank 31**
|
||||
|
||||
Only non-empty bank preludes are evaluated. Last-evaluated wins for name collisions — a bank prelude can override a project-level definition.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `p` | Open current bank's prelude editor |
|
||||
| `P` | Open project prelude editor |
|
||||
| `d` | Re-evaluate all preludes (project + all banks) |
|
||||
|
||||
## Naming Your Sounds
|
||||
|
||||
The most common use of the prelude is to define words for your instruments. Without a prelude, every step that plays a bass has to spell out the full sound design or to create a new word before using it:
|
||||
The most common use of a bank prelude is to define words for your instruments. Without a prelude, every step that plays a bass has to spell out the full sound design:
|
||||
|
||||
```forth
|
||||
pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
```
|
||||
|
||||
Repeat this across eight steps without making a new word and you have eight copies of the same thing. Change the filter? Change it eight times.
|
||||
|
||||
In the prelude, define it once:
|
||||
In the bank prelude, define it once:
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
@@ -20,22 +59,8 @@ In the prelude, define it once:
|
||||
|
||||
Now every step just writes `c2 note bass`. Change the sound in one place, every step follows.
|
||||
|
||||
A step that used to read:
|
||||
|
||||
```forth
|
||||
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```forth
|
||||
c2 note bass
|
||||
```
|
||||
|
||||
## Building a Vocabulary
|
||||
|
||||
The prelude is where you build the vocabulary for your music. Not just instruments but any combination of code / words you want to reuse:
|
||||
|
||||
```forth
|
||||
;; instruments
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
@@ -49,11 +74,11 @@ The prelude is where you build the vocabulary for your music. Not just instrumen
|
||||
: loud 0.9 gain ;
|
||||
```
|
||||
|
||||
By using the prelude and predefined words, steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
|
||||
Steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
|
||||
|
||||
## Setting Initial State
|
||||
|
||||
The prelude also runs plain Forth, not just definitions. You can use it to set variables and seed the random generator:
|
||||
The project prelude is the right place for global state:
|
||||
|
||||
```forth
|
||||
c2 !root
|
||||
@@ -61,18 +86,18 @@ c2 !root
|
||||
42 seed
|
||||
```
|
||||
|
||||
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible — same seed, same sequence every time you hit play.
|
||||
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible.
|
||||
|
||||
## When It Runs
|
||||
## When Preludes Run
|
||||
|
||||
The prelude evaluates at three moments:
|
||||
Preludes evaluate at three moments:
|
||||
|
||||
1. When you press **Space** to start playback
|
||||
2. When you **load** a project
|
||||
3. When you press **D** manually
|
||||
3. When you press **d** manually
|
||||
|
||||
It runs once at these moments, not on every step. This makes it the right place for definitions and initial values. If you edit the prelude while playing, press `D` to push changes into the running session. New definitions take effect immediately; the next time a step runs, it sees the updated words.
|
||||
They run once at these moments, not on every step. If you edit a prelude while playing, press `d` to push changes into the running session.
|
||||
|
||||
## What Not to Put Here
|
||||
|
||||
The prelude has no access to sequencer state. Words like `step`, `beat`, `iter`, and `phase` are meaningless here because no step is playing yet. Use the prelude for definitions and setup, not for logic that depends on timing. The prelude also should not emit sounds. It runs silently — any `.` calls here would fire before the sequencer clock is running and produce nothing useful.
|
||||
Preludes have no access to sequencer state. Words like `step`, `beat`, `iter`, and `phase` are meaningless here because no step is playing yet. Use preludes for definitions and setup, not for logic that depends on timing. Preludes also should not emit sounds — any `.` calls here would fire before the sequencer clock is running.
|
||||
|
||||
@@ -32,12 +32,13 @@ To create mirrors: copy a step with `Ctrl+C`, then paste with `Ctrl+B` instead o
|
||||
- `Ctrl+D` — Duplicate selection
|
||||
- `Ctrl+H` — Harden mirrors (convert to independent copies)
|
||||
|
||||
## Prelude
|
||||
## Preludes
|
||||
|
||||
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
|
||||
Each bank has its own prelude — a Forth script for definitions and setup that travels with the bank when shared. There is also a project-wide prelude for global configuration.
|
||||
|
||||
- `p` — Open the prelude editor
|
||||
- `d` — Evaluate the prelude
|
||||
- `p` — Open current bank's prelude editor
|
||||
- `P` — Open project prelude editor
|
||||
- `d` — Evaluate all preludes (project + all banks)
|
||||
|
||||
## Pattern Controls
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ Crossfade between two sounds:
|
||||
|
||||
```forth
|
||||
1 1 ccval 127 / ;; normalize to 0.0-1.0
|
||||
dup saw s swap gain .
|
||||
1 swap - tri s swap gain .
|
||||
dup saw snd swap gain .
|
||||
1 swap - tri snd swap gain .
|
||||
```
|
||||
|
||||
## Scaling Values
|
||||
|
||||
@@ -15,7 +15,7 @@ Configure your MIDI devices in the **Options** view. Select input and output dev
|
||||
The audio engine (`Doux`) and MIDI are independent systems. Use `.` to emit audio commands, use `m.` to emit MIDI messages. You can use both in the same script:
|
||||
|
||||
```forth
|
||||
saw s c4 note 0.5 gain . ;; audio
|
||||
saw snd c4 note 0.5 gain . ;; audio
|
||||
60 note 100 velocity m. ;; MIDI
|
||||
```
|
||||
|
||||
|
||||
@@ -7,19 +7,19 @@ Every step has a duration. By default, sounds emit at the very start of that dur
|
||||
`at` drains the entire stack and stores the values as timing offsets. Each value is a fraction of the step duration: 0 = start, 0.5 = halfway, 1.0 = next step boundary.
|
||||
|
||||
```forth
|
||||
0.5 at kick s . ;; kick at the midpoint
|
||||
0.5 at kick snd . ;; kick at the midpoint
|
||||
```
|
||||
|
||||
Push multiple values before calling `at` to get multiple emits from a single `.`:
|
||||
|
||||
```forth
|
||||
0 0.5 at kick s .
|
||||
0 0.5 at kick snd .
|
||||
```
|
||||
|
||||
Two kicks: one at start, one at midpoint.
|
||||
|
||||
```forth
|
||||
0 0.25 0.5 0.75 at hat s .
|
||||
0 0.25 0.5 0.75 at hat snd .
|
||||
```
|
||||
|
||||
Four hats, evenly spaced.
|
||||
@@ -28,10 +28,10 @@ The deltas persist across multiple `.` calls until `clear` or a new `at`:
|
||||
|
||||
```forth
|
||||
0 0.5 at
|
||||
kick s . ;; 2 kicks
|
||||
hat s . ;; 2 hats (same timing)
|
||||
kick snd . ;; 2 kicks
|
||||
hat snd . ;; 2 hats (same timing)
|
||||
clear
|
||||
snare s . ;; 1 snare (deltas cleared)
|
||||
snare snd . ;; 1 snare (deltas cleared)
|
||||
```
|
||||
|
||||
## Cross-product: at Without arp
|
||||
@@ -40,7 +40,7 @@ Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2
|
||||
|
||||
```forth
|
||||
0 0.5 at
|
||||
c4 e4 g4 note 1.5 decay sine s .
|
||||
c4 e4 g4 note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
6 emits: 3 notes x 2 deltas. A chord played twice per step.
|
||||
@@ -51,7 +51,7 @@ c4 e4 g4 note 1.5 decay sine s .
|
||||
|
||||
```forth
|
||||
0 0.33 0.66 at
|
||||
c4 e4 g4 arp note 0.5 decay sine s .
|
||||
c4 e4 g4 arp note 0.5 decay sine snd .
|
||||
```
|
||||
|
||||
C4 at 0, E4 at 0.33, G4 at 0.66.
|
||||
@@ -60,7 +60,7 @@ If the lists differ in length, the shorter one wraps around:
|
||||
|
||||
```forth
|
||||
0 0.25 0.5 0.75 at
|
||||
c4 e4 arp note 0.3 decay sine s .
|
||||
c4 e4 arp note 0.3 decay sine snd .
|
||||
```
|
||||
|
||||
C4, E4, C4, E4 — the shorter list wraps to fill 4 time points.
|
||||
@@ -74,25 +74,25 @@ You rarely type deltas by hand. Use generators:
|
||||
Evenly spaced via `.,`:
|
||||
|
||||
```forth
|
||||
0 1 0.25 ., at hat s . ;; 0 0.25 0.5 0.75 1.0
|
||||
0 1 0.25 ., at hat snd . ;; 0 0.25 0.5 0.75 1.0
|
||||
```
|
||||
|
||||
Euclidean distribution via `euclid`:
|
||||
|
||||
```forth
|
||||
3 8 euclid at hat s . ;; 3 hats at positions 0, 3, 5
|
||||
3 8 euclid at hat snd . ;; 3 hats at positions 0, 3, 5
|
||||
```
|
||||
|
||||
Random timing via `gen`:
|
||||
|
||||
```forth
|
||||
( 0.0 1.0 rand ) 4 gen at hat s . ;; 4 hats at random positions
|
||||
( 0.0 1.0 rand ) 4 gen at hat snd . ;; 4 hats at random positions
|
||||
```
|
||||
|
||||
Geometric spacing via `geom..`:
|
||||
|
||||
```forth
|
||||
0.0 2.0 4 geom.. at hat s . ;; exponentially spaced
|
||||
0.0 2.0 4 geom.. at hat snd . ;; exponentially spaced
|
||||
```
|
||||
|
||||
## Gating at
|
||||
@@ -101,14 +101,14 @@ Wrap `at` expressions in quotations for conditional timing:
|
||||
|
||||
```forth
|
||||
( 0 0.25 0.5 0.75 at ) 2 every
|
||||
hat s .
|
||||
hat snd .
|
||||
```
|
||||
|
||||
16th-note hats every other bar.
|
||||
|
||||
```forth
|
||||
( 0 0.5 at ) 0.5 chance
|
||||
kick s .
|
||||
kick snd .
|
||||
```
|
||||
|
||||
50% chance of double-hit.
|
||||
|
||||
@@ -48,7 +48,7 @@ Contrast with `times`, which executes for side effects and does not collect. `ti
|
||||
|
||||
```forth
|
||||
4 ( @i ) times ;; 0 1 2 3 (pushes @i each iteration)
|
||||
4 ( @i 60 + note sine s . ) times ;; plays 4 notes, collects nothing
|
||||
4 ( @i 60 + note sine snd . ) times ;; plays 4 notes, collects nothing
|
||||
```
|
||||
|
||||
The distinction: `gen` is for building data. `times` is for doing things.
|
||||
@@ -124,9 +124,9 @@ c4 4 dupn ;; c4 c4 c4 c4
|
||||
Build a drone chord -- same note, different octaves:
|
||||
|
||||
```forth
|
||||
c3 note 0.5 gain sine s .
|
||||
c3 note 12 + 0.5 gain sine s .
|
||||
c3 note 24 + 0.3 gain sine s .
|
||||
c3 note 0.5 gain sine snd .
|
||||
c3 note 12 + 0.5 gain sine snd .
|
||||
c3 note 24 + 0.3 gain sine snd .
|
||||
```
|
||||
|
||||
Or replicate a value for batch processing:
|
||||
|
||||
@@ -7,17 +7,17 @@ This tutorial covers everything pitch-related: notes, intervals, chords, voicing
|
||||
A note name followed by an octave number compiles to a MIDI integer:
|
||||
|
||||
```forth
|
||||
c4 note sine s .
|
||||
c4 note sine snd .
|
||||
```
|
||||
|
||||
That plays middle C (MIDI 60). `a4` is concert A (69), `e3` is 52. Sharps use `s` or `#`, flats use `b`:
|
||||
|
||||
```forth
|
||||
fs4 note 0.5 decay saw s .
|
||||
fs4 note 0.5 decay saw snd .
|
||||
```
|
||||
|
||||
```forth
|
||||
eb4 note 0.8 decay tri s .
|
||||
eb4 note 0.8 decay tri snd .
|
||||
```
|
||||
|
||||
`fs4` and `f#4` both mean F sharp 4 (MIDI 66). `bb3` is B flat 3 (58). Octave range is -1 to 9.
|
||||
@@ -29,13 +29,13 @@ Notes are just integers. They work anywhere an integer works — you can do arit
|
||||
An interval duplicates the top of the stack and adds semitones. Stack two intervals to build a chord by hand:
|
||||
|
||||
```forth
|
||||
c4 M3 P5 note 1.5 decay sine s .
|
||||
c4 M3 P5 note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
That builds a C major triad from scratch: C4 (60), then a major third above (64), then a perfect fifth above the root (67). Three notes on the stack, all played together.
|
||||
|
||||
```forth
|
||||
a3 m3 P5 note 1.2 decay va s .
|
||||
a3 m3 P5 note 1.2 decay va snd .
|
||||
```
|
||||
|
||||
A minor triad: A3, C4, E4.
|
||||
@@ -78,7 +78,7 @@ A minor triad: A3, C4, E4.
|
||||
Custom voicings with wide intervals:
|
||||
|
||||
```forth
|
||||
c3 P5 P8 M10 note 1.5 decay sine s .
|
||||
c3 P5 P8 M10 note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
C3, G3, C4, E4 — an open-voiced C major spread across two octaves.
|
||||
@@ -88,21 +88,21 @@ C3, G3, C4, E4 — an open-voiced C major spread across two octaves.
|
||||
Chord words replace a root note with all the chord tones. They're shortcuts for what intervals do manually:
|
||||
|
||||
```forth
|
||||
c4 maj note 1.5 decay sine s .
|
||||
c4 maj note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
That's the same C major triad, but in one word instead of `M3 P5`. A few more:
|
||||
|
||||
```forth
|
||||
d3 min7 note 1.5 decay va s .
|
||||
d3 min7 note 1.5 decay va snd .
|
||||
```
|
||||
|
||||
```forth
|
||||
e3 dom9 note 1.2 decay saw s .
|
||||
e3 dom9 note 1.2 decay saw snd .
|
||||
```
|
||||
|
||||
```forth
|
||||
a3 sus2 note 1.5 decay tri s .
|
||||
a3 sus2 note 1.5 decay tri snd .
|
||||
```
|
||||
|
||||
Common triads:
|
||||
@@ -140,19 +140,19 @@ 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 .
|
||||
c4 maj inv note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
The root C goes up, giving E4 G4 C5 — first inversion. Apply it twice for second inversion:
|
||||
|
||||
```forth
|
||||
c4 maj inv inv note 1.5 decay sine s .
|
||||
c4 maj inv inv note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
G4 C5 E5. `dinv` does the opposite — moves the top note down an octave:
|
||||
|
||||
```forth
|
||||
c4 maj dinv note 1.5 decay sine s .
|
||||
c4 maj dinv note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
G3 C4 E4. The fifth drops below the root.
|
||||
@@ -160,13 +160,13 @@ G3 C4 E4. The fifth drops below the root.
|
||||
`drop2` and `drop3` are jazz voicing techniques for four-note chords. `drop2` takes the second-from-top note and drops it an octave:
|
||||
|
||||
```forth
|
||||
c4 maj7 drop2 note 1.2 decay va s .
|
||||
c4 maj7 drop2 note 1.2 decay va snd .
|
||||
```
|
||||
|
||||
From C4 E4 G4 B4, the G drops to G3: G3 C4 E4 B4. `drop3` drops the third-from-top:
|
||||
|
||||
```forth
|
||||
c4 maj7 drop3 note 1.2 decay va s .
|
||||
c4 maj7 drop3 note 1.2 decay va snd .
|
||||
```
|
||||
|
||||
E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jazz guitar and piano.
|
||||
@@ -176,13 +176,13 @@ E drops to E3: E3 C4 G4 B4. These create wider, more open voicings common in jaz
|
||||
`tp` shifts every note on the stack by N semitones:
|
||||
|
||||
```forth
|
||||
c4 maj 3 tp note 1.5 decay sine s .
|
||||
c4 maj 3 tp note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
C major transposed up 3 semitones becomes Eb major. Works with any number of notes:
|
||||
|
||||
```forth
|
||||
c4 min7 -2 tp note 1.5 decay va s .
|
||||
c4 min7 -2 tp note 1.5 decay va snd .
|
||||
```
|
||||
|
||||
Shifts the whole chord down 2 semitones (Bb minor 7).
|
||||
@@ -190,14 +190,14 @@ 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 .
|
||||
c4 1 oct note 0.3 decay sine snd .
|
||||
```
|
||||
|
||||
C5 (one octave up). Useful for bass lines:
|
||||
|
||||
```forth
|
||||
0 2 4 5 7 5 4 2 8 cycle minor note
|
||||
-2 oct 0.8 gain sine s .
|
||||
-2 oct 0.8 gain sine snd .
|
||||
```
|
||||
|
||||
## Scales
|
||||
@@ -205,7 +205,7 @@ C5 (one octave up). Useful for bass lines:
|
||||
Scale words convert a degree index into a MIDI note. By default the root is C4 (MIDI 60):
|
||||
|
||||
```forth
|
||||
0 major note 0.5 decay sine s .
|
||||
0 major note 0.5 decay sine snd .
|
||||
```
|
||||
|
||||
Degree 0 of the major scale: C4. Degrees wrap with octave transposition — degree 7 gives C5 (72), degree -1 gives B3 (59).
|
||||
@@ -213,13 +213,13 @@ Degree 0 of the major scale: C4. Degrees wrap with octave transposition — degr
|
||||
Walk through a scale with `cycle`:
|
||||
|
||||
```forth
|
||||
0 1 2 3 4 5 6 7 8 cycle minor note 0.5 decay sine s .
|
||||
0 1 2 3 4 5 6 7 8 cycle minor note 0.5 decay sine snd .
|
||||
```
|
||||
|
||||
Random notes from a scale:
|
||||
|
||||
```forth
|
||||
0 7 rand pentatonic note 0.8 decay va s .
|
||||
0 7 rand pentatonic note 0.8 decay va snd .
|
||||
```
|
||||
|
||||
### Setting the key
|
||||
@@ -227,13 +227,13 @@ Random notes from a scale:
|
||||
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 .
|
||||
g3 key! 0 major note 0.5 decay sine snd .
|
||||
```
|
||||
|
||||
Now degree 0 is G3 (55) instead of C4. The key persists across steps until changed again:
|
||||
|
||||
```forth
|
||||
a3 key! 0 3 5 7 3 cycle minor note 0.8 decay tri s .
|
||||
a3 key! 0 3 5 7 3 cycle minor note 0.8 decay tri snd .
|
||||
```
|
||||
|
||||
A minor melody starting from A3.
|
||||
@@ -261,19 +261,19 @@ Jazz, symmetric, and modal variant scales are listed in the Reference section.
|
||||
`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 .
|
||||
0 major triad note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
Degree 0 of the major scale, stacked in thirds: C E G — a major triad. The scale determines the chord quality automatically. Degree 1 gives D F A (minor), degree 4 gives G B D (major):
|
||||
|
||||
```forth
|
||||
4 major triad note 1.5 decay sine s .
|
||||
4 major triad note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
`seventh` adds a fourth note:
|
||||
|
||||
```forth
|
||||
0 major seventh note 1.2 decay va s .
|
||||
0 major seventh note 1.2 decay va snd .
|
||||
```
|
||||
|
||||
C E G B — Cmaj7. Degree 1 gives Dm7, degree 4 gives G7 (dominant). The diatonic context determines everything.
|
||||
@@ -281,7 +281,7 @@ C E G B — Cmaj7. Degree 1 gives Dm7, degree 4 gives G7 (dominant). The diatoni
|
||||
Combine with `key!` to play diatonic chords in any key:
|
||||
|
||||
```forth
|
||||
g3 key! 0 major triad note 1.5 decay sine s .
|
||||
g3 key! 0 major triad note 1.5 decay sine snd .
|
||||
```
|
||||
|
||||
G major triad rooted at G3.
|
||||
@@ -291,7 +291,7 @@ A I-vi-IV-V chord progression using `pcycle`:
|
||||
```forth
|
||||
( 0 major seventh ) ( 5 major seventh )
|
||||
( 3 major seventh ) ( 4 major seventh ) 4 pcycle
|
||||
note 1.2 decay va s .
|
||||
note 1.2 decay va snd .
|
||||
```
|
||||
|
||||
Combine with voicings for smoother voice leading:
|
||||
@@ -299,13 +299,13 @@ 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 .
|
||||
note 1.5 decay va snd .
|
||||
```
|
||||
|
||||
Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for details on `arp`):
|
||||
|
||||
```forth
|
||||
0 major seventh arp note 0.5 decay sine s .
|
||||
0 major seventh arp note 0.5 decay sine snd .
|
||||
```
|
||||
|
||||
## Frequency Conversion
|
||||
@@ -313,7 +313,7 @@ Arpeggiate diatonic chords using `arp` (see the *Timing with at* tutorial for de
|
||||
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
|
||||
|
||||
```forth
|
||||
c4 mtof freq sine s .
|
||||
c4 mtof freq sine snd .
|
||||
```
|
||||
|
||||
Useful when a synth parameter expects Hz rather than MIDI.
|
||||
|
||||
@@ -21,7 +21,7 @@ Press `Enter` to focus the editor. Write Forth code as you would in any step. Pr
|
||||
|
||||
```forth
|
||||
;; a simple drone
|
||||
saw s c2 note 0.3 gain 0.4 verb .
|
||||
saw snd c2 note 0.3 gain 0.4 verb .
|
||||
```
|
||||
|
||||
## Speed and Length
|
||||
|
||||
@@ -17,15 +17,15 @@ sine sound
|
||||
`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 .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
|
||||
60 72 rand note sine snd 0.5 decay . ;; random MIDI note from 60 to 72
|
||||
0.3 0.9 rand gain sine snd 0.5 decay . ;; random gain between 0.3 and 0.9
|
||||
```
|
||||
|
||||
`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 .5 decay . ;; mostly low frequencies
|
||||
200.0 8000.0 logrand freq sine s .5 decay . ;; mostly high frequencies
|
||||
200.0 8000.0 exprand freq sine snd 0.5 decay . ;; mostly low frequencies
|
||||
200.0 8000.0 logrand freq sine snd 0.5 decay . ;; mostly high frequencies
|
||||
```
|
||||
|
||||
These are useful for parameters where perception is logarithmic, like frequency and duration.
|
||||
@@ -35,8 +35,8 @@ These are useful for parameters where perception is logarithmic, like frequency
|
||||
The probability words take a quotation and execute it with some chance. `chance` takes a float from 0.0 to 1.0, `prob` takes a percentage from 0 to 100:
|
||||
|
||||
```forth
|
||||
( hat s . ) 0.25 chance ;; 25% chance
|
||||
( kick s . ) 75 prob ;; 75% chance
|
||||
( hat snd . ) 0.25 chance ;; 25% chance
|
||||
( kick snd . ) 75 prob ;; 75% chance
|
||||
```
|
||||
|
||||
Named probability words save you from remembering numbers:
|
||||
@@ -52,9 +52,9 @@ Named probability words save you from remembering numbers:
|
||||
| `never` | 0% |
|
||||
|
||||
```forth
|
||||
( hat s . ) often ;; 75%
|
||||
( snare s . ) sometimes ;; 50%
|
||||
( clap s . ) rarely ;; 25%
|
||||
( hat snd . ) often ;; 75%
|
||||
( snare snd . ) sometimes ;; 50%
|
||||
( clap snd . ) rarely ;; 25%
|
||||
```
|
||||
|
||||
`always` and `never` are useful when you want to temporarily mute or unmute a voice without deleting code. Change `sometimes` to `never` to silence it, `always` to bring it back.
|
||||
@@ -62,8 +62,8 @@ Named probability words save you from remembering numbers:
|
||||
Use `?` and `!?` with `coin` for quick coin-flip decisions:
|
||||
|
||||
```forth
|
||||
( hat s . ) coin ? ;; execute if coin is 1
|
||||
( rim s . ) coin !? ;; execute if coin is 0
|
||||
( hat snd . ) coin ? ;; execute if coin is 1
|
||||
( rim snd . ) coin !? ;; execute if coin is 0
|
||||
```
|
||||
|
||||
## Selection
|
||||
@@ -71,21 +71,21 @@ Use `?` and `!?` with `coin` for quick coin-flip decisions:
|
||||
`choose` picks randomly from n items on the stack:
|
||||
|
||||
```forth
|
||||
kick snare hat 3 choose s . ;; random drum hit
|
||||
60 64 67 72 4 choose note sine s .5 decay . ;; random note from a set
|
||||
kick snare hat 3 choose snd . ;; random drum hit
|
||||
60 64 67 72 4 choose note sine snd 0.5 decay . ;; random note from a set
|
||||
```
|
||||
|
||||
When a chosen item is a quotation, it gets executed:
|
||||
|
||||
```forth
|
||||
( 0.1 decay ) ( 0.5 decay ) ( 0.9 decay ) 3 choose
|
||||
sine s .
|
||||
sine snd .
|
||||
```
|
||||
|
||||
`wchoose` lets you assign weights to each option. Push value/weight pairs:
|
||||
|
||||
```forth
|
||||
kick 0.5 snare 0.3 hat 0.2 3 wchoose s .
|
||||
kick 0.5 snare 0.3 hat 0.2 3 wchoose snd .
|
||||
```
|
||||
|
||||
Kick plays 50% of the time, snare 30%, hat 20%. Weights don't need to sum to 1 -- they're normalized automatically.
|
||||
@@ -103,7 +103,7 @@ Combined with `note`, this gives you a random permutation of a chord every time
|
||||
`every` runs a quotation once every n pattern iterations:
|
||||
|
||||
```forth
|
||||
( crash s . ) 4 every ;; crash cymbal every 4th iteration
|
||||
( crash snd . ) 4 every ;; crash cymbal every 4th iteration
|
||||
```
|
||||
|
||||
`except` is the inverse -- it runs a quotation on all iterations *except* every nth:
|
||||
@@ -115,22 +115,22 @@ Combined with `note`, this gives you a random permutation of a chord every time
|
||||
`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...
|
||||
( snare snd . ) 4 2 every+ ;; fires at iter 2, 6, 10, 14...
|
||||
( snare snd . ) 4 2 except+ ;; skips at iter 2, 6, 10, 14...
|
||||
```
|
||||
|
||||
Without the offset, `every` fires at 0, 4, 8... The offset shifts that by 2, so it fires at 2, 6, 10... This lets you interleave patterns that share the same period:
|
||||
|
||||
```forth
|
||||
( kick s . ) 4 every ;; kick at 0, 4, 8...
|
||||
( snare s . ) 4 2 every+ ;; snare at 2, 6, 10...
|
||||
( kick snd . ) 4 every ;; kick at 0, 4, 8...
|
||||
( snare snd . ) 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
|
||||
( hat s . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
|
||||
( hat s . ) 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
|
||||
( hat snd . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
|
||||
( hat snd . ) 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
|
||||
```
|
||||
|
||||
`bjork` counts by step runs (how many times this particular step has played). `pbjork` counts by pattern iterations. Some classic patterns:
|
||||
@@ -148,7 +148,7 @@ By default, every run produces different random values. Use `seed` to make rando
|
||||
|
||||
```forth
|
||||
42 seed
|
||||
60 72 rand note sine s . ;; always the same "random" note
|
||||
60 72 rand note sine snd . ;; always the same "random" note
|
||||
```
|
||||
|
||||
The seed is set at the start of the script. Same seed, same sequence. Useful when you want a specific random pattern to repeat.
|
||||
@@ -158,7 +158,7 @@ The seed is set at the start of the script. Same seed, same sequence. Useful whe
|
||||
The real power comes from mixing techniques. A hi-hat pattern with ghost notes:
|
||||
|
||||
```forth
|
||||
hat s
|
||||
hat snd
|
||||
( 0.3 0.6 rand gain ) ( 0.8 gain ) 2 cycle
|
||||
.
|
||||
```
|
||||
@@ -170,16 +170,16 @@ A bass line that changes every 4 bars:
|
||||
```forth
|
||||
( c2 note ) ( e2 note ) ( g2 note ) ( a2 note ) 4 pcycle
|
||||
( 0.5 decay ) often
|
||||
sine s .
|
||||
sine snd .
|
||||
```
|
||||
|
||||
Layered percussion with different densities:
|
||||
|
||||
```forth
|
||||
( kick s . ) always
|
||||
( snare s . ) 2 every
|
||||
( hat s . ) 5 8 bjork
|
||||
( rim s . ) rarely
|
||||
( kick snd . ) always
|
||||
( snare snd . ) 2 every
|
||||
( hat snd . ) 5 8 bjork
|
||||
( rim snd . ) rarely
|
||||
```
|
||||
|
||||
A melodic step with weighted note selection and random timbre:
|
||||
@@ -188,7 +188,7 @@ A melodic step with weighted note selection and random timbre:
|
||||
c4 0.4 e4 0.3 g4 0.2 b4 0.1 4 wchoose note
|
||||
0.3 0.7 rand decay
|
||||
1.0 4.0 exprand harmonics
|
||||
modal s .
|
||||
modal snd .
|
||||
```
|
||||
|
||||
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.
|
||||
|
||||
@@ -19,7 +19,7 @@ Play something -- a pattern, a live input, anything that makes sound. When you'r
|
||||
The recording is now available as a sample:
|
||||
|
||||
```forth
|
||||
drums s .
|
||||
drums snd .
|
||||
```
|
||||
|
||||
## Playback
|
||||
@@ -27,10 +27,10 @@ drums s .
|
||||
Recorded samples are ordinary samples. Everything you can do with a loaded sample works here:
|
||||
|
||||
```forth
|
||||
drums s 0.5 speed . ;; half speed
|
||||
drums s 0.25 begin 0.5 end . ;; slice the middle quarter
|
||||
drums s 800 lpf 0.3 verb . ;; filter and reverb
|
||||
drums s -1 speed . ;; reverse
|
||||
drums snd 0.5 speed . ;; half speed
|
||||
drums snd 0.25 begin 0.5 end . ;; slice the middle quarter
|
||||
drums snd 800 lpf 0.3 verb . ;; filter and reverb
|
||||
drums snd -1 speed . ;; reverse
|
||||
```
|
||||
|
||||
## Overdub
|
||||
@@ -70,7 +70,7 @@ Record a foundation, then overdub to build up:
|
||||
"loop" dub
|
||||
|
||||
;; 4. play the result
|
||||
loop s .
|
||||
loop snd .
|
||||
```
|
||||
|
||||
Each overdub pass adds to what's already there. The buffer wraps, so longer passes layer cyclically over the original length.
|
||||
@@ -80,16 +80,16 @@ Each overdub pass adds to what's already there. The buffer wraps, so longer pass
|
||||
Once you have a recording, carve it up:
|
||||
|
||||
```forth
|
||||
loop s 0.0 begin 0.25 end . ;; first quarter
|
||||
loop s 0.25 begin 0.5 end . ;; second quarter
|
||||
loop s 0.5 begin 0.75 end . ;; third quarter
|
||||
loop s 0.75 begin 1.0 end . ;; last quarter
|
||||
loop snd 0.0 begin 0.25 end . ;; first quarter
|
||||
loop snd 0.25 begin 0.5 end . ;; second quarter
|
||||
loop snd 0.5 begin 0.75 end . ;; third quarter
|
||||
loop snd 0.75 begin 1.0 end . ;; last quarter
|
||||
```
|
||||
|
||||
Combine with randomness for variation:
|
||||
|
||||
```forth
|
||||
loop s
|
||||
loop snd
|
||||
0.0 0.25 0.5 0.75 4 choose begin
|
||||
0.5 speed
|
||||
.
|
||||
|
||||
@@ -11,9 +11,9 @@ Drop an `.sf2` file into one of your samples directories. The engine finds and l
|
||||
Use `gm` as the sound source. The `n` parameter selects a program by name or number (0-127):
|
||||
|
||||
```forth
|
||||
gm s piano n . ;; acoustic piano
|
||||
gm s strings n c4 note . ;; strings playing middle C
|
||||
gm s 0 n e4 note . ;; program 0 (piano) playing E4
|
||||
gm snd piano n . ;; acoustic piano
|
||||
gm snd strings n c4 note . ;; strings playing middle C
|
||||
gm snd 0 n e4 note . ;; program 0 (piano) playing E4
|
||||
```
|
||||
|
||||
## Drums
|
||||
@@ -21,10 +21,10 @@ gm s 0 n e4 note . ;; program 0 (piano) playing E4
|
||||
Drums live on a separate bank. Use `drums` or `percussion` as the `n` value. Each MIDI note triggers a different instrument:
|
||||
|
||||
```forth
|
||||
gm s drums n 36 note . ;; kick
|
||||
gm s drums n 38 note . ;; snare
|
||||
gm s drums n 42 note . ;; closed hi-hat
|
||||
gm s percussion n 49 note . ;; crash cymbal
|
||||
gm snd drums n 36 note . ;; kick
|
||||
gm snd drums n 38 note . ;; snare
|
||||
gm snd drums n 42 note . ;; closed hi-hat
|
||||
gm snd percussion n 49 note . ;; crash cymbal
|
||||
```
|
||||
|
||||
## Envelope
|
||||
@@ -32,8 +32,8 @@ gm s percussion n 49 note . ;; crash cymbal
|
||||
The soundfont embeds ADSR envelope data per preset. The engine applies it automatically. Override any parameter explicitly:
|
||||
|
||||
```forth
|
||||
gm s piano n 0.01 attack 0.3 decay .
|
||||
gm s strings n 0.5 attack 2.0 release .
|
||||
gm snd piano n 0.01 attack 0.3 decay .
|
||||
gm snd strings n 0.5 attack 2.0 release .
|
||||
```
|
||||
|
||||
If you set `attack`, `decay`, `sustain`, or `release`, your value wins. Unspecified parameters keep the soundfont default.
|
||||
@@ -43,9 +43,9 @@ If you set `attack`, `decay`, `sustain`, or `release`, your value wins. Unspecif
|
||||
All standard engine parameters work on GM voices. Filter, distort, spatialize:
|
||||
|
||||
```forth
|
||||
gm s bass n 800 lpf 0.3 verb .
|
||||
gm s epiano n 0.5 delay 1.5 distort .
|
||||
gm s choir n 0.8 pan 2000 hpf .
|
||||
gm snd bass n 800 lpf 0.3 verb .
|
||||
gm snd epiano n 0.5 delay 1.5 distort .
|
||||
gm snd choir n 0.8 pan 2000 hpf .
|
||||
```
|
||||
|
||||
## Preset Names
|
||||
@@ -79,22 +79,22 @@ A simple GM drum pattern across four steps:
|
||||
|
||||
```forth
|
||||
;; step 1: kick
|
||||
gm s drums n 36 note .
|
||||
gm snd drums n 36 note .
|
||||
|
||||
;; step 2: closed hat
|
||||
gm s drums n 42 note 0.6 gain .
|
||||
gm snd drums n 42 note 0.6 gain .
|
||||
|
||||
;; step 3: snare
|
||||
gm s drums n 38 note .
|
||||
gm snd drums n 38 note .
|
||||
|
||||
;; step 4: closed hat
|
||||
gm s drums n 42 note 0.6 gain .
|
||||
gm snd drums n 42 note 0.6 gain .
|
||||
```
|
||||
|
||||
Layer piano chords with randomness:
|
||||
|
||||
```forth
|
||||
gm s piano n
|
||||
gm snd piano n
|
||||
c4 e4 g4 3 choose note
|
||||
0.3 0.8 rand gain
|
||||
0.1 0.4 rand verb
|
||||
@@ -104,7 +104,7 @@ c4 e4 g4 3 choose note
|
||||
A bass line with envelope override:
|
||||
|
||||
```forth
|
||||
gm s bass n
|
||||
gm snd bass n
|
||||
c2 e2 g2 a2 4 cycle note
|
||||
0.01 attack 0.2 decay 0.0 sustain
|
||||
.
|
||||
|
||||
@@ -17,13 +17,13 @@ Variables let you name values and share data between steps. They are global -- a
|
||||
`,name` stores just like `!name` but keeps the value on the stack. Useful when you want to name something and keep using it:
|
||||
|
||||
```forth
|
||||
440 ,freq sine s . ;; stores 440 in freq AND passes it to the pipeline
|
||||
440 ,freq sine snd . ;; stores 440 in freq AND passes it to the pipeline
|
||||
```
|
||||
|
||||
Without `,`, you'd need `dup`:
|
||||
|
||||
```forth
|
||||
440 dup !freq sine s . ;; equivalent, but noisier
|
||||
440 dup !freq sine snd . ;; equivalent, but noisier
|
||||
```
|
||||
|
||||
## Sharing Between Steps
|
||||
@@ -35,7 +35,7 @@ Variables are shared across all steps. One step can store a value that another r
|
||||
c4 iter 7 mod + !root
|
||||
|
||||
;; step 4: read it
|
||||
@root 7 + note sine s .
|
||||
@root 7 + note sine snd .
|
||||
```
|
||||
|
||||
Every time the pattern loops, step 0 picks a new root. Step 4 always harmonizes with it.
|
||||
@@ -46,7 +46,7 @@ Fetch, modify, store back. A classic pattern for evolving values:
|
||||
|
||||
```forth
|
||||
@n 1 + !n ;; increment n each time this step runs
|
||||
@n 12 mod note sine s . ;; cycle through 12 notes
|
||||
@n 12 mod note sine snd . ;; cycle through 12 notes
|
||||
```
|
||||
|
||||
Reset on some condition:
|
||||
@@ -69,7 +69,7 @@ Store a sound name in a variable, reuse it across steps:
|
||||
"sine" !synth
|
||||
|
||||
;; step 1, 2, 3...
|
||||
c4 note @synth s .
|
||||
c4 note @synth snd .
|
||||
```
|
||||
|
||||
Change one step, all steps follow.
|
||||
|
||||
98
nsis/cagire.nsi
Normal file
98
nsis/cagire.nsi
Normal file
@@ -0,0 +1,98 @@
|
||||
; Cagire NSIS Installer Script
|
||||
; Receives defines from command line:
|
||||
; -DVERSION=x.y.z
|
||||
; -DCLI_EXE=/path/to/cagire.exe
|
||||
; -DDESKTOP_EXE=/path/to/cagire-desktop.exe
|
||||
; -DICON=/path/to/Cagire.ico
|
||||
; -DOUTDIR=/path/to/releases
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "WordFunc.nsh"
|
||||
|
||||
Name "Cagire ${VERSION}"
|
||||
OutFile "${OUTDIR}\cagire-${VERSION}-windows-x86_64-setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\Cagire"
|
||||
InstallDirRegKey HKLM "Software\Cagire" "InstallDir"
|
||||
RequestExecutionLevel admin
|
||||
Unicode True
|
||||
|
||||
!define MUI_ICON "${ICON}"
|
||||
!define MUI_UNICON "${ICON}"
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_HEADERIMAGE_BITMAP "header.bmp"
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
|
||||
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Section "Cagire (required)" SecCore
|
||||
SectionIn RO
|
||||
SetOutPath "$INSTDIR"
|
||||
File "/oname=cagire.exe" "${CLI_EXE}"
|
||||
File "/oname=cagire-desktop.exe" "${DESKTOP_EXE}"
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Cagire" "InstallDir" "$INSTDIR"
|
||||
|
||||
; Add/Remove Programs entry
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayName" "Cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "Publisher" "Raphael Forment"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayIcon" '"$INSTDIR\cagire-desktop.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://github.com/Bubobubobubobubo/cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "HelpLink" "https://cagire.raphaelforment.fr"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoRepair" 1
|
||||
SectionEnd
|
||||
|
||||
Section "Add to PATH" SecPath
|
||||
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
|
||||
StrCpy $0 "$0;$INSTDIR"
|
||||
WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0"
|
||||
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||
SectionEnd
|
||||
|
||||
Section "Start Menu Shortcut" SecStartMenu
|
||||
CreateDirectory "$SMPROGRAMS\Cagire"
|
||||
CreateShortCut "$SMPROGRAMS\Cagire\Cagire.lnk" "$INSTDIR\cagire-desktop.exe" "" "$INSTDIR\cagire-desktop.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\Cagire\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
||||
SectionEnd
|
||||
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} "Installs Cagire CLI and Desktop binaries."
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecPath} "Add the install location to the PATH system environment variable."
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} "Add a Cagire shortcut to the Start Menu."
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$INSTDIR\cagire.exe"
|
||||
Delete "$INSTDIR\cagire-desktop.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
Delete "$SMPROGRAMS\Cagire\Cagire.lnk"
|
||||
Delete "$SMPROGRAMS\Cagire\Uninstall.lnk"
|
||||
RMDir "$SMPROGRAMS\Cagire"
|
||||
|
||||
; Remove from PATH
|
||||
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
|
||||
; Remove ";$INSTDIR" from the path string
|
||||
${WordReplace} $0 ";$INSTDIR" "" "+" $0
|
||||
WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0"
|
||||
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire"
|
||||
DeleteRegKey HKLM "Software\Cagire"
|
||||
SectionEnd
|
||||
BIN
nsis/header.bmp
Normal file
BIN
nsis/header.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
nsis/sidebar.bmp
Normal file
BIN
nsis/sidebar.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
@@ -279,8 +279,6 @@ impl Plugin for CagirePlugin {
|
||||
};
|
||||
let lookahead_end = beat + lookahead_beats;
|
||||
|
||||
let engine_time = self.sample_pos as f64 / self.sample_rate as f64;
|
||||
|
||||
// Drain commands from the editor
|
||||
let commands: Vec<SeqCommand> = self.bridge.cmd_rx.try_iter().collect();
|
||||
|
||||
@@ -294,7 +292,8 @@ impl Plugin for CagirePlugin {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time,
|
||||
audio_sample_pos: self.sample_pos,
|
||||
sr: self.sample_rate as f64,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
@@ -310,12 +309,12 @@ impl Plugin for CagirePlugin {
|
||||
// Drain audio commands from the editor (preview, hush, load samples, etc.)
|
||||
for audio_cmd in self.bridge.audio_cmd_rx.try_iter() {
|
||||
match audio_cmd {
|
||||
AudioCommand::Evaluate { ref cmd, time } => {
|
||||
let cmd_ref = match time {
|
||||
AudioCommand::Evaluate { ref cmd, tick } => {
|
||||
let cmd_ref = match tick {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{cmd}/time/{t:.6}");
|
||||
let _ = write!(&mut self.cmd_buffer, "{cmd}/tick/{t}");
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => cmd.as_str(),
|
||||
@@ -419,11 +418,11 @@ impl Plugin for CagirePlugin {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let cmd_ref = match tsc.time {
|
||||
let cmd_ref = match tsc.tick {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{}/time/{t:.6}", tsc.cmd);
|
||||
let _ = write!(&mut self.cmd_buffer, "{}/tick/{t}", tsc.cmd);
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => &tsc.cmd,
|
||||
|
||||
@@ -327,11 +327,20 @@ copy_artifacts() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# MSI installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v cargo-wix &>/dev/null; then
|
||||
echo " Building MSI installer..."
|
||||
cargo wix --no-build --nocapture --package cagire -C -arch -C x64
|
||||
cp target/wix/*.msi "$OUT/" 2>/dev/null && echo " MSI -> $OUT/" || true
|
||||
# NSIS installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then
|
||||
echo " Building NSIS installer..."
|
||||
local version
|
||||
version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
local abs_root
|
||||
abs_root=$(pwd)
|
||||
makensis -DVERSION="$version" \
|
||||
-DCLI_EXE="$abs_root/$rd/cagire.exe" \
|
||||
-DDESKTOP_EXE="$abs_root/$rd/cagire-desktop.exe" \
|
||||
-DICON="$abs_root/assets/Cagire.ico" \
|
||||
-DOUTDIR="$abs_root/$OUT" \
|
||||
nsis/cagire.nsi
|
||||
echo " Installer -> $OUT/cagire-${version}-windows-x86_64-setup.exe"
|
||||
fi
|
||||
|
||||
# AppImage for Linux targets
|
||||
|
||||
@@ -469,6 +469,13 @@ impl App {
|
||||
AppCommand::SavePrelude => self.save_prelude(),
|
||||
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
|
||||
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
|
||||
AppCommand::OpenBankPreludeEditor => self.open_bank_prelude_editor(),
|
||||
AppCommand::SaveBankPrelude => self.save_bank_prelude(),
|
||||
AppCommand::EvaluateBankPrelude => {
|
||||
let bank = self.editor_ctx.bank;
|
||||
self.evaluate_bank_prelude(bank, link);
|
||||
}
|
||||
AppCommand::CloseBankPreludeEditor => self.close_bank_prelude_editor(),
|
||||
|
||||
// Periodic script
|
||||
AppCommand::OpenScriptModal(field) => self.open_script_modal(field),
|
||||
|
||||
@@ -109,24 +109,101 @@ impl App {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
/// Evaluate the project prelude to seed variables and definitions.
|
||||
pub fn evaluate_prelude(&mut self, link: &LinkState) {
|
||||
let prelude = &self.project_state.project.prelude;
|
||||
/// Switch the editor to the current bank's prelude script.
|
||||
pub fn open_bank_prelude_editor(&mut self) {
|
||||
let bank = self.editor_ctx.bank;
|
||||
let prelude = &self.project_state.project.banks[bank].prelude;
|
||||
let lines: Vec<String> = if prelude.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
prelude.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor_ctx.editor.set_content(lines);
|
||||
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.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());
|
||||
self.editor_ctx.target = EditorTarget::BankPrelude;
|
||||
self.ui.modal = Modal::Editor;
|
||||
}
|
||||
|
||||
pub fn save_bank_prelude(&mut self) {
|
||||
let bank = self.editor_ctx.bank;
|
||||
let text = self.editor_ctx.editor.content();
|
||||
self.project_state.project.banks[bank].prelude = text;
|
||||
}
|
||||
|
||||
pub fn close_bank_prelude_editor(&mut self) {
|
||||
self.editor_ctx.target = EditorTarget::Step;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
/// Evaluate a single bank's prelude.
|
||||
pub fn evaluate_bank_prelude(&mut self, bank: usize, link: &LinkState) {
|
||||
let prelude = &self.project_state.project.banks[bank].prelude;
|
||||
if prelude.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(prelude, &ctx) {
|
||||
Ok(_) => {
|
||||
self.ui.flash("Prelude evaluated", 150, FlashKind::Info);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Prelude error: {e}"), 300, FlashKind::Error);
|
||||
let fallback = format!("Bank {}", bank + 1);
|
||||
let bank_name = self.project_state.project.banks[bank]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or(&fallback);
|
||||
self.ui.flash(
|
||||
&format!("{bank_name} prelude error: {e}"),
|
||||
300,
|
||||
FlashKind::Error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the project prelude and all bank preludes.
|
||||
pub fn evaluate_prelude(&mut self, link: &LinkState) {
|
||||
let project_prelude = &self.project_state.project.prelude;
|
||||
if !project_prelude.trim().is_empty() {
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(project_prelude, &ctx) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Project prelude error: {e}"), 300, FlashKind::Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for bank_idx in 0..self.project_state.project.banks.len() {
|
||||
let prelude = &self.project_state.project.banks[bank_idx].prelude;
|
||||
if prelude.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(prelude, &ctx) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let bank_name = self.project_state.project.banks[bank_idx]
|
||||
.name
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| format!("Bank {}", bank_idx + 1));
|
||||
self.ui.flash(
|
||||
&format!("{bank_name} prelude error: {e}"),
|
||||
300,
|
||||
FlashKind::Error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ui.flash("Preludes evaluated", 150, FlashKind::Info);
|
||||
}
|
||||
|
||||
/// Evaluate a script and immediately send its audio commands.
|
||||
/// Returns collected `print` output, if any.
|
||||
pub fn execute_script_oneshot(
|
||||
@@ -148,7 +225,7 @@ impl App {
|
||||
}
|
||||
let _ = audio_tx
|
||||
.load()
|
||||
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
|
||||
.send(crate::engine::AudioCommand::Evaluate { cmd, tick: None });
|
||||
}
|
||||
Ok(if print_output.is_empty() {
|
||||
None
|
||||
|
||||
@@ -42,6 +42,11 @@ impl App {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::SaveBankPrelude => {
|
||||
let bank = self.editor_ctx.bank;
|
||||
let data = self.project_state.project.banks[bank].clone();
|
||||
Some(UndoScope::Bank { bank, data })
|
||||
}
|
||||
AppCommand::ResetBank { bank }
|
||||
| AppCommand::PasteBank { bank }
|
||||
| AppCommand::ImportBank { bank } => {
|
||||
|
||||
@@ -301,6 +301,10 @@ pub enum AppCommand {
|
||||
SavePrelude,
|
||||
EvaluatePrelude,
|
||||
ClosePreludeEditor,
|
||||
OpenBankPreludeEditor,
|
||||
SaveBankPrelude,
|
||||
EvaluateBankPrelude,
|
||||
CloseBankPreludeEditor,
|
||||
|
||||
// Onboarding
|
||||
DismissOnboarding,
|
||||
|
||||
@@ -454,12 +454,12 @@ pub fn build_stream(
|
||||
|
||||
while let Ok(cmd) = audio_rx.try_recv() {
|
||||
match cmd {
|
||||
AudioCommand::Evaluate { cmd, time } => {
|
||||
let cmd_ref = match time {
|
||||
AudioCommand::Evaluate { cmd, tick } => {
|
||||
let cmd_ref = match tick {
|
||||
Some(t) => {
|
||||
cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut cmd_buffer, "{cmd}/time/{t:.6}");
|
||||
let _ = write!(&mut cmd_buffer, "{cmd}/tick/{t}");
|
||||
cmd_buffer.as_str()
|
||||
}
|
||||
None => &cmd,
|
||||
|
||||
@@ -49,7 +49,7 @@ impl PatternChange {
|
||||
}
|
||||
|
||||
pub enum AudioCommand {
|
||||
Evaluate { cmd: String, time: Option<f64> },
|
||||
Evaluate { cmd: String, tick: Option<u64> },
|
||||
Hush,
|
||||
Panic,
|
||||
LoadSamples(Vec<doux::sampling::SampleEntry>),
|
||||
@@ -521,7 +521,8 @@ pub struct TickInput {
|
||||
pub fill: bool,
|
||||
pub nudge_secs: f64,
|
||||
pub current_time_us: SyncTime,
|
||||
pub engine_time: f64,
|
||||
pub audio_sample_pos: u64,
|
||||
pub sr: f64,
|
||||
pub mouse_x: f64,
|
||||
pub mouse_y: f64,
|
||||
pub mouse_down: f64,
|
||||
@@ -529,7 +530,7 @@ pub struct TickInput {
|
||||
|
||||
pub struct TimestampedCommand {
|
||||
pub cmd: String,
|
||||
pub time: Option<f64>,
|
||||
pub tick: Option<u64>,
|
||||
}
|
||||
|
||||
pub struct TickOutput {
|
||||
@@ -774,7 +775,8 @@ impl SequencerState {
|
||||
input.fill,
|
||||
input.nudge_secs,
|
||||
input.current_time_us,
|
||||
input.engine_time,
|
||||
input.audio_sample_pos,
|
||||
input.sr,
|
||||
input.mouse_x,
|
||||
input.mouse_y,
|
||||
input.mouse_down,
|
||||
@@ -788,7 +790,8 @@ impl SequencerState {
|
||||
input.quantum,
|
||||
input.fill,
|
||||
input.nudge_secs,
|
||||
input.engine_time,
|
||||
input.audio_sample_pos,
|
||||
input.sr,
|
||||
input.mouse_x,
|
||||
input.mouse_y,
|
||||
input.mouse_down,
|
||||
@@ -922,7 +925,8 @@ impl SequencerState {
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
_current_time_us: SyncTime,
|
||||
engine_time: f64,
|
||||
audio_sample_pos: u64,
|
||||
sr: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
mouse_down: f64,
|
||||
@@ -969,7 +973,7 @@ impl SequencerState {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let event_time = Some(engine_time + time_delta);
|
||||
let event_tick = Some(audio_sample_pos + (time_delta * sr).round() as u64);
|
||||
|
||||
if let Some(step) = pattern.steps.get(step_idx) {
|
||||
let resolved_script = pattern.resolve_script(step_idx);
|
||||
@@ -1037,7 +1041,7 @@ impl SequencerState {
|
||||
self.event_count += 1;
|
||||
self.buf_audio_commands.push(TimestampedCommand {
|
||||
cmd,
|
||||
time: event_time,
|
||||
tick: event_tick,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1080,7 +1084,8 @@ impl SequencerState {
|
||||
quantum: f64,
|
||||
fill: bool,
|
||||
nudge_secs: f64,
|
||||
engine_time: f64,
|
||||
audio_sample_pos: u64,
|
||||
sr: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
mouse_down: f64,
|
||||
@@ -1105,7 +1110,7 @@ impl SequencerState {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let event_time = Some(engine_time + time_delta);
|
||||
let event_tick = Some(audio_sample_pos + (time_delta * sr).round() as u64);
|
||||
|
||||
let step_in_cycle = self.script_step % self.script_length;
|
||||
|
||||
@@ -1149,7 +1154,7 @@ impl SequencerState {
|
||||
self.event_count += 1;
|
||||
self.buf_audio_commands.push(TimestampedCommand {
|
||||
cmd,
|
||||
time: event_time,
|
||||
tick: event_tick,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1305,11 +1310,6 @@ fn sequencer_loop(
|
||||
|
||||
let sr = sample_rate.load(Ordering::Relaxed) as f64;
|
||||
let audio_samples = audio_sample_pos.load(Ordering::Acquire);
|
||||
let engine_time = if sr > 0.0 {
|
||||
audio_samples as f64 / sr
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing: playing.load(Ordering::Relaxed),
|
||||
@@ -1320,7 +1320,8 @@ fn sequencer_loop(
|
||||
fill: live_keys.fill(),
|
||||
nudge_secs: nudge_us.load(Ordering::Relaxed) as f64 / 1_000_000.0,
|
||||
current_time_us,
|
||||
engine_time,
|
||||
audio_sample_pos: audio_samples,
|
||||
sr,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
@@ -1370,7 +1371,7 @@ fn sequencer_loop(
|
||||
} else {
|
||||
let _ = audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: tsc.cmd,
|
||||
time: tsc.time,
|
||||
tick: tsc.tick,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1542,7 +1543,8 @@ mod tests {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time: 0.0,
|
||||
audio_sample_pos: 0,
|
||||
sr: 48000.0,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
@@ -1560,7 +1562,8 @@ mod tests {
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time: 0.0,
|
||||
audio_sample_pos: 0,
|
||||
sr: 48000.0,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
|
||||
@@ -300,7 +300,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Char('t') if !ctx.app.plugin_mode => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
time: None,
|
||||
tick: None,
|
||||
});
|
||||
}
|
||||
KeyCode::Char('s') => super::open_save(ctx),
|
||||
|
||||
@@ -82,7 +82,8 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
|
||||
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
|
||||
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
|
||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenPreludeEditor),
|
||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenBankPreludeEditor),
|
||||
KeyCode::Char('P') => ctx.dispatch(AppCommand::OpenPreludeEditor),
|
||||
KeyCode::Delete | KeyCode::Backspace => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
if let Some(range) = ctx.app.editor_ctx.selection_range() {
|
||||
|
||||
@@ -351,6 +351,11 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
EditorTarget::BankPrelude => {
|
||||
ctx.dispatch(AppCommand::SaveBankPrelude);
|
||||
ctx.dispatch(AppCommand::EvaluateBankPrelude);
|
||||
ctx.dispatch(AppCommand::CloseBankPreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
@@ -364,6 +369,10 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
EditorTarget::BankPrelude => {
|
||||
ctx.dispatch(AppCommand::SaveBankPrelude);
|
||||
ctx.dispatch(AppCommand::EvaluateBankPrelude);
|
||||
}
|
||||
},
|
||||
KeyCode::Char('b') if ctrl => {
|
||||
editor.activate_sample_finder();
|
||||
@@ -787,7 +796,7 @@ fn execute_palette_entry(
|
||||
.load()
|
||||
.send(crate::engine::AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
time: None,
|
||||
tick: None,
|
||||
});
|
||||
}
|
||||
None => {}
|
||||
|
||||
@@ -941,6 +941,11 @@ fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
EditorTarget::BankPrelude => {
|
||||
ctx.dispatch(AppCommand::SaveBankPrelude);
|
||||
ctx.dispatch(AppCommand::EvaluateBankPrelude);
|
||||
ctx.dispatch(AppCommand::CloseBankPreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ pub(super) fn handle_sample_explorer(ctx: &mut InputContext, key: KeyEvent) -> I
|
||||
let _ = ctx
|
||||
.audio_tx
|
||||
.load()
|
||||
.send(AudioCommand::Evaluate { cmd, time: None });
|
||||
.send(AudioCommand::Evaluate { cmd, tick: None });
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"\u{25B8} {}/{}",
|
||||
folder, entry.label
|
||||
|
||||
@@ -70,7 +70,7 @@ pub const DOCS: &[DocEntry] = &[
|
||||
),
|
||||
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("Preludes", include_str!("../../docs/forth/prelude.md")),
|
||||
Topic(
|
||||
"Cagire vs Classic",
|
||||
include_str!("../../docs/forth/oddities.md"),
|
||||
|
||||
@@ -766,17 +766,26 @@ pub static COMMANDS: LazyLock<Vec<CommandEntry>> = LazyLock::new(|| {
|
||||
|
||||
// === Prelude ===
|
||||
CommandEntry {
|
||||
name: "Edit Prelude",
|
||||
description: "Open prelude editor",
|
||||
name: "Edit Bank Prelude",
|
||||
description: "Open current bank's prelude editor",
|
||||
category: "Prelude",
|
||||
keybinding: "p",
|
||||
pages: &[Main],
|
||||
normal_mode: false,
|
||||
action: Some(PaletteAction::Resolve(|_| Some(AppCommand::OpenBankPreludeEditor))),
|
||||
},
|
||||
CommandEntry {
|
||||
name: "Edit Project Prelude",
|
||||
description: "Open project-wide prelude editor",
|
||||
category: "Prelude",
|
||||
keybinding: "P",
|
||||
pages: &[Main],
|
||||
normal_mode: false,
|
||||
action: Some(PaletteAction::Resolve(|_| Some(AppCommand::OpenPreludeEditor))),
|
||||
},
|
||||
CommandEntry {
|
||||
name: "Evaluate Prelude",
|
||||
description: "Re-evaluate prelude script",
|
||||
name: "Evaluate All Preludes",
|
||||
description: "Re-evaluate project and all bank preludes",
|
||||
category: "Prelude",
|
||||
keybinding: "d",
|
||||
pages: &[Main],
|
||||
|
||||
@@ -8,6 +8,7 @@ pub enum EditorTarget {
|
||||
#[default]
|
||||
Step,
|
||||
Prelude,
|
||||
BankPrelude,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -76,7 +76,11 @@ fn render_top_layout(
|
||||
}
|
||||
if has_preview {
|
||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|
||||
|| !app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.prelude
|
||||
.trim()
|
||||
.is_empty();
|
||||
if has_prelude {
|
||||
let [script_area, prelude_area] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]);
|
||||
@@ -195,7 +199,11 @@ fn render_viz_area(
|
||||
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
|
||||
VizPanel::Preview => {
|
||||
let user_words = user_words_once.as_ref().expect("user_words initialized");
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|
||||
|| !app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.prelude
|
||||
.trim()
|
||||
.is_empty();
|
||||
if has_prelude {
|
||||
let [script_area, prelude_area] = if is_vertical_layout {
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)])
|
||||
@@ -655,11 +663,20 @@ pub(crate) fn render_prelude_preview(
|
||||
area: Rect,
|
||||
) {
|
||||
let theme = theme::get();
|
||||
let prelude = &app.project_state.project.prelude;
|
||||
let bank_prelude = &app.project_state.project.banks[app.editor_ctx.bank].prelude;
|
||||
let (prelude, title) = if !bank_prelude.trim().is_empty() {
|
||||
let bank_name = app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or("Bank");
|
||||
(bank_prelude.as_str(), format!(" {bank_name} Prelude "))
|
||||
} else {
|
||||
(app.project_state.project.prelude.as_str(), " Prelude ".to_string())
|
||||
};
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Prelude ")
|
||||
.title(title)
|
||||
.border_style(Style::new().fg(theme.ui.border));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
@@ -911,7 +911,13 @@ fn render_modal_editor(
|
||||
};
|
||||
|
||||
let title = match app.editor_ctx.target {
|
||||
EditorTarget::Prelude => "Prelude".to_string(),
|
||||
EditorTarget::Prelude => "Project Prelude".to_string(),
|
||||
EditorTarget::BankPrelude => {
|
||||
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
|
||||
let fallback = format!("Bank {}", app.editor_ctx.bank + 1);
|
||||
let bank_name = bank.name.as_deref().unwrap_or(&fallback);
|
||||
format!("{bank_name} Prelude")
|
||||
}
|
||||
EditorTarget::Step => {
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
|
||||
@@ -114,7 +114,11 @@ fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) {
|
||||
if app.audio.config.show_lissajous {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|
||||
|| !app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.prelude
|
||||
.trim()
|
||||
.is_empty();
|
||||
if has_prelude {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user