This commit is contained in:
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ;"
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user