Feat: refactoring codebase

This commit is contained in:
2026-02-16 16:00:57 +01:00
parent c749ed6f85
commit b60703aa16
49 changed files with 1852 additions and 1853 deletions

100
docs/tutorials/at.md Normal file
View File

@@ -0,0 +1,100 @@
# Timing with at
Every step has a duration. By default, sounds emit at the very start of that duration. `at` changes *when* within the step sounds fire -- giving you sub-step rhythmic control without adding more steps.
## The Basics
`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
```
Push multiple values before calling `at` to get multiple emits from a single `.`:
```forth
0 0.5 at kick s . ;; two kicks: one at start, one at midpoint
0 0.25 0.5 0.75 at hat s . ;; four hats, evenly spaced
```
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)
clear
snare s . ;; 1 snare (deltas cleared)
```
## Cross-product: at Without arp
Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2 deltas, you get 6 emits -- every note at every delta:
```forth
0 0.5 at
c4 e4 g4 note sine s . ;; 6 emits: 3 notes x 2 deltas
```
This is a chord played twice per step.
## 1:1 Pairing: at With arp
`arp` changes the behavior. Instead of cross-product, deltas and arp values pair up 1:1. Each delta gets one note from the arpeggio:
```forth
0 0.33 0.66 at
c4 e4 g4 arp note sine s . ;; c4 at 0, e4 at 0.33, g4 at 0.66
```
If the lists differ in length, the shorter one wraps around:
```forth
0 0.25 0.5 0.75 at
c4 e4 arp note sine s . ;; c4, e4, c4, e4 at 4 time points
```
This is THE key distinction. Without `arp`: every note at every time. With `arp`: one note per time slot.
## Generating Deltas
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
```
Euclidean distribution via `euclid`:
```forth
3 8 euclid at hat s . ;; 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
```
Geometric spacing via `geom..`:
```forth
0.0 2.0 4 geom.. at hat s . ;; exponentially spaced
```
## Gating at
Wrap `at` expressions in quotations for conditional timing:
```forth
{ 0 0.25 0.5 0.75 at } 2 every ;; 16th-note hats every other bar
hat s .
{ 0 0.5 at } 0.5 chance ;; 50% chance of double-hit
kick s .
```
When the quotation doesn't execute, no deltas are set -- you get the default single emit at beat start.

View File

@@ -0,0 +1,138 @@
# Generators & Sequences
Sequences of values drive music: arpeggios, parameter sweeps, rhythmic patterns. Cagire has dedicated words for building sequences on the stack, transforming them, and collapsing them to single values.
## Ranges
`..` pushes an integer range onto the stack. Both endpoints are inclusive. If start exceeds end, it counts down:
```forth
1 5 .. ;; 1 2 3 4 5
5 1 .. ;; 5 4 3 2 1
0 7 .. ;; 0 1 2 3 4 5 6 7
```
`.,` adds a step parameter. Works with floats:
```forth
0 1 0.25 ., ;; 0 0.25 0.5 0.75 1
0 10 2 ., ;; 0 2 4 6 8 10
1 0 0.5 ., ;; 1 0.5 0 (descending)
```
`geom..` builds a geometric sequence. Takes start, ratio, and count:
```forth
1 2 4 geom.. ;; 1 2 4 8
100 0.5 4 geom.. ;; 100 50 25 12.5
```
Build a harmonic series:
```forth
110 2 5 geom.. 5 rev freq
```
That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `freq`.
## Computed Sequences
`gen` executes a quotation n times and collects all results. The quotation must push exactly one value per call:
```forth
{ 1 6 rand } 4 gen ;; 4 random values between 1 and 6
{ coin } 8 gen ;; 8 random 0s and 1s
```
Contrast with `times`, which executes for side effects and does not collect. `times` sets `@i` to the current index:
```forth
4 { @i } times ;; 0 1 2 3 (pushes @i each iteration)
4 { @i 60 + note sine s . } times ;; plays 4 notes, collects nothing
```
The distinction: `gen` is for building data. `times` is for doing things.
## Euclidean Patterns
`euclid` distributes k hits evenly across n positions and pushes the hit indices:
```forth
3 8 euclid ;; 0 3 5
4 8 euclid ;; 0 2 4 6
5 8 euclid ;; 0 1 3 5 6
```
`euclidrot` adds a rotation parameter that shifts the pattern:
```forth
3 8 0 euclidrot ;; 0 3 5 (no rotation)
3 8 1 euclidrot ;; 1 4 6
3 8 2 euclidrot ;; 1 4 6
```
These give you raw indices as data on the stack. This is different from `bjork` and `pbjork` (covered in the Randomness tutorial), which execute a quotation on matching steps. `euclid` gives you numbers to work with; `bjork` triggers actions.
## Transforming Sequences
Four words reshape values already on the stack. All take n (the count of items to operate on) from the top:
`rev` reverses order:
```forth
1 2 3 4 4 rev ;; 4 3 2 1
c4 e4 g4 3 rev ;; g4 e4 c4 (descending arpeggio)
```
`shuffle` randomizes order:
```forth
c4 e4 g4 b4 4 shuffle ;; random permutation each time
```
`sort` and `rsort` for ascending and descending:
```forth
3 1 4 1 5 5 sort ;; 1 1 3 4 5
3 1 4 1 5 5 rsort ;; 5 4 3 1 1
```
## Reducing Sequences
`sum` and `prod` collapse n values into one:
```forth
1 2 3 4 4 sum ;; 10
1 2 3 4 4 prod ;; 24
```
Useful for computing averages or accumulating values:
```forth
{ 1 6 rand } 4 gen 4 sum ;; sum of 4 dice rolls
```
## Replication
`dupn` (alias `!`) duplicates a value n times:
```forth
440 3 dupn ;; 440 440 440
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 .
```
Or replicate a value for batch processing:
```forth
0.5 4 dupn 4 sum ;; 2.0
```
The generator words produce raw material. The transform words shape it. Together they let you express complex musical ideas in a few words.

