Compare commits
6 Commits
5310b98542
...
971d06ba31
| Author | SHA1 | Date | |
|---|---|---|---|
| 971d06ba31 | |||
| 2868e28ecc | |||
| 64233647a6 | |||
| 1ba946ebe6 | |||
| f88691c09c | |||
| a2e941d385 |
@@ -1,6 +1,9 @@
|
|||||||
# Uncomment to use local doux for development
|
# Uncomment to use local doux for development
|
||||||
paths = ["/Users/bubo/doux"]
|
paths = ["/Users/bubo/doux"]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = "12.0"
|
||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
xtask = "run --package xtask --release --"
|
xtask = "run --package xtask --release --"
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -7,6 +7,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "12.0"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
/.cache
|
/.cache
|
||||||
*.prof
|
*.prof
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
releases/
|
||||||
|
|
||||||
# Local cargo overrides (doux path patch)
|
# Local cargo overrides (doux path patch)
|
||||||
.cargo/config.local.toml
|
.cargo/config.local.toml
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundf
|
|||||||
rusty_link = "0.4"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
cpal = { version = "0.17", features = ["jack"], optional = true }
|
cpal = { version = "0.17", optional = true }
|
||||||
clap = { version = "4", features = ["derive"], optional = true }
|
clap = { version = "4", features = ["derive"], optional = true }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -83,6 +83,9 @@ rustc-hash = { version = "2", optional = true }
|
|||||||
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
cpal = { version = "0.17", optional = true, features = ["jack"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
winres = "0.1"
|
winres = "0.1"
|
||||||
|
|
||||||
@@ -109,3 +112,4 @@ icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
|
|||||||
copyright = "Copyright (c) 2025 Raphaël Forment"
|
copyright = "Copyright (c) 2025 Raphaël Forment"
|
||||||
category = "Music"
|
category = "Music"
|
||||||
short_description = "Forth-based music sequencer"
|
short_description = "Forth-based music sequencer"
|
||||||
|
minimum_system_version = "12.0"
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
Cagire - A Forth-based music sequencer
|
# Cagire - A Forth-based music sequencer
|
||||||
Made by BuboBubo and his friends
|
|
||||||
======================================
|
## Installation
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
Drag Cagire.app into the Applications folder.
|
Drag Cagire.app into the Applications folder.
|
||||||
|
|
||||||
Unquarantine
|
## Unquarantine
|
||||||
------------
|
|
||||||
Since this app is not signed with an Apple Developer certificate,
|
Since this app is not signed with an Apple Developer certificate,
|
||||||
macOS will block it from running. To fix this, open Terminal and run:
|
macOS will block it from running. Thanks Apple! To fix this, open
|
||||||
|
Terminal and run:
|
||||||
|
|
||||||
xattr -cr /Applications/Cagire.app
|
xattr -cr /Applications/Cagire.app
|
||||||
|
|
||||||
Support
|
## Support
|
||||||
-------
|
|
||||||
If you enjoy Cagire, consider supporting development:
|
If you enjoy this software, consider supporting development:
|
||||||
https://ko-fi.com/raphaelbubo
|
https://ko-fi.com/raphaelbubo
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if c == '(' || c == ')' {
|
if c == '{' || c == '}' {
|
||||||
chars.next();
|
chars.next();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
|
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
|
||||||
Token::Word(w, span) => {
|
Token::Word(w, span) => {
|
||||||
let word = w.as_str();
|
let word = w.as_str();
|
||||||
if word == "{" {
|
if word == "(" {
|
||||||
let (quote_ops, consumed, end_span) =
|
let (quote_ops, consumed, end_span) =
|
||||||
compile_quotation(&tokens[i + 1..], dict)?;
|
compile_quotation(&tokens[i + 1..], dict)?;
|
||||||
i += consumed;
|
i += consumed;
|
||||||
@@ -142,8 +142,8 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
|||||||
end: end_span.end,
|
end: end_span.end,
|
||||||
};
|
};
|
||||||
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
||||||
} else if word == "}" {
|
} else if word == ")" {
|
||||||
return Err("unexpected }".into());
|
return Err("unexpected )".into());
|
||||||
} else if word == "[" {
|
} else if word == "[" {
|
||||||
let (bracket_ops, consumed, end_span) =
|
let (bracket_ops, consumed, end_span) =
|
||||||
compile_bracket(&tokens[i + 1..], dict)?;
|
compile_bracket(&tokens[i + 1..], dict)?;
|
||||||
@@ -203,8 +203,8 @@ fn compile_quotation(
|
|||||||
for (i, tok) in tokens.iter().enumerate() {
|
for (i, tok) in tokens.iter().enumerate() {
|
||||||
if let Token::Word(w, _) = tok {
|
if let Token::Word(w, _) = tok {
|
||||||
match w.as_str() {
|
match w.as_str() {
|
||||||
"{" => depth += 1,
|
"(" => depth += 1,
|
||||||
"}" => {
|
")" => {
|
||||||
depth -= 1;
|
depth -= 1;
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
end_idx = Some(i);
|
end_idx = Some(i);
|
||||||
@@ -216,7 +216,7 @@ fn compile_quotation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let end_idx = end_idx.ok_or("missing }")?;
|
let end_idx = end_idx.ok_or("missing )")?;
|
||||||
let end_span = match &tokens[end_idx] {
|
let end_span = match &tokens[end_idx] {
|
||||||
Token::Word(_, span) => *span,
|
Token::Word(_, span) => *span,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(true-quot false-quot bool --)",
|
stack: "(true-quot false-quot bool --)",
|
||||||
desc: "Execute true-quot if true, else false-quot",
|
desc: "Execute true-quot if true, else false-quot",
|
||||||
example: "{ 1 } { 2 } coin ifelse",
|
example: "( 1 ) ( 2 ) coin ifelse",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -512,7 +512,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(..quots n --)",
|
stack: "(..quots n --)",
|
||||||
desc: "Execute nth quotation (0-indexed)",
|
desc: "Execute nth quotation (0-indexed)",
|
||||||
example: "{ 1 } { 2 } { 3 } 2 select => 3",
|
example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
@@ -522,7 +522,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(quot bool --)",
|
stack: "(quot bool --)",
|
||||||
desc: "Execute quotation if true",
|
desc: "Execute quotation if true",
|
||||||
example: "{ 2 distort } 0.5 chance ?",
|
example: "( 2 distort ) 0.5 chance ?",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -532,7 +532,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(quot bool --)",
|
stack: "(quot bool --)",
|
||||||
desc: "Execute quotation if false",
|
desc: "Execute quotation if false",
|
||||||
example: "{ 1 distort } 0.5 chance !?",
|
example: "( 1 distort ) 0.5 chance !?",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -542,7 +542,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation unconditionally",
|
desc: "Execute quotation unconditionally",
|
||||||
example: "{ 2 * } apply",
|
example: "( 2 * ) apply",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -553,7 +553,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Control",
|
category: "Control",
|
||||||
stack: "(n quot --)",
|
stack: "(n quot --)",
|
||||||
desc: "Execute quotation n times, @i holds current index",
|
desc: "Execute quotation n times, @i holds current index",
|
||||||
example: "4 { @i . } times => 0 1 2 3",
|
example: "4 ( @i . ) times => 0 1 2 3",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot prob --)",
|
stack: "(quot prob --)",
|
||||||
desc: "Execute quotation with probability (0.0-1.0)",
|
desc: "Execute quotation with probability (0.0-1.0)",
|
||||||
example: "{ 2 distort } 0.75 chance",
|
example: "( 2 distort ) 0.75 chance",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -70,7 +70,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot pct --)",
|
stack: "(quot pct --)",
|
||||||
desc: "Execute quotation with probability (0-100)",
|
desc: "Execute quotation with probability (0-100)",
|
||||||
example: "{ 2 distort } 75 prob",
|
example: "( 2 distort ) 75 prob",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -150,7 +150,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Always execute quotation",
|
desc: "Always execute quotation",
|
||||||
example: "{ 2 distort } always",
|
example: "( 2 distort ) always",
|
||||||
compile: Probability(1.0),
|
compile: Probability(1.0),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -160,7 +160,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Never execute quotation",
|
desc: "Never execute quotation",
|
||||||
example: "{ 2 distort } never",
|
example: "( 2 distort ) never",
|
||||||
compile: Probability(0.0),
|
compile: Probability(0.0),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -170,7 +170,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 75% of the time",
|
desc: "Execute quotation 75% of the time",
|
||||||
example: "{ 2 distort } often",
|
example: "( 2 distort ) often",
|
||||||
compile: Probability(0.75),
|
compile: Probability(0.75),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -180,7 +180,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 50% of the time",
|
desc: "Execute quotation 50% of the time",
|
||||||
example: "{ 2 distort } sometimes",
|
example: "( 2 distort ) sometimes",
|
||||||
compile: Probability(0.5),
|
compile: Probability(0.5),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -190,7 +190,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 25% of the time",
|
desc: "Execute quotation 25% of the time",
|
||||||
example: "{ 2 distort } rarely",
|
example: "( 2 distort ) rarely",
|
||||||
compile: Probability(0.25),
|
compile: Probability(0.25),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -200,7 +200,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 10% of the time",
|
desc: "Execute quotation 10% of the time",
|
||||||
example: "{ 2 distort } almostNever",
|
example: "( 2 distort ) almostNever",
|
||||||
compile: Probability(0.1),
|
compile: Probability(0.1),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -210,7 +210,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Probability",
|
category: "Probability",
|
||||||
stack: "(quot --)",
|
stack: "(quot --)",
|
||||||
desc: "Execute quotation 90% of the time",
|
desc: "Execute quotation 90% of the time",
|
||||||
example: "{ 2 distort } almostAlways",
|
example: "( 2 distort ) almostAlways",
|
||||||
compile: Probability(0.9),
|
compile: Probability(0.9),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -221,7 +221,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot n --)",
|
stack: "(quot n --)",
|
||||||
desc: "Execute quotation every nth iteration",
|
desc: "Execute quotation every nth iteration",
|
||||||
example: "{ 2 distort } 4 every",
|
example: "( 2 distort ) 4 every",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -231,7 +231,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot n --)",
|
stack: "(quot n --)",
|
||||||
desc: "Execute quotation on all iterations except every nth",
|
desc: "Execute quotation on all iterations except every nth",
|
||||||
example: "{ 2 distort } 4 except",
|
example: "( 2 distort ) 4 except",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -241,7 +241,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot n offset --)",
|
stack: "(quot n offset --)",
|
||||||
desc: "Execute quotation every nth iteration with phase offset",
|
desc: "Execute quotation every nth iteration with phase offset",
|
||||||
example: "{ snare } 4 2 every+ => fires at iter 2, 6, 10...",
|
example: "( snare ) 4 2 every+ => fires at iter 2, 6, 10...",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -251,7 +251,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot n offset --)",
|
stack: "(quot n offset --)",
|
||||||
desc: "Skip quotation every nth iteration with phase offset",
|
desc: "Skip quotation every nth iteration with phase offset",
|
||||||
example: "{ snare } 4 2 except+ => skips at iter 2, 6, 10...",
|
example: "( snare ) 4 2 except+ => skips at iter 2, 6, 10...",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -261,7 +261,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot k n --)",
|
stack: "(quot k n --)",
|
||||||
desc: "Execute quotation using Euclidean distribution over step runs",
|
desc: "Execute quotation using Euclidean distribution over step runs",
|
||||||
example: "{ 2 distort } 3 8 bjork",
|
example: "( 2 distort ) 3 8 bjork",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -271,7 +271,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Time",
|
category: "Time",
|
||||||
stack: "(quot k n --)",
|
stack: "(quot k n --)",
|
||||||
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
||||||
example: "{ 2 distort } 3 8 pbjork",
|
example: "( 2 distort ) 3 8 pbjork",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -456,7 +456,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Desktop",
|
category: "Desktop",
|
||||||
stack: "(-- bool)",
|
stack: "(-- bool)",
|
||||||
desc: "1 when mouse button held, 0 otherwise",
|
desc: "1 when mouse button held, 0 otherwise",
|
||||||
example: "mdown { \"crash\" s . } ?",
|
example: "mdown ( \"crash\" s . ) ?",
|
||||||
compile: Context("mdown"),
|
compile: Context("mdown"),
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
@@ -487,7 +487,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
category: "Generator",
|
category: "Generator",
|
||||||
stack: "(quot n -- results...)",
|
stack: "(quot n -- results...)",
|
||||||
desc: "Execute quotation n times, push all results",
|
desc: "Execute quotation n times, push all results",
|
||||||
example: "{ 1 6 rand } 4 gen => 4 random values",
|
example: "( 1 6 rand ) 4 gen => 4 random values",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::theme;
|
|||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
/// Node type in the sample tree.
|
/// Node type in the sample tree.
|
||||||
@@ -116,13 +116,13 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||||
let height = area.height as usize;
|
let height = area.height as usize;
|
||||||
if self.entries.is_empty() {
|
if self.entries.is_empty() {
|
||||||
let msg = if self.search_query.is_empty() {
|
if self.search_query.is_empty() {
|
||||||
"No samples loaded"
|
self.render_empty_guide(frame, area, colors);
|
||||||
} else {
|
} else {
|
||||||
"No matches"
|
let line =
|
||||||
};
|
Line::from(Span::styled("No matches", Style::new().fg(colors.browser.empty_text)));
|
||||||
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
|
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,4 +179,47 @@ impl<'a> SampleBrowser<'a> {
|
|||||||
|
|
||||||
frame.render_widget(Paragraph::new(lines), area);
|
frame.render_widget(Paragraph::new(lines), area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_empty_guide(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||||
|
let muted = Style::new().fg(colors.browser.empty_text);
|
||||||
|
let heading = Style::new().fg(colors.ui.text_primary);
|
||||||
|
let key = Style::new().fg(colors.hint.key);
|
||||||
|
let desc = Style::new().fg(colors.hint.text);
|
||||||
|
let code = Style::new().fg(colors.ui.accent);
|
||||||
|
|
||||||
|
let lines = vec![
|
||||||
|
Line::from(Span::styled(" No samples loaded.", muted)),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(Span::styled(" Load from the Engine page:", heading)),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(" F6 ", key),
|
||||||
|
Span::styled("Go to Engine page", desc),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(" A ", key),
|
||||||
|
Span::styled("Add a sample folder", desc),
|
||||||
|
]),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(Span::styled(" Organize samples like this:", heading)),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(Span::styled(" samples/", code)),
|
||||||
|
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} kick/", code)),
|
||||||
|
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} kick.wav", code)),
|
||||||
|
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} snare/", code)),
|
||||||
|
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} snare.wav", code)),
|
||||||
|
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} hats/", code)),
|
||||||
|
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} closed.wav", code)),
|
||||||
|
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} open.wav", code)),
|
||||||
|
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} pedal.wav", code)),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(Span::styled(" Folders become Forth words:", heading)),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(Span::styled(" kick sound .", code)),
|
||||||
|
Line::from(Span::styled(" hats sound 2 n .", code)),
|
||||||
|
Line::from(Span::styled(" snare sound 0.5 speed .", code)),
|
||||||
|
];
|
||||||
|
|
||||||
|
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"i": 0,
|
"i": 0,
|
||||||
"script": "0 8 12 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n{ inv } rarely\n{ inv } sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n2 release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand \n0.0 1.0 rand timbre\n0.5 gain\n0.8 sustain\n2 8 rand release\n."
|
"script": "0 8 12 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n( inv ) rarely\n( inv ) sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n2 release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand \n0.0 1.0 rand timbre\n0.5 gain\n0.8 sustain\n2 8 rand release\n."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"i": 4,
|
"i": 4,
|
||||||
"script": "0 12 20 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n{ inv } rarely\n{ inv } sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n10 16 rand release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand 0.0 1.0 rand timbre\n0.5 gain\n{ . } 2 every"
|
"script": "0 12 20 rand ..\nc3 c4 g3 g2 4 pcycle key!\n0 1 2 choose 2\n6 12 rand pentatonic\n( inv ) rarely\n( inv ) sometimes arp note\ngrain sound 2 8 rand decay \n2 vib 0.125 2 / vibmod\n0.01 1.0 exprand pan\n10 16 rand release\n0.8 verb 1.0 verbdiff\n0.2 chorus\n1 morph\n0.0 1.0 rand 0.0 1.0 rand timbre\n0.5 gain\n( . ) 2 every"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"length": 16,
|
"length": 16,
|
||||||
|
|||||||
@@ -20,15 +20,17 @@ The engine scans these directories and builds a registry of available samples. S
|
|||||||
|
|
||||||
```
|
```
|
||||||
samples/
|
samples/
|
||||||
├── kick.wav → "kick"
|
├── kick/ → "kick"
|
||||||
├── snare.wav → "snare"
|
│ └── kick.wav
|
||||||
|
├── snare/ → "snare"
|
||||||
|
│ └── snare.wav
|
||||||
└── hats/
|
└── hats/
|
||||||
├── closed.wav → "hats" n 0
|
├── closed.wav → "hats" n 0
|
||||||
├── open.wav → "hats" n 1
|
├── open.wav → "hats" n 1
|
||||||
└── pedal.wav → "hats" n 2
|
└── pedal.wav → "hats" n 2
|
||||||
```
|
```
|
||||||
|
|
||||||
Folders at the root of your directory are used as the name of a sample bank. Folders create sample banks where each file gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
|
Folders at the root of your sample directory become sample banks named after the folder. Each file within a folder gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
|
||||||
|
|
||||||
## Playing Samples
|
## Playing Samples
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Four basic types of values can live on the stack:
|
|||||||
- **Integers**: `42`, `-7`, `0`
|
- **Integers**: `42`, `-7`, `0`
|
||||||
- **Floats**: `0.5`, `3.14`, `-1.0`
|
- **Floats**: `0.5`, `3.14`, `-1.0`
|
||||||
- **Strings**: `"kick"`, `"hello"`
|
- **Strings**: `"kick"`, `"hello"`
|
||||||
- **Quotations**: `{ dup + }` (code as data)
|
- **Quotations**: `( dup + )` (code as data)
|
||||||
|
|
||||||
Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`.
|
Floats can omit the leading zero: `.25` is the same as `0.25`, and `-.5` is `-0.5`.
|
||||||
|
|
||||||
|
|||||||
@@ -29,22 +29,22 @@ These are compiled directly into branch instructions. For that reason, these wor
|
|||||||
When you already have a quotation, `?` executes it if the condition is truthy:
|
When you already have a quotation, `?` executes it if the condition is truthy:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0.4 verb } coin ?
|
( 0.4 verb ) coin ?
|
||||||
saw s c4 note 0.5 gain . ;; reverb on half the hits
|
saw s c4 note 0.5 gain . ;; reverb on half the hits
|
||||||
```
|
```
|
||||||
|
|
||||||
`!?` is the opposite — executes when falsy:
|
`!?` is the opposite — executes when falsy:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0.2 gain } coin !?
|
( 0.2 gain ) coin !?
|
||||||
saw s c4 note . ;; quiet on half the hits
|
saw s c4 note . ;; quiet on half the hits
|
||||||
```
|
```
|
||||||
|
|
||||||
These pair well with `chance`, `prob`, and the other probability words:
|
These pair well with `chance`, `prob`, and the other probability words:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0.5 verb } 0.3 chance ? ;; occasional reverb wash
|
( 0.5 verb ) 0.3 chance ? ;; occasional reverb wash
|
||||||
{ 12 + } fill ? ;; octave up during fills
|
( 12 + ) fill ? ;; octave up during fills
|
||||||
```
|
```
|
||||||
|
|
||||||
## ifelse
|
## ifelse
|
||||||
@@ -52,14 +52,14 @@ These pair well with `chance`, `prob`, and the other probability words:
|
|||||||
Two quotations, one condition. The true branch comes first:
|
Two quotations, one condition. The true branch comes first:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c3 note } { c4 note } coin ifelse
|
( c3 note ) ( c4 note ) coin ifelse
|
||||||
saw s 0.6 gain . ;; bass or lead, coin flip
|
saw s 0.6 gain . ;; bass or lead, coin flip
|
||||||
```
|
```
|
||||||
|
|
||||||
Reads naturally: "c3 or c4, depending on the coin."
|
Reads naturally: "c3 or c4, depending on the coin."
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0.8 gain } { 0.3 gain } fill ifelse
|
( 0.8 gain ) ( 0.3 gain ) fill ifelse
|
||||||
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
|
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
|
|||||||
Choose the nth option from a list of quotations:
|
Choose the nth option from a list of quotations:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c4 } { e4 } { g4 } { b4 } iter 4 mod select
|
( c4 ) ( e4 ) ( g4 ) ( b4 ) iter 4 mod select
|
||||||
note sine s 0.5 decay .
|
note sine s 0.5 decay .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ Four notes cycling through a major seventh chord, one per pattern iteration. The
|
|||||||
When you have a quotation and want to execute it unconditionally, use `apply`:
|
When you have a quotation and want to execute it unconditionally, use `apply`:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ dup + } apply ;; doubles the top value
|
( dup + ) apply ;; doubles the top value
|
||||||
```
|
```
|
||||||
|
|
||||||
This is simpler than `?` when there is no condition to check. It pops the quotation and runs it.
|
This is simpler than `?` when there is no condition to check. It pops the quotation and runs it.
|
||||||
@@ -115,14 +115,14 @@ saw s c4 note .
|
|||||||
Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0):
|
Repeat a quotation n times. The variable `@i` is automatically set to the current iteration index (starting from 0):
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
3 { c4 @i 4 * + note } times
|
3 ( c4 @i 4 * + note ) times
|
||||||
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 — a chord
|
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4 — a chord
|
||||||
```
|
```
|
||||||
|
|
||||||
Subdivide with `at`:
|
Subdivide with `at`:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
4 { @i 4 / at sine s c4 note 0.3 gain . } times
|
4 ( @i 4 / at sine s c4 note 0.3 gain . ) times
|
||||||
```
|
```
|
||||||
|
|
||||||
Four evenly spaced notes within the step.
|
Four evenly spaced notes within the step.
|
||||||
@@ -130,11 +130,11 @@ Four evenly spaced notes within the step.
|
|||||||
Vary intensity per iteration:
|
Vary intensity per iteration:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
8 {
|
8 (
|
||||||
@i 8 / at
|
@i 8 / at
|
||||||
@i 4 mod 0 = if 0.7 else 0.2 then gain
|
@i 4 mod 0 = if 0.7 else 0.2 then gain
|
||||||
tri s c5 note 0.1 decay .
|
tri s c5 note 0.1 decay .
|
||||||
} times
|
) times
|
||||||
```
|
```
|
||||||
|
|
||||||
Eight notes per step. Every fourth one louder.
|
Eight notes per step. Every fourth one louder.
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ Everything after `;;` until the end of the line is ignored.
|
|||||||
|
|
||||||
Classic Forth has no quotations. Code is not a value you can pass around.
|
Classic Forth has no quotations. Code is not a value you can pass around.
|
||||||
|
|
||||||
Cagire has first-class quotations using curly braces:
|
Cagire has first-class quotations using parentheses:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ dup + }
|
( dup + )
|
||||||
```
|
```
|
||||||
|
|
||||||
This pushes a block of code onto the stack. You can store it, pass it to other words, and execute it later. Quotations enable conditionals, probability, and cycling.
|
This pushes a block of code onto the stack. You can store it, pass it to other words, and execute it later. Quotations enable conditionals, probability, and cycling.
|
||||||
@@ -41,14 +41,14 @@ x 0 > IF 1 ELSE -1 THEN
|
|||||||
Cagire supports this syntax but also provides quotation-based conditionals:
|
Cagire supports this syntax but also provides quotation-based conditionals:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 1 } { -1 } x 0 > ifelse
|
( 1 ) ( -1 ) x 0 > ifelse
|
||||||
```
|
```
|
||||||
|
|
||||||
The words `?` and `!?` execute a quotation based on a condition:
|
The words `?` and `!?` execute a quotation based on a condition:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ "kick" s . } coin ? ;; execute if coin is 1
|
( "kick" s . ) coin ? ;; execute if coin is 1
|
||||||
{ "snare" s . } coin !? ;; execute if coin is 0
|
( "snare" s . ) coin !? ;; execute if coin is 0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Strings
|
## Strings
|
||||||
@@ -116,21 +116,21 @@ Classic Forth has `DO ... LOOP`:
|
|||||||
Cagire uses a quotation-based loop with `times`:
|
Cagire uses a quotation-based loop with `times`:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
4 { @i . } times ;; prints 0 1 2 3
|
4 ( @i . ) times ;; prints 0 1 2 3
|
||||||
```
|
```
|
||||||
|
|
||||||
The loop counter is stored in the variable `i`, accessed with `@i`. This fits Cagire's style where control flow uses quotations.
|
The loop counter is stored in the variable `i`, accessed with `@i`. This fits Cagire's style where control flow uses quotations.
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
4 { @i 4 / at hat s . } times ;; hat at 0, 0.25, 0.5, 0.75
|
4 ( @i 4 / at hat s . ) times ;; hat at 0, 0.25, 0.5, 0.75
|
||||||
4 { c4 @i + note sine s . } times ;; ascending notes
|
4 ( c4 @i + note sine s . ) times ;; ascending notes
|
||||||
```
|
```
|
||||||
|
|
||||||
For generating sequences without side effects, use `..` or `gen`:
|
For generating sequences without side effects, use `..` or `gen`:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
1 5 .. ;; pushes 1 2 3 4 5
|
1 5 .. ;; pushes 1 2 3 4 5
|
||||||
{ dup * } 4 gen ;; pushes 0 1 4 9 (squares)
|
( dup * ) 4 gen ;; pushes 0 1 4 9 (squares)
|
||||||
```
|
```
|
||||||
|
|
||||||
## The Command Register
|
## The Command Register
|
||||||
@@ -167,11 +167,11 @@ These have no equivalent in classic Forth. They connect your script to the seque
|
|||||||
Classic Forth is deterministic. Cagire has built-in randomness:
|
Classic Forth is deterministic. Cagire has built-in randomness:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ "snare" s . } 50 prob ;; 50% chance
|
( "snare" s . ) 50 prob ;; 50% chance
|
||||||
{ "clap" s . } 0.25 chance ;; 25% chance
|
( "clap" s . ) 0.25 chance ;; 25% chance
|
||||||
{ "hat" s . } often ;; 75% chance
|
( "hat" s . ) often ;; 75% chance
|
||||||
{ "rim" s . } sometimes ;; 50% chance
|
( "rim" s . ) sometimes ;; 50% chance
|
||||||
{ "tom" s . } rarely ;; 25% chance
|
( "tom" s . ) rarely ;; 25% chance
|
||||||
```
|
```
|
||||||
|
|
||||||
These words take a quotation and execute it probabilistically.
|
These words take a quotation and execute it probabilistically.
|
||||||
@@ -181,9 +181,9 @@ These words take a quotation and execute it probabilistically.
|
|||||||
Execute a quotation on specific iterations:
|
Execute a quotation on specific iterations:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ "snare" s . } 4 every ;; every 4th pattern iteration
|
( "snare" s . ) 4 every ;; every 4th pattern iteration
|
||||||
{ "hat" s . } 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
|
( "hat" s . ) 3 8 bjork ;; Euclidean: 3 hits across 8 step runs
|
||||||
{ "hat" s . } 5 8 pbjork ;; Euclidean: 5 hits across 8 pattern iterations
|
( "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.
|
`every` checks the pattern iteration count. On iteration 0, 4, 8, 12... the quotation runs. On all other iterations it is skipped.
|
||||||
@@ -203,7 +203,7 @@ Each time the step runs, a different note is selected. The `3` tells `cycle` how
|
|||||||
You can also use quotations if you need to execute code:
|
You can also use quotations if you need to execute code:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c4 note } { e4 note } { g4 note } 3 cycle
|
( c4 note ) ( e4 note ) ( g4 note ) 3 cycle
|
||||||
```
|
```
|
||||||
|
|
||||||
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.
|
When the selected value is a quotation, it gets executed. When it is a plain value, it gets pushed onto the stack.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Euclidean distribution via `euclid`:
|
|||||||
Random timing via `gen`:
|
Random timing via `gen`:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0.0 1.0 rand } 4 gen at hat s . ;; 4 hats at random positions
|
( 0.0 1.0 rand ) 4 gen at hat s . ;; 4 hats at random positions
|
||||||
```
|
```
|
||||||
|
|
||||||
Geometric spacing via `geom..`:
|
Geometric spacing via `geom..`:
|
||||||
@@ -89,10 +89,10 @@ Geometric spacing via `geom..`:
|
|||||||
Wrap `at` expressions in quotations for conditional timing:
|
Wrap `at` expressions in quotations for conditional timing:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0 0.25 0.5 0.75 at } 2 every ;; 16th-note hats every other bar
|
( 0 0.25 0.5 0.75 at ) 2 every ;; 16th-note hats every other bar
|
||||||
hat s .
|
hat s .
|
||||||
|
|
||||||
{ 0 0.5 at } 0.5 chance ;; 50% chance of double-hit
|
( 0 0.5 at ) 0.5 chance ;; 50% chance of double-hit
|
||||||
kick s .
|
kick s .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -40,15 +40,15 @@ That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `freq`.
|
|||||||
`gen` executes a quotation n times and collects all results. The quotation must push exactly one value per call:
|
`gen` executes a quotation n times and collects all results. The quotation must push exactly one value per call:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 1 6 rand } 4 gen ;; 4 random values between 1 and 6
|
( 1 6 rand ) 4 gen ;; 4 random values between 1 and 6
|
||||||
{ coin } 8 gen ;; 8 random 0s and 1s
|
( 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:
|
Contrast with `times`, which executes for side effects and does not collect. `times` sets `@i` to the current index:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
4 { @i } times ;; 0 1 2 3 (pushes @i each iteration)
|
4 ( @i ) times ;; 0 1 2 3 (pushes @i each iteration)
|
||||||
4 { @i 60 + note sine s . } times ;; plays 4 notes, collects nothing
|
4 ( @i 60 + note sine s . ) times ;; plays 4 notes, collects nothing
|
||||||
```
|
```
|
||||||
|
|
||||||
The distinction: `gen` is for building data. `times` is for doing things.
|
The distinction: `gen` is for building data. `times` is for doing things.
|
||||||
@@ -109,7 +109,7 @@ c4 e4 g4 b4 4 shuffle ;; random permutation each time
|
|||||||
Useful for computing averages or accumulating values:
|
Useful for computing averages or accumulating values:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 1 6 rand } 4 gen 4 sum ;; sum of 4 dice rolls
|
( 1 6 rand ) 4 gen 4 sum ;; sum of 4 dice rolls
|
||||||
```
|
```
|
||||||
|
|
||||||
## Replication
|
## Replication
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ c4 mtof freq sine s .
|
|||||||
A chord progression cycling every pattern iteration:
|
A chord progression cycling every pattern iteration:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c3 maj7 } { f3 maj7 } { g3 dom7 } { c3 maj7 } 4 pcycle
|
( c3 maj7 ) ( f3 maj7 ) ( g3 dom7 ) ( c3 maj7 ) 4 pcycle
|
||||||
note sine s .
|
note sine s .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ Chord voicings with random inversion:
|
|||||||
|
|
||||||
```forth
|
```forth
|
||||||
e3 min9
|
e3 min9
|
||||||
{ } { 1 oct } 2 choose
|
( ) ( 1 oct ) 2 choose
|
||||||
note modal s .
|
note modal s .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ These are useful for parameters where perception is logarithmic, like frequency
|
|||||||
The probability words take a quotation and execute it with some chance. `chance` takes a float from 0.0 to 1.0, `prob` takes a percentage from 0 to 100:
|
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
|
```forth
|
||||||
{ hat s . } 0.25 chance ;; 25% chance
|
( hat s . ) 0.25 chance ;; 25% chance
|
||||||
{ hat s . } 75 prob ;; 75% chance
|
( hat s . ) 75 prob ;; 75% chance
|
||||||
```
|
```
|
||||||
|
|
||||||
Named probability words save you from remembering numbers:
|
Named probability words save you from remembering numbers:
|
||||||
@@ -48,9 +48,9 @@ Named probability words save you from remembering numbers:
|
|||||||
| `never` | 0% |
|
| `never` | 0% |
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ hat s . } often ;; 75%
|
( hat s . ) often ;; 75%
|
||||||
{ snare s . } sometimes ;; 50%
|
( snare s . ) sometimes ;; 50%
|
||||||
{ clap s . } rarely ;; 25%
|
( 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.
|
`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.
|
||||||
@@ -58,8 +58,8 @@ Named probability words save you from remembering numbers:
|
|||||||
Use `?` and `!?` with `coin` for quick coin-flip decisions:
|
Use `?` and `!?` with `coin` for quick coin-flip decisions:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ hat s . } coin ? ;; execute if coin is 1
|
( hat s . ) coin ? ;; execute if coin is 1
|
||||||
{ rim s . } coin !? ;; execute if coin is 0
|
( rim s . ) coin !? ;; execute if coin is 0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Selection
|
## Selection
|
||||||
@@ -74,7 +74,7 @@ kick snare hat 3 choose s . ;; random drum hit
|
|||||||
When a chosen item is a quotation, it gets executed:
|
When a chosen item is a quotation, it gets executed:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 0.1 decay } { 0.5 decay } { 0.9 decay } 3 choose
|
( 0.1 decay ) ( 0.5 decay ) ( 0.9 decay ) 3 choose
|
||||||
sine s .
|
sine s .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ The difference matters when patterns have different lengths. `cycle` counts per-
|
|||||||
Quotations work here too:
|
Quotations work here too:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c4 note } { e4 note } { g4 note } 3 cycle
|
( c4 note ) ( e4 note ) ( g4 note ) 3 cycle
|
||||||
sine s .
|
sine s .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -130,20 +130,20 @@ sine s .
|
|||||||
`every` runs a quotation once every n pattern iterations:
|
`every` runs a quotation once every n pattern iterations:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ crash s . } 4 every ;; crash cymbal every 4th iteration
|
( crash s . ) 4 every ;; crash cymbal every 4th iteration
|
||||||
```
|
```
|
||||||
|
|
||||||
`except` is the inverse -- it runs a quotation on all iterations *except* every nth:
|
`except` is the inverse -- it runs a quotation on all iterations *except* every nth:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ 2 distort } 4 except ;; distort on all iterations except every 4th
|
( 2 distort ) 4 except ;; distort on all iterations except every 4th
|
||||||
```
|
```
|
||||||
|
|
||||||
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms:
|
`bjork` and `pbjork` use Bjorklund's algorithm to distribute k hits across n positions as evenly as possible. Classic Euclidean rhythms:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ hat s . } 3 8 bjork ;; tresillo: x..x..x. (by step runs)
|
( hat s . ) 3 8 bjork ;; tresillo: x..x..x. (by step runs)
|
||||||
{ hat s . } 5 8 pbjork ;; cinquillo: x.xx.xx. (by pattern iterations)
|
( hat 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:
|
`bjork` counts by step runs (how many times this particular step has played). `pbjork` counts by pattern iterations. Some classic patterns:
|
||||||
@@ -172,7 +172,7 @@ The real power comes from mixing techniques. A hi-hat pattern with ghost notes:
|
|||||||
|
|
||||||
```forth
|
```forth
|
||||||
hat s
|
hat s
|
||||||
{ 0.3 0.6 rand gain } { 0.8 gain } 2 cycle
|
( 0.3 0.6 rand gain ) ( 0.8 gain ) 2 cycle
|
||||||
.
|
.
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,18 +181,18 @@ Full volume on even runs, random quiet on odd runs.
|
|||||||
A bass line that changes every 4 bars:
|
A bass line that changes every 4 bars:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c2 note } { e2 note } { g2 note } { a2 note } 4 pcycle
|
( c2 note ) ( e2 note ) ( g2 note ) ( a2 note ) 4 pcycle
|
||||||
{ 0.5 decay } often
|
( 0.5 decay ) often
|
||||||
sine s .
|
sine s .
|
||||||
```
|
```
|
||||||
|
|
||||||
Layered percussion with different densities:
|
Layered percussion with different densities:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ kick s . } always
|
( kick s . ) always
|
||||||
{ snare s . } 2 every
|
( snare s . ) 2 every
|
||||||
{ hat s . } 5 8 bjork
|
( hat s . ) 5 8 bjork
|
||||||
{ rim s . } rarely
|
( rim s . ) rarely
|
||||||
```
|
```
|
||||||
|
|
||||||
A melodic step with weighted note selection and random timbre:
|
A melodic step with weighted note selection and random timbre:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Reset on some condition:
|
|||||||
|
|
||||||
```forth
|
```forth
|
||||||
@n 1 + !n
|
@n 1 + !n
|
||||||
{ 0 !n } @n 16 > ? ;; reset after 16
|
( 0 !n ) @n 16 > ? ;; reset after 16
|
||||||
```
|
```
|
||||||
|
|
||||||
## When Changes Take Effect
|
## When Changes Take Effect
|
||||||
|
|||||||
36
releases/Cagire-aarch64.app/Contents/Info.plist
Normal file
36
releases/Cagire-aarch64.app/Contents/Info.plist
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Cagire</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>cagire-desktop</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>Cagire.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.sova.cagire</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Cagire</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.9</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>20260228.204543</string>
|
||||||
|
<key>CSResourcesFileMapped</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.music</string>
|
||||||
|
<key>LSRequiresCarbon</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright (c) 2025 Raphaël Forment</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
releases/Cagire-aarch64.app/Contents/MacOS/cagire-desktop
Executable file
BIN
releases/Cagire-aarch64.app/Contents/MacOS/cagire-desktop
Executable file
Binary file not shown.
BIN
releases/Cagire-aarch64.app/Contents/Resources/Cagire.icns
Normal file
BIN
releases/Cagire-aarch64.app/Contents/Resources/Cagire.icns
Normal file
Binary file not shown.
BIN
releases/Cagire-aarch64.dmg
Normal file
BIN
releases/Cagire-aarch64.dmg
Normal file
Binary file not shown.
BIN
releases/cagire-desktop-macos-aarch64
Executable file
BIN
releases/cagire-desktop-macos-aarch64
Executable file
Binary file not shown.
@@ -1,11 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="12.0"
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
PLUGIN_NAME="cagire-plugins"
|
PLUGIN_NAME="cagire-plugins"
|
||||||
LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores
|
LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores
|
||||||
OUT="target/releases"
|
OUT="releases"
|
||||||
|
|
||||||
PLATFORMS=(
|
PLATFORMS=(
|
||||||
"aarch64-apple-darwin"
|
"aarch64-apple-darwin"
|
||||||
@@ -312,13 +314,16 @@ copy_artifacts() {
|
|||||||
# macOS .app bundle
|
# macOS .app bundle
|
||||||
if [[ "$os" == "macos" ]]; then
|
if [[ "$os" == "macos" ]]; then
|
||||||
local app_src="$rd/bundle/osx/Cagire.app"
|
local app_src="$rd/bundle/osx/Cagire.app"
|
||||||
if [[ -d "$app_src" ]]; then
|
if [[ ! -d "$app_src" ]]; then
|
||||||
local app_dst="$OUT/Cagire-${arch}.app"
|
echo " ERROR: .app bundle not found at $app_src"
|
||||||
rm -rf "$app_dst"
|
echo " Did 'cargo bundle' succeed?"
|
||||||
cp -R "$app_src" "$app_dst"
|
return 1
|
||||||
echo " Cagire.app -> $app_dst"
|
|
||||||
scripts/make-dmg.sh "$app_dst" "$OUT"
|
|
||||||
fi
|
fi
|
||||||
|
local app_dst="$OUT/Cagire-${arch}.app"
|
||||||
|
rm -rf "$app_dst"
|
||||||
|
cp -R "$app_src" "$app_dst"
|
||||||
|
echo " Cagire.app -> $app_dst"
|
||||||
|
scripts/make-dmg.sh "$app_dst" "$OUT"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppCommand::AudioTriggerRestart => self.audio.trigger_restart(),
|
AppCommand::AudioTriggerRestart => self.audio.trigger_restart(),
|
||||||
AppCommand::RemoveLastSamplePath => self.audio.remove_last_sample_path(),
|
AppCommand::RemoveSamplePath(index) => self.audio.remove_sample_path(index),
|
||||||
AppCommand::AudioRefreshDevices => self.audio.refresh_devices(),
|
AppCommand::AudioRefreshDevices => self.audio.refresh_devices(),
|
||||||
|
|
||||||
// Options page
|
// Options page
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ impl App {
|
|||||||
channels: self.audio.config.channels,
|
channels: self.audio.config.channels,
|
||||||
buffer_size: self.audio.config.buffer_size,
|
buffer_size: self.audio.config.buffer_size,
|
||||||
max_voices: self.audio.config.max_voices,
|
max_voices: self.audio.config.max_voices,
|
||||||
|
sample_paths: self.audio.config.sample_paths.clone(),
|
||||||
},
|
},
|
||||||
display: crate::settings::DisplaySettings {
|
display: crate::settings::DisplaySettings {
|
||||||
fps: self.audio.config.refresh_rate.to_fps(),
|
fps: self.audio.config.refresh_rate.to_fps(),
|
||||||
|
|||||||
@@ -245,11 +245,12 @@ impl CagireDesktop {
|
|||||||
self.stream_error_rx = new_error_rx;
|
self.stream_error_rx = new_error_rx;
|
||||||
|
|
||||||
let mut restart_samples = Vec::new();
|
let mut restart_samples = Vec::new();
|
||||||
|
self.app.audio.config.sample_counts.clear();
|
||||||
for path in &self.app.audio.config.sample_paths {
|
for path in &self.app.audio.config.sample_paths {
|
||||||
let index = doux::sampling::scan_samples_dir(path);
|
let index = doux::sampling::scan_samples_dir(path);
|
||||||
|
self.app.audio.config.sample_counts.push(index.len());
|
||||||
restart_samples.extend(index);
|
restart_samples.extend(index);
|
||||||
}
|
}
|
||||||
self.app.audio.config.sample_count = restart_samples.len();
|
|
||||||
|
|
||||||
self.audio_sample_pos.store(0, Ordering::Release);
|
self.audio_sample_pos.store(0, Ordering::Release);
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ pub enum AppCommand {
|
|||||||
delta: i32,
|
delta: i32,
|
||||||
},
|
},
|
||||||
AudioTriggerRestart,
|
AudioTriggerRestart,
|
||||||
RemoveLastSamplePath,
|
RemoveSamplePath(usize),
|
||||||
AudioRefreshDevices,
|
AudioRefreshDevices,
|
||||||
|
|
||||||
// Options page
|
// Options page
|
||||||
|
|||||||
@@ -104,7 +104,11 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
||||||
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
||||||
app.audio.config.max_voices = settings.audio.max_voices;
|
app.audio.config.max_voices = settings.audio.max_voices;
|
||||||
app.audio.config.sample_paths = args.samples;
|
app.audio.config.sample_paths = if args.samples.is_empty() {
|
||||||
|
settings.audio.sample_paths.clone()
|
||||||
|
} else {
|
||||||
|
args.samples
|
||||||
|
};
|
||||||
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
||||||
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
||||||
app.audio.config.show_scope = settings.display.show_scope;
|
app.audio.config.show_scope = settings.display.show_scope;
|
||||||
@@ -154,7 +158,7 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
let mut initial_samples = Vec::new();
|
let mut initial_samples = Vec::new();
|
||||||
for path in &app.audio.config.sample_paths {
|
for path in &app.audio.config.sample_paths {
|
||||||
let index = doux::sampling::scan_samples_dir(path);
|
let index = doux::sampling::scan_samples_dir(path);
|
||||||
app.audio.config.sample_count += index.len();
|
app.audio.config.sample_counts.push(index.len());
|
||||||
initial_samples.extend(index);
|
initial_samples.extend(index);
|
||||||
}
|
}
|
||||||
let preload_entries: Vec<(String, std::path::PathBuf)> = initial_samples
|
let preload_entries: Vec<(String, std::path::PathBuf)> = initial_samples
|
||||||
@@ -238,6 +242,7 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
app.evaluate_prelude(&link);
|
||||||
app.mark_all_patterns_dirty();
|
app.mark_all_patterns_dirty();
|
||||||
|
|
||||||
Init {
|
Init {
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
EngineSection::Settings => {
|
EngineSection::Settings => {
|
||||||
ctx.dispatch(AppCommand::AudioSettingPrev);
|
ctx.dispatch(AppCommand::AudioSettingPrev);
|
||||||
}
|
}
|
||||||
|
EngineSection::Samples => {
|
||||||
|
ctx.app.audio.sample_list.move_up();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
KeyCode::Down => match ctx.app.audio.section {
|
KeyCode::Down => match ctx.app.audio.section {
|
||||||
@@ -65,6 +68,10 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
EngineSection::Settings => {
|
EngineSection::Settings => {
|
||||||
ctx.dispatch(AppCommand::AudioSettingNext);
|
ctx.dispatch(AppCommand::AudioSettingNext);
|
||||||
}
|
}
|
||||||
|
EngineSection::Samples => {
|
||||||
|
let count = ctx.app.audio.config.sample_paths.len();
|
||||||
|
ctx.app.audio.sample_list.move_down(count);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
KeyCode::PageUp => {
|
KeyCode::PageUp => {
|
||||||
@@ -128,14 +135,16 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
KeyCode::Char('R') if !ctx.app.plugin_mode => {
|
KeyCode::Char('R') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::AudioTriggerRestart);
|
ctx.dispatch(AppCommand::AudioTriggerRestart);
|
||||||
}
|
}
|
||||||
KeyCode::Char('A') => {
|
KeyCode::Char('A') if ctx.app.audio.section == EngineSection::Samples => {
|
||||||
use crate::state::file_browser::FileBrowserState;
|
use crate::state::file_browser::FileBrowserState;
|
||||||
let state = FileBrowserState::new_load(String::new());
|
let state = FileBrowserState::new_load(String::new());
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(Box::new(state))));
|
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(Box::new(state))));
|
||||||
}
|
}
|
||||||
KeyCode::Char('D') => {
|
KeyCode::Char('D') => {
|
||||||
if ctx.app.audio.section == EngineSection::Samples {
|
if ctx.app.audio.section == EngineSection::Samples {
|
||||||
ctx.dispatch(AppCommand::RemoveLastSamplePath);
|
let cursor = ctx.app.audio.sample_list.cursor;
|
||||||
|
ctx.dispatch(AppCommand::RemoveSamplePath(cursor));
|
||||||
|
ctx.app.save_settings(ctx.link);
|
||||||
} else if !ctx.app.plugin_mode {
|
} else if !ctx.app.plugin_mode {
|
||||||
ctx.dispatch(AppCommand::AudioRefreshDevices);
|
ctx.dispatch(AppCommand::AudioRefreshDevices);
|
||||||
let out_count = ctx.app.audio.output_devices.len();
|
let out_count = ctx.app.audio.output_devices.len();
|
||||||
|
|||||||
@@ -69,9 +69,7 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
|
|
||||||
if ctx.app.ui.show_title {
|
if ctx.app.ui.show_title {
|
||||||
ctx.dispatch(AppCommand::HideTitle);
|
ctx.dispatch(AppCommand::HideTitle);
|
||||||
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
|
return InputResult::Continue;
|
||||||
return InputResult::Continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.dispatch(AppCommand::ClearStatus);
|
ctx.dispatch(AppCommand::ClearStatus);
|
||||||
@@ -134,7 +132,7 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ctrl {
|
if ctrl {
|
||||||
let minimap_timed = MinimapMode::Timed(Instant::now() + Duration::from_millis(250));
|
let minimap_timed = MinimapMode::Timed(Instant::now() + Duration::from_millis(1000));
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
ctx.app.ui.minimap = minimap_timed;
|
ctx.app.ui.minimap = minimap_timed;
|
||||||
@@ -170,7 +168,7 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::F(7) => Some(Page::Script),
|
KeyCode::F(7) => Some(Page::Script),
|
||||||
_ => None,
|
_ => None,
|
||||||
} {
|
} {
|
||||||
ctx.app.ui.minimap = MinimapMode::Timed(Instant::now() + Duration::from_millis(250));
|
ctx.app.ui.minimap = MinimapMode::Timed(Instant::now() + Duration::from_millis(1000));
|
||||||
ctx.dispatch(AppCommand::GoToPage(page));
|
ctx.dispatch(AppCommand::GoToPage(page));
|
||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
@@ -226,21 +224,25 @@ fn load_project_samples(ctx: &mut InputContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut total_count = 0;
|
let mut total_count = 0;
|
||||||
|
let mut counts = Vec::new();
|
||||||
let mut all_preload_entries = Vec::new();
|
let mut all_preload_entries = Vec::new();
|
||||||
for path in &paths {
|
for path in &paths {
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
let index = doux::sampling::scan_samples_dir(path);
|
let index = doux::sampling::scan_samples_dir(path);
|
||||||
let count = index.len();
|
let count = index.len();
|
||||||
total_count += count;
|
total_count += count;
|
||||||
|
counts.push(count);
|
||||||
for e in &index {
|
for e in &index {
|
||||||
all_preload_entries.push((e.name.clone(), e.path.clone()));
|
all_preload_entries.push((e.name.clone(), e.path.clone()));
|
||||||
}
|
}
|
||||||
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
|
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
|
||||||
|
} else {
|
||||||
|
counts.push(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.app.audio.config.sample_paths = paths;
|
ctx.app.audio.config.sample_paths = paths;
|
||||||
ctx.app.audio.config.sample_count = total_count;
|
ctx.app.audio.config.sample_counts = counts;
|
||||||
|
|
||||||
for path in &ctx.app.audio.config.sample_paths {
|
for path in &ctx.app.audio.config.sample_paths {
|
||||||
if let Some(sf2_path) = doux::soundfont::find_sf2_file(path) {
|
if let Some(sf2_path) = doux::soundfont::find_sf2_file(path) {
|
||||||
|
|||||||
@@ -245,8 +245,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
if let Some(sf2_path) = doux::soundfont::find_sf2_file(&path) {
|
if let Some(sf2_path) = doux::soundfont::find_sf2_file(&path) {
|
||||||
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSoundfont(sf2_path));
|
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSoundfont(sf2_path));
|
||||||
}
|
}
|
||||||
ctx.app.audio.config.sample_count += count;
|
ctx.app.audio.add_sample_path(path, count);
|
||||||
ctx.app.audio.add_sample_path(path);
|
|
||||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||||
let sr = ctx.app.audio.config.sample_rate;
|
let sr = ctx.app.audio.config.sample_rate;
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
@@ -256,6 +255,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
})
|
})
|
||||||
.expect("failed to spawn preload thread");
|
.expect("failed to spawn preload thread");
|
||||||
}
|
}
|
||||||
|
ctx.app.save_settings(ctx.link);
|
||||||
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
||||||
ctx.dispatch(AppCommand::CloseModal);
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -134,11 +134,12 @@ fn main() -> io::Result<()> {
|
|||||||
stream_error_rx = new_error_rx;
|
stream_error_rx = new_error_rx;
|
||||||
|
|
||||||
let mut restart_samples = Vec::new();
|
let mut restart_samples = Vec::new();
|
||||||
|
app.audio.config.sample_counts.clear();
|
||||||
for path in &app.audio.config.sample_paths {
|
for path in &app.audio.config.sample_paths {
|
||||||
let index = doux::sampling::scan_samples_dir(path);
|
let index = doux::sampling::scan_samples_dir(path);
|
||||||
|
app.audio.config.sample_counts.push(index.len());
|
||||||
restart_samples.extend(index);
|
restart_samples.extend(index);
|
||||||
}
|
}
|
||||||
app.audio.config.sample_count = restart_samples.len();
|
|
||||||
|
|
||||||
audio_sample_pos.store(0, Ordering::Relaxed);
|
audio_sample_pos.store(0, Ordering::Relaxed);
|
||||||
|
|
||||||
@@ -277,9 +278,6 @@ fn main() -> io::Result<()> {
|
|||||||
app.metrics.nudge_ms = nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;
|
app.metrics.nudge_ms = nudge_us.load(Ordering::Relaxed) as f64 / 1000.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let seq_snapshot = sequencer.snapshot();
|
|
||||||
app.metrics.event_count = seq_snapshot.event_count;
|
|
||||||
|
|
||||||
app.flush_dirty_patterns(&sequencer.cmd_tx);
|
app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||||
app.flush_dirty_script(&sequencer.cmd_tx);
|
app.flush_dirty_script(&sequencer.cmd_tx);
|
||||||
app.flush_queued_changes(&sequencer.cmd_tx);
|
app.flush_queued_changes(&sequencer.cmd_tx);
|
||||||
@@ -288,6 +286,9 @@ fn main() -> io::Result<()> {
|
|||||||
app.audio.config.refresh_rate.millis(),
|
app.audio.config.refresh_rate.millis(),
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
|
let seq_snapshot = sequencer.snapshot();
|
||||||
|
app.metrics.event_count = seq_snapshot.event_count;
|
||||||
|
|
||||||
if had_event {
|
if had_event {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) => {
|
Event::Key(key) => {
|
||||||
@@ -340,7 +341,7 @@ fn main() -> io::Result<()> {
|
|||||||
|| app.ui.modal_fx.borrow().is_some()
|
|| app.ui.modal_fx.borrow().is_some()
|
||||||
|| app.ui.title_fx.borrow().is_some()
|
|| app.ui.title_fx.borrow().is_some()
|
||||||
|| app.ui.nav_fx.borrow().is_some();
|
|| app.ui.nav_fx.borrow().is_some();
|
||||||
if app.playback.playing || had_event || app.ui.show_title || effects_active {
|
if app.playback.playing || had_event || app.ui.show_title || effects_active || app.ui.show_minimap() {
|
||||||
if app.ui.show_title {
|
if app.ui.show_title {
|
||||||
app.ui.sparkles.tick(terminal.get_frame().area());
|
app.ui.sparkles.tick(terminal.get_frame().area());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,12 +97,14 @@ pub fn delete_step(project: &mut Project, bank: usize, pattern: usize, step: usi
|
|||||||
if s.source == Some(step as u8) {
|
if s.source == Some(step as u8) {
|
||||||
s.source = None;
|
s.source = None;
|
||||||
s.script.clear();
|
s.script.clear();
|
||||||
|
s.name = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set_step_script(project, bank, pattern, step, String::new());
|
set_step_script(project, bank, pattern, step, String::new());
|
||||||
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
||||||
s.source = None;
|
s.source = None;
|
||||||
|
s.name = None;
|
||||||
}
|
}
|
||||||
PatternEdit::new(bank, pattern)
|
PatternEdit::new(bank, pattern)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::state::{ColorScheme, MainLayout};
|
use crate::state::{ColorScheme, MainLayout};
|
||||||
@@ -30,6 +32,8 @@ pub struct AudioSettings {
|
|||||||
pub buffer_size: u32,
|
pub buffer_size: u32,
|
||||||
#[serde(default = "default_max_voices")]
|
#[serde(default = "default_max_voices")]
|
||||||
pub max_voices: usize,
|
pub max_voices: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sample_paths: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_voices() -> usize { 32 }
|
fn default_max_voices() -> usize { 32 }
|
||||||
@@ -97,6 +101,7 @@ impl Default for AudioSettings {
|
|||||||
channels: 2,
|
channels: 2,
|
||||||
buffer_size: 512,
|
buffer_size: 512,
|
||||||
max_voices: 32,
|
max_voices: 32,
|
||||||
|
sample_paths: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ pub struct AudioConfig {
|
|||||||
pub sample_rate: f32,
|
pub sample_rate: f32,
|
||||||
pub host_name: String,
|
pub host_name: String,
|
||||||
pub sample_paths: Vec<PathBuf>,
|
pub sample_paths: Vec<PathBuf>,
|
||||||
pub sample_count: usize,
|
pub sample_counts: Vec<usize>,
|
||||||
pub refresh_rate: RefreshRate,
|
pub refresh_rate: RefreshRate,
|
||||||
pub show_scope: bool,
|
pub show_scope: bool,
|
||||||
pub show_spectrum: bool,
|
pub show_spectrum: bool,
|
||||||
@@ -140,7 +140,7 @@ impl Default for AudioConfig {
|
|||||||
sample_rate: 44100.0,
|
sample_rate: 44100.0,
|
||||||
host_name: String::new(),
|
host_name: String::new(),
|
||||||
sample_paths: Vec::new(),
|
sample_paths: Vec::new(),
|
||||||
sample_count: 0,
|
sample_counts: Vec::new(),
|
||||||
refresh_rate: RefreshRate::default(),
|
refresh_rate: RefreshRate::default(),
|
||||||
show_scope: true,
|
show_scope: true,
|
||||||
show_spectrum: true,
|
show_spectrum: true,
|
||||||
@@ -275,6 +275,7 @@ pub struct AudioSettings {
|
|||||||
pub input_devices: Vec<AudioDeviceInfo>,
|
pub input_devices: Vec<AudioDeviceInfo>,
|
||||||
pub output_list: ListSelectState,
|
pub output_list: ListSelectState,
|
||||||
pub input_list: ListSelectState,
|
pub input_list: ListSelectState,
|
||||||
|
pub sample_list: ListSelectState,
|
||||||
pub restart_pending: bool,
|
pub restart_pending: bool,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub sample_registry: Option<std::sync::Arc<doux::SampleRegistry>>,
|
pub sample_registry: Option<std::sync::Arc<doux::SampleRegistry>>,
|
||||||
@@ -297,6 +298,10 @@ impl Default for AudioSettings {
|
|||||||
cursor: 0,
|
cursor: 0,
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
},
|
},
|
||||||
|
sample_list: ListSelectState {
|
||||||
|
cursor: 0,
|
||||||
|
scroll_offset: 0,
|
||||||
|
},
|
||||||
restart_pending: false,
|
restart_pending: false,
|
||||||
error: None,
|
error: None,
|
||||||
sample_registry: None,
|
sample_registry: None,
|
||||||
@@ -321,6 +326,10 @@ impl AudioSettings {
|
|||||||
cursor: 0,
|
cursor: 0,
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
},
|
},
|
||||||
|
sample_list: ListSelectState {
|
||||||
|
cursor: 0,
|
||||||
|
scroll_offset: 0,
|
||||||
|
},
|
||||||
restart_pending: false,
|
restart_pending: false,
|
||||||
error: None,
|
error: None,
|
||||||
sample_registry: None,
|
sample_registry: None,
|
||||||
@@ -429,14 +438,29 @@ impl AudioSettings {
|
|||||||
self.config.refresh_rate = self.config.refresh_rate.toggle();
|
self.config.refresh_rate = self.config.refresh_rate.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_sample_path(&mut self, path: PathBuf) {
|
pub fn total_sample_count(&self) -> usize {
|
||||||
|
self.config.sample_counts.iter().sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_sample_path(&mut self, path: PathBuf, count: usize) {
|
||||||
if !self.config.sample_paths.contains(&path) {
|
if !self.config.sample_paths.contains(&path) {
|
||||||
self.config.sample_paths.push(path);
|
self.config.sample_paths.push(path);
|
||||||
|
self.config.sample_counts.push(count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_last_sample_path(&mut self) {
|
pub fn remove_sample_path(&mut self, index: usize) {
|
||||||
self.config.sample_paths.pop();
|
if index < self.config.sample_paths.len() {
|
||||||
|
self.config.sample_paths.remove(index);
|
||||||
|
self.config.sample_counts.remove(index);
|
||||||
|
let len = self.config.sample_paths.len();
|
||||||
|
if len == 0 {
|
||||||
|
self.sample_list.cursor = 0;
|
||||||
|
self.sample_list.scroll_offset = 0;
|
||||||
|
} else if self.sample_list.cursor >= len {
|
||||||
|
self.sample_list.cursor = len - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_restart(&mut self) {
|
pub fn trigger_restart(&mut self) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Calculate section heights
|
// Calculate section heights
|
||||||
|
let intro_lines: usize = 3;
|
||||||
let plugin_mode = app.plugin_mode;
|
let plugin_mode = app.plugin_mode;
|
||||||
let devices_lines = if plugin_mode {
|
let devices_lines = if plugin_mode {
|
||||||
0
|
0
|
||||||
@@ -55,17 +56,19 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
devices_section_height(app) as usize
|
devices_section_height(app) as usize
|
||||||
};
|
};
|
||||||
let settings_lines: usize = if plugin_mode { 5 } else { 8 }; // plugin: header(1) + divider(1) + 3 rows
|
let settings_lines: usize = if plugin_mode { 5 } else { 8 }; // plugin: header(1) + divider(1) + 3 rows
|
||||||
let samples_lines: usize = 6; // header(1) + divider(1) + content(3) + hint(1)
|
let sample_content = app.audio.config.sample_paths.len().max(2); // at least 2 for empty message
|
||||||
|
let samples_lines: usize = 2 + sample_content; // header(2) + content
|
||||||
|
|
||||||
let sections_gap = if plugin_mode { 1 } else { 2 }; // 1 gap without devices, 2 gaps with
|
let sections_gap = if plugin_mode { 1 } else { 2 }; // 1 gap without devices, 2 gaps with
|
||||||
let total_lines = devices_lines + settings_lines + samples_lines + sections_gap;
|
let total_lines = intro_lines + 1 + devices_lines + settings_lines + samples_lines + sections_gap;
|
||||||
|
|
||||||
let max_visible = padded.height as usize;
|
let max_visible = padded.height as usize;
|
||||||
|
|
||||||
// Calculate scroll offset based on focused section
|
// Calculate scroll offset based on focused section
|
||||||
let settings_start = if plugin_mode { 0 } else { devices_lines + 1 };
|
let intro_offset = intro_lines + 1;
|
||||||
|
let settings_start = if plugin_mode { intro_offset } else { intro_offset + devices_lines + 1 };
|
||||||
let (focus_start, focus_height) = match app.audio.section {
|
let (focus_start, focus_height) = match app.audio.section {
|
||||||
EngineSection::Devices => (0, devices_lines),
|
EngineSection::Devices => (intro_offset, devices_lines),
|
||||||
EngineSection::Settings => (settings_start, settings_lines),
|
EngineSection::Settings => (settings_start, settings_lines),
|
||||||
EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
|
EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
|
||||||
};
|
};
|
||||||
@@ -86,6 +89,29 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
|
|
||||||
let mut y = viewport_top - scroll_offset as i32;
|
let mut y = viewport_top - scroll_offset as i32;
|
||||||
|
|
||||||
|
// Intro text
|
||||||
|
let intro_top = y;
|
||||||
|
let intro_bottom = y + intro_lines as i32;
|
||||||
|
if intro_bottom > viewport_top && intro_top < viewport_bottom {
|
||||||
|
let clipped_y = intro_top.max(viewport_top) as u16;
|
||||||
|
let clipped_height =
|
||||||
|
(intro_bottom.min(viewport_bottom) - intro_top.max(viewport_top)) as u16;
|
||||||
|
let intro_area = Rect {
|
||||||
|
x: padded.x,
|
||||||
|
y: clipped_y,
|
||||||
|
width: padded.width,
|
||||||
|
height: clipped_height,
|
||||||
|
};
|
||||||
|
let dim = Style::new().fg(theme.engine.dim);
|
||||||
|
let intro = Paragraph::new(vec![
|
||||||
|
Line::from(Span::styled(" Audio devices, settings, and sample paths.", dim)),
|
||||||
|
Line::from(Span::styled(" Supports .wav, .ogg, .mp3 samples and .sf2 soundfonts.", dim)),
|
||||||
|
Line::from(Span::styled(" Press R to restart the audio engine after changes.", dim)),
|
||||||
|
]);
|
||||||
|
frame.render_widget(intro, intro_area);
|
||||||
|
}
|
||||||
|
y += intro_lines as i32 + 1;
|
||||||
|
|
||||||
// Devices section (skip in plugin mode)
|
// Devices section (skip in plugin mode)
|
||||||
if !plugin_mode {
|
if !plugin_mode {
|
||||||
let devices_top = y;
|
let devices_top = y;
|
||||||
@@ -495,21 +521,26 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let section_focused = app.audio.section == EngineSection::Samples;
|
let section_focused = app.audio.section == EngineSection::Samples;
|
||||||
|
|
||||||
let [header_area, content_area, _, hint_area] = Layout::vertical([
|
let [header_area, content_area] = Layout::vertical([
|
||||||
Constraint::Length(2),
|
Constraint::Length(2),
|
||||||
Constraint::Min(1),
|
Constraint::Min(1),
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
])
|
||||||
.areas(area);
|
.areas(area);
|
||||||
|
|
||||||
let path_count = app.audio.config.sample_paths.len();
|
let path_count = app.audio.config.sample_paths.len();
|
||||||
let sample_count = app.audio.config.sample_count;
|
let sample_count: usize = app.audio.total_sample_count();
|
||||||
let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed");
|
let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed");
|
||||||
render_section_header(frame, &header_text, section_focused, header_area);
|
render_section_header(frame, &header_text, section_focused, header_area);
|
||||||
|
|
||||||
let dim = Style::new().fg(theme.engine.dim);
|
let dim = Style::new().fg(theme.engine.dim);
|
||||||
let path_style = Style::new().fg(theme.engine.path);
|
let path_style = Style::new().fg(theme.engine.path);
|
||||||
|
let cursor_style = Style::new()
|
||||||
|
.fg(theme.engine.focused)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
|
|
||||||
|
let cursor = app.audio.sample_list.cursor;
|
||||||
|
let scroll_offset = app.audio.sample_list.scroll_offset;
|
||||||
|
let visible_rows = content_area.height as usize;
|
||||||
|
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
if app.audio.config.sample_paths.is_empty() {
|
if app.audio.config.sample_paths.is_empty() {
|
||||||
@@ -522,35 +553,32 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
dim,
|
dim,
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
for (i, path) in app.audio.config.sample_paths.iter().take(4).enumerate() {
|
for (i, path) in app
|
||||||
|
.audio
|
||||||
|
.config
|
||||||
|
.sample_paths
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(scroll_offset)
|
||||||
|
.take(visible_rows)
|
||||||
|
{
|
||||||
|
let is_cursor = section_focused && i == cursor;
|
||||||
|
let prefix = if is_cursor { "> " } else { " " };
|
||||||
|
let count = app.audio.config.sample_counts.get(i).copied().unwrap_or(0);
|
||||||
let path_str = path.to_string_lossy();
|
let path_str = path.to_string_lossy();
|
||||||
let display = truncate_name(&path_str, 40);
|
let count_str = format!(" ({count})");
|
||||||
|
let max_path = (content_area.width as usize)
|
||||||
|
.saturating_sub(prefix.len() + count_str.len());
|
||||||
|
let display = truncate_name(&path_str, max_path);
|
||||||
|
let style = if is_cursor { cursor_style } else { path_style };
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(format!(" {} ", i + 1), dim),
|
Span::styled(prefix.to_string(), if is_cursor { cursor_style } else { dim }),
|
||||||
Span::styled(display, path_style),
|
Span::styled(display, style),
|
||||||
|
Span::styled(count_str, dim),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
if path_count > 4 {
|
|
||||||
lines.push(Line::from(Span::styled(
|
|
||||||
format!(" ... and {} more", path_count - 4),
|
|
||||||
dim,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
frame.render_widget(Paragraph::new(lines), content_area);
|
frame.render_widget(Paragraph::new(lines), content_area);
|
||||||
|
|
||||||
let hint_style = if section_focused {
|
|
||||||
Style::new().fg(theme.engine.hint_active)
|
|
||||||
} else {
|
|
||||||
Style::new().fg(theme.engine.hint_inactive)
|
|
||||||
};
|
|
||||||
let hint = Line::from(vec![
|
|
||||||
Span::styled("A", hint_style),
|
|
||||||
Span::styled(":add ", Style::new().fg(theme.engine.dim)),
|
|
||||||
Span::styled("D", hint_style),
|
|
||||||
Span::styled(":remove", Style::new().fg(theme.engine.dim)),
|
|
||||||
]);
|
|
||||||
frame.render_widget(Paragraph::new(hint), hint_area);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
||||||
|
|||||||
@@ -551,15 +551,16 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
Page::Patterns => vec![
|
Page::Patterns => vec![
|
||||||
("Enter", "Select"),
|
("Enter", "Select"),
|
||||||
("Space", "Play"),
|
("Space", "Play"),
|
||||||
|
("c", "Commit"),
|
||||||
("r", "Rename"),
|
("r", "Rename"),
|
||||||
("?", "Keys"),
|
("?", "Keys"),
|
||||||
],
|
],
|
||||||
Page::Engine => vec![
|
Page::Engine => vec![
|
||||||
("Tab", "Section"),
|
("Tab", "Section"),
|
||||||
("←→", "Switch/Adjust"),
|
("←→", "Switch/Adjust"),
|
||||||
("Enter", "Select"),
|
("A", "Add Samples"),
|
||||||
|
("D", "Remove"),
|
||||||
("R", "Restart"),
|
("R", "Restart"),
|
||||||
("h", "Hush"),
|
|
||||||
("?", "Keys"),
|
("?", "Keys"),
|
||||||
],
|
],
|
||||||
Page::Options => vec![
|
Page::Options => vec![
|
||||||
|
|||||||
@@ -187,30 +187,30 @@ fn nor_ff() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ifelse_true() {
|
fn ifelse_true() {
|
||||||
expect_int("{ 42 } { 99 } 1 ifelse", 42);
|
expect_int("( 42 ) ( 99 ) 1 ifelse", 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ifelse_false() {
|
fn ifelse_false() {
|
||||||
expect_int("{ 42 } { 99 } 0 ifelse", 99);
|
expect_int("( 42 ) ( 99 ) 0 ifelse", 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_first() {
|
fn select_first() {
|
||||||
expect_int("{ 10 } { 20 } { 30 } 0 select", 10);
|
expect_int("( 10 ) ( 20 ) ( 30 ) 0 select", 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_second() {
|
fn select_second() {
|
||||||
expect_int("{ 10 } { 20 } { 30 } 1 select", 20);
|
expect_int("( 10 ) ( 20 ) ( 30 ) 1 select", 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_third() {
|
fn select_third() {
|
||||||
expect_int("{ 10 } { 20 } { 30 } 2 select", 30);
|
expect_int("( 10 ) ( 20 ) ( 30 ) 2 select", 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_preserves_stack() {
|
fn select_preserves_stack() {
|
||||||
expect_int("5 { 10 } { 20 } 0 select +", 15);
|
expect_int("5 ( 10 ) ( 20 ) 0 select +", 15);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,14 +59,14 @@ fn iter() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_true_on_zero() {
|
fn every_true_on_zero() {
|
||||||
let ctx = ctx_with(|c| c.iter = 0);
|
let ctx = ctx_with(|c| c.iter = 0);
|
||||||
let f = run_ctx("{ 100 } 4 every", &ctx);
|
let f = run_ctx("( 100 ) 4 every", &ctx);
|
||||||
assert_eq!(stack_int(&f), 100);
|
assert_eq!(stack_int(&f), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_true_on_multiple() {
|
fn every_true_on_multiple() {
|
||||||
let ctx = ctx_with(|c| c.iter = 8);
|
let ctx = ctx_with(|c| c.iter = 8);
|
||||||
let f = run_ctx("{ 100 } 4 every", &ctx);
|
let f = run_ctx("( 100 ) 4 every", &ctx);
|
||||||
assert_eq!(stack_int(&f), 100);
|
assert_eq!(stack_int(&f), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +74,14 @@ fn every_true_on_multiple() {
|
|||||||
fn every_false_between() {
|
fn every_false_between() {
|
||||||
for i in 1..4 {
|
for i in 1..4 {
|
||||||
let ctx = ctx_with(|c| c.iter = i);
|
let ctx = ctx_with(|c| c.iter = i);
|
||||||
let f = run_ctx("{ 100 } 4 every", &ctx);
|
let f = run_ctx("( 100 ) 4 every", &ctx);
|
||||||
assert!(f.stack().is_empty(), "iter={} should not execute quotation", i);
|
assert!(f.stack().is_empty(), "iter={} should not execute quotation", i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_zero_count() {
|
fn every_zero_count() {
|
||||||
expect_error("{ 1 } 0 every", "every count must be > 0");
|
expect_error("( 1 ) 0 every", "every count must be > 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -105,7 +105,7 @@ fn bjork_tresillo() {
|
|||||||
// Bresenham(3,8) hits at positions 0, 2, 5
|
// Bresenham(3,8) hits at positions 0, 2, 5
|
||||||
for runs in 0..8 {
|
for runs in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = run_ctx("{ 100 } 3 8 bjork", &ctx);
|
let f = run_ctx("( 100 ) 3 8 bjork", &ctx);
|
||||||
let hit = ((runs + 1) * 3) / 8 != (runs * 3) / 8;
|
let hit = ((runs + 1) * 3) / 8 != (runs * 3) / 8;
|
||||||
if hit {
|
if hit {
|
||||||
assert_eq!(stack_int(&f), 100, "runs={} should hit", runs);
|
assert_eq!(stack_int(&f), 100, "runs={} should hit", runs);
|
||||||
@@ -121,7 +121,7 @@ fn bjork_hit_count() {
|
|||||||
let mut hit_count = 0;
|
let mut hit_count = 0;
|
||||||
for runs in 0..8 {
|
for runs in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = run_ctx("{ 100 } 3 8 bjork", &ctx);
|
let f = run_ctx("( 100 ) 3 8 bjork", &ctx);
|
||||||
if !f.stack().is_empty() {
|
if !f.stack().is_empty() {
|
||||||
hit_count += 1;
|
hit_count += 1;
|
||||||
}
|
}
|
||||||
@@ -132,20 +132,20 @@ fn bjork_hit_count() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn bjork_all_hits() {
|
fn bjork_all_hits() {
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("{ 100 } 8 8 bjork", &ctx);
|
let f = run_ctx("( 100 ) 8 8 bjork", &ctx);
|
||||||
assert_eq!(stack_int(&f), 100);
|
assert_eq!(stack_int(&f), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bjork_zero_hits() {
|
fn bjork_zero_hits() {
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("{ 100 } 0 8 bjork", &ctx);
|
let f = run_ctx("( 100 ) 0 8 bjork", &ctx);
|
||||||
assert!(f.stack().is_empty());
|
assert!(f.stack().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bjork_invalid() {
|
fn bjork_invalid() {
|
||||||
expect_error("{ 1 } 3 0 bjork", "bjork");
|
expect_error("( 1 ) 3 0 bjork", "bjork");
|
||||||
}
|
}
|
||||||
|
|
||||||
// pbjork (iter-based)
|
// pbjork (iter-based)
|
||||||
@@ -155,7 +155,7 @@ fn pbjork_cinquillo() {
|
|||||||
let mut hit_count = 0;
|
let mut hit_count = 0;
|
||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = run_ctx("{ 100 } 5 8 pbjork", &ctx);
|
let f = run_ctx("( 100 ) 5 8 pbjork", &ctx);
|
||||||
if !f.stack().is_empty() {
|
if !f.stack().is_empty() {
|
||||||
hit_count += 1;
|
hit_count += 1;
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ fn pbjork_cinquillo() {
|
|||||||
fn pbjork_wraps() {
|
fn pbjork_wraps() {
|
||||||
let ctx0 = ctx_with(|c| c.iter = 0);
|
let ctx0 = ctx_with(|c| c.iter = 0);
|
||||||
let ctx8 = ctx_with(|c| c.iter = 8);
|
let ctx8 = ctx_with(|c| c.iter = 8);
|
||||||
let f0 = run_ctx("{ 100 } 3 8 pbjork", &ctx0);
|
let f0 = run_ctx("( 100 ) 3 8 pbjork", &ctx0);
|
||||||
let f8 = run_ctx("{ 100 } 3 8 pbjork", &ctx8);
|
let f8 = run_ctx("( 100 ) 3 8 pbjork", &ctx8);
|
||||||
assert_eq!(f0.stack().is_empty(), f8.stack().is_empty());
|
assert_eq!(f0.stack().is_empty(), f8.stack().is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ fn unexpected_semicolon_errors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_executes_quotation() {
|
fn apply_executes_quotation() {
|
||||||
expect_int("5 { 2 * } apply", 10);
|
expect_int("5 ( 2 * ) apply", 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_with_stack_ops() {
|
fn apply_with_stack_ops() {
|
||||||
expect_int("3 4 { + } apply", 7);
|
expect_int("3 4 ( + ) apply", 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -88,12 +88,12 @@ fn apply_non_quotation_errors() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_nested() {
|
fn apply_nested() {
|
||||||
expect_int("2 { { 3 * } apply } apply", 6);
|
expect_int("2 ( ( 3 * ) apply ) apply", 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn define_word_containing_quotation() {
|
fn define_word_containing_quotation() {
|
||||||
expect_int(": dbl { 2 * } apply ; 7 dbl", 14);
|
expect_int(": dbl ( 2 * ) apply ; 7 dbl", 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ fn range_underflow() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gen_basic() {
|
fn gen_basic() {
|
||||||
expect_stack("{ 42 } 3 gen", &[int(42), int(42), int(42)]);
|
expect_stack("( 42 ) 3 gen", &[int(42), int(42), int(42)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -42,7 +42,7 @@ fn gen_with_computation() {
|
|||||||
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
||||||
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
// 0 → dup(0,0) 1+(0,1) → pop 1, stack [0]
|
||||||
// So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s
|
// So we get [0, 1, 1, 1] - the 0 stays, we collect three 1s
|
||||||
expect_stack("0 { dup 1 + } 3 gen", &[int(0), int(1), int(1), int(1)]);
|
expect_stack("0 ( dup 1 + ) 3 gen", &[int(0), int(1), int(1), int(1)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -50,12 +50,12 @@ fn gen_chained() {
|
|||||||
// Start with 1, each iteration: dup, multiply by 2
|
// Start with 1, each iteration: dup, multiply by 2
|
||||||
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
||||||
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
// 1 → dup(1,1) 2*(1,2) → pop 2, stack [1]
|
||||||
expect_stack("1 { dup 2 * } 3 gen", &[int(1), int(2), int(2), int(2)]);
|
expect_stack("1 ( dup 2 * ) 3 gen", &[int(1), int(2), int(2), int(2)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gen_zero() {
|
fn gen_zero() {
|
||||||
expect_stack("{ 1 } 0 gen", &[]);
|
expect_stack("( 1 ) 0 gen", &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -65,17 +65,17 @@ fn gen_underflow() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gen_not_a_number() {
|
fn gen_not_a_number() {
|
||||||
expect_error("{ 1 } gen", "expected number");
|
expect_error("( 1 ) gen", "expected number");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gen_negative() {
|
fn gen_negative() {
|
||||||
expect_error("{ 1 } -1 gen", "gen count must be >= 0");
|
expect_error("( 1 ) -1 gen", "gen count must be >= 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gen_empty_quot_error() {
|
fn gen_empty_quot_error() {
|
||||||
expect_error("{ } 3 gen", "quotation must produce");
|
expect_error("( ) 3 gen", "quotation must produce");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -46,20 +46,20 @@ fn pcycle_by_iter() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn cycle_with_quotations() {
|
fn cycle_with_quotations() {
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
|
let f = run_ctx("5 ( dup ) ( 2 * ) 2 cycle", &ctx);
|
||||||
let stack = f.stack();
|
let stack = f.stack();
|
||||||
assert_eq!(stack.len(), 2);
|
assert_eq!(stack.len(), 2);
|
||||||
assert_eq!(stack_int(&f), 5);
|
assert_eq!(stack_int(&f), 5);
|
||||||
|
|
||||||
let ctx = ctx_with(|c| c.runs = 1);
|
let ctx = ctx_with(|c| c.runs = 1);
|
||||||
let f = run_ctx("5 { dup } { 2 * } 2 cycle", &ctx);
|
let f = run_ctx("5 ( dup ) ( 2 * ) 2 cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 10);
|
assert_eq!(stack_int(&f), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_executes_quotation() {
|
fn cycle_executes_quotation() {
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("10 { 3 + } { 5 + } 2 cycle", &ctx);
|
let f = run_ctx("10 ( 3 + ) ( 5 + ) 2 cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 13);
|
assert_eq!(stack_int(&f), 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +108,11 @@ fn bracket_cycle() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn bracket_with_quotations() {
|
fn bracket_with_quotations() {
|
||||||
let ctx = ctx_with(|c| c.runs = 0);
|
let ctx = ctx_with(|c| c.runs = 0);
|
||||||
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx);
|
let f = run_ctx("5 [ ( 3 + ) ( 5 + ) ] cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 8);
|
assert_eq!(stack_int(&f), 8);
|
||||||
|
|
||||||
let ctx = ctx_with(|c| c.runs = 1);
|
let ctx = ctx_with(|c| c.runs = 1);
|
||||||
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx);
|
let f = run_ctx("5 [ ( 3 + ) ( 5 + ) ] cycle", &ctx);
|
||||||
assert_eq!(stack_int(&f), 10);
|
assert_eq!(stack_int(&f), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ fn index_negative_wraps() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn index_with_quotation() {
|
fn index_with_quotation() {
|
||||||
expect_int("5 [ { 3 + } { 5 + } ] 0 index", 8);
|
expect_int("5 [ ( 3 + ) ( 5 + ) ] 0 index", 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,26 +4,26 @@ use super::harness::*;
|
|||||||
fn quotation_on_stack() {
|
fn quotation_on_stack() {
|
||||||
// Quotation should be pushable to stack
|
// Quotation should be pushable to stack
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let result = f.evaluate("{ 1 2 + }", &default_ctx());
|
let result = f.evaluate("( 1 2 + )", &default_ctx());
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn when_true_executes() {
|
fn when_true_executes() {
|
||||||
let f = run("{ 42 } 1 ?");
|
let f = run("( 42 ) 1 ?");
|
||||||
assert_eq!(stack_int(&f), 42);
|
assert_eq!(stack_int(&f), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn when_false_skips() {
|
fn when_false_skips() {
|
||||||
let f = run("99 { 42 } 0 ?");
|
let f = run("99 ( 42 ) 0 ?");
|
||||||
// Stack should still have 99, quotation not executed
|
// Stack should still have 99, quotation not executed
|
||||||
assert_eq!(stack_int(&f), 99);
|
assert_eq!(stack_int(&f), 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn when_with_arithmetic() {
|
fn when_with_arithmetic() {
|
||||||
let f = run("10 { 5 + } 1 ?");
|
let f = run("10 ( 5 + ) 1 ?");
|
||||||
assert_eq!(stack_int(&f), 15);
|
assert_eq!(stack_int(&f), 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,48 +31,48 @@ fn when_with_arithmetic() {
|
|||||||
fn when_with_every() {
|
fn when_with_every() {
|
||||||
// iter=0, every 2 executes quotation
|
// iter=0, every 2 executes quotation
|
||||||
let ctx = ctx_with(|c| c.iter = 0);
|
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);
|
assert_eq!(stack_int(&f), 100);
|
||||||
|
|
||||||
// iter=1, every 2 skips quotation
|
// iter=1, every 2 skips quotation
|
||||||
let ctx = ctx_with(|c| c.iter = 1);
|
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
|
assert_eq!(stack_int(&f), 50); // quotation not executed
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn when_with_chance_deterministic() {
|
fn when_with_chance_deterministic() {
|
||||||
// 1.0 chance always executes quotation
|
// 1.0 chance always executes quotation
|
||||||
let f = run("{ 42 } 1.0 chance");
|
let f = run("( 42 ) 1.0 chance");
|
||||||
assert_eq!(stack_int(&f), 42);
|
assert_eq!(stack_int(&f), 42);
|
||||||
|
|
||||||
// 0.0 chance never executes quotation
|
// 0.0 chance never executes quotation
|
||||||
let f = run("99 { 42 } 0.0 chance");
|
let f = run("99 ( 42 ) 0.0 chance");
|
||||||
assert_eq!(stack_int(&f), 99);
|
assert_eq!(stack_int(&f), 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_quotations() {
|
fn nested_quotations() {
|
||||||
let f = run("{ { 42 } 1 ? } 1 ?");
|
let f = run("( ( 42 ) 1 ? ) 1 ?");
|
||||||
assert_eq!(stack_int(&f), 42);
|
assert_eq!(stack_int(&f), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quotation_with_param() {
|
fn quotation_with_param() {
|
||||||
let outputs = expect_outputs(r#""kick" s { 2 distort } 1 ? ."#, 1);
|
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 1 ? ."#, 1);
|
||||||
assert!(outputs[0].contains("distort/2"));
|
assert!(outputs[0].contains("distort/2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quotation_skips_param() {
|
fn quotation_skips_param() {
|
||||||
let outputs = expect_outputs(r#""kick" s { 2 distort } 0 ? ."#, 1);
|
let outputs = expect_outputs(r#""kick" s ( 2 distort ) 0 ? ."#, 1);
|
||||||
assert!(!outputs[0].contains("distort"));
|
assert!(!outputs[0].contains("distort"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quotation_with_emit() {
|
fn quotation_with_emit() {
|
||||||
// When true, . should fire
|
// When true, . should fire
|
||||||
let outputs = expect_outputs(r#""kick" s { . } 1 ?"#, 1);
|
let outputs = expect_outputs(r#""kick" s ( . ) 1 ?"#, 1);
|
||||||
assert!(outputs[0].contains("kick"));
|
assert!(outputs[0].contains("kick"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ fn quotation_skips_emit() {
|
|||||||
// When false, . should not fire
|
// When false, . should not fire
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(r#""kick" s { . } 0 ?"#, &default_ctx())
|
.evaluate(r#""kick" s ( . ) 0 ?"#, &default_ctx())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// No output since . was skipped and no implicit emit
|
// No output since . was skipped and no implicit emit
|
||||||
assert_eq!(outputs.len(), 0);
|
assert_eq!(outputs.len(), 0);
|
||||||
@@ -94,22 +94,22 @@ fn missing_quotation_error() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unclosed_quotation_error() {
|
fn unclosed_quotation_error() {
|
||||||
expect_error("{ 1 2", "missing }");
|
expect_error("( 1 2", "missing )");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unexpected_close_error() {
|
fn unexpected_close_error() {
|
||||||
expect_error("1 2 }", "unexpected }");
|
expect_error("1 2 )", "unexpected )");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_with_quotation_integration() {
|
fn every_with_quotation_integration() {
|
||||||
// { 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 {
|
for iter in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(r#""kick" s { 2 distort } 2 every ."#, &ctx)
|
.evaluate(r#""kick" s ( 2 distort ) 2 every ."#, &ctx)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if iter % 2 == 0 {
|
if iter % 2 == 0 {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -134,7 +134,7 @@ fn bjork_with_sound() {
|
|||||||
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
|
let ctx = ctx_with(|c| c.runs = 2); // position 2 is a hit for (3,8)
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(r#""kick" s { 2 distort } 3 8 bjork ."#, &ctx)
|
.evaluate(r#""kick" s ( 2 distort ) 3 8 bjork ."#, &ctx)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(outputs[0].contains("distort/2"));
|
assert!(outputs[0].contains("distort/2"));
|
||||||
}
|
}
|
||||||
@@ -143,13 +143,13 @@ fn bjork_with_sound() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unless_false_executes() {
|
fn unless_false_executes() {
|
||||||
let f = run("{ 42 } 0 !?");
|
let f = run("( 42 ) 0 !?");
|
||||||
assert_eq!(stack_int(&f), 42);
|
assert_eq!(stack_int(&f), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unless_true_skips() {
|
fn unless_true_skips() {
|
||||||
let f = run("99 { 42 } 1 !?");
|
let f = run("99 ( 42 ) 1 !?");
|
||||||
assert_eq!(stack_int(&f), 99);
|
assert_eq!(stack_int(&f), 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ fn when_and_unless_complementary() {
|
|||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f
|
let outputs = f
|
||||||
.evaluate(
|
.evaluate(
|
||||||
r#""kick" s { 2 distort } iter 2 mod 0 = ? { 4 distort } iter 2 mod 0 = !? ."#,
|
r#""kick" s ( 2 distort ) iter 2 mod 0 = ? ( 4 distort ) iter 2 mod 0 = !? ."#,
|
||||||
&ctx,
|
&ctx,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ fn coin_binary() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn chance_zero() {
|
fn chance_zero() {
|
||||||
// 0.0 probability should never execute the quotation
|
// 0.0 probability should never execute the quotation
|
||||||
let f = run("99 { 42 } 0.0 chance");
|
let f = run("99 ( 42 ) 0.0 chance");
|
||||||
assert_eq!(stack_int(&f), 99); // quotation not executed, 99 still on stack
|
assert_eq!(stack_int(&f), 99); // quotation not executed, 99 still on stack
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chance_one() {
|
fn chance_one() {
|
||||||
// 1.0 probability should always execute the quotation
|
// 1.0 probability should always execute the quotation
|
||||||
let f = run("{ 42 } 1.0 chance");
|
let f = run("( 42 ) 1.0 chance");
|
||||||
assert_eq!(stack_int(&f), 42);
|
assert_eq!(stack_int(&f), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +281,7 @@ fn wchoose_negative_weight() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wchoose_quotation() {
|
fn wchoose_quotation() {
|
||||||
let f = forth_seeded(42);
|
let f = forth_seeded(42);
|
||||||
f.evaluate("{ 10 } 0.0 { 20 } 1.0 2 wchoose", &default_ctx())
|
f.evaluate("( 10 ) 0.0 ( 20 ) 1.0 2 wchoose", &default_ctx())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(stack_int(&f), 20);
|
assert_eq!(stack_int(&f), 20);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ fn cycle_picks_by_runs() {
|
|||||||
for runs in 0..4 {
|
for runs in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s { . } { } 2 cycle"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap();
|
||||||
if runs % 2 == 0 {
|
if runs % 2 == 0 {
|
||||||
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
|
||||||
} else {
|
} else {
|
||||||
@@ -113,7 +113,7 @@ fn pcycle_picks_by_iter() {
|
|||||||
for iter in 0..4 {
|
for iter in 0..4 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s { . } { } 2 pcycle"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
|
||||||
if iter % 2 == 0 {
|
if iter % 2 == 0 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -128,7 +128,7 @@ fn cycle_with_sounds() {
|
|||||||
let ctx = ctx_with(|c| c.runs = runs);
|
let ctx = ctx_with(|c| c.runs = runs);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(
|
let outputs = f.evaluate(
|
||||||
r#"{ "kick" s . } { "hat" s . } { "snare" s . } 3 cycle"#,
|
r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#,
|
||||||
&ctx
|
&ctx
|
||||||
).unwrap();
|
).unwrap();
|
||||||
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
|
||||||
@@ -346,7 +346,7 @@ fn every_offset_fires_at_offset() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s { . } 4 2 every+"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap();
|
||||||
if iter % 4 == 2 {
|
if iter % 4 == 2 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -361,7 +361,7 @@ fn every_offset_wraps_large_offset() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s { . } 4 6 every+"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap();
|
||||||
if iter % 4 == 2 {
|
if iter % 4 == 2 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -375,7 +375,7 @@ fn except_offset_inverse() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let outputs = f.evaluate(r#""kick" s { . } 4 2 except+"#, &ctx).unwrap();
|
let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap();
|
||||||
if iter % 4 != 2 {
|
if iter % 4 != 2 {
|
||||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||||
} else {
|
} else {
|
||||||
@@ -389,8 +389,8 @@ fn every_offset_zero_is_same_as_every() {
|
|||||||
for iter in 0..8 {
|
for iter in 0..8 {
|
||||||
let ctx = ctx_with(|c| c.iter = iter);
|
let ctx = ctx_with(|c| c.iter = iter);
|
||||||
let f = forth();
|
let f = forth();
|
||||||
let a = f.evaluate(r#""kick" s { . } 3 every"#, &ctx).unwrap();
|
let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap();
|
||||||
let b = f.evaluate(r#""kick" s { . } 3 0 every+"#, &ctx).unwrap();
|
let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap();
|
||||||
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user