Feat: improving MIDI
This commit is contained in:
@@ -1231,47 +1231,115 @@ impl Forth {
|
|||||||
// MIDI operations
|
// MIDI operations
|
||||||
Op::MidiEmit => {
|
Op::MidiEmit => {
|
||||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||||
let get_int = |name: &str| -> Option<i64> {
|
|
||||||
params
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.find(|(k, _)| *k == name)
|
|
||||||
.and_then(|(_, v)| v.as_int().ok())
|
|
||||||
};
|
|
||||||
let get_float = |name: &str| -> Option<f64> {
|
|
||||||
params
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.find(|(k, _)| *k == name)
|
|
||||||
.and_then(|(_, v)| v.as_float().ok())
|
|
||||||
};
|
|
||||||
let chan = get_int("chan")
|
|
||||||
.map(|c| (c.clamp(1, 16) - 1) as u8)
|
|
||||||
.unwrap_or(0);
|
|
||||||
let dev = get_int("dev").map(|d| d.clamp(0, 3) as u8).unwrap_or(0);
|
|
||||||
|
|
||||||
if let (Some(cc), Some(val)) = (get_int("ccnum"), get_int("ccout")) {
|
// Build schedule: (emit_idx, delta_secs) — same logic as Op::Emit
|
||||||
let cc = cc.clamp(0, 127) as u8;
|
let schedule: Vec<(usize, f64)> = if has_arp_list(cmd) {
|
||||||
let val = val.clamp(0, 127) as u8;
|
let arp_count = compute_arp_count(cmd);
|
||||||
outputs.push(format!("/midi/cc/{cc}/{val}/chan/{chan}/dev/{dev}"));
|
let explicit = !cmd.deltas().is_empty();
|
||||||
} else if let Some(bend) = get_float("bend") {
|
let delta_list = cmd.deltas();
|
||||||
let bend_clamped = bend.clamp(-1.0, 1.0);
|
let count = if explicit {
|
||||||
let bend_14bit = ((bend_clamped + 1.0) * 8191.5) as u16;
|
arp_count.max(delta_list.len())
|
||||||
outputs.push(format!("/midi/bend/{bend_14bit}/chan/{chan}/dev/{dev}"));
|
} else {
|
||||||
} else if let Some(pressure) = get_int("pressure") {
|
arp_count
|
||||||
let pressure = pressure.clamp(0, 127) as u8;
|
};
|
||||||
outputs.push(format!("/midi/pressure/{pressure}/chan/{chan}/dev/{dev}"));
|
(0..count)
|
||||||
} else if let Some(program) = get_int("program") {
|
.map(|i| {
|
||||||
let program = program.clamp(0, 127) as u8;
|
let delta_secs = if explicit {
|
||||||
outputs.push(format!("/midi/program/{program}/chan/{chan}/dev/{dev}"));
|
let frac = delta_list[i % delta_list.len()]
|
||||||
|
.as_float()
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
ctx.nudge_secs + frac * ctx.step_duration()
|
||||||
|
} else {
|
||||||
|
ctx.nudge_secs
|
||||||
|
+ (i as f64 / count as f64) * ctx.step_duration()
|
||||||
|
};
|
||||||
|
(i, delta_secs)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
let poly_count = compute_poly_count(cmd);
|
||||||
let velocity = get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
|
let deltas: Vec<f64> = if cmd.deltas().is_empty() {
|
||||||
let dur = get_float("dur").unwrap_or(1.0);
|
vec![0.0]
|
||||||
let dur_secs = dur * ctx.step_duration();
|
} else {
|
||||||
outputs.push(format!(
|
cmd.deltas()
|
||||||
"/midi/note/{note}/vel/{velocity}/chan/{chan}/dur/{dur_secs}/dev/{dev}"
|
.iter()
|
||||||
));
|
.filter_map(|v| v.as_float().ok())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let mut sched = Vec::with_capacity(poly_count * deltas.len());
|
||||||
|
for poly_idx in 0..poly_count {
|
||||||
|
for &frac in &deltas {
|
||||||
|
sched.push((
|
||||||
|
poly_idx,
|
||||||
|
ctx.nudge_secs + frac * ctx.step_duration(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sched
|
||||||
|
};
|
||||||
|
|
||||||
|
for (emit_idx, delta_secs) in schedule {
|
||||||
|
let get_int = |name: &str| -> Option<i64> {
|
||||||
|
params
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|(k, _)| *k == name)
|
||||||
|
.and_then(|(_, v)| {
|
||||||
|
resolve_cycling(v, emit_idx).as_int().ok()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let get_float = |name: &str| -> Option<f64> {
|
||||||
|
params
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|(k, _)| *k == name)
|
||||||
|
.and_then(|(_, v)| {
|
||||||
|
resolve_cycling(v, emit_idx).as_float().ok()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let chan = get_int("chan")
|
||||||
|
.map(|c| (c.clamp(1, 16) - 1) as u8)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let dev =
|
||||||
|
get_int("dev").map(|d| d.clamp(0, 3) as u8).unwrap_or(0);
|
||||||
|
let delta_suffix = if delta_secs > 0.0 {
|
||||||
|
format!("/delta/{delta_secs}")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let (Some(cc), Some(val)) = (get_int("ccnum"), get_int("ccout")) {
|
||||||
|
let cc = cc.clamp(0, 127) as u8;
|
||||||
|
let val = val.clamp(0, 127) as u8;
|
||||||
|
outputs.push(format!(
|
||||||
|
"/midi/cc/{cc}/{val}/chan/{chan}/dev/{dev}{delta_suffix}"
|
||||||
|
));
|
||||||
|
} else if let Some(bend) = get_float("bend") {
|
||||||
|
let bend_clamped = bend.clamp(-1.0, 1.0);
|
||||||
|
let bend_14bit = ((bend_clamped + 1.0) * 8191.5) as u16;
|
||||||
|
outputs.push(format!(
|
||||||
|
"/midi/bend/{bend_14bit}/chan/{chan}/dev/{dev}{delta_suffix}"
|
||||||
|
));
|
||||||
|
} else if let Some(pressure) = get_int("pressure") {
|
||||||
|
let pressure = pressure.clamp(0, 127) as u8;
|
||||||
|
outputs.push(format!(
|
||||||
|
"/midi/pressure/{pressure}/chan/{chan}/dev/{dev}{delta_suffix}"
|
||||||
|
));
|
||||||
|
} else if let Some(program) = get_int("program") {
|
||||||
|
let program = program.clamp(0, 127) as u8;
|
||||||
|
outputs.push(format!(
|
||||||
|
"/midi/program/{program}/chan/{chan}/dev/{dev}{delta_suffix}"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let note = get_int("note").unwrap_or(60).clamp(0, 127) as u8;
|
||||||
|
let velocity =
|
||||||
|
get_int("velocity").unwrap_or(100).clamp(0, 127) as u8;
|
||||||
|
let dur = get_float("dur").unwrap_or(1.0);
|
||||||
|
let dur_secs = dur * ctx.step_duration();
|
||||||
|
outputs.push(format!(
|
||||||
|
"/midi/note/{note}/vel/{velocity}/chan/{chan}/dur/{dur_secs}/dev/{dev}{delta_suffix}"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Op::MidiClock => {
|
Op::MidiClock => {
|
||||||
|
|||||||
100
docs/tutorial_at.md
Normal file
100
docs/tutorial_at.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Timing with at
|
||||||
|
|
||||||
|
Every step has a duration. By default, sounds emit at the very start of that duration. `at` changes *when* within the step sounds fire -- giving you sub-step rhythmic control without adding more steps.
|
||||||
|
|
||||||
|
## The Basics
|
||||||
|
|
||||||
|
`at` drains the entire stack and stores the values as timing offsets. Each value is a fraction of the step duration: 0 = start, 0.5 = halfway, 1.0 = next step boundary.
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0.5 at kick s . ;; kick at the midpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
Push multiple values before calling `at` to get multiple emits from a single `.`:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 0.5 at kick s . ;; two kicks: one at start, one at midpoint
|
||||||
|
0 0.25 0.5 0.75 at hat s . ;; four hats, evenly spaced
|
||||||
|
```
|
||||||
|
|
||||||
|
The deltas persist across multiple `.` calls until `clear` or a new `at`:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 0.5 at
|
||||||
|
kick s . ;; 2 kicks
|
||||||
|
hat s . ;; 2 hats (same timing)
|
||||||
|
clear
|
||||||
|
snare s . ;; 1 snare (deltas cleared)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cross-product: at Without arp
|
||||||
|
|
||||||
|
Without `arp`, deltas multiply with polyphonic voices. If you have 3 notes and 2 deltas, you get 6 emits -- every note at every delta:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 0.5 at
|
||||||
|
c4 e4 g4 note sine s . ;; 6 emits: 3 notes x 2 deltas
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a chord played twice per step.
|
||||||
|
|
||||||
|
## 1:1 Pairing: at With arp
|
||||||
|
|
||||||
|
`arp` changes the behavior. Instead of cross-product, deltas and arp values pair up 1:1. Each delta gets one note from the arpeggio:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 0.33 0.66 at
|
||||||
|
c4 e4 g4 arp note sine s . ;; c4 at 0, e4 at 0.33, g4 at 0.66
|
||||||
|
```
|
||||||
|
|
||||||
|
If the lists differ in length, the shorter one wraps around:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 0.25 0.5 0.75 at
|
||||||
|
c4 e4 arp note sine s . ;; c4, e4, c4, e4 at 4 time points
|
||||||
|
```
|
||||||
|
|
||||||
|
This is THE key distinction. Without `arp`: every note at every time. With `arp`: one note per time slot.
|
||||||
|
|
||||||
|
## Generating Deltas
|
||||||
|
|
||||||
|
You rarely type deltas by hand. Use generators:
|
||||||
|
|
||||||
|
Evenly spaced via `.,`:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0 1 0.25 ., at hat s . ;; 0 0.25 0.5 0.75 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Euclidean distribution via `euclid`:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
3 8 euclid at hat s . ;; 3 hats at positions 0, 3, 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Random timing via `gen`:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
{ 0.0 1.0 rand } 4 gen at hat s . ;; 4 hats at random positions
|
||||||
|
```
|
||||||
|
|
||||||
|
Geometric spacing via `geom..`:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
0.0 2.0 4 geom.. at hat s . ;; exponentially spaced
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gating at
|
||||||
|
|
||||||
|
Wrap `at` expressions in quotations for conditional timing:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
{ 0 0.25 0.5 0.75 at } 2 every ;; 16th-note hats every other bar
|
||||||
|
hat s .
|
||||||
|
|
||||||
|
{ 0 0.5 at } 0.5 chance ;; 50% chance of double-hit
|
||||||
|
kick s .
|
||||||
|
```
|
||||||
|
|
||||||
|
When the quotation doesn't execute, no deltas are set -- you get the default single emit at beat start.
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@ Sequences of values drive music: arpeggios, parameter sweeps, rhythmic patterns.
|
|||||||
100 0.5 4 geom.. ;; 100 50 25 12.5
|
100 0.5 4 geom.. ;; 100 50 25 12.5
|
||||||
```
|
```
|
||||||
|
|
||||||
Musical use -- build a harmonic series:
|
Build a harmonic series:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
110 2 5 geom.. 5 rev note
|
110 2 5 geom.. 5 rev freq
|
||||||
```
|
```
|
||||||
|
|
||||||
That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `note` or `freq`.
|
That gives you 110, 220, 440, 880, 1760 (reversed), ready to feed into `freq`.
|
||||||
|
|
||||||
## Computed Sequences
|
## Computed Sequences
|
||||||
|
|
||||||
@@ -73,14 +73,6 @@ The distinction: `gen` is for building data. `times` is for doing things.
|
|||||||
|
|
||||||
These give you raw indices as data on the stack. This is different from `bjork` and `pbjork` (covered in the Randomness tutorial), which execute a quotation on matching steps. `euclid` gives you numbers to work with; `bjork` triggers actions.
|
These give you raw indices as data on the stack. This is different from `bjork` and `pbjork` (covered in the Randomness tutorial), which execute a quotation on matching steps. `euclid` gives you numbers to work with; `bjork` triggers actions.
|
||||||
|
|
||||||
Use euclid indices to pick notes from a scale:
|
|
||||||
|
|
||||||
```forth
|
|
||||||
: pick ( ..vals n i -- val ) rot drop swap ;
|
|
||||||
c4 d4 e4 g4 a4 ;; pentatonic scale on the stack
|
|
||||||
3 8 euclid ;; get 3 hit positions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Transforming Sequences
|
## Transforming Sequences
|
||||||
|
|
||||||
Four words reshape values already on the stack. All take n (the count of items to operate on) from the top:
|
Four words reshape values already on the stack. All take n (the count of items to operate on) from the top:
|
||||||
@@ -143,39 +135,4 @@ Or replicate a value for batch processing:
|
|||||||
0.5 4 dupn 4 sum ;; 2.0
|
0.5 4 dupn 4 sum ;; 2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Combining Techniques
|
|
||||||
|
|
||||||
An arpeggio that shuffles every time the step plays:
|
|
||||||
|
|
||||||
```forth
|
|
||||||
c4 e4 g4 b4 4 shuffle
|
|
||||||
drop drop drop ;; keep only the first note
|
|
||||||
note sine s .
|
|
||||||
```
|
|
||||||
|
|
||||||
Parameter spread across voices -- four sines with geometrically spaced frequencies:
|
|
||||||
|
|
||||||
```forth
|
|
||||||
220 1.5 4 geom..
|
|
||||||
4 { @i 1 + pick note sine s . } times
|
|
||||||
```
|
|
||||||
|
|
||||||
Euclidean rhythm driving note selection from a generated sequence:
|
|
||||||
|
|
||||||
```forth
|
|
||||||
3 8 euclid ;; 3 hit indices
|
|
||||||
```
|
|
||||||
|
|
||||||
A chord built from a range, then sorted high to low:
|
|
||||||
|
|
||||||
```forth
|
|
||||||
60 67 .. 8 rsort
|
|
||||||
```
|
|
||||||
|
|
||||||
Rhythmic density control -- generate hits, keep only the loud ones:
|
|
||||||
|
|
||||||
```forth
|
|
||||||
{ 0.0 1.0 rand } 8 gen
|
|
||||||
```
|
|
||||||
|
|
||||||
The generator words produce raw material. The transform words shape it. Together they let you express complex musical ideas in a few words.
|
The generator words produce raw material. The transform words shape it. Together they let you express complex musical ideas in a few words.
|
||||||
@@ -1180,10 +1180,12 @@ fn sequencer_loop(
|
|||||||
|
|
||||||
// Route commands: audio direct to doux, MIDI through dispatcher
|
// Route commands: audio direct to doux, MIDI through dispatcher
|
||||||
for tsc in output.audio_commands {
|
for tsc in output.audio_commands {
|
||||||
if let Some((midi_cmd, dur)) = parse_midi_command(&tsc.cmd) {
|
if let Some((midi_cmd, dur, delta_secs)) = parse_midi_command(&tsc.cmd) {
|
||||||
|
let target_time_us =
|
||||||
|
current_time_us + (delta_secs * 1_000_000.0) as SyncTime;
|
||||||
let _ = dispatch_tx.send(TimedMidiCommand {
|
let _ = dispatch_tx.send(TimedMidiCommand {
|
||||||
command: MidiDispatch::Send(midi_cmd.clone()),
|
command: MidiDispatch::Send(midi_cmd.clone()),
|
||||||
target_time_us: current_time_us,
|
target_time_us,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let (
|
if let (
|
||||||
@@ -1196,7 +1198,7 @@ fn sequencer_loop(
|
|||||||
Some(dur_secs),
|
Some(dur_secs),
|
||||||
) = (&midi_cmd, dur)
|
) = (&midi_cmd, dur)
|
||||||
{
|
{
|
||||||
let off_time_us = current_time_us + (dur_secs * 1_000_000.0) as SyncTime;
|
let off_time_us = target_time_us + (dur_secs * 1_000_000.0) as SyncTime;
|
||||||
let _ = dispatch_tx.send(TimedMidiCommand {
|
let _ = dispatch_tx.send(TimedMidiCommand {
|
||||||
command: MidiDispatch::Send(MidiCommand::NoteOff {
|
command: MidiDispatch::Send(MidiCommand::NoteOff {
|
||||||
device: *device,
|
device: *device,
|
||||||
@@ -1229,7 +1231,7 @@ fn sequencer_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>, f64)> {
|
||||||
if !cmd.starts_with("/midi/") {
|
if !cmd.starts_with("/midi/") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1254,10 +1256,10 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let device: u8 = find_param("dev").and_then(|s| s.parse().ok()).unwrap_or(0);
|
let device: u8 = find_param("dev").and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||||
|
let delta: f64 = find_param("delta").and_then(|s| s.parse().ok()).unwrap_or(0.0);
|
||||||
|
|
||||||
match parts[1] {
|
match parts[1] {
|
||||||
"note" => {
|
"note" => {
|
||||||
// /midi/note/<note>/vel/<vel>/chan/<chan>/dur/<dur>/dev/<dev>
|
|
||||||
let note: u8 = parts.get(2)?.parse().ok()?;
|
let note: u8 = parts.get(2)?.parse().ok()?;
|
||||||
let vel: u8 = find_param("vel")?.parse().ok()?;
|
let vel: u8 = find_param("vel")?.parse().ok()?;
|
||||||
let chan: u8 = find_param("chan")?.parse().ok()?;
|
let chan: u8 = find_param("chan")?.parse().ok()?;
|
||||||
@@ -1270,10 +1272,10 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
velocity: vel,
|
velocity: vel,
|
||||||
},
|
},
|
||||||
dur,
|
dur,
|
||||||
|
delta,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
"cc" => {
|
"cc" => {
|
||||||
// /midi/cc/<cc>/<val>/chan/<chan>/dev/<dev>
|
|
||||||
let cc: u8 = parts.get(2)?.parse().ok()?;
|
let cc: u8 = parts.get(2)?.parse().ok()?;
|
||||||
let val: u8 = parts.get(3)?.parse().ok()?;
|
let val: u8 = parts.get(3)?.parse().ok()?;
|
||||||
let chan: u8 = find_param("chan")?.parse().ok()?;
|
let chan: u8 = find_param("chan")?.parse().ok()?;
|
||||||
@@ -1285,10 +1287,10 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
value: val,
|
value: val,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
delta,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
"bend" => {
|
"bend" => {
|
||||||
// /midi/bend/<value>/chan/<chan>/dev/<dev>
|
|
||||||
let value: u16 = parts.get(2)?.parse().ok()?;
|
let value: u16 = parts.get(2)?.parse().ok()?;
|
||||||
let chan: u8 = find_param("chan")?.parse().ok()?;
|
let chan: u8 = find_param("chan")?.parse().ok()?;
|
||||||
Some((
|
Some((
|
||||||
@@ -1298,10 +1300,10 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
delta,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
"pressure" => {
|
"pressure" => {
|
||||||
// /midi/pressure/<value>/chan/<chan>/dev/<dev>
|
|
||||||
let value: u8 = parts.get(2)?.parse().ok()?;
|
let value: u8 = parts.get(2)?.parse().ok()?;
|
||||||
let chan: u8 = find_param("chan")?.parse().ok()?;
|
let chan: u8 = find_param("chan")?.parse().ok()?;
|
||||||
Some((
|
Some((
|
||||||
@@ -1311,10 +1313,10 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
delta,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
"program" => {
|
"program" => {
|
||||||
// /midi/program/<value>/chan/<chan>/dev/<dev>
|
|
||||||
let program: u8 = parts.get(2)?.parse().ok()?;
|
let program: u8 = parts.get(2)?.parse().ok()?;
|
||||||
let chan: u8 = find_param("chan")?.parse().ok()?;
|
let chan: u8 = find_param("chan")?.parse().ok()?;
|
||||||
Some((
|
Some((
|
||||||
@@ -1324,12 +1326,13 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
program,
|
program,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
delta,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
"clock" => Some((MidiCommand::Clock { device }, None)),
|
"clock" => Some((MidiCommand::Clock { device }, None, delta)),
|
||||||
"start" => Some((MidiCommand::Start { device }, None)),
|
"start" => Some((MidiCommand::Start { device }, None, delta)),
|
||||||
"stop" => Some((MidiCommand::Stop { device }, None)),
|
"stop" => Some((MidiCommand::Stop { device }, None, delta)),
|
||||||
"continue" => Some((MidiCommand::Continue { device }, None)),
|
"continue" => Some((MidiCommand::Continue { device }, None, delta)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ pub const DOCS: &[DocEntry] = &[
|
|||||||
"Generators",
|
"Generators",
|
||||||
include_str!("../../docs/tutorial_generators.md"),
|
include_str!("../../docs/tutorial_generators.md"),
|
||||||
),
|
),
|
||||||
|
Topic("Timing with at", include_str!("../../docs/tutorial_at.md")),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn topic_count() -> usize {
|
pub fn topic_count() -> usize {
|
||||||
|
|||||||
@@ -230,6 +230,52 @@ fn test_midi_continue() {
|
|||||||
assert_eq!(outputs[0], "/midi/continue/dev/0");
|
assert_eq!(outputs[0], "/midi/continue/dev/0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// at (delta) tests
|
||||||
|
#[test]
|
||||||
|
fn test_midi_at_single_delta() {
|
||||||
|
let outputs = expect_outputs("0.5 at 60 note m.", 1);
|
||||||
|
assert!(outputs[0].contains("/note/60/"));
|
||||||
|
assert!(outputs[0].contains("/delta/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_at_multiple_deltas() {
|
||||||
|
let outputs = expect_outputs("0 0.5 at 60 note m.", 2);
|
||||||
|
assert!(outputs[0].contains("/note/60/"));
|
||||||
|
assert!(outputs[1].contains("/note/60/"));
|
||||||
|
assert!(outputs[1].contains("/delta/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_at_with_polyphony() {
|
||||||
|
// 2 notes × 2 deltas = 4 events
|
||||||
|
expect_outputs("0 0.5 at 60 64 note m.", 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_arp_notes() {
|
||||||
|
let outputs = expect_outputs("c4 e4 g4 arp note m.", 3);
|
||||||
|
assert!(outputs[0].contains("/note/60/"));
|
||||||
|
assert!(outputs[1].contains("/note/64/"));
|
||||||
|
assert!(outputs[2].contains("/note/67/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_arp_with_at() {
|
||||||
|
let outputs = expect_outputs("0 0.25 0.5 at c4 e4 g4 arp note m.", 3);
|
||||||
|
assert!(outputs[0].contains("/note/60/"));
|
||||||
|
assert!(outputs[1].contains("/note/64/"));
|
||||||
|
assert!(outputs[2].contains("/note/67/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_at_cc() {
|
||||||
|
let outputs = expect_outputs("0 0.5 at 1 ccnum 64 ccout m.", 2);
|
||||||
|
assert!(outputs[0].contains("/midi/cc/1/64/"));
|
||||||
|
assert!(outputs[1].contains("/midi/cc/1/64/"));
|
||||||
|
assert!(outputs[1].contains("/delta/"));
|
||||||
|
}
|
||||||
|
|
||||||
// Test message type priority (first matching type wins)
|
// Test message type priority (first matching type wins)
|
||||||
#[test]
|
#[test]
|
||||||
fn test_midi_message_priority_cc_over_note() {
|
fn test_midi_message_priority_cc_over_note() {
|
||||||
@@ -286,3 +332,34 @@ fn test_midi_note_duration_with_speed() {
|
|||||||
let outputs = f.evaluate("60 note m.", &ctx).unwrap();
|
let outputs = f.evaluate("60 note m.", &ctx).unwrap();
|
||||||
assert!(outputs[0].contains("/dur/0.0625"));
|
assert!(outputs[0].contains("/dur/0.0625"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Polyphonic MIDI tests
|
||||||
|
#[test]
|
||||||
|
fn test_midi_polyphonic_notes() {
|
||||||
|
let outputs = expect_outputs("60 64 67 note m.", 3);
|
||||||
|
assert!(outputs[0].contains("/midi/note/60/"));
|
||||||
|
assert!(outputs[1].contains("/midi/note/64/"));
|
||||||
|
assert!(outputs[2].contains("/midi/note/67/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_polyphonic_notes_with_velocity() {
|
||||||
|
let outputs = expect_outputs("60 64 67 note 100 80 60 velocity m.", 3);
|
||||||
|
assert!(outputs[0].contains("/note/60/vel/100/"));
|
||||||
|
assert!(outputs[1].contains("/note/64/vel/80/"));
|
||||||
|
assert!(outputs[2].contains("/note/67/vel/60/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_polyphonic_channel() {
|
||||||
|
let outputs = expect_outputs("60 note 1 2 chan m.", 2);
|
||||||
|
assert!(outputs[0].contains("/note/60/") && outputs[0].contains("/chan/0"));
|
||||||
|
assert!(outputs[1].contains("/note/60/") && outputs[1].contains("/chan/1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_midi_polyphonic_cc() {
|
||||||
|
let outputs = expect_outputs("1 2 ccnum 64 127 ccout m.", 2);
|
||||||
|
assert!(outputs[0].contains("/midi/cc/1/64/"));
|
||||||
|
assert!(outputs[1].contains("/midi/cc/2/127/"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user