302
docs/tutorials/harmony.md Normal file
View File

@@ -0,0 +1,302 @@
# Notes & Harmony
Cagire speaks music theory. Notes, intervals, chords, and scales are all first-class words that compile to stack operations on MIDI values. This tutorial covers every pitch-related feature.
## MIDI Notes
Write a note name followed by an octave number. It compiles to a MIDI integer:
```forth
c4 ;; 60 (middle C)
a4 ;; 69 (concert A)
e3 ;; 52
```
Sharps use `s` or `#`. Flats use `b`:
```forth
fs4 ;; 66 (F sharp 4)
f#4 ;; 66 (same thing)
bb3 ;; 58 (B flat 3)
eb4 ;; 63
```
Octave range is -1 to 9. The formula is `(octave + 1) * 12 + base + modifier`, where C=0, D=2, E=4, F=5, G=7, A=9, B=11.
Note literals push a single integer onto the stack, just like writing `60` directly. They work everywhere an integer works:
```forth
c4 note sine s . ;; play middle C as a sine
a4 note 0.5 gain modal s . ;; concert A, quieter
```
## Intervals
An interval duplicates the top of the stack and adds semitones. This lets you build chords by stacking:
```forth
c4 M3 P5 ;; stack: 60 64 67 (C major triad)
c4 m3 P5 ;; stack: 60 63 67 (C minor triad)
a3 P5 ;; stack: 57 64 (A plus a fifth)
```
Simple intervals (within one octave):
| Interval | Semitones | Name |
|----------|-----------|------|
| `P1` / `unison` | 0 | Perfect unison |
| `m2` | 1 | Minor 2nd |
| `M2` | 2 | Major 2nd |
| `m3` | 3 | Minor 3rd |
| `M3` | 4 | Major 3rd |
| `P4` | 5 | Perfect 4th |
| `aug4` / `dim5` / `tritone` | 6 | Tritone |
| `P5` | 7 | Perfect 5th |
| `m6` | 8 | Minor 6th |
| `M6` | 9 | Major 6th |
| `m7` | 10 | Minor 7th |
| `M7` | 11 | Major 7th |
| `P8` | 12 | Octave |
Compound intervals (beyond one octave):
| Interval | Semitones |
|----------|-----------|
| `m9` | 13 |
| `M9` | 14 |
| `m10` | 15 |
| `M10` | 16 |
| `P11` | 17 |
| `aug11` | 18 |
| `P12` | 19 |
| `m13` | 20 |
| `M13` | 21 |
| `m14` | 22 |
| `M14` | 23 |
| `P15` | 24 |
## Chords
Chord words take a root note and push all the chord tones. They eat the root and replace it with the full voicing:
```forth
c4 maj ;; stack: 60 64 67
c4 min7 ;; stack: 60 63 67 70
c4 dom9 ;; stack: 60 64 67 70 74
```
**Triads:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `maj` | 0 4 7 | 60 64 67 |
| `m` | 0 3 7 | 60 63 67 |
| `dim` | 0 3 6 | 60 63 66 |
| `aug` | 0 4 8 | 60 64 68 |
| `sus2` | 0 2 7 | 60 62 67 |
| `sus4` | 0 5 7 | 60 65 67 |
**Seventh chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `maj7` | 0 4 7 11 | 60 64 67 71 |
| `min7` | 0 3 7 10 | 60 63 67 70 |
| `dom7` | 0 4 7 10 | 60 64 67 70 |
| `dim7` | 0 3 6 9 | 60 63 66 69 |
| `m7b5` | 0 3 6 10 | 60 63 66 70 |
| `minmaj7` | 0 3 7 11 | 60 63 67 71 |
| `aug7` | 0 4 8 10 | 60 64 68 70 |
**Sixth chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `maj6` | 0 4 7 9 | 60 64 67 69 |
| `min6` | 0 3 7 9 | 60 63 67 69 |
**Extended chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `dom9` | 0 4 7 10 14 | 60 64 67 70 74 |
| `maj9` | 0 4 7 11 14 | 60 64 67 71 74 |
| `min9` | 0 3 7 10 14 | 60 63 67 70 74 |
| `dom11` | 0 4 7 10 14 17 | 60 64 67 70 74 77 |
| `min11` | 0 3 7 10 14 17 | 60 63 67 70 74 77 |
| `dom13` | 0 4 7 10 14 21 | 60 64 67 70 74 81 |
**Add chords:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `add9` | 0 4 7 14 | 60 64 67 74 |
| `add11` | 0 4 7 17 | 60 64 67 77 |
| `madd9` | 0 3 7 14 | 60 63 67 74 |
**Altered dominants:**
| Word | Intervals | Example (C4) |
|------|-----------|-------------|
| `dom7b9` | 0 4 7 10 13 | 60 64 67 70 73 |
| `dom7s9` | 0 4 7 10 15 | 60 64 67 70 75 |
| `dom7b5` | 0 4 6 10 | 60 64 66 70 |
| `dom7s5` | 0 4 8 10 | 60 64 68 70 |
Chord tones are varargs -- they eat the entire stack. So a chord word should come right after the root note:
```forth
c4 maj note sine s . ;; plays all 3 notes as one chord
```
## Scales
Scale words convert a degree index into a MIDI note. The base note is C4 (MIDI 60). Degrees wrap around with octave transposition:
```forth
0 major ;; 60 (C4 -- degree 0)
4 major ;; 67 (G4 -- degree 4)
7 major ;; 72 (C5 -- degree 7, wraps to next octave)
-1 major ;; 59 (B3 -- negative degrees go down)
```
Use scales with `cycle` or `rand` to walk through pitches:
```forth
0 1 2 3 4 5 6 7 8 cycle minor note sine s .
```
**Standard modes:**
| Word | Pattern (semitones) |
|------|-------------------|
| `major` | 0 2 4 5 7 9 11 |
| `minor` | 0 2 3 5 7 8 10 |
| `dorian` | 0 2 3 5 7 9 10 |
| `phrygian` | 0 1 3 5 7 8 10 |
| `lydian` | 0 2 4 6 7 9 11 |
| `mixolydian` | 0 2 4 5 7 9 10 |
| `aeolian` | 0 2 3 5 7 8 10 |
| `locrian` | 0 1 3 5 6 8 10 |
**Pentatonic and blues:**
| Word | Pattern |
|------|---------|
| `pentatonic` | 0 2 4 7 9 |
| `minpent` | 0 3 5 7 10 |
| `blues` | 0 3 5 6 7 10 |
**Chromatic and whole tone:**
| Word | Pattern |
|------|---------|
| `chromatic` | 0 1 2 3 4 5 6 7 8 9 10 11 |
| `wholetone` | 0 2 4 6 8 10 |
**Harmonic and melodic minor:**
| Word | Pattern |
|------|---------|
| `harmonicminor` | 0 2 3 5 7 8 11 |
| `melodicminor` | 0 2 3 5 7 9 11 |
**Jazz / Bebop:**
| Word | Pattern |
|------|---------|
| `bebop` | 0 2 4 5 7 9 10 11 |
| `bebopmaj` | 0 2 4 5 7 8 9 11 |
| `bebopmin` | 0 2 3 5 7 8 9 10 |
| `altered` | 0 1 3 4 6 8 10 |
| `lyddom` | 0 2 4 6 7 9 10 |
**Symmetric:**
| Word | Pattern |
|------|---------|
| `halfwhole` | 0 1 3 4 6 7 9 10 |
| `wholehalf` | 0 2 3 5 6 8 9 11 |
| `augmented` | 0 3 4 7 8 11 |
| `tritone` | 0 1 4 6 7 10 |
| `prometheus` | 0 2 4 6 9 10 |
**Modal variants (from melodic minor):**
| Word | Pattern |
|------|---------|
| `dorianb2` | 0 1 3 5 7 9 10 |
| `lydianaug` | 0 2 4 6 8 9 11 |
| `mixb6` | 0 2 4 5 7 8 10 |
| `locrian2` | 0 2 3 5 6 8 10 |
## Octave Shifting
`oct` transposes a note by octaves:
```forth
c4 1 oct ;; 72 (C5)
c4 -1 oct ;; 48 (C3)
c4 2 oct ;; 84 (C6)
```
Stack effect: `(note shift -- transposed)`. The shift is multiplied by 12 and added to the note.
## Frequency Conversion
`mtof` converts a MIDI note to frequency in Hz. `ftom` does the reverse:
```forth
69 mtof ;; 440.0 (A4)
60 mtof ;; 261.63 (C4)
440 ftom ;; 69.0
```
Useful when a synth parameter expects Hz rather than MIDI:
```forth
c4 mtof freq sine s .
```
## Putting It Together
A chord progression cycling every pattern iteration:
```forth
{ c3 maj7 } { f3 maj7 } { g3 dom7 } { c3 maj7 } 4 pcycle
note sine s .
```
Arpeggiate a chord across the step's time divisions:
```forth
c4 min7 arp note 0.5 decay sine s .
```
Random notes from a scale:
```forth
0 7 rand minor note sine s .
```
A bass line walking scale degrees:
```forth
0 2 4 5 7 5 4 2 8 cycle minor note
-2 oct 0.8 gain sine s .
```
Chord voicings with random inversion:
```forth
e3 min9
{ } { 1 oct } 2 choose
note modal s .
```
Stacked intervals for custom voicings:
```forth
c3 P5 P8 M10 ;; C3, G3, C4, E4
note sine s .
```

