Feat: fixes and demo
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s

This commit is contained in:
2026-02-23 01:18:43 +01:00
parent f6c7438886
commit 2a2b3c5651
3 changed files with 94 additions and 42 deletions

View File

@@ -237,12 +237,14 @@ impl Forth {
}; };
let emit_with_cycling = |cmd: &CmdRegister, let emit_with_cycling = |cmd: &CmdRegister,
emit_idx: usize, arp_idx: usize,
poly_idx: usize,
delta_secs: f64, delta_secs: f64,
outputs: &mut Vec<String>| outputs: &mut Vec<String>|
-> Result<Option<Value>, String> { -> Result<Option<Value>, String> {
let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?; let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?;
let resolved_sound_val = sound_opt.map(|sv| resolve_cycling(sv, emit_idx)); let resolved_sound_val =
sound_opt.map(|sv| resolve_value(sv, arp_idx, poly_idx));
let sound_str = match &resolved_sound_val { let sound_str = match &resolved_sound_val {
Some(v) => Some(v.as_str()?.to_string()), Some(v) => Some(v.as_str()?.to_string()),
None => None, None => None,
@@ -250,7 +252,7 @@ impl Forth {
let resolved_params: Vec<(&str, String)> = params let resolved_params: Vec<(&str, String)> = params
.iter() .iter()
.map(|(k, v)| { .map(|(k, v)| {
let resolved = resolve_cycling(v, emit_idx); let resolved = resolve_value(v, arp_idx, poly_idx);
if let Value::CycleList(_) | Value::ArpList(_) = v { if let Value::CycleList(_) | Value::ArpList(_) = v {
if let Some(span) = resolved.span() { if let Some(span) = resolved.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() { if let Some(trace) = trace_cell.borrow_mut().as_mut() {
@@ -544,6 +546,7 @@ impl Forth {
Op::Emit => { Op::Emit => {
if has_arp_list(cmd) { if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd); let arp_count = compute_arp_count(cmd);
let poly_count = compute_poly_count(cmd);
let explicit_deltas = !cmd.deltas().is_empty(); let explicit_deltas = !cmd.deltas().is_empty();
let delta_list: Vec<Value> = if explicit_deltas { let delta_list: Vec<Value> = if explicit_deltas {
cmd.deltas().to_vec() cmd.deltas().to_vec()
@@ -570,12 +573,16 @@ impl Forth {
ctx.nudge_secs ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration() + (i as f64 / count as f64) * ctx.step_duration()
}; };
if let Some(sound_val) = for poly_i in 0..poly_count {
emit_with_cycling(cmd, i, delta_secs, outputs)? if let Some(sound_val) =
{ emit_with_cycling(cmd, i, poly_i, delta_secs, outputs)?
if let Some(span) = sound_val.span() { {
if let Some(trace) = trace_cell.borrow_mut().as_mut() { if let Some(span) = sound_val.span() {
trace.selected_spans.push(span); if let Some(trace) =
trace_cell.borrow_mut().as_mut()
{
trace.selected_spans.push(span);
}
} }
} }
} }
@@ -599,7 +606,7 @@ impl Forth {
} }
} }
if let Some(sound_val) = if let Some(sound_val) =
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)? emit_with_cycling(cmd, 0, poly_idx, delta_secs, outputs)?
{ {
if let Some(span) = sound_val.span() { if let Some(span) = sound_val.span() {
if let Some(trace) = if let Some(trace) =
@@ -1241,9 +1248,10 @@ impl Forth {
Op::MidiEmit => { Op::MidiEmit => {
let (_, params) = cmd.snapshot().unwrap_or((None, &[])); let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
// Build schedule: (emit_idx, delta_secs) — same logic as Op::Emit // Build schedule: (arp_idx, poly_idx, delta_secs)
let schedule: Vec<(usize, f64)> = if has_arp_list(cmd) { let schedule: Vec<(usize, usize, f64)> = if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd); let arp_count = compute_arp_count(cmd);
let poly_count = compute_poly_count(cmd);
let explicit = !cmd.deltas().is_empty(); let explicit = !cmd.deltas().is_empty();
let delta_list = cmd.deltas(); let delta_list = cmd.deltas();
let count = if explicit { let count = if explicit {
@@ -1251,20 +1259,22 @@ impl Forth {
} else { } else {
arp_count arp_count
}; };
(0..count) let mut sched = Vec::with_capacity(count * poly_count);
.map(|i| { for i in 0..count {
let delta_secs = if explicit { let delta_secs = if explicit {
let frac = delta_list[i % delta_list.len()] let frac = delta_list[i % delta_list.len()]
.as_float() .as_float()
.unwrap_or(0.0); .unwrap_or(0.0);
ctx.nudge_secs + frac * ctx.step_duration() ctx.nudge_secs + frac * ctx.step_duration()
} else { } else {
ctx.nudge_secs ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration() + (i as f64 / count as f64) * ctx.step_duration()
}; };
(i, delta_secs) for poly_i in 0..poly_count {
}) sched.push((i, poly_i, delta_secs));
.collect() }
}
sched
} else { } else {
let poly_count = compute_poly_count(cmd); let poly_count = compute_poly_count(cmd);
let deltas: Vec<f64> = if cmd.deltas().is_empty() { let deltas: Vec<f64> = if cmd.deltas().is_empty() {
@@ -1279,6 +1289,7 @@ impl Forth {
for poly_idx in 0..poly_count { for poly_idx in 0..poly_count {
for &frac in &deltas { for &frac in &deltas {
sched.push(( sched.push((
0,
poly_idx, poly_idx,
ctx.nudge_secs + frac * ctx.step_duration(), ctx.nudge_secs + frac * ctx.step_duration(),
)); ));
@@ -1287,14 +1298,14 @@ impl Forth {
sched sched
}; };
for (emit_idx, delta_secs) in schedule { for (arp_idx, poly_idx, delta_secs) in schedule {
let get_int = |name: &str| -> Option<i64> { let get_int = |name: &str| -> Option<i64> {
params params
.iter() .iter()
.rev() .rev()
.find(|(k, _)| *k == name) .find(|(k, _)| *k == name)
.and_then(|(_, v)| { .and_then(|(_, v)| {
resolve_cycling(v, emit_idx).as_int().ok() resolve_value(v, arp_idx, poly_idx).as_int().ok()
}) })
}; };
let get_float = |name: &str| -> Option<f64> { let get_float = |name: &str| -> Option<f64> {
@@ -1303,7 +1314,7 @@ impl Forth {
.rev() .rev()
.find(|(k, _)| *k == name) .find(|(k, _)| *k == name)
.and_then(|(_, v)| { .and_then(|(_, v)| {
resolve_cycling(v, emit_idx).as_float().ok() resolve_value(v, arp_idx, poly_idx).as_float().ok()
}) })
}; };
let chan = get_int("chan") let chan = get_int("chan")
@@ -1680,10 +1691,13 @@ where
Ok(()) Ok(())
} }
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> { fn resolve_value(val: &Value, arp_idx: usize, poly_idx: usize) -> Cow<'_, Value> {
match val { match val {
Value::CycleList(items) | Value::ArpList(items) if !items.is_empty() => { Value::ArpList(items) if !items.is_empty() => {
Cow::Owned(items[emit_idx % items.len()].clone()) Cow::Owned(items[arp_idx % items.len()].clone())
}
Value::CycleList(items) if !items.is_empty() => {
Cow::Owned(items[poly_idx % items.len()].clone())
} }
other => Cow::Borrowed(other), other => Cow::Borrowed(other),
} }

View File

@@ -7,22 +7,45 @@
"steps": [ "steps": [
{ {
"i": 0, "i": 0,
"script": "sine sound ." "script": "0 7 .. at\n c2 maj9 arp note\n wide bigverb mysynth \n 2000 1000 0.4 0.8 rand expslide llpf\n 0.4 0.8 rand llpq\n ."
},
{
"i": 8,
"source": 0
} }
], ],
"length": 16, "length": 16,
"speed": [ "speed": [
1, 1,
1 1
] ],
"name": "bigsynth"
}, },
{ {
"steps": [], "steps": [
{
"i": 0,
"script": "kick sound ."
},
{
"i": 4,
"source": 0
},
{
"i": 8,
"source": 0
},
{
"i": 12,
"source": 0
}
],
"length": 16, "length": 16,
"speed": [ "speed": [
1, 1,
1 1
] ],
"name": "kick"
}, },
{ {
"steps": [], "steps": [],
@@ -8365,6 +8388,11 @@
[ [
0, 0,
0 0
],
[
0,
1
] ]
] ],
"prelude": ": mysynth saw pulse white sound \n0.7 gain 1 decay ;\n: bigverb 0.5 verb 0.1 verbdamp ;\n: wide 0.2 haas 2 width ;"
} }

View File

@@ -316,15 +316,25 @@ fn arp_no_arp_unchanged() {
#[test] #[test]
fn arp_mixed_cycle_and_arp() { fn arp_mixed_cycle_and_arp() {
// CycleList sound + ArpList note → flat loop, sound cycles // CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 3); // Each arp step plays both sine and saw simultaneously (poly stacking)
let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 6);
let sounds = get_sounds(&outputs); let sounds = get_sounds(&outputs);
// Sound is CycleList, cycles across the 3 arp emissions // Arp step 0: poly 0=sine, poly 1=saw
assert_eq!(sounds[0], "sine"); assert_eq!(sounds[0], "sine");
assert_eq!(sounds[1], "saw"); assert_eq!(sounds[1], "saw");
// Arp step 1: poly 0=sine, poly 1=saw
assert_eq!(sounds[2], "sine"); assert_eq!(sounds[2], "sine");
assert_eq!(sounds[3], "saw");
// Arp step 2: poly 0=sine, poly 1=saw
assert_eq!(sounds[4], "sine");
assert_eq!(sounds[5], "saw");
let notes = get_notes(&outputs); let notes = get_notes(&outputs);
// Both poly voices in each arp step share the same note
assert!(approx_eq(notes[0], 60.0)); assert!(approx_eq(notes[0], 60.0));
assert!(approx_eq(notes[1], 64.0)); assert!(approx_eq(notes[1], 60.0));
assert!(approx_eq(notes[2], 67.0)); assert!(approx_eq(notes[2], 64.0));
assert!(approx_eq(notes[3], 64.0));
assert!(approx_eq(notes[4], 67.0));
assert!(approx_eq(notes[5], 67.0));
} }