Feat: lots of things, preparing for live gig
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
This commit is contained in:
@@ -49,7 +49,7 @@ cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { git = "https://github.com/Bubobubobubobubo/doux.git", features = ["native"] }
|
||||
doux = { path = "/Users/bubo/doux", features = ["native"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
|
||||
@@ -80,7 +80,9 @@ pub enum Op {
|
||||
Mtof,
|
||||
Ftom,
|
||||
SetTempo,
|
||||
Every,
|
||||
Every(Option<SourceSpan>),
|
||||
Bjork(Option<SourceSpan>),
|
||||
PBjork(Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
When,
|
||||
Unless,
|
||||
|
||||
@@ -838,13 +838,36 @@ impl Forth {
|
||||
stack.push(Value::Int(if result { 1 } else { 0 }, None));
|
||||
}
|
||||
|
||||
Op::Every => {
|
||||
Op::Every(word_span) => {
|
||||
let n = pop_int(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
if n <= 0 {
|
||||
return Err("every count must be > 0".into());
|
||||
}
|
||||
let result = ctx.iter as i64 % n == 0;
|
||||
stack.push(Value::Int(if result { 1 } else { 0 }, None));
|
||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
||||
if result {
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::Bjork(word_span) | Op::PBjork(word_span) => {
|
||||
let n = pop_int(stack)?;
|
||||
let k = pop_int(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
if n <= 0 || k < 0 {
|
||||
return Err("bjork: n must be > 0, k must be >= 0".into());
|
||||
}
|
||||
let counter = match &ops[pc] {
|
||||
Op::Bjork(_) => ctx.runs,
|
||||
_ => ctx.iter,
|
||||
};
|
||||
let pos = counter % n as usize;
|
||||
let hit = k >= n || euclidean_hit(k as usize, n as usize, pos);
|
||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(hit));
|
||||
if hit {
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::Quotation(quote_ops, body_span) => {
|
||||
@@ -1424,6 +1447,13 @@ fn emit_output(
|
||||
outputs.push(out);
|
||||
}
|
||||
|
||||
fn euclidean_hit(k: usize, n: usize, pos: usize) -> bool {
|
||||
if k == 0 {
|
||||
return false;
|
||||
}
|
||||
((pos + 1) * k) / n != (pos * k) / n
|
||||
}
|
||||
|
||||
fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
|
||||
if k == 0 || n == 0 {
|
||||
return Vec::new();
|
||||
|
||||
@@ -68,7 +68,9 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"choose" => Op::Choose(None),
|
||||
"bounce" => Op::Bounce(None),
|
||||
"wchoose" => Op::WChoose(None),
|
||||
"every" => Op::Every,
|
||||
"every" => Op::Every(None),
|
||||
"bjork" => Op::Bjork(None),
|
||||
"pbjork" => Op::PBjork(None),
|
||||
"chance" => Op::ChanceExec(None),
|
||||
"prob" => Op::ProbExec(None),
|
||||
"coin" => Op::Coin(None),
|
||||
@@ -192,7 +194,9 @@ fn attach_span(op: &mut Op, span: SourceSpan) {
|
||||
match op {
|
||||
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
|
||||
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
|
||||
| Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s) => *s = Some(span),
|
||||
| Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
|
||||
| Op::Every(s)
|
||||
| Op::Bjork(s) | Op::PBjork(s) => *s = Some(span),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,6 +839,36 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "smear",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set smear allpass chain wet/dry mix (0=bypass, 1=full wet)",
|
||||
example: "0.5 smear",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "smearfreq",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set smear allpass break frequency in Hz",
|
||||
example: "800 smearfreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "smearfb",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set smear feedback for resonance (0-0.95)",
|
||||
example: "0.8 smearfb",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "chorus",
|
||||
aliases: &[],
|
||||
|
||||
@@ -198,9 +198,29 @@ pub(super) const WORDS: &[Word] = &[
|
||||
name: "every",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(n -- bool)",
|
||||
desc: "True every nth iteration",
|
||||
example: "4 every",
|
||||
stack: "(quot n --)",
|
||||
desc: "Execute quotation every nth iteration",
|
||||
example: "{ 2 distort } 4 every",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "bjork",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot k n --)",
|
||||
desc: "Execute quotation using Euclidean distribution over step runs",
|
||||
example: "{ 2 distort } 3 8 bjork",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "pbjork",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot k n --)",
|
||||
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
||||
example: "{ 2 distort } 3 8 pbjork",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ mod project;
|
||||
|
||||
pub const MAX_BANKS: usize = 32;
|
||||
pub const MAX_PATTERNS: usize = 32;
|
||||
pub const MAX_STEPS: usize = 128;
|
||||
pub const MAX_STEPS: usize = 1024;
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, save, FileError};
|
||||
|
||||
@@ -220,6 +220,10 @@ impl Step {
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
||||
}
|
||||
|
||||
pub fn has_content(&self) -> bool {
|
||||
!self.script.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Step {
|
||||
|
||||
@@ -9,7 +9,7 @@ thread_local! {
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
|
||||
@@ -63,6 +63,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(200, 235, 235),
|
||||
active_fg: teal,
|
||||
content_bg: Color::Rgb(185, 225, 225),
|
||||
inactive_bg: surface0,
|
||||
inactive_fg: subtext0,
|
||||
active_selected_bg: Color::Rgb(215, 210, 240),
|
||||
|
||||
@@ -63,6 +63,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(40, 55, 55),
|
||||
active_fg: teal,
|
||||
content_bg: Color::Rgb(47, 62, 62),
|
||||
inactive_bg: surface0,
|
||||
inactive_fg: subtext0,
|
||||
active_selected_bg: Color::Rgb(70, 60, 80),
|
||||
|
||||
@@ -57,6 +57,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(50, 70, 70),
|
||||
active_fg: cyan,
|
||||
content_bg: Color::Rgb(57, 77, 77),
|
||||
inactive_bg: current_line,
|
||||
inactive_fg: comment,
|
||||
active_selected_bg: Color::Rgb(80, 70, 95),
|
||||
|
||||
@@ -60,6 +60,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(14, 28, 26),
|
||||
active_fg: cyan,
|
||||
content_bg: Color::Rgb(21, 35, 33),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(22, 36, 28),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(20, 30, 30),
|
||||
active_fg: cyan,
|
||||
content_bg: Color::Rgb(27, 37, 37),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(40, 30, 35),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(70, 100, 100),
|
||||
active_fg: mint,
|
||||
content_bg: Color::Rgb(77, 107, 107),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(120, 90, 130),
|
||||
|
||||
@@ -62,6 +62,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(0, 16, 32),
|
||||
active_fg: lightblue,
|
||||
content_bg: Color::Rgb(7, 23, 39),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(10, 20, 36),
|
||||
|
||||
@@ -59,6 +59,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(50, 65, 55),
|
||||
active_fg: aqua,
|
||||
content_bg: Color::Rgb(57, 72, 62),
|
||||
inactive_bg: bg1,
|
||||
inactive_fg: fg3,
|
||||
active_selected_bg: Color::Rgb(85, 70, 60),
|
||||
|
||||
@@ -54,6 +54,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: black,
|
||||
active_bg: Color::Rgb(200, 50, 50),
|
||||
active_fg: yellow,
|
||||
content_bg: Color::Rgb(210, 60, 60),
|
||||
inactive_bg: dark_red,
|
||||
inactive_fg: gold,
|
||||
active_selected_bg: Color::Rgb(200, 200, 0),
|
||||
|
||||
@@ -59,6 +59,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: fg_dim,
|
||||
active_bg: Color::Rgb(45, 55, 70),
|
||||
active_fg: crystal_blue,
|
||||
content_bg: Color::Rgb(52, 62, 77),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(65, 55, 70),
|
||||
|
||||
@@ -59,6 +59,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: Color::Rgb(180, 140, 20),
|
||||
active_bg: Color::Rgb(210, 235, 240),
|
||||
active_fg: function,
|
||||
content_bg: Color::Rgb(195, 225, 230),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: text_dim,
|
||||
active_selected_bg: Color::Rgb(210, 215, 245),
|
||||
|
||||
@@ -139,6 +139,7 @@ pub struct TileColors {
|
||||
pub playing_inactive_fg: Color,
|
||||
pub active_bg: Color,
|
||||
pub active_fg: Color,
|
||||
pub content_bg: Color,
|
||||
pub inactive_bg: Color,
|
||||
pub inactive_fg: Color,
|
||||
pub active_selected_bg: Color,
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: medium,
|
||||
active_bg: Color::Rgb(45, 45, 45),
|
||||
active_fg: bright,
|
||||
content_bg: Color::Rgb(55, 55, 55),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(80, 80, 80),
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: medium,
|
||||
active_bg: Color::Rgb(210, 210, 210),
|
||||
active_fg: dark,
|
||||
content_bg: Color::Rgb(195, 195, 195),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(170, 170, 170),
|
||||
|
||||
@@ -57,6 +57,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(55, 75, 70),
|
||||
active_fg: blue,
|
||||
content_bg: Color::Rgb(62, 82, 77),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(85, 65, 80),
|
||||
|
||||
@@ -57,6 +57,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: aurora_yellow,
|
||||
active_bg: Color::Rgb(50, 65, 65),
|
||||
active_fg: frost0,
|
||||
content_bg: Color::Rgb(57, 72, 72),
|
||||
inactive_bg: polar_night1,
|
||||
inactive_fg: snow_storm0,
|
||||
active_selected_bg: Color::Rgb(75, 75, 95),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(15, 40, 40),
|
||||
active_fg: cyan,
|
||||
content_bg: Color::Rgb(22, 47, 47),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(45, 40, 55),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: subtle,
|
||||
active_bg: Color::Rgb(35, 50, 60),
|
||||
active_fg: foam,
|
||||
content_bg: Color::Rgb(42, 57, 67),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(60, 50, 70),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub fn theme() -> ThemeColors {
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(45, 60, 75),
|
||||
active_fg: blue,
|
||||
content_bg: Color::Rgb(52, 67, 82),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(70, 55, 85),
|
||||
|
||||
@@ -123,6 +123,7 @@ pub fn rotate_theme(theme: ThemeColors, degrees: f32) -> ThemeColors {
|
||||
playing_inactive_fg: rotate_color(theme.tile.playing_inactive_fg, degrees),
|
||||
active_bg: rotate_color(theme.tile.active_bg, degrees),
|
||||
active_fg: rotate_color(theme.tile.active_fg, degrees),
|
||||
content_bg: rotate_color(theme.tile.content_bg, degrees),
|
||||
inactive_bg: rotate_color(theme.tile.inactive_bg, degrees),
|
||||
inactive_fg: rotate_color(theme.tile.inactive_fg, degrees),
|
||||
active_selected_bg: rotate_color(theme.tile.active_selected_bg, degrees),
|
||||
|
||||
@@ -12,10 +12,10 @@ Cagire organizes all your patterns and data following a strict hierarchy:
|
||||
Project
|
||||
└── 32 Banks
|
||||
└── 32 Patterns (per bank)
|
||||
└── 128 Steps (per pattern)
|
||||
└── 1024 Steps (per pattern)
|
||||
```
|
||||
|
||||
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~131.000 steps.
|
||||
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps.
|
||||
|
||||
## Patterns
|
||||
|
||||
@@ -23,7 +23,7 @@ Each pattern is an independent sequence of steps with its own properties:
|
||||
|
||||
| Property | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| Length | Steps before the pattern loops (`1`-`128`) | `16` |
|
||||
| Length | Steps before the pattern loops (`1`-`1024`) | `16` |
|
||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||
| Quantization | When the pattern launches | `Bar` |
|
||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||
|
||||
@@ -8,9 +8,9 @@ Cagire can run multiple patterns concurrently. Each pattern contains a given num
|
||||
|
||||
- **32 Banks**
|
||||
- **32 Patterns** per bank
|
||||
- **128 Steps** per pattern
|
||||
- **1024 Steps** per pattern
|
||||
|
||||
That's over 130,000 possible steps per project. Most of my sessions use 15-20 at best.
|
||||
That's over 1,000,000 possible steps per project. Most of my sessions use 15-20 at best.
|
||||
|
||||
## What does a script look like?
|
||||
|
||||
|
||||
@@ -36,4 +36,5 @@ These work on most views:
|
||||
- `Arrow keys` - move or scroll
|
||||
- `Tab` - switch focus between panels
|
||||
- `/` or `Ctrl+f` - search (where available)
|
||||
- `:` - jump to step number (sequencer view)
|
||||
- `q` - quit (with confirmation)
|
||||
|
||||
@@ -175,6 +175,20 @@ Classic Forth is deterministic. Cagire has built-in randomness:
|
||||
|
||||
These words take a quotation and execute it probabilistically.
|
||||
|
||||
## Periodic Execution
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
`every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped.
|
||||
|
||||
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits as evenly as possible across n positions. `bjork` counts by step runs, `pbjork` counts by pattern iterations. Classic Euclidean rhythms: tresillo (3,8), cinquillo (5,8), son clave (5,16).
|
||||
|
||||
## Cycling
|
||||
|
||||
Cagire has built-in support for cycling through values. Push values onto the stack, then select one based on pattern state:
|
||||
|
||||
181
docs/tutorial_generators.md
Normal file
181
docs/tutorial_generators.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
Musical use -- build a harmonic series:
|
||||
|
||||
```forth
|
||||
110 2 5 geom.. 5 rev note
|
||||
```
|
||||
|
||||
That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `note` or `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.
|
||||
|
||||
Use euclid indices to pick notes from a scale:
|
||||
|
||||
```forth
|
||||
: pick ( ..vals n i -- val ) rot drop swap ;
|
||||
c4 d4 e4 g4 a4 ;; pentatonic scale on the stack
|
||||
3 8 euclid ;; get 3 hit positions
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Combining Techniques
|
||||
|
||||
An arpeggio that shuffles every time the step plays:
|
||||
|
||||
```forth
|
||||
c4 e4 g4 b4 4 shuffle
|
||||
drop drop drop ;; keep only the first note
|
||||
note sine s .
|
||||
```
|
||||
|
||||
Parameter spread across voices -- four sines with geometrically spaced frequencies:
|
||||
|
||||
```forth
|
||||
220 1.5 4 geom..
|
||||
4 { @i 1 + pick note sine s . } times
|
||||
```
|
||||
|
||||
Euclidean rhythm driving note selection from a generated sequence:
|
||||
|
||||
```forth
|
||||
3 8 euclid ;; 3 hit indices
|
||||
```
|
||||
|
||||
A chord built from a range, then sorted high to low:
|
||||
|
||||
```forth
|
||||
60 67 .. 8 rsort
|
||||
```
|
||||
|
||||
Rhythmic density control -- generate hits, keep only the loud ones:
|
||||
|
||||
```forth
|
||||
{ 0.0 1.0 rand } 8 gen
|
||||
```
|
||||
|
||||
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/tutorial_harmony.md
Normal file
302
docs/tutorial_harmony.md
Normal 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 .
|
||||
```
|
||||
201
docs/tutorial_randomness.md
Normal file
201
docs/tutorial_randomness.md
Normal 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.
|
||||
168
src/app.rs
168
src/app.rs
@@ -20,6 +20,7 @@ use crate::page::Page;
|
||||
use crate::services::{clipboard, dict_nav, euclidean, help_nav, pattern_editor};
|
||||
use crate::settings::Settings;
|
||||
use crate::state::{
|
||||
undo::{UndoEntry, UndoHistory, UndoScope},
|
||||
AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics,
|
||||
Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, SampleTree, StagedChange, StagedPropChange, UiState,
|
||||
@@ -61,6 +62,8 @@ pub struct App {
|
||||
pub copied_patterns: Option<Vec<Pattern>>,
|
||||
pub copied_banks: Option<Vec<Bank>>,
|
||||
|
||||
pub undo: UndoHistory,
|
||||
|
||||
pub audio: AudioSettings,
|
||||
pub options: OptionsState,
|
||||
pub panel: PanelState,
|
||||
@@ -103,6 +106,8 @@ impl App {
|
||||
copied_patterns: None,
|
||||
copied_banks: None,
|
||||
|
||||
undo: UndoHistory::default(),
|
||||
|
||||
audio: AudioSettings::default(),
|
||||
options: OptionsState::default(),
|
||||
panel: PanelState::default(),
|
||||
@@ -124,6 +129,7 @@ impl App {
|
||||
runtime_highlight: self.ui.runtime_highlight,
|
||||
show_scope: self.audio.config.show_scope,
|
||||
show_spectrum: self.audio.config.show_spectrum,
|
||||
show_preview: self.audio.config.show_preview,
|
||||
show_completion: self.ui.show_completion,
|
||||
color_scheme: self.ui.color_scheme,
|
||||
layout: self.audio.config.layout,
|
||||
@@ -617,6 +623,7 @@ impl App {
|
||||
link.set_tempo(tempo);
|
||||
|
||||
self.playback.clear_queues();
|
||||
self.undo.clear();
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
|
||||
@@ -742,6 +749,46 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift_patterns_up(&mut self) {
|
||||
let bank = self.patterns_nav.bank_cursor;
|
||||
let patterns = self.patterns_nav.selected_patterns();
|
||||
let start = *patterns.first().unwrap();
|
||||
let end = *patterns.last().unwrap();
|
||||
if let Some(dirty) = clipboard::shift_patterns_up(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
start..=end,
|
||||
) {
|
||||
for (b, p) in &dirty {
|
||||
self.project_state.mark_dirty(*b, *p);
|
||||
}
|
||||
self.patterns_nav.pattern_cursor -= 1;
|
||||
if let Some(ref mut anchor) = self.patterns_nav.pattern_anchor {
|
||||
*anchor -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift_patterns_down(&mut self) {
|
||||
let bank = self.patterns_nav.bank_cursor;
|
||||
let patterns = self.patterns_nav.selected_patterns();
|
||||
let start = *patterns.first().unwrap();
|
||||
let end = *patterns.last().unwrap();
|
||||
if let Some(dirty) = clipboard::shift_patterns_down(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
start..=end,
|
||||
) {
|
||||
for (b, p) in &dirty {
|
||||
self.project_state.mark_dirty(*b, *p);
|
||||
}
|
||||
self.patterns_nav.pattern_cursor += 1;
|
||||
if let Some(ref mut anchor) = self.patterns_nav.pattern_anchor {
|
||||
*anchor += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_bank(&mut self, bank: usize) {
|
||||
self.copied_banks = Some(vec![clipboard::copy_bank(
|
||||
&self.project_state.project,
|
||||
@@ -986,8 +1033,120 @@ impl App {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
fn undoable_scope(&self, cmd: &AppCommand) -> Option<UndoScope> {
|
||||
match cmd {
|
||||
// Pattern-level
|
||||
AppCommand::ToggleSteps
|
||||
| AppCommand::LengthIncrease
|
||||
| AppCommand::LengthDecrease
|
||||
| AppCommand::SpeedIncrease
|
||||
| AppCommand::SpeedDecrease
|
||||
| AppCommand::SaveEditorToStep
|
||||
| AppCommand::CompileCurrentStep
|
||||
| AppCommand::HardenSteps
|
||||
| AppCommand::PasteSteps
|
||||
| AppCommand::LinkPasteSteps
|
||||
| AppCommand::DuplicateSteps
|
||||
| AppCommand::CopySteps => {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let data = self.project_state.project.pattern_at(bank, pattern).clone();
|
||||
Some(UndoScope::Pattern { bank, pattern, data })
|
||||
}
|
||||
AppCommand::SetLength { bank, pattern, .. }
|
||||
| AppCommand::SetSpeed { bank, pattern, .. }
|
||||
| AppCommand::DeleteStep { bank, pattern, .. }
|
||||
| AppCommand::DeleteSteps { bank, pattern, .. }
|
||||
| AppCommand::ResetPattern { bank, pattern }
|
||||
| AppCommand::PastePattern { bank, pattern }
|
||||
| AppCommand::RenamePattern { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::RenameStep { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::ApplyEuclideanDistribution { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
// Bank-level
|
||||
AppCommand::ResetBank { bank } | AppCommand::PasteBank { bank } => {
|
||||
let data = self.project_state.project.banks[*bank].clone();
|
||||
Some(UndoScope::Bank { bank: *bank, data })
|
||||
}
|
||||
AppCommand::ShiftPatternsUp | AppCommand::ShiftPatternsDown => {
|
||||
let bank = self.patterns_nav.bank_cursor;
|
||||
let data = self.project_state.project.banks[bank].clone();
|
||||
Some(UndoScope::Bank { bank, data })
|
||||
}
|
||||
AppCommand::PastePatterns { bank, .. }
|
||||
| AppCommand::ResetPatterns { bank, .. }
|
||||
| AppCommand::RenameBank { bank, .. } => {
|
||||
let data = self.project_state.project.banks[*bank].clone();
|
||||
Some(UndoScope::Bank { bank: *bank, data })
|
||||
}
|
||||
AppCommand::PasteBanks { .. } | AppCommand::ResetBanks { .. } => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_undo_entry(&mut self, entry: UndoEntry) -> UndoEntry {
|
||||
let cursor = (self.editor_ctx.bank, self.editor_ctx.pattern, self.editor_ctx.step);
|
||||
let reverse_scope = match entry.scope {
|
||||
UndoScope::Pattern { bank, pattern, data } => {
|
||||
let current = self.project_state.project.pattern_at(bank, pattern).clone();
|
||||
*self.project_state.project.pattern_at_mut(bank, pattern) = data;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
UndoScope::Pattern { bank, pattern, data: current }
|
||||
}
|
||||
UndoScope::Bank { bank, data } => {
|
||||
let current = self.project_state.project.banks[bank].clone();
|
||||
let pat_count = current.patterns.len();
|
||||
self.project_state.project.banks[bank] = data;
|
||||
for p in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, p);
|
||||
}
|
||||
UndoScope::Bank { bank, data: current }
|
||||
}
|
||||
};
|
||||
self.editor_ctx.bank = entry.cursor.0;
|
||||
self.editor_ctx.pattern = entry.cursor.1;
|
||||
self.editor_ctx.step = entry.cursor.2;
|
||||
self.load_step_to_editor();
|
||||
UndoEntry { scope: reverse_scope, cursor }
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
// Handle undo/redo before the undoable snapshot
|
||||
match cmd {
|
||||
AppCommand::Undo => {
|
||||
if let Some(entry) = self.undo.pop_undo() {
|
||||
let reverse = self.apply_undo_entry(entry);
|
||||
self.undo.push_redo(reverse);
|
||||
self.ui.flash("Undo", 100, FlashKind::Info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
AppCommand::Redo => {
|
||||
if let Some(entry) = self.undo.pop_redo() {
|
||||
let reverse = self.apply_undo_entry(entry);
|
||||
self.undo.undo_stack.push(reverse);
|
||||
self.ui.flash("Redo", 100, FlashKind::Info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(scope) = self.undoable_scope(&cmd) {
|
||||
let cursor = (self.editor_ctx.bank, self.editor_ctx.pattern, self.editor_ctx.step);
|
||||
self.undo.push(UndoEntry { scope, cursor });
|
||||
}
|
||||
|
||||
match cmd {
|
||||
AppCommand::Undo | AppCommand::Redo => unreachable!(),
|
||||
|
||||
// Playback
|
||||
AppCommand::TogglePlaying => self.toggle_playing(link),
|
||||
AppCommand::TempoUp => self.tempo_up(link),
|
||||
@@ -1092,6 +1251,10 @@ impl App {
|
||||
self.reset_banks(&banks);
|
||||
}
|
||||
|
||||
// Reorder
|
||||
AppCommand::ShiftPatternsUp => self.shift_patterns_up(),
|
||||
AppCommand::ShiftPatternsDown => self.shift_patterns_down(),
|
||||
|
||||
// Clipboard
|
||||
AppCommand::HardenSteps => self.harden_steps(),
|
||||
AppCommand::CopySteps => self.copy_steps(),
|
||||
@@ -1406,6 +1569,9 @@ impl App {
|
||||
AppCommand::ToggleSpectrum => {
|
||||
self.audio.config.show_spectrum = !self.audio.config.show_spectrum;
|
||||
}
|
||||
AppCommand::TogglePreview => {
|
||||
self.audio.config.show_preview = !self.audio.config.show_preview;
|
||||
}
|
||||
|
||||
// Metrics
|
||||
AppCommand::ResetPeakVoices => {
|
||||
|
||||
@@ -5,6 +5,10 @@ use crate::page::Page;
|
||||
use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, SettingKind};
|
||||
|
||||
pub enum AppCommand {
|
||||
// Undo/Redo
|
||||
Undo,
|
||||
Redo,
|
||||
|
||||
// Playback
|
||||
TogglePlaying,
|
||||
TempoUp,
|
||||
@@ -89,6 +93,10 @@ pub enum AppCommand {
|
||||
banks: Vec<usize>,
|
||||
},
|
||||
|
||||
// Reorder
|
||||
ShiftPatternsUp,
|
||||
ShiftPatternsDown,
|
||||
|
||||
// Clipboard
|
||||
HardenSteps,
|
||||
CopySteps,
|
||||
@@ -239,6 +247,7 @@ pub enum AppCommand {
|
||||
ToggleRefreshRate,
|
||||
ToggleScope,
|
||||
ToggleSpectrum,
|
||||
TogglePreview,
|
||||
|
||||
// Metrics
|
||||
ResetPeakVoices,
|
||||
|
||||
@@ -81,6 +81,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
||||
app.audio.config.show_scope = settings.display.show_scope;
|
||||
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||
app.audio.config.show_preview = settings.display.show_preview;
|
||||
app.ui.show_completion = settings.display.show_completion;
|
||||
app.ui.color_scheme = settings.display.color_scheme;
|
||||
app.ui.hue_rotation = settings.display.hue_rotation;
|
||||
|
||||
@@ -96,6 +96,12 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
let state = FileBrowserState::new_save(initial);
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
|
||||
}
|
||||
KeyCode::Char('z') if ctrl && !shift => {
|
||||
ctx.dispatch(AppCommand::Undo);
|
||||
}
|
||||
KeyCode::Char('Z') if ctrl => {
|
||||
ctx.dispatch(AppCommand::Redo);
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
ctx.dispatch(AppCommand::CopySteps);
|
||||
}
|
||||
@@ -134,6 +140,10 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
let current = format!("{:.1}", ctx.link.tempo());
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
||||
}
|
||||
KeyCode::Char(':') => {
|
||||
let current = (ctx.app.editor_ctx.step + 1).to_string();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::JumpToStep(current)));
|
||||
}
|
||||
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
|
||||
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
|
||||
KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease),
|
||||
|
||||
@@ -138,6 +138,22 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Char(c) => input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::JumpToStep(input) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
if let Ok(step) = input.parse::<usize>() {
|
||||
if step > 0 {
|
||||
ctx.dispatch(AppCommand::GoToStep(step - 1));
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() => input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::SetTempo(input) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
if let Ok(tempo) = input.parse::<f64>() {
|
||||
@@ -466,7 +482,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
EuclideanField::Rotation => rotation,
|
||||
};
|
||||
if let Ok(val) = target.parse::<usize>() {
|
||||
*target = (val + 1).min(128).to_string();
|
||||
*target = (val + 1).min(1024).to_string();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() => match field {
|
||||
|
||||
@@ -312,7 +312,8 @@ fn handle_main_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) {
|
||||
// Replay viz/sequencer split
|
||||
let show_scope = ctx.app.audio.config.show_scope;
|
||||
let show_spectrum = ctx.app.audio.config.show_spectrum;
|
||||
let has_viz = show_scope || show_spectrum;
|
||||
let show_preview = ctx.app.audio.config.show_preview;
|
||||
let has_viz = show_scope || show_spectrum || show_preview;
|
||||
let layout = ctx.app.audio.config.layout;
|
||||
|
||||
let sequencer_area = match layout {
|
||||
|
||||
@@ -25,6 +25,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
OptionsFocus::ShowScope => ctx.dispatch(AppCommand::ToggleScope),
|
||||
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
|
||||
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
|
||||
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
|
||||
OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
OptionsFocus::StartStopSync => ctx
|
||||
.link
|
||||
|
||||
@@ -8,8 +8,15 @@ use crate::state::{ConfirmAction, Modal, PatternsColumn, RenameTarget};
|
||||
pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
let alt = key.modifiers.contains(KeyModifiers::ALT);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Up if alt && ctx.app.patterns_nav.column == PatternsColumn::Patterns => {
|
||||
ctx.dispatch(AppCommand::ShiftPatternsUp);
|
||||
}
|
||||
KeyCode::Down if alt && ctx.app.patterns_nav.column == PatternsColumn::Patterns => {
|
||||
ctx.dispatch(AppCommand::ShiftPatternsDown);
|
||||
}
|
||||
KeyCode::Up if shift => {
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
@@ -94,6 +101,12 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('z') if ctrl && !shift => {
|
||||
ctx.dispatch(AppCommand::Undo);
|
||||
}
|
||||
KeyCode::Char('Z') if ctrl => {
|
||||
ctx.dispatch(AppCommand::Redo);
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
|
||||
@@ -56,6 +56,17 @@ pub const DOCS: &[DocEntry] = &[
|
||||
Topic("Introduction", include_str!("../../docs/midi_intro.md")),
|
||||
Topic("MIDI Output", include_str!("../../docs/midi_output.md")),
|
||||
Topic("MIDI Input", include_str!("../../docs/midi_input.md")),
|
||||
// Tutorials
|
||||
Section("Tutorials"),
|
||||
Topic("Randomness", include_str!("../../docs/tutorial_randomness.md")),
|
||||
Topic(
|
||||
"Notes & Harmony",
|
||||
include_str!("../../docs/tutorial_harmony.md"),
|
||||
),
|
||||
Topic(
|
||||
"Generators",
|
||||
include_str!("../../docs/tutorial_generators.md"),
|
||||
),
|
||||
];
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
|
||||
@@ -9,6 +9,34 @@ fn annotate_copy_name(name: &Option<String>) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift_patterns_up(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
range: std::ops::RangeInclusive<usize>,
|
||||
) -> Option<Vec<(usize, usize)>> {
|
||||
if *range.start() == 0 {
|
||||
return None;
|
||||
}
|
||||
let slice_start = range.start() - 1;
|
||||
let slice_end = *range.end();
|
||||
project.banks[bank].patterns[slice_start..=slice_end].rotate_left(1);
|
||||
Some((slice_start..=slice_end).map(|p| (bank, p)).collect())
|
||||
}
|
||||
|
||||
pub fn shift_patterns_down(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
range: std::ops::RangeInclusive<usize>,
|
||||
) -> Option<Vec<(usize, usize)>> {
|
||||
if *range.end() >= crate::model::MAX_PATTERNS - 1 {
|
||||
return None;
|
||||
}
|
||||
let slice_start = *range.start();
|
||||
let slice_end = range.end() + 1;
|
||||
project.banks[bank].patterns[slice_start..=slice_end].rotate_right(1);
|
||||
Some((slice_start..=slice_end).map(|p| (bank, p)).collect())
|
||||
}
|
||||
|
||||
pub fn copy_pattern(project: &Project, bank: usize, pattern: usize) -> Pattern {
|
||||
project.banks[bank].patterns[pattern].clone()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub struct DisplaySettings {
|
||||
pub show_scope: bool,
|
||||
pub show_spectrum: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_preview: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_completion: bool,
|
||||
#[serde(default = "default_font")]
|
||||
pub font: String,
|
||||
@@ -83,6 +85,7 @@ impl Default for DisplaySettings {
|
||||
runtime_highlight: false,
|
||||
show_scope: true,
|
||||
show_spectrum: true,
|
||||
show_preview: true,
|
||||
show_completion: true,
|
||||
font: default_font(),
|
||||
color_scheme: ColorScheme::default(),
|
||||
|
||||
@@ -83,6 +83,7 @@ pub struct AudioConfig {
|
||||
pub refresh_rate: RefreshRate,
|
||||
pub show_scope: bool,
|
||||
pub show_spectrum: bool,
|
||||
pub show_preview: bool,
|
||||
pub layout: MainLayout,
|
||||
}
|
||||
|
||||
@@ -101,6 +102,7 @@ impl Default for AudioConfig {
|
||||
refresh_rate: RefreshRate::default(),
|
||||
show_scope: true,
|
||||
show_spectrum: true,
|
||||
show_preview: true,
|
||||
layout: MainLayout::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ pub mod patterns_nav;
|
||||
pub mod playback;
|
||||
pub mod project;
|
||||
pub mod sample_browser;
|
||||
pub mod undo;
|
||||
pub mod ui;
|
||||
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
||||
|
||||
@@ -64,6 +64,7 @@ pub enum Modal {
|
||||
input: String,
|
||||
},
|
||||
SetTempo(String),
|
||||
JumpToStep(String),
|
||||
AddSamplePath(Box<FileBrowserState>),
|
||||
Editor,
|
||||
Preview,
|
||||
|
||||
@@ -10,6 +10,7 @@ pub enum OptionsFocus {
|
||||
ShowScope,
|
||||
ShowSpectrum,
|
||||
ShowCompletion,
|
||||
ShowPreview,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
Quantum,
|
||||
@@ -32,6 +33,7 @@ impl CyclicEnum for OptionsFocus {
|
||||
Self::ShowScope,
|
||||
Self::ShowSpectrum,
|
||||
Self::ShowCompletion,
|
||||
Self::ShowPreview,
|
||||
Self::LinkEnabled,
|
||||
Self::StartStopSync,
|
||||
Self::Quantum,
|
||||
@@ -54,17 +56,18 @@ const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::ShowScope, 6),
|
||||
(OptionsFocus::ShowSpectrum, 7),
|
||||
(OptionsFocus::ShowCompletion, 8),
|
||||
(OptionsFocus::LinkEnabled, 12),
|
||||
(OptionsFocus::StartStopSync, 13),
|
||||
(OptionsFocus::Quantum, 14),
|
||||
(OptionsFocus::MidiOutput0, 24),
|
||||
(OptionsFocus::MidiOutput1, 25),
|
||||
(OptionsFocus::MidiOutput2, 26),
|
||||
(OptionsFocus::MidiOutput3, 27),
|
||||
(OptionsFocus::MidiInput0, 31),
|
||||
(OptionsFocus::MidiInput1, 32),
|
||||
(OptionsFocus::MidiInput2, 33),
|
||||
(OptionsFocus::MidiInput3, 34),
|
||||
(OptionsFocus::ShowPreview, 9),
|
||||
(OptionsFocus::LinkEnabled, 13),
|
||||
(OptionsFocus::StartStopSync, 14),
|
||||
(OptionsFocus::Quantum, 15),
|
||||
(OptionsFocus::MidiOutput0, 25),
|
||||
(OptionsFocus::MidiOutput1, 26),
|
||||
(OptionsFocus::MidiOutput2, 27),
|
||||
(OptionsFocus::MidiOutput3, 28),
|
||||
(OptionsFocus::MidiInput0, 32),
|
||||
(OptionsFocus::MidiInput1, 33),
|
||||
(OptionsFocus::MidiInput2, 34),
|
||||
(OptionsFocus::MidiInput3, 35),
|
||||
];
|
||||
|
||||
impl OptionsFocus {
|
||||
|
||||
53
src/state/undo.rs
Normal file
53
src/state/undo.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::model::{Bank, Pattern};
|
||||
|
||||
const MAX_UNDO: usize = 100;
|
||||
|
||||
pub enum UndoScope {
|
||||
Pattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
data: Pattern,
|
||||
},
|
||||
Bank {
|
||||
bank: usize,
|
||||
data: Bank,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct UndoEntry {
|
||||
pub scope: UndoScope,
|
||||
pub cursor: (usize, usize, usize),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UndoHistory {
|
||||
pub(crate) undo_stack: Vec<UndoEntry>,
|
||||
redo_stack: Vec<UndoEntry>,
|
||||
}
|
||||
|
||||
impl UndoHistory {
|
||||
pub fn push(&mut self, entry: UndoEntry) {
|
||||
self.redo_stack.clear();
|
||||
if self.undo_stack.len() >= MAX_UNDO {
|
||||
self.undo_stack.remove(0);
|
||||
}
|
||||
self.undo_stack.push(entry);
|
||||
}
|
||||
|
||||
pub fn pop_undo(&mut self) -> Option<UndoEntry> {
|
||||
self.undo_stack.pop()
|
||||
}
|
||||
|
||||
pub fn push_redo(&mut self, entry: UndoEntry) {
|
||||
self.redo_stack.push(entry);
|
||||
}
|
||||
|
||||
pub fn pop_redo(&mut self) -> Option<UndoEntry> {
|
||||
self.redo_stack.pop()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.undo_stack.clear();
|
||||
self.redo_stack.clear();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
||||
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
||||
bindings.push(("r", "Rename", "Rename current step"));
|
||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||
bindings.push((":", "Jump", "Jump to step number"));
|
||||
bindings.push(("e", "Euclidean", "Distribute linked steps using Euclidean rhythm"));
|
||||
bindings.push(("m", "Mute", "Stage mute for current pattern"));
|
||||
bindings.push(("x", "Solo", "Stage solo for current pattern"));
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::SequencerSnapshot;
|
||||
use crate::model::SourceSpan;
|
||||
use crate::state::MainLayout;
|
||||
use crate::theme;
|
||||
use crate::views::highlight::highlight_line_with_runtime;
|
||||
use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line};
|
||||
use crate::widgets::{ActivePatterns, Orientation, Scope, Spectrum, VuMeter};
|
||||
|
||||
pub fn layout(area: Rect) -> [Rect; 5] {
|
||||
@@ -25,7 +31,8 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
|
||||
|
||||
let show_scope = app.audio.config.show_scope;
|
||||
let show_spectrum = app.audio.config.show_spectrum;
|
||||
let has_viz = show_scope || show_spectrum;
|
||||
let show_preview = app.audio.config.show_preview;
|
||||
let has_viz = show_scope || show_spectrum || show_preview;
|
||||
let layout = app.audio.config.layout;
|
||||
|
||||
let (viz_area, sequencer_area) = match layout {
|
||||
@@ -70,7 +77,7 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
|
||||
};
|
||||
|
||||
if has_viz {
|
||||
render_viz_area(frame, app, viz_area, layout, show_scope, show_spectrum);
|
||||
render_viz_area(frame, app, snapshot, viz_area);
|
||||
}
|
||||
|
||||
render_sequencer(frame, app, snapshot, sequencer_area);
|
||||
@@ -78,43 +85,48 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area:
|
||||
render_active_patterns(frame, app, snapshot, patterns_area);
|
||||
}
|
||||
|
||||
enum VizPanel {
|
||||
Scope,
|
||||
Spectrum,
|
||||
Preview,
|
||||
}
|
||||
|
||||
fn render_viz_area(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
snapshot: &SequencerSnapshot,
|
||||
area: Rect,
|
||||
layout: MainLayout,
|
||||
show_scope: bool,
|
||||
show_spectrum: bool,
|
||||
) {
|
||||
let is_vertical_layout = matches!(layout, MainLayout::Left | MainLayout::Right);
|
||||
let is_vertical_layout = matches!(app.audio.config.layout, MainLayout::Left | MainLayout::Right);
|
||||
let show_scope = app.audio.config.show_scope;
|
||||
let show_spectrum = app.audio.config.show_spectrum;
|
||||
let show_preview = app.audio.config.show_preview;
|
||||
|
||||
if show_scope && show_spectrum {
|
||||
if is_vertical_layout {
|
||||
let [scope_area, spectrum_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(area);
|
||||
render_scope(frame, app, scope_area, Orientation::Vertical);
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
let mut panels = Vec::new();
|
||||
if show_scope { panels.push(VizPanel::Scope); }
|
||||
if show_spectrum { panels.push(VizPanel::Spectrum); }
|
||||
if show_preview { panels.push(VizPanel::Preview); }
|
||||
|
||||
let constraints: Vec<Constraint> = panels.iter().map(|_| Constraint::Fill(1)).collect();
|
||||
|
||||
let areas: Vec<Rect> = if is_vertical_layout {
|
||||
Layout::vertical(&constraints).split(area).to_vec()
|
||||
} else {
|
||||
let [scope_area, spectrum_area] = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(area);
|
||||
render_scope(frame, app, scope_area, Orientation::Horizontal);
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
}
|
||||
} else if show_scope {
|
||||
Layout::horizontal(&constraints).split(area).to_vec()
|
||||
};
|
||||
|
||||
let orientation = if is_vertical_layout {
|
||||
Orientation::Vertical
|
||||
} else {
|
||||
Orientation::Horizontal
|
||||
};
|
||||
render_scope(frame, app, area, orientation);
|
||||
} else if show_spectrum {
|
||||
render_spectrum(frame, app, area);
|
||||
|
||||
for (panel, panel_area) in panels.iter().zip(areas.iter()) {
|
||||
match panel {
|
||||
VizPanel::Scope => render_scope(frame, app, *panel_area, orientation),
|
||||
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
|
||||
VizPanel::Preview => render_script_preview(frame, app, snapshot, *panel_area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +199,7 @@ fn render_tile(
|
||||
let pattern = app.current_edit_pattern();
|
||||
let step = pattern.step(step_idx);
|
||||
let is_active = step.map(|s| s.active).unwrap_or(false);
|
||||
let has_content = step.map(|s| s.has_content()).unwrap_or(false);
|
||||
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
||||
let is_selected = step_idx == app.editor_ctx.step;
|
||||
let in_selection = app.editor_ctx.selection_range()
|
||||
@@ -217,7 +230,10 @@ fn render_tile(
|
||||
let (r, g, b) = link_color.unwrap().1;
|
||||
(Color::Rgb(r, g, b), theme.tile.active_fg)
|
||||
}
|
||||
(false, true, false, false, _) => (theme.tile.active_bg, theme.tile.active_fg),
|
||||
(false, true, false, false, _) => {
|
||||
let bg = if has_content { theme.tile.content_bg } else { theme.tile.active_bg };
|
||||
(bg, theme.tile.active_fg)
|
||||
}
|
||||
(false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg),
|
||||
(false, false, _, _, true) => (theme.selection.in_range, theme.selection.cursor_fg),
|
||||
(false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
|
||||
@@ -234,6 +250,8 @@ fn render_tile(
|
||||
"▶".to_string()
|
||||
} else if let Some(source) = source_idx {
|
||||
format!("→{:02}", source + 1)
|
||||
} else if has_content {
|
||||
format!("·{:02}·", step_idx + 1)
|
||||
} else {
|
||||
format!("{:02}", step_idx + 1)
|
||||
};
|
||||
@@ -314,6 +332,90 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
fn render_script_preview(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
snapshot: &SequencerSnapshot,
|
||||
area: Rect,
|
||||
) {
|
||||
let theme = theme::get();
|
||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||
|
||||
let pattern = app.current_edit_pattern();
|
||||
let step_idx = app.editor_ctx.step;
|
||||
let step = pattern.step(step_idx);
|
||||
let source_idx = step.and_then(|s| s.source);
|
||||
let step_name = step.and_then(|s| s.name.as_ref());
|
||||
|
||||
let title = match (source_idx, step_name) {
|
||||
(Some(src), Some(name)) => format!(" {:02}: {} -> {:02} ", step_idx + 1, name, src + 1),
|
||||
(None, Some(name)) => format!(" {:02}: {} ", step_idx + 1, name),
|
||||
(Some(src), None) => format!(" {:02} -> {:02} ", step_idx + 1, src + 1),
|
||||
(None, None) => format!(" Step {:02} ", step_idx + 1),
|
||||
};
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
.border_style(Style::new().fg(theme.ui.border));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let script = pattern.resolve_script(step_idx).unwrap_or("");
|
||||
if script.is_empty() {
|
||||
let empty = Paragraph::new("(empty)")
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::new().fg(theme.ui.text_dim));
|
||||
let centered = Rect {
|
||||
y: inner.y + inner.height / 2,
|
||||
height: 1,
|
||||
..inner
|
||||
};
|
||||
frame.render_widget(empty, centered);
|
||||
return;
|
||||
}
|
||||
|
||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
||||
let source = pattern.resolve_source(step_idx);
|
||||
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let resolved_display: Vec<(SourceSpan, String)> = trace
|
||||
.map(|t| {
|
||||
t.resolved
|
||||
.iter()
|
||||
.map(|(s, v)| (*s, v.display()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut line_start = 0usize;
|
||||
let lines: Vec<Line> = script
|
||||
.lines()
|
||||
.take(inner.height as usize)
|
||||
.map(|line_str| {
|
||||
let tokens = if let Some(t) = trace {
|
||||
let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len());
|
||||
let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len());
|
||||
let res = adjust_resolved_for_line(&resolved_display, line_start, line_str.len());
|
||||
highlight_line_with_runtime(line_str, &exec, &sel, &res, &user_words)
|
||||
} else {
|
||||
highlight_line_with_runtime(line_str, &[], &[], &[], &user_words)
|
||||
};
|
||||
line_start += line_str.len() + 1;
|
||||
let spans: Vec<Span> = tokens
|
||||
.into_iter()
|
||||
.map(|(style, text, _)| Span::styled(text, style))
|
||||
.collect();
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), inner);
|
||||
}
|
||||
|
||||
fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
|
||||
@@ -161,6 +161,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
focus == OptionsFocus::ShowCompletion,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Show preview",
|
||||
if app.audio.config.show_preview { "On" } else { "Off" },
|
||||
focus == OptionsFocus::ShowPreview,
|
||||
&theme,
|
||||
),
|
||||
Line::from(""),
|
||||
link_header,
|
||||
render_divider(content_width, &theme),
|
||||
|
||||
@@ -38,7 +38,7 @@ fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<Sou
|
||||
})
|
||||
}
|
||||
|
||||
fn adjust_spans_for_line(
|
||||
pub fn adjust_spans_for_line(
|
||||
spans: &[SourceSpan],
|
||||
line_start: usize,
|
||||
line_len: usize,
|
||||
@@ -49,7 +49,7 @@ fn adjust_spans_for_line(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn adjust_resolved_for_line(
|
||||
pub fn adjust_resolved_for_line(
|
||||
resolved: &[(SourceSpan, String)],
|
||||
line_start: usize,
|
||||
line_len: usize,
|
||||
@@ -518,7 +518,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
}
|
||||
Modal::SetPattern { field, input } => {
|
||||
let (title, hint) = match field {
|
||||
PatternField::Length => ("Set Length (1-128)", "Enter number"),
|
||||
PatternField::Length => ("Set Length (1-1024)", "Enter number"),
|
||||
PatternField::Speed => ("Set Speed", "e.g. 1/3, 2/5, 1x, 2x"),
|
||||
};
|
||||
TextInputModal::new(title, input)
|
||||
@@ -527,6 +527,15 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::JumpToStep(input) => {
|
||||
let pattern_len = app.current_edit_pattern().length;
|
||||
let title = format!("Jump to Step (1-{})", pattern_len);
|
||||
TextInputModal::new(&title, input)
|
||||
.hint("Enter step number")
|
||||
.width(30)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::SetTempo(input) => TextInputModal::new("Set Tempo (20-300 BPM)", input)
|
||||
.hint("Enter BPM")
|
||||
.width(30)
|
||||
|
||||
@@ -59,29 +59,29 @@ fn iter() {
|
||||
#[test]
|
||||
fn every_true_on_zero() {
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
let f = run_ctx("{ 100 } 4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_true_on_multiple() {
|
||||
let ctx = ctx_with(|c| c.iter = 8);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
let f = run_ctx("{ 100 } 4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_false_between() {
|
||||
for i in 1..4 {
|
||||
let ctx = ctx_with(|c| c.iter = i);
|
||||
let f = run_ctx("4 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 0, "iter={} should be false", i);
|
||||
let f = run_ctx("{ 100 } 4 every", &ctx);
|
||||
assert!(f.stack().is_empty(), "iter={} should not execute quotation", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_zero_count() {
|
||||
expect_error("0 every", "every count must be > 0");
|
||||
expect_error("{ 1 } 0 every", "every count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -97,3 +97,77 @@ fn context_in_computation() {
|
||||
let f = run_ctx("60 step +", &ctx);
|
||||
assert_eq!(stack_int(&f), 63);
|
||||
}
|
||||
|
||||
// bjork (runs-based)
|
||||
|
||||
#[test]
|
||||
fn bjork_tresillo() {
|
||||
// Bresenham(3,8) hits at positions 0, 2, 5
|
||||
for runs in 0..8 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = run_ctx("{ 100 } 3 8 bjork", &ctx);
|
||||
let hit = ((runs + 1) * 3) / 8 != (runs * 3) / 8;
|
||||
if hit {
|
||||
assert_eq!(stack_int(&f), 100, "runs={} should hit", runs);
|
||||
} else {
|
||||
assert!(f.stack().is_empty(), "runs={} should miss", runs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bjork_hit_count() {
|
||||
// Bjorklund(3,8) should produce exactly 3 hits
|
||||
let mut hit_count = 0;
|
||||
for runs in 0..8 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = run_ctx("{ 100 } 3 8 bjork", &ctx);
|
||||
if !f.stack().is_empty() {
|
||||
hit_count += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(hit_count, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bjork_all_hits() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("{ 100 } 8 8 bjork", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bjork_zero_hits() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("{ 100 } 0 8 bjork", &ctx);
|
||||
assert!(f.stack().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bjork_invalid() {
|
||||
expect_error("{ 1 } 3 0 bjork", "bjork");
|
||||
}
|
||||
|
||||
// pbjork (iter-based)
|
||||
|
||||
#[test]
|
||||
fn pbjork_cinquillo() {
|
||||
let mut hit_count = 0;
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = run_ctx("{ 100 } 5 8 pbjork", &ctx);
|
||||
if !f.stack().is_empty() {
|
||||
hit_count += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(hit_count, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbjork_wraps() {
|
||||
let ctx0 = ctx_with(|c| c.iter = 0);
|
||||
let ctx8 = ctx_with(|c| c.iter = 8);
|
||||
let f0 = run_ctx("{ 100 } 3 8 pbjork", &ctx0);
|
||||
let f8 = run_ctx("{ 100 } 3 8 pbjork", &ctx8);
|
||||
assert_eq!(f0.stack().is_empty(), f8.stack().is_empty());
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ fn when_with_arithmetic() {
|
||||
|
||||
#[test]
|
||||
fn when_with_every() {
|
||||
// iter=0, every 2 should be true
|
||||
// iter=0, every 2 executes quotation
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("{ 100 } 2 every ?", &ctx);
|
||||
let f = run_ctx("{ 100 } 2 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
|
||||
// iter=1, every 2 should be false
|
||||
// iter=1, every 2 skips quotation
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("50 { 100 } 2 every ?", &ctx);
|
||||
let f = run_ctx("50 { 100 } 2 every", &ctx);
|
||||
assert_eq!(stack_int(&f), 50); // quotation not executed
|
||||
}
|
||||
|
||||
@@ -104,13 +104,12 @@ fn unexpected_close_error() {
|
||||
|
||||
#[test]
|
||||
fn every_with_quotation_integration() {
|
||||
// Simulating: { 2 distort } 2 every ?
|
||||
// On even iterations, distort is applied
|
||||
// { 2 distort } 2 every — on even iterations, distort is applied
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { 2 distort } 2 every ? ."#, &ctx)
|
||||
.evaluate(r#""kick" s { 2 distort } 2 every ."#, &ctx)
|
||||
.unwrap();
|
||||
if iter % 2 == 0 {
|
||||
assert!(
|
||||
@@ -128,6 +127,18 @@ fn every_with_quotation_integration() {
|
||||
}
|
||||
}
|
||||
|
||||
// bjork with sound
|
||||
|
||||
#[test]
|
||||
fn bjork_with_sound() {
|
||||
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(r#""kick" s { 2 distort } 3 8 bjork ."#, &ctx)
|
||||
.unwrap();
|
||||
assert!(outputs[0].contains("distort/2"));
|
||||
}
|
||||
|
||||
// Unless (!?) tests
|
||||
|
||||
#[test]
|
||||
@@ -142,28 +153,15 @@ fn unless_true_skips() {
|
||||
assert_eq!(stack_int(&f), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unless_with_every() {
|
||||
// iter=0, every 2 is true, so unless skips
|
||||
let ctx = ctx_with(|c| c.iter = 0);
|
||||
let f = run_ctx("50 { 100 } 2 every !?", &ctx);
|
||||
assert_eq!(stack_int(&f), 50);
|
||||
|
||||
// iter=1, every 2 is false, so unless executes
|
||||
let ctx = ctx_with(|c| c.iter = 1);
|
||||
let f = run_ctx("{ 100 } 2 every !?", &ctx);
|
||||
assert_eq!(stack_int(&f), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_and_unless_complementary() {
|
||||
// Using both ? and !? for if-else like behavior
|
||||
// Using iter mod + ?/!? for if-else behavior (every no longer pushes bool)
|
||||
for iter in 0..4 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f
|
||||
.evaluate(
|
||||
r#""kick" s { 2 distort } 2 every ? { 4 distort } 2 every !? ."#,
|
||||
r#""kick" s { 2 distort } iter 2 mod 0 = ? { 4 distort } iter 2 mod 0 = !? ."#,
|
||||
&ctx,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -100,7 +100,7 @@ const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/
|
||||
<button data-desc="Conditional execution and probability branching built into the language. Weighted random choices, coin flips, euclidean rhythms with rotation. Multiple random distributions, Perlin noise, seeded randomness. Cagire is designed to be generative and fun.">Probability</button>
|
||||
<button data-desc="Everything moves. Audio-rate LFOs, slides, ramps, envelopes, jitter, drunk walks, vibrato, ring modulation — all available as simple words you drop into any script. Shape any parameter over time with multiple curve types. Add controlled randomness or smooth motion at your heart's content.">Modulation</button>
|
||||
<button data-desc="Tempo sync with any Ableton Link-enabled app or hardware on the same network. Shared tempo and phase across all connected devices. You can jam with your friends!">Ableton Link</button>
|
||||
<button data-desc="32 banks * 32 patterns * 128 steps per project. That's ~131.000 scripts per project, way too much! Play as much patterns as you want simultaneously.">Projects</button>
|
||||
<button data-desc="32 banks * 32 patterns * 1024 steps per project. That's ~1.048.000 scripts per project, way too much! Play as much patterns as you want simultaneously.">Projects</button>
|
||||
<button data-desc="Forth is concatenative, minimal and radically extensible. No syntax to learn — just words separated by spaces. You build your own vocabulary on top of the language, growing it toward your musical ideas. Expressions are terse and compositional: small words combine into larger ones. Forth rewards exploration and improvisation, making it a natural fit for live coding.">Forth</button>
|
||||
<button data-desc="Per-pattern speed control, sync modes (loop, play-once, trigger) and launch quantization (bar, beat, step or immediate). Pattern chaining for longer sequences. Scripts are time-aware: they know their beat position, phase, iteration count and tempo.">Sequencer</button>
|
||||
<button data-desc="Play multiple patterns simultaneously. Mute and solo staging with quantized execution. Fill mode for live key input. Stage pattern starts and stops to land on the beat. Built for improvisation on stage.">Play Live</button>
|
||||
|
||||
Reference in New Issue
Block a user