View File

@@ -0,0 +1,201 @@
# Randomness
Music needs surprise. A pattern that plays identically every time gets boring fast. Cagire has a rich set of words for injecting randomness and controlled variation into your sequences.
## Random Numbers
`coin` pushes 0 or 1 with equal probability:
```forth
coin note sine s . ;; either 0 or 1 as the note
```
`rand` takes a range and returns a random value. If both bounds are integers, the result is an integer. If either is a float, you get a float:
```forth
60 72 rand note sine s . ;; random MIDI note from 60 to 72
0.3 0.9 rand gain sine s . ;; random gain between 0.3 and 0.9
```
`exprand` and `logrand` give you weighted distributions. `exprand` is biased toward the low end, `logrand` toward the high end:
```forth
200.0 8000.0 exprand freq sine s . ;; mostly low frequencies
200.0 8000.0 logrand freq sine s . ;; mostly high frequencies
```
These are useful for parameters where perception is logarithmic, like frequency and duration.
## Conditional Execution
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
{ hat s . } 75 prob ;; 75% chance
```
Named probability words save you from remembering numbers:
| Word | Probability |
|------|------------|
| `always` | 100% |
| `almostAlways` | 90% |
| `often` | 75% |
| `sometimes` | 50% |
| `rarely` | 25% |
| `almostNever` | 10% |
| `never` | 0% |
```forth
{ hat s . } often ;; 75%
{ snare s . } sometimes ;; 50%
{ clap s . } 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.
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
```
## Selection
`choose` picks randomly from n items on the stack:
```forth
kick snare hat 3 choose s . ;; random drum hit
60 64 67 72 4 choose note sine s . ;; random note from a set
```
When a chosen item is a quotation, it gets executed:
```forth
{ 0.1 decay } { 0.5 decay } { 0.9 decay } 3 choose
sine s .
```
`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 plays 50% of the time, snare 30%, hat 20%. Weights don't need to sum to 1 -- they're normalized automatically.
`shuffle` randomizes the order of n items on the stack:
```forth
60 64 67 72 4 shuffle ;; stack now has the same 4 values in random order
```
Combined with `note`, this gives you a random permutation of a chord every time the step runs.
## Cycling
Cycling steps through values deterministically. No randomness -- pure rotation.
`cycle` selects based on how many times this step has played (its `runs` count):
```forth
60 64 67 3 cycle note sine s . ;; 60, 64, 67, 60, 64, 67, ...
```
`pcycle` selects based on the pattern iteration count (`iter`):
```forth
kick snare 2 pcycle s . ;; kick on even iterations, snare on odd
```
The difference matters when patterns have different lengths. `cycle` counts per-step, `pcycle` counts per-pattern.
Quotations work here too:
```forth
{ c4 note } { e4 note } { g4 note } 3 cycle
sine s .
```
`bounce` ping-pongs instead of wrapping around:
```forth
60 64 67 72 4 bounce note sine s . ;; 60, 64, 67, 72, 67, 64, 60, 64, ...
```
## Periodic Execution
`every` runs a quotation once every n pattern iterations:
```forth
{ crash s . } 4 every ;; crash cymbal every 4th iteration
```
`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)
```
`bjork` counts by step runs (how many times this particular step has played). `pbjork` counts by pattern iterations. Some classic patterns:
| k | n | Name |
|---|---|------|
| 3 | 8 | tresillo |
| 5 | 8 | cinquillo |
| 5 | 16 | bossa nova |
| 7 | 16 | samba |
## Seeding
By default, every run produces different random values. Use `seed` to make randomness reproducible:
```forth
42 seed
60 72 rand note sine s . ;; 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.
## Combining Words
The real power comes from mixing techniques. A hi-hat pattern with ghost notes:
```forth
hat s
{ 0.3 0.6 rand gain } { 0.8 gain } 2 cycle
.
```
Full volume on even runs, random quiet on odd runs.
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 .
```
Layered percussion with different densities:
```forth
{ kick s . } always
{ snare s . } 2 every
{ hat s . } 5 8 bjork
{ rim s . } rarely
```
A melodic step with weighted note selection and random timbre:
```forth
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 .
```
The root note plays most often. Higher chord tones are rarer. Decay and harmonics vary continuously.

View File

@@ -0,0 +1,71 @@
# Using Variables
Variables let you name values and share data between steps. They are global -- any step can read what another step wrote.
## Store and Fetch
`!name` stores the top of the stack into a variable. `@name` fetches it back. Variables spring into existence when you first store to them. Fetching a variable that was never stored returns 0.
```forth
10 !x ;; store 10 in x
@x ;; pushes 10
@y ;; pushes 0 (never stored)
```
## Store and Keep
`,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
```
Without `,`, you'd need `dup`:
```forth
440 dup !freq sine s . ;; equivalent, but noisier
```
## Sharing Between Steps
Variables are shared across all steps. One step can store a value that another reads:
```forth
;; step 0: pick a root note
c4 iter 7 mod + !root
;; step 4: read it
@root 7 + note sine s .
```
Every time the pattern loops, step 0 picks a new root. Step 4 always harmonizes with it.
## Accumulators
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
```
Reset on some condition:
```forth
@n 1 + !n
{ 0 !n } @n 16 > ? ;; reset after 16
```
## Naming Sounds
Store a sound name in a variable, reuse it across steps:
```forth
;; step 0: choose the sound
"sine" !synth
;; step 1, 2, 3...
c4 note @synth s .
```
Change one step, all steps follow.