Compare commits
2 Commits
78b20b5ff9
...
f0de312d6b
| Author | SHA1 | Date | |
|---|---|---|---|
| f0de312d6b | |||
| 7632bc76f7 |
@@ -143,6 +143,19 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
||||
} else if word == "}" {
|
||||
return Err("unexpected }".into());
|
||||
} else if word == "[" {
|
||||
let (bracket_ops, consumed, end_span) =
|
||||
compile_bracket(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
ops.push(Op::Mark);
|
||||
ops.extend(bracket_ops);
|
||||
let count_span = SourceSpan {
|
||||
start: span.start,
|
||||
end: end_span.end,
|
||||
};
|
||||
ops.push(Op::Count(Some(count_span)));
|
||||
} else if word == "]" {
|
||||
return Err("unexpected ]".into());
|
||||
} else if word == ":" {
|
||||
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
@@ -211,6 +224,38 @@ fn compile_quotation(
|
||||
Ok((quote_ops, end_idx + 1, end_span))
|
||||
}
|
||||
|
||||
fn compile_bracket(
|
||||
tokens: &[Token],
|
||||
dict: &Dictionary,
|
||||
) -> Result<(Vec<Op>, usize, SourceSpan), String> {
|
||||
let mut depth = 1;
|
||||
let mut end_idx = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"[" => depth += 1,
|
||||
"]" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_idx = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let end_idx = end_idx.ok_or("missing ]")?;
|
||||
let end_span = match &tokens[end_idx] {
|
||||
Token::Word(_, span) => *span,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let body_ops = compile(&tokens[..end_idx], dict)?;
|
||||
Ok((body_ops, end_idx + 1, end_span))
|
||||
}
|
||||
|
||||
fn token_span(tok: &Token) -> Option<SourceSpan> {
|
||||
match tok {
|
||||
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
|
||||
|
||||
@@ -76,6 +76,7 @@ pub enum Op {
|
||||
PCycle(Option<SourceSpan>),
|
||||
Choose(Option<SourceSpan>),
|
||||
Bounce(Option<SourceSpan>),
|
||||
PBounce(Option<SourceSpan>),
|
||||
WChoose(Option<SourceSpan>),
|
||||
ChanceExec(Option<SourceSpan>),
|
||||
ProbExec(Option<SourceSpan>),
|
||||
@@ -84,6 +85,9 @@ pub enum Op {
|
||||
Ftom,
|
||||
SetTempo,
|
||||
Every(Option<SourceSpan>),
|
||||
Except(Option<SourceSpan>),
|
||||
EveryOffset(Option<SourceSpan>),
|
||||
ExceptOffset(Option<SourceSpan>),
|
||||
Bjork(Option<SourceSpan>),
|
||||
PBjork(Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
@@ -136,4 +140,8 @@ pub enum Op {
|
||||
MidiStart,
|
||||
MidiStop,
|
||||
MidiContinue,
|
||||
// Bracket syntax (mark/count for auto-counting)
|
||||
Mark,
|
||||
Count(Option<SourceSpan>),
|
||||
Index(Option<SourceSpan>),
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ impl Forth {
|
||||
var_writes: &mut HashMap<String, Value>,
|
||||
) -> Result<(), String> {
|
||||
let mut pc = 0;
|
||||
let mut marks: Vec<usize> = Vec::new();
|
||||
let trace_cell = std::cell::RefCell::new(trace);
|
||||
let var_writes_cell = std::cell::RefCell::new(Some(var_writes));
|
||||
|
||||
@@ -798,16 +799,20 @@ impl Forth {
|
||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::Bounce(word_span) => {
|
||||
Op::Bounce(word_span) | Op::PBounce(word_span) => {
|
||||
let count = pop_int(stack)? as usize;
|
||||
if count == 0 {
|
||||
return Err("bounce count must be > 0".into());
|
||||
}
|
||||
let counter = match &ops[pc] {
|
||||
Op::Bounce(_) => ctx.runs,
|
||||
_ => ctx.iter,
|
||||
};
|
||||
let idx = if count == 1 {
|
||||
0
|
||||
} else {
|
||||
let period = 2 * (count - 1);
|
||||
let raw = ctx.runs % period;
|
||||
let raw = counter % period;
|
||||
if raw < count { raw } else { period - raw }
|
||||
};
|
||||
if let Some(span) = word_span {
|
||||
@@ -894,6 +899,47 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
|
||||
Op::Except(word_span) => {
|
||||
let n = pop_int(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
if n <= 0 {
|
||||
return Err("except count must be > 0".into());
|
||||
}
|
||||
let result = ctx.iter as i64 % n != 0;
|
||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
||||
if result {
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::EveryOffset(word_span) => {
|
||||
let offset = pop_int(stack)?;
|
||||
let n = pop_int(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
if n <= 0 {
|
||||
return Err("every+ count must be > 0".into());
|
||||
}
|
||||
let result = ctx.iter as i64 % n == offset.rem_euclid(n);
|
||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
||||
if result {
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::ExceptOffset(word_span) => {
|
||||
let offset = pop_int(stack)?;
|
||||
let n = pop_int(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
if n <= 0 {
|
||||
return Err("except+ count must be > 0".into());
|
||||
}
|
||||
let result = ctx.iter as i64 % n != offset.rem_euclid(n);
|
||||
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
|
||||
if result {
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
Op::Bjork(word_span) | Op::PBjork(word_span) => {
|
||||
let n = pop_int(stack)?;
|
||||
let k = pop_int(stack)?;
|
||||
@@ -1541,6 +1587,29 @@ impl Forth {
|
||||
.unwrap_or(0);
|
||||
stack.push(Value::Int(val as i64, None));
|
||||
}
|
||||
Op::Mark => {
|
||||
marks.push(stack.len());
|
||||
}
|
||||
Op::Count(span) => {
|
||||
let mark = marks.pop().ok_or("count without mark")?;
|
||||
stack.push(Value::Int((stack.len() - mark) as i64, *span));
|
||||
}
|
||||
Op::Index(word_span) => {
|
||||
let idx = pop_int(stack)?;
|
||||
let count = pop_int(stack)? as usize;
|
||||
if count == 0 {
|
||||
return Err("index count must be > 0".into());
|
||||
}
|
||||
let resolved_idx = ((idx % count as i64 + count as i64) % count as i64) as usize;
|
||||
if let Some(span) = word_span {
|
||||
if stack.len() >= count {
|
||||
let start = stack.len() - count;
|
||||
let selected = &stack[start + resolved_idx];
|
||||
record_resolved_from_value(&trace_cell, Some(*span), selected);
|
||||
}
|
||||
}
|
||||
drain_select_run(count, resolved_idx, stack, outputs, cmd)?;
|
||||
}
|
||||
Op::Forget => {
|
||||
let name = pop(stack)?;
|
||||
self.dict.lock().remove(name.as_str()?);
|
||||
|
||||
@@ -67,8 +67,12 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"pcycle" => Op::PCycle(None),
|
||||
"choose" => Op::Choose(None),
|
||||
"bounce" => Op::Bounce(None),
|
||||
"pbounce" => Op::PBounce(None),
|
||||
"wchoose" => Op::WChoose(None),
|
||||
"every" => Op::Every(None),
|
||||
"except" => Op::Except(None),
|
||||
"every+" => Op::EveryOffset(None),
|
||||
"except+" => Op::ExceptOffset(None),
|
||||
"bjork" => Op::Bjork(None),
|
||||
"pbjork" => Op::PBjork(None),
|
||||
"chance" => Op::ChanceExec(None),
|
||||
@@ -110,6 +114,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"mstop" => Op::MidiStop,
|
||||
"mcont" => Op::MidiContinue,
|
||||
"forget" => Op::Forget,
|
||||
"index" => Op::Index(None),
|
||||
"key!" => Op::SetKey,
|
||||
"tp" => Op::Transpose,
|
||||
"inv" => Op::Invert,
|
||||
@@ -203,9 +208,10 @@ fn attach_span(op: &mut Op, span: SourceSpan) {
|
||||
match op {
|
||||
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
|
||||
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
|
||||
| Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
|
||||
| Op::Every(s)
|
||||
| Op::Bjork(s) | Op::PBjork(s) => *s = Some(span),
|
||||
| Op::Bounce(s) | Op::PBounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
|
||||
| Op::Every(s) | Op::Except(s) | Op::EveryOffset(s) | Op::ExceptOffset(s)
|
||||
| Op::Bjork(s) | Op::PBjork(s)
|
||||
| Op::Count(s) | Op::Index(s) => *s = Some(span),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,4 +959,45 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Compressor
|
||||
Word {
|
||||
name: "comp",
|
||||
aliases: &[],
|
||||
category: "Compressor",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sidechain duck amount (0-1)",
|
||||
example: "0.8 comp",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "compattack",
|
||||
aliases: &["cattack"],
|
||||
category: "Compressor",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set compressor attack time in seconds",
|
||||
example: "0.01 compattack",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "comprelease",
|
||||
aliases: &["crelease"],
|
||||
category: "Compressor",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set compressor release time in seconds",
|
||||
example: "0.15 comprelease",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "comporbit",
|
||||
aliases: &["corbit"],
|
||||
category: "Compressor",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sidechain source orbit",
|
||||
example: "0 comporbit",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -113,6 +113,26 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pbounce",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n -- selected)",
|
||||
desc: "Ping-pong cycle through n items by pattern iteration",
|
||||
example: "60 64 67 72 4 pbounce",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "index",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n idx -- selected)",
|
||||
desc: "Select item at explicit index",
|
||||
example: "[ c4 e4 g4 ] step index",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "wchoose",
|
||||
aliases: &[],
|
||||
@@ -204,6 +224,36 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "except",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n --)",
|
||||
desc: "Execute quotation on all iterations except every nth",
|
||||
example: "{ 2 distort } 4 except",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "every+",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n offset --)",
|
||||
desc: "Execute quotation every nth iteration with phase offset",
|
||||
example: "{ snare } 4 2 every+ => fires at iter 2, 6, 10...",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "except+",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n offset --)",
|
||||
desc: "Skip quotation every nth iteration with phase offset",
|
||||
example: "{ snare } 4 2 except+ => skips at iter 2, 6, 10...",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "bjork",
|
||||
aliases: &[],
|
||||
|
||||
@@ -13,6 +13,7 @@ pub struct Lissajous<'a> {
|
||||
left: &'a [f32],
|
||||
right: &'a [f32],
|
||||
color: Option<Color>,
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl<'a> Lissajous<'a> {
|
||||
@@ -21,6 +22,7 @@ impl<'a> Lissajous<'a> {
|
||||
left,
|
||||
right,
|
||||
color: None,
|
||||
gain: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +30,11 @@ impl<'a> Lissajous<'a> {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Lissajous<'_> {
|
||||
@@ -43,14 +50,6 @@ impl Widget for Lissajous<'_> {
|
||||
let fine_height = height * 4;
|
||||
let len = self.left.len().min(self.right.len());
|
||||
|
||||
let peak = self
|
||||
.left
|
||||
.iter()
|
||||
.chain(self.right.iter())
|
||||
.map(|s| s.abs())
|
||||
.fold(0.0f32, f32::max);
|
||||
let gain = if peak > 0.001 { 1.0 / peak } else { 1.0 };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
@@ -58,8 +57,8 @@ impl Widget for Lissajous<'_> {
|
||||
patterns.resize(size, 0);
|
||||
|
||||
for i in 0..len {
|
||||
let l = (self.left[i] * gain).clamp(-1.0, 1.0);
|
||||
let r = (self.right[i] * gain).clamp(-1.0, 1.0);
|
||||
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
|
||||
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
|
||||
|
||||
// X = right channel, Y = left channel (inverted so up = positive)
|
||||
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
|
||||
@@ -41,6 +41,11 @@ impl<'a> Scope<'a> {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scope<'_> {
|
||||
@@ -66,9 +71,6 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
@@ -77,7 +79,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
|
||||
for fine_x in 0..fine_width {
|
||||
let sample_idx = (fine_x * data.len()) / fine_width;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fine_y = fine_y.min(fine_height - 1);
|
||||
@@ -122,9 +124,6 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
@@ -133,7 +132,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
|
||||
for fine_y in 0..fine_height {
|
||||
let sample_idx = (fine_y * data.len()) / fine_height;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 1);
|
||||
|
||||
@@ -8,11 +8,17 @@ const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2
|
||||
|
||||
pub struct Spectrum<'a> {
|
||||
data: &'a [f32; 32],
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl<'a> Spectrum<'a> {
|
||||
pub fn new(data: &'a [f32; 32]) -> Self {
|
||||
Self { data }
|
||||
Self { data, gain: 1.0 }
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +42,7 @@ impl Widget for Spectrum<'_> {
|
||||
if w == 0 {
|
||||
continue;
|
||||
}
|
||||
let bar_height = mag * height;
|
||||
let bar_height = (mag * self.gain).min(1.0) * height;
|
||||
let full_cells = bar_height as usize;
|
||||
let frac = bar_height - full_cells as f32;
|
||||
let frac_idx = (frac * 8.0) as usize;
|
||||
|
||||
39
crates/ratatui/src/theme/everforest.rs
Normal file
39
crates/ratatui/src/theme/everforest.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (45, 53, 59),
|
||||
surface: (52, 62, 68),
|
||||
surface2: (68, 80, 86),
|
||||
fg: (211, 198, 170),
|
||||
fg_dim: (135, 131, 116),
|
||||
fg_muted: (80, 80, 68),
|
||||
accent: (167, 192, 128),
|
||||
red: (230, 126, 128),
|
||||
green: (167, 192, 128),
|
||||
yellow: (219, 188, 127),
|
||||
blue: (127, 187, 179),
|
||||
purple: (214, 153, 182),
|
||||
cyan: (131, 192, 146),
|
||||
orange: (230, 152, 117),
|
||||
tempo_color: (214, 153, 182),
|
||||
bank_color: (127, 187, 179),
|
||||
pattern_color: (131, 192, 146),
|
||||
title_accent: (167, 192, 128),
|
||||
title_author: (127, 187, 179),
|
||||
secondary: (230, 152, 117),
|
||||
link_bright: [
|
||||
(167, 192, 128), (214, 153, 182), (230, 152, 117),
|
||||
(127, 187, 179), (219, 188, 127),
|
||||
],
|
||||
link_dim: [
|
||||
(56, 66, 46), (70, 52, 62), (72, 52, 42),
|
||||
(44, 64, 60), (70, 62, 44),
|
||||
],
|
||||
sparkle: [
|
||||
(167, 192, 128), (230, 152, 117), (131, 192, 146),
|
||||
(214, 153, 182), (219, 188, 127),
|
||||
],
|
||||
meter: [(148, 172, 110), (200, 170, 108), (210, 108, 110)],
|
||||
}
|
||||
}
|
||||
39
crates/ratatui/src/theme/fauve.rs
Normal file
39
crates/ratatui/src/theme/fauve.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (28, 22, 18),
|
||||
surface: (42, 33, 26),
|
||||
surface2: (58, 46, 36),
|
||||
fg: (240, 228, 210),
|
||||
fg_dim: (170, 150, 130),
|
||||
fg_muted: (100, 82, 66),
|
||||
accent: (230, 60, 20),
|
||||
red: (220, 38, 32),
|
||||
green: (30, 170, 80),
|
||||
yellow: (255, 210, 0),
|
||||
blue: (20, 80, 200),
|
||||
purple: (170, 40, 150),
|
||||
cyan: (0, 150, 180),
|
||||
orange: (240, 120, 0),
|
||||
tempo_color: (230, 60, 20),
|
||||
bank_color: (20, 80, 200),
|
||||
pattern_color: (0, 150, 180),
|
||||
title_accent: (230, 60, 20),
|
||||
title_author: (20, 80, 200),
|
||||
secondary: (170, 40, 150),
|
||||
link_bright: [
|
||||
(230, 60, 20), (20, 80, 200), (240, 120, 0),
|
||||
(0, 150, 180), (30, 170, 80),
|
||||
],
|
||||
link_dim: [
|
||||
(72, 24, 10), (10, 28, 65), (76, 40, 6),
|
||||
(6, 48, 58), (14, 54, 28),
|
||||
],
|
||||
sparkle: [
|
||||
(230, 60, 20), (255, 210, 0), (30, 170, 80),
|
||||
(20, 80, 200), (170, 40, 150),
|
||||
],
|
||||
meter: [(26, 152, 72), (235, 190, 0), (200, 34, 28)],
|
||||
}
|
||||
}
|
||||
39
crates/ratatui/src/theme/iceberg.rs
Normal file
39
crates/ratatui/src/theme/iceberg.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (22, 24, 33),
|
||||
surface: (30, 33, 46),
|
||||
surface2: (45, 48, 64),
|
||||
fg: (198, 200, 209),
|
||||
fg_dim: (109, 112, 126),
|
||||
fg_muted: (64, 66, 78),
|
||||
accent: (132, 160, 198),
|
||||
red: (226, 120, 120),
|
||||
green: (180, 190, 130),
|
||||
yellow: (226, 164, 120),
|
||||
blue: (132, 160, 198),
|
||||
purple: (160, 147, 199),
|
||||
cyan: (137, 184, 194),
|
||||
orange: (226, 164, 120),
|
||||
tempo_color: (160, 147, 199),
|
||||
bank_color: (132, 160, 198),
|
||||
pattern_color: (137, 184, 194),
|
||||
title_accent: (132, 160, 198),
|
||||
title_author: (160, 147, 199),
|
||||
secondary: (226, 164, 120),
|
||||
link_bright: [
|
||||
(132, 160, 198), (160, 147, 199), (226, 164, 120),
|
||||
(137, 184, 194), (180, 190, 130),
|
||||
],
|
||||
link_dim: [
|
||||
(45, 55, 70), (55, 50, 68), (70, 55, 42),
|
||||
(46, 62, 66), (58, 62, 44),
|
||||
],
|
||||
sparkle: [
|
||||
(132, 160, 198), (226, 164, 120), (180, 190, 130),
|
||||
(160, 147, 199), (226, 120, 120),
|
||||
],
|
||||
meter: [(160, 175, 115), (210, 150, 105), (200, 105, 105)],
|
||||
}
|
||||
}
|
||||
39
crates/ratatui/src/theme/jaipur.rs
Normal file
39
crates/ratatui/src/theme/jaipur.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (30, 24, 22),
|
||||
surface: (44, 36, 32),
|
||||
surface2: (60, 48, 42),
|
||||
fg: (238, 222, 200),
|
||||
fg_dim: (165, 145, 125),
|
||||
fg_muted: (95, 78, 65),
|
||||
accent: (210, 90, 100),
|
||||
red: (200, 44, 52),
|
||||
green: (30, 160, 120),
|
||||
yellow: (240, 180, 20),
|
||||
blue: (60, 60, 180),
|
||||
purple: (150, 50, 120),
|
||||
cyan: (0, 155, 155),
|
||||
orange: (220, 120, 50),
|
||||
tempo_color: (210, 90, 100),
|
||||
bank_color: (60, 60, 180),
|
||||
pattern_color: (0, 155, 155),
|
||||
title_accent: (210, 90, 100),
|
||||
title_author: (60, 60, 180),
|
||||
secondary: (220, 120, 50),
|
||||
link_bright: [
|
||||
(210, 90, 100), (60, 60, 180), (220, 120, 50),
|
||||
(0, 155, 155), (30, 160, 120),
|
||||
],
|
||||
link_dim: [
|
||||
(66, 30, 34), (22, 22, 58), (70, 40, 18),
|
||||
(6, 48, 48), (12, 50, 38),
|
||||
],
|
||||
sparkle: [
|
||||
(210, 90, 100), (240, 180, 20), (30, 160, 120),
|
||||
(60, 60, 180), (150, 50, 120),
|
||||
],
|
||||
meter: [(26, 144, 106), (222, 164, 18), (184, 40, 46)],
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,22 @@ mod catppuccin_mocha;
|
||||
mod dracula;
|
||||
mod eden;
|
||||
mod ember;
|
||||
mod everforest;
|
||||
mod georges;
|
||||
mod fairyfloss;
|
||||
mod gruvbox_dark;
|
||||
mod hot_dog_stand;
|
||||
mod iceberg;
|
||||
mod jaipur;
|
||||
mod kanagawa;
|
||||
mod letz_light;
|
||||
mod monochrome_black;
|
||||
mod monochrome_white;
|
||||
mod monokai;
|
||||
mod nord;
|
||||
mod fauve;
|
||||
mod pitch_black;
|
||||
mod tropicalia;
|
||||
mod rose_pine;
|
||||
mod tokyo_night;
|
||||
pub mod transform;
|
||||
@@ -51,6 +56,11 @@ pub const THEMES: &[ThemeEntry] = &[
|
||||
ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette },
|
||||
ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette },
|
||||
ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette },
|
||||
ThemeEntry { id: "Iceberg", label: "Iceberg", palette: iceberg::palette },
|
||||
ThemeEntry { id: "Everforest", label: "Everforest", palette: everforest::palette },
|
||||
ThemeEntry { id: "Fauve", label: "Fauve", palette: fauve::palette },
|
||||
ThemeEntry { id: "Tropicalia", label: "Tropicalia", palette: tropicalia::palette },
|
||||
ThemeEntry { id: "Jaipur", label: "Jaipur", palette: jaipur::palette },
|
||||
];
|
||||
|
||||
thread_local! {
|
||||
|
||||
39
crates/ratatui/src/theme/tropicalia.rs
Normal file
39
crates/ratatui/src/theme/tropicalia.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (20, 26, 22),
|
||||
surface: (30, 40, 34),
|
||||
surface2: (44, 56, 48),
|
||||
fg: (235, 225, 200),
|
||||
fg_dim: (155, 145, 120),
|
||||
fg_muted: (85, 80, 62),
|
||||
accent: (230, 50, 120),
|
||||
red: (240, 70, 70),
|
||||
green: (80, 200, 50),
|
||||
yellow: (255, 195, 0),
|
||||
blue: (0, 160, 200),
|
||||
purple: (180, 60, 180),
|
||||
cyan: (0, 200, 170),
|
||||
orange: (255, 140, 30),
|
||||
tempo_color: (230, 50, 120),
|
||||
bank_color: (0, 160, 200),
|
||||
pattern_color: (0, 200, 170),
|
||||
title_accent: (230, 50, 120),
|
||||
title_author: (0, 160, 200),
|
||||
secondary: (255, 140, 30),
|
||||
link_bright: [
|
||||
(230, 50, 120), (0, 160, 200), (255, 140, 30),
|
||||
(0, 200, 170), (80, 200, 50),
|
||||
],
|
||||
link_dim: [
|
||||
(72, 20, 40), (6, 50, 64), (80, 44, 12),
|
||||
(6, 62, 54), (26, 62, 18),
|
||||
],
|
||||
sparkle: [
|
||||
(230, 50, 120), (255, 195, 0), (80, 200, 50),
|
||||
(0, 160, 200), (180, 60, 180),
|
||||
],
|
||||
meter: [(70, 182, 44), (236, 178, 0), (220, 62, 62)],
|
||||
}
|
||||
}
|
||||
@@ -81,9 +81,6 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
let fine_height = height * 4;
|
||||
let len = data.len();
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
patterns.clear();
|
||||
@@ -97,7 +94,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
let mut min_s = f32::MAX;
|
||||
let mut max_s = f32::MIN;
|
||||
for &s in slice {
|
||||
let s = (s * auto_gain).clamp(-1.0, 1.0);
|
||||
let s = (s * gain).clamp(-1.0, 1.0);
|
||||
if s < min_s {
|
||||
min_s = s;
|
||||
}
|
||||
@@ -142,9 +139,6 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
let fine_height = height * 4;
|
||||
let len = data.len();
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
patterns.clear();
|
||||
@@ -158,7 +152,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
let mut min_s = f32::MAX;
|
||||
let mut max_s = f32::MIN;
|
||||
for &s in slice {
|
||||
let s = (s * auto_gain).clamp(-1.0, 1.0);
|
||||
let s = (s * gain).clamp(-1.0, 1.0);
|
||||
if s < min_s {
|
||||
min_s = s;
|
||||
}
|
||||
|
||||
@@ -133,6 +133,12 @@ sine s .
|
||||
{ crash s . } 4 every ;; crash cymbal every 4th iteration
|
||||
```
|
||||
|
||||
`except` is the inverse -- it runs a quotation on all iterations *except* every nth:
|
||||
|
||||
```forth
|
||||
{ 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:
|
||||
|
||||
```forth
|
||||
|
||||
@@ -391,6 +391,8 @@ impl App {
|
||||
AppCommand::ToggleSpectrum => self.audio.config.show_spectrum = !self.audio.config.show_spectrum,
|
||||
AppCommand::ToggleLissajous => self.audio.config.show_lissajous = !self.audio.config.show_lissajous,
|
||||
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
||||
AppCommand::SetGainBoost(g) => self.audio.config.gain_boost = g,
|
||||
AppCommand::ToggleNormalizeViz => self.audio.config.normalize_viz = !self.audio.config.normalize_viz,
|
||||
AppCommand::TogglePerformanceMode => self.ui.performance_mode = !self.ui.performance_mode,
|
||||
|
||||
// Metrics
|
||||
|
||||
@@ -36,6 +36,8 @@ impl App {
|
||||
demo_index: self.ui.demo_index,
|
||||
font: self.ui.font.clone(),
|
||||
zoom_factor: self.ui.zoom_factor,
|
||||
gain_boost: self.audio.config.gain_boost,
|
||||
normalize_viz: self.audio.config.normalize_viz,
|
||||
},
|
||||
link: crate::settings::LinkSettings {
|
||||
enabled: link.is_enabled(),
|
||||
|
||||
@@ -255,6 +255,8 @@ pub enum AppCommand {
|
||||
ToggleSpectrum,
|
||||
ToggleLissajous,
|
||||
TogglePreview,
|
||||
SetGainBoost(f32),
|
||||
ToggleNormalizeViz,
|
||||
TogglePerformanceMode,
|
||||
|
||||
// Metrics
|
||||
|
||||
@@ -111,6 +111,8 @@ pub fn init(args: InitArgs) -> Init {
|
||||
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||
app.audio.config.show_lissajous = settings.display.show_lissajous;
|
||||
app.audio.config.show_preview = settings.display.show_preview;
|
||||
app.audio.config.gain_boost = settings.display.gain_boost;
|
||||
app.audio.config.normalize_viz = settings.display.normalize_viz;
|
||||
app.ui.show_completion = settings.display.show_completion;
|
||||
app.ui.performance_mode = settings.display.performance_mode;
|
||||
app.ui.color_scheme = settings.display.color_scheme;
|
||||
|
||||
@@ -112,8 +112,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
||||
}
|
||||
KeyCode::Char(':') => {
|
||||
let current = (ctx.app.editor_ctx.step + 1).to_string();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::JumpToStep(current)));
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::JumpToStep(String::new())));
|
||||
}
|
||||
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
|
||||
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
|
||||
|
||||
@@ -68,8 +68,10 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
|
||||
if ctx.app.ui.show_title {
|
||||
ctx.dispatch(AppCommand::HideTitle);
|
||||
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.dispatch(AppCommand::ClearStatus);
|
||||
|
||||
|
||||
@@ -25,6 +25,17 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
OptionsFocus::ShowScope => ctx.dispatch(AppCommand::ToggleScope),
|
||||
OptionsFocus::ShowSpectrum => ctx.dispatch(AppCommand::ToggleSpectrum),
|
||||
OptionsFocus::ShowLissajous => ctx.dispatch(AppCommand::ToggleLissajous),
|
||||
OptionsFocus::GainBoost => {
|
||||
const GAINS: &[f32] = &[1.0, 2.0, 4.0, 8.0, 16.0];
|
||||
let pos = GAINS.iter().position(|g| (*g - ctx.app.audio.config.gain_boost).abs() < 0.01).unwrap_or(0);
|
||||
let new_pos = if right {
|
||||
(pos + 1) % GAINS.len()
|
||||
} else {
|
||||
(pos + GAINS.len() - 1) % GAINS.len()
|
||||
};
|
||||
ctx.dispatch(AppCommand::SetGainBoost(GAINS[new_pos]));
|
||||
}
|
||||
OptionsFocus::NormalizeViz => ctx.dispatch(AppCommand::ToggleNormalizeViz),
|
||||
OptionsFocus::ShowCompletion => ctx.dispatch(AppCommand::ToggleCompletion),
|
||||
OptionsFocus::ShowPreview => ctx.dispatch(AppCommand::TogglePreview),
|
||||
OptionsFocus::PerformanceMode => ctx.dispatch(AppCommand::TogglePerformanceMode),
|
||||
|
||||
@@ -60,15 +60,13 @@ pub fn convert_egui_events(ctx: &egui::Context) -> Vec<KeyEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
for event in &ctx.input(|i| i.events.clone()) {
|
||||
if let Some(key_event) = convert_event(event) {
|
||||
events.push(key_event);
|
||||
}
|
||||
convert_event(event, &mut events);
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
fn convert_event(event: &egui::Event) -> Option<KeyEvent> {
|
||||
fn convert_event(event: &egui::Event, events: &mut Vec<KeyEvent>) {
|
||||
match event {
|
||||
egui::Event::Key {
|
||||
key,
|
||||
@@ -77,33 +75,39 @@ fn convert_event(event: &egui::Event) -> Option<KeyEvent> {
|
||||
..
|
||||
} => {
|
||||
if !*pressed {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
let mods = convert_modifiers(*modifiers);
|
||||
// For character keys without ctrl/alt, let Event::Text handle it
|
||||
if is_character_key(*key) && !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
{
|
||||
return None;
|
||||
// For character keys, only handle Ctrl+key (without Alt) as shortcuts.
|
||||
// All other character input (bare, Shift, Alt/Option, AltGr=Ctrl+Alt)
|
||||
// defers to Event::Text which respects the active keyboard layout.
|
||||
if is_character_key(*key) {
|
||||
let ctrl_without_alt =
|
||||
mods.contains(KeyModifiers::CONTROL) && !mods.contains(KeyModifiers::ALT);
|
||||
if !ctrl_without_alt {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(code) = convert_key(*key) {
|
||||
events.push(KeyEvent::new(code, mods));
|
||||
}
|
||||
let code = convert_key(*key)?;
|
||||
Some(KeyEvent::new(code, mods))
|
||||
}
|
||||
egui::Event::Text(text) => {
|
||||
if text.len() == 1 {
|
||||
let c = text.chars().next()?;
|
||||
for c in text.chars() {
|
||||
if !c.is_control() {
|
||||
return Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()));
|
||||
events.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
// egui intercepts Ctrl+C/V/X and converts them to these high-level events
|
||||
// instead of passing through raw Key events (see egui issue #4065).
|
||||
// Synthesize the equivalent KeyEvent so the application's input handler receives them.
|
||||
egui::Event::Copy => Some(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
|
||||
egui::Event::Cut => Some(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)),
|
||||
egui::Event::Paste(_) => Some(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL)),
|
||||
_ => None,
|
||||
egui::Event::Copy => events.push(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
|
||||
egui::Event::Cut => events.push(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)),
|
||||
egui::Event::Paste(_) => {
|
||||
events.push(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +140,14 @@ fn convert_key(key: egui::Key) -> Option<KeyCode> {
|
||||
egui::Key::F10 => KeyCode::F(10),
|
||||
egui::Key::F11 => KeyCode::F(11),
|
||||
egui::Key::F12 => KeyCode::F(12),
|
||||
egui::Key::F13 => KeyCode::F(13),
|
||||
egui::Key::F14 => KeyCode::F(14),
|
||||
egui::Key::F15 => KeyCode::F(15),
|
||||
egui::Key::F16 => KeyCode::F(16),
|
||||
egui::Key::F17 => KeyCode::F(17),
|
||||
egui::Key::F18 => KeyCode::F(18),
|
||||
egui::Key::F19 => KeyCode::F(19),
|
||||
egui::Key::F20 => KeyCode::F(20),
|
||||
egui::Key::A => KeyCode::Char('a'),
|
||||
egui::Key::B => KeyCode::Char('b'),
|
||||
egui::Key::C => KeyCode::Char('c'),
|
||||
@@ -183,6 +195,13 @@ fn convert_key(key: egui::Key) -> Option<KeyCode> {
|
||||
egui::Key::Backslash => KeyCode::Char('\\'),
|
||||
egui::Key::Backtick => KeyCode::Char('`'),
|
||||
egui::Key::Quote => KeyCode::Char('\''),
|
||||
egui::Key::Colon => KeyCode::Char(':'),
|
||||
egui::Key::Pipe => KeyCode::Char('|'),
|
||||
egui::Key::Questionmark => KeyCode::Char('?'),
|
||||
egui::Key::Exclamationmark => KeyCode::Char('!'),
|
||||
egui::Key::OpenCurlyBracket => KeyCode::Char('{'),
|
||||
egui::Key::CloseCurlyBracket => KeyCode::Char('}'),
|
||||
egui::Key::Plus => KeyCode::Char('+'),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -252,5 +271,12 @@ fn is_character_key(key: egui::Key) -> bool {
|
||||
| egui::Key::Backslash
|
||||
| egui::Key::Backtick
|
||||
| egui::Key::Quote
|
||||
| egui::Key::Colon
|
||||
| egui::Key::Pipe
|
||||
| egui::Key::Questionmark
|
||||
| egui::Key::Exclamationmark
|
||||
| egui::Key::OpenCurlyBracket
|
||||
| egui::Key::CloseCurlyBracket
|
||||
| egui::Key::Plus
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ pub struct DisplaySettings {
|
||||
pub load_demo_on_startup: bool,
|
||||
#[serde(default)]
|
||||
pub demo_index: usize,
|
||||
#[serde(default = "default_gain_boost")]
|
||||
pub gain_boost: f32,
|
||||
#[serde(default)]
|
||||
pub normalize_viz: bool,
|
||||
}
|
||||
|
||||
fn default_font() -> String {
|
||||
@@ -74,6 +78,10 @@ fn default_zoom() -> f32 {
|
||||
1.5
|
||||
}
|
||||
|
||||
fn default_gain_boost() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LinkSettings {
|
||||
pub enabled: bool,
|
||||
@@ -114,6 +122,8 @@ impl Default for DisplaySettings {
|
||||
onboarding_dismissed: Vec::new(),
|
||||
load_demo_on_startup: true,
|
||||
demo_index: 0,
|
||||
gain_boost: 1.0,
|
||||
normalize_viz: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@ pub struct AudioConfig {
|
||||
pub show_spectrum: bool,
|
||||
pub show_lissajous: bool,
|
||||
pub show_preview: bool,
|
||||
pub gain_boost: f32,
|
||||
pub normalize_viz: bool,
|
||||
pub layout: MainLayout,
|
||||
}
|
||||
|
||||
@@ -105,6 +107,8 @@ impl Default for AudioConfig {
|
||||
show_spectrum: true,
|
||||
show_lissajous: true,
|
||||
show_preview: true,
|
||||
gain_boost: 1.0,
|
||||
normalize_viz: false,
|
||||
layout: MainLayout::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ pub enum OptionsFocus {
|
||||
ShowScope,
|
||||
ShowSpectrum,
|
||||
ShowLissajous,
|
||||
GainBoost,
|
||||
NormalizeViz,
|
||||
ShowCompletion,
|
||||
ShowPreview,
|
||||
PerformanceMode,
|
||||
@@ -40,6 +42,8 @@ impl CyclicEnum for OptionsFocus {
|
||||
Self::ShowScope,
|
||||
Self::ShowSpectrum,
|
||||
Self::ShowLissajous,
|
||||
Self::GainBoost,
|
||||
Self::NormalizeViz,
|
||||
Self::ShowCompletion,
|
||||
Self::ShowPreview,
|
||||
Self::PerformanceMode,
|
||||
@@ -96,30 +100,32 @@ const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
|
||||
(OptionsFocus::ShowScope, 6),
|
||||
(OptionsFocus::ShowSpectrum, 7),
|
||||
(OptionsFocus::ShowLissajous, 8),
|
||||
(OptionsFocus::ShowCompletion, 9),
|
||||
(OptionsFocus::ShowPreview, 10),
|
||||
(OptionsFocus::PerformanceMode, 11),
|
||||
(OptionsFocus::Font, 12),
|
||||
(OptionsFocus::ZoomFactor, 13),
|
||||
(OptionsFocus::WindowSize, 14),
|
||||
// blank=15, ABLETON LINK header=16, divider=17
|
||||
(OptionsFocus::LinkEnabled, 18),
|
||||
(OptionsFocus::StartStopSync, 19),
|
||||
(OptionsFocus::Quantum, 20),
|
||||
// blank=21, SESSION header=22, divider=23, Tempo=24, Beat=25, Phase=26
|
||||
// blank=27, MIDI OUTPUTS header=28, divider=29
|
||||
(OptionsFocus::MidiOutput0, 30),
|
||||
(OptionsFocus::MidiOutput1, 31),
|
||||
(OptionsFocus::MidiOutput2, 32),
|
||||
(OptionsFocus::MidiOutput3, 33),
|
||||
// blank=34, MIDI INPUTS header=35, divider=36
|
||||
(OptionsFocus::MidiInput0, 37),
|
||||
(OptionsFocus::MidiInput1, 38),
|
||||
(OptionsFocus::MidiInput2, 39),
|
||||
(OptionsFocus::MidiInput3, 40),
|
||||
// blank=41, ONBOARDING header=42, divider=43
|
||||
(OptionsFocus::ResetOnboarding, 44),
|
||||
(OptionsFocus::LoadDemoOnStartup, 45),
|
||||
(OptionsFocus::GainBoost, 9),
|
||||
(OptionsFocus::NormalizeViz, 10),
|
||||
(OptionsFocus::ShowCompletion, 11),
|
||||
(OptionsFocus::ShowPreview, 12),
|
||||
(OptionsFocus::PerformanceMode, 13),
|
||||
(OptionsFocus::Font, 14),
|
||||
(OptionsFocus::ZoomFactor, 15),
|
||||
(OptionsFocus::WindowSize, 16),
|
||||
// blank=17, ABLETON LINK header=18, divider=19
|
||||
(OptionsFocus::LinkEnabled, 20),
|
||||
(OptionsFocus::StartStopSync, 21),
|
||||
(OptionsFocus::Quantum, 22),
|
||||
// blank=23, SESSION header=24, divider=25, Tempo=26, Beat=27, Phase=28
|
||||
// blank=29, MIDI OUTPUTS header=30, divider=31
|
||||
(OptionsFocus::MidiOutput0, 32),
|
||||
(OptionsFocus::MidiOutput1, 33),
|
||||
(OptionsFocus::MidiOutput2, 34),
|
||||
(OptionsFocus::MidiOutput3, 35),
|
||||
// blank=36, MIDI INPUTS header=37, divider=38
|
||||
(OptionsFocus::MidiInput0, 39),
|
||||
(OptionsFocus::MidiInput1, 40),
|
||||
(OptionsFocus::MidiInput2, 41),
|
||||
(OptionsFocus::MidiInput3, 42),
|
||||
// blank=43, ONBOARDING header=44, divider=45
|
||||
(OptionsFocus::ResetOnboarding, 46),
|
||||
(OptionsFocus::LoadDemoOnStartup, 47),
|
||||
];
|
||||
|
||||
impl OptionsFocus {
|
||||
@@ -175,13 +181,13 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
||||
// based on which sections are hidden.
|
||||
let mut offset: usize = 0;
|
||||
|
||||
// Font/Zoom/Window lines (12,13,14) hidden when !plugin_mode
|
||||
// Font/Zoom/Window lines (14,15,16) hidden when !plugin_mode
|
||||
if !plugin_mode {
|
||||
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
|
||||
}
|
||||
|
||||
// Link + Session + MIDI sections hidden when plugin_mode
|
||||
// These span from blank(15) through MidiInput3(40) = 26 lines
|
||||
// These span from blank(17) through MidiInput3(42) = 26 lines
|
||||
if plugin_mode {
|
||||
let link_section_lines = 26;
|
||||
offset += link_section_lines;
|
||||
@@ -192,10 +198,10 @@ fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
||||
if !focus.is_visible(plugin_mode) {
|
||||
continue;
|
||||
}
|
||||
// Lines at or below index 11 (PerformanceMode) are never shifted
|
||||
let adjusted = if raw_line <= 11 {
|
||||
// Lines at or below index 13 (PerformanceMode) are never shifted
|
||||
let adjusted = if raw_line <= 13 {
|
||||
raw_line
|
||||
} else if !plugin_mode && raw_line <= 14 {
|
||||
} else if !plugin_mode && raw_line <= 16 {
|
||||
// Font/Zoom/Window — these are hidden, skip
|
||||
continue;
|
||||
} else {
|
||||
|
||||
@@ -163,6 +163,15 @@ fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
}
|
||||
|
||||
fn viz_gain(data: &[f32], config: &crate::state::audio::AudioConfig) -> f32 {
|
||||
if config.normalize_viz {
|
||||
let peak = data.iter().fold(0.0_f32, |m, s| m.max(s.abs()));
|
||||
if peak > 0.0001 { 1.0 / peak } else { 1.0 }
|
||||
} else {
|
||||
config.gain_boost
|
||||
}
|
||||
}
|
||||
|
||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
@@ -173,9 +182,11 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let gain = viz_gain(&app.metrics.scope, &app.audio.config);
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(Orientation::Horizontal)
|
||||
.color(theme.meter.low);
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
|
||||
@@ -189,8 +200,16 @@ fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let peak = app.metrics.scope.iter().chain(app.metrics.scope_right.iter())
|
||||
.fold(0.0_f32, |m, s| m.max(s.abs()));
|
||||
let gain = if app.audio.config.normalize_viz {
|
||||
if peak > 0.0001 { 1.0 / peak } else { 1.0 }
|
||||
} else {
|
||||
app.audio.config.gain_boost
|
||||
};
|
||||
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
|
||||
.color(theme.meter.low);
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(lissajous, inner);
|
||||
}
|
||||
|
||||
@@ -204,7 +223,13 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum);
|
||||
let gain = if app.audio.config.normalize_viz {
|
||||
viz_gain(&app.metrics.spectrum, &app.audio.config)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum)
|
||||
.gain(gain);
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
|
||||
@@ -482,6 +482,15 @@ fn render_tile(
|
||||
}
|
||||
}
|
||||
|
||||
fn viz_gain(data: &[f32], config: &crate::state::audio::AudioConfig) -> f32 {
|
||||
if config.normalize_viz {
|
||||
let peak = data.iter().fold(0.0_f32, |m, s| m.max(s.abs()));
|
||||
if peak > 0.0001 { 1.0 / peak } else { 1.0 }
|
||||
} else {
|
||||
config.gain_boost
|
||||
}
|
||||
}
|
||||
|
||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) {
|
||||
let theme = theme::get();
|
||||
let block = Block::default()
|
||||
@@ -490,9 +499,11 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientati
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let gain = viz_gain(&app.metrics.scope, &app.audio.config);
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(orientation)
|
||||
.color(theme.meter.low);
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
|
||||
@@ -504,7 +515,13 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum);
|
||||
let gain = if app.audio.config.normalize_viz {
|
||||
viz_gain(&app.metrics.spectrum, &app.audio.config)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum)
|
||||
.gain(gain);
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
@@ -516,8 +533,16 @@ fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let peak = app.metrics.scope.iter().chain(app.metrics.scope_right.iter())
|
||||
.fold(0.0_f32, |m, s| m.max(s.abs()));
|
||||
let gain = if app.audio.config.normalize_viz {
|
||||
if peak > 0.0001 { 1.0 / peak } else { 1.0 }
|
||||
} else {
|
||||
app.audio.config.gain_boost
|
||||
};
|
||||
let lissajous = Lissajous::new(&app.metrics.scope, &app.metrics.scope_right)
|
||||
.color(theme.meter.low);
|
||||
.color(theme.meter.low)
|
||||
.gain(gain);
|
||||
frame.render_widget(lissajous, inner);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,18 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
focus == OptionsFocus::ShowLissajous,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Gain boost",
|
||||
&gain_boost_label(app.audio.config.gain_boost),
|
||||
focus == OptionsFocus::GainBoost,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Normalize",
|
||||
if app.audio.config.normalize_viz { "On" } else { "Off" },
|
||||
focus == OptionsFocus::NormalizeViz,
|
||||
&theme,
|
||||
),
|
||||
render_option_line(
|
||||
"Completion",
|
||||
if app.ui.show_completion { "On" } else { "Off" },
|
||||
@@ -354,9 +366,11 @@ fn option_description(focus: OptionsFocus) -> Option<&'static str> {
|
||||
OptionsFocus::HueRotation => Some("Shift all theme colors by a hue angle"),
|
||||
OptionsFocus::RefreshRate => Some("Lower values reduce CPU usage"),
|
||||
OptionsFocus::RuntimeHighlight => Some("Highlight executed code spans during playback"),
|
||||
OptionsFocus::ShowScope => Some("Oscilloscope on the engine page"),
|
||||
OptionsFocus::ShowSpectrum => Some("Spectrum analyzer on the engine page"),
|
||||
OptionsFocus::ShowScope => Some("Oscilloscope on the main view"),
|
||||
OptionsFocus::ShowSpectrum => Some("Spectrum analyzer on the main view"),
|
||||
OptionsFocus::ShowLissajous => Some("XY stereo phase scope (left vs right)"),
|
||||
OptionsFocus::GainBoost => Some("Amplify scope and lissajous waveforms"),
|
||||
OptionsFocus::NormalizeViz => Some("Auto-scale visualizations to fill the display"),
|
||||
OptionsFocus::ShowCompletion => Some("Word completion popup in the editor"),
|
||||
OptionsFocus::ShowPreview => Some("Step script preview on the sequencer grid"),
|
||||
OptionsFocus::PerformanceMode => Some("Hide header and footer bars"),
|
||||
@@ -386,6 +400,10 @@ fn render_description_line(desc: &str, theme: &ThemeColors) -> Line<'static> {
|
||||
))
|
||||
}
|
||||
|
||||
fn gain_boost_label(gain: f32) -> String {
|
||||
format!("{:.0}x", gain)
|
||||
}
|
||||
|
||||
fn render_readonly_line(label: &str, value: &str, value_style: Style, theme: &theme::ThemeColors) -> Line<'static> {
|
||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||
let label_width = 20;
|
||||
|
||||
@@ -465,7 +465,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
Page::Main => vec![
|
||||
("Space", "Play"),
|
||||
("Enter", "Edit"),
|
||||
("t", "Toggle"),
|
||||
("t", "On/Off"),
|
||||
("Tab", "Samples"),
|
||||
("?", "Keys"),
|
||||
],
|
||||
|
||||
@@ -83,3 +83,99 @@ fn cycle_zero_count_error() {
|
||||
fn choose_zero_count_error() {
|
||||
expect_error("1 2 3 0 choose", "choose count must be > 0");
|
||||
}
|
||||
|
||||
// Bracket syntax tests
|
||||
|
||||
#[test]
|
||||
fn bracket_cycle() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("[ 10 20 30 ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 1);
|
||||
let f = run_ctx("[ 10 20 30 ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 20);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 2);
|
||||
let f = run_ctx("[ 10 20 30 ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 30);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 3);
|
||||
let f = run_ctx("[ 10 20 30 ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bracket_with_quotations() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 8);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 1);
|
||||
let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bracket_nested() {
|
||||
let ctx = ctx_with(|c| { c.runs = 0; c.iter = 0; });
|
||||
let f = run_ctx("[ [ 10 20 ] cycle [ 30 40 ] cycle ] pcycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 10);
|
||||
|
||||
let ctx = ctx_with(|c| { c.runs = 0; c.iter = 1; });
|
||||
let f = run_ctx("[ [ 10 20 ] cycle [ 30 40 ] cycle ] pcycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bracket_with_generator() {
|
||||
let ctx = ctx_with(|c| c.runs = 0);
|
||||
let f = run_ctx("[ 1 4 .. ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 1);
|
||||
|
||||
let ctx = ctx_with(|c| c.runs = 3);
|
||||
let f = run_ctx("[ 1 4 .. ] cycle", &ctx);
|
||||
assert_eq!(stack_int(&f), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stray_bracket_error() {
|
||||
expect_error("10 ] cycle", "unexpected ]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_bracket_error() {
|
||||
expect_error("[ 10 20", "missing ]");
|
||||
}
|
||||
|
||||
// Index tests
|
||||
|
||||
#[test]
|
||||
fn index_basic() {
|
||||
expect_int("10 20 30 3 1 index", 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_with_brackets() {
|
||||
expect_int("[ 10 20 30 ] 1 index", 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_modulo_wraps() {
|
||||
expect_int("[ 10 20 30 ] 5 index", 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_negative_wraps() {
|
||||
expect_int("[ 10 20 30 ] -1 index", 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_with_quotation() {
|
||||
expect_int("5 [ { 3 + } { 5 + } ] 0 index", 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_zero_count_error() {
|
||||
expect_error("0 0 index", "index count must be > 0");
|
||||
}
|
||||
|
||||
@@ -209,6 +209,37 @@ fn bounce_underflow() {
|
||||
expect_error("1 2 5 bounce", "stack underflow");
|
||||
}
|
||||
|
||||
// pbounce
|
||||
|
||||
#[test]
|
||||
fn pbounce_by_iter() {
|
||||
let expected = [60, 64, 67, 72, 67, 64, 60, 64];
|
||||
for (iter, &exp) in expected.iter().enumerate() {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = run_ctx("60 64 67 72 4 pbounce", &ctx);
|
||||
assert_eq!(stack_int(&f), exp, "pbounce at iter={}", iter);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbounce_single() {
|
||||
for iter in 0..5 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = run_ctx("42 1 pbounce", &ctx);
|
||||
assert_eq!(stack_int(&f), 42, "pbounce single at iter={}", iter);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbounce_with_bracket() {
|
||||
let expected = [60, 64, 67, 64, 60, 64];
|
||||
for (iter, &exp) in expected.iter().enumerate() {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = run_ctx("[ 60 64 67 ] pbounce", &ctx);
|
||||
assert_eq!(stack_int(&f), exp, "pbounce bracket at iter={}", iter);
|
||||
}
|
||||
}
|
||||
|
||||
// wchoose
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -338,3 +338,59 @@ fn arp_mixed_cycle_and_arp() {
|
||||
assert!(approx_eq(notes[4], 67.0));
|
||||
assert!(approx_eq(notes[5], 67.0));
|
||||
}
|
||||
|
||||
// --- every+ / except+ tests ---
|
||||
|
||||
#[test]
|
||||
fn every_offset_fires_at_offset() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s { . } 4 2 every+"#, &ctx).unwrap();
|
||||
if iter % 4 == 2 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "iter={}: should not fire", iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_offset_wraps_large_offset() {
|
||||
// offset 6 with n=4 → 6 % 4 = 2, same as offset 2
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s { . } 4 6 every+"#, &ctx).unwrap();
|
||||
if iter % 4 == 2 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "iter={}: should not fire", iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn except_offset_inverse() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let outputs = f.evaluate(r#""kick" s { . } 4 2 except+"#, &ctx).unwrap();
|
||||
if iter % 4 != 2 {
|
||||
assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
|
||||
} else {
|
||||
assert_eq!(outputs.len(), 0, "iter={}: should not fire", iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_offset_zero_is_same_as_every() {
|
||||
for iter in 0..8 {
|
||||
let ctx = ctx_with(|c| c.iter = iter);
|
||||
let f = forth();
|
||||
let a = f.evaluate(r#""kick" s { . } 3 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user