Feat: UI/UX and ducking compressor
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s

This commit is contained in:
2026-02-24 02:57:27 +01:00
parent 7632bc76f7
commit f0de312d6b
24 changed files with 402 additions and 71 deletions

View File

@@ -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>),

View File

@@ -799,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 {
@@ -895,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)?;

View File

@@ -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),
@@ -204,8 +208,8 @@ 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::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),
_ => {}

View File

@@ -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,
},
];

View File

@@ -113,6 +113,16 @@ 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: &[],
@@ -214,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: &[],

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}