use crate::audio::AudioBuffers;
use crate::error::Result;
use crate::midi::MidiEvent;
use crate::parameters::ParameterAutomation;
use crate::plugin::Plugin;
#[derive(Debug, Clone, Default)]
pub struct MidiClip {
events: Vec<(f64, MidiEvent)>,
}
impl MidiClip {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, beat: f64, event: MidiEvent) -> Self {
self.events.push((beat, event));
self
}
pub fn add(&mut self, beat: f64, event: MidiEvent) {
self.events.push((beat, event));
}
}
#[derive(Debug, Clone)]
pub struct AutomationLane {
pub param_id: u32,
pub automation: ParameterAutomation,
pub points_per_block: usize,
}
impl AutomationLane {
pub fn new(param_id: u32, automation: ParameterAutomation, points_per_block: usize) -> Self {
Self {
param_id,
automation,
points_per_block,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct BlockEvents {
pub midi: Vec<(MidiEvent, i32)>,
pub params: Vec<(u32, i32, f64)>,
}
#[derive(Debug, Clone)]
pub struct Timeline {
sample_rate: f64,
bpm: f64,
sample_clock: u64,
clips: Vec<MidiClip>,
lanes: Vec<AutomationLane>,
}
impl Timeline {
pub fn new(sample_rate: f64, bpm: f64) -> Self {
let bpm = if bpm.is_finite() && bpm > 0.0 {
bpm
} else {
120.0
};
Self {
sample_rate,
bpm,
sample_clock: 0,
clips: Vec::new(),
lanes: Vec::new(),
}
}
pub fn with_clip(mut self, clip: MidiClip) -> Self {
self.clips.push(clip);
self
}
pub fn with_lane(mut self, lane: AutomationLane) -> Self {
self.lanes.push(lane);
self
}
pub fn add_clip(&mut self, clip: MidiClip) {
self.clips.push(clip);
}
pub fn add_lane(&mut self, lane: AutomationLane) {
self.lanes.push(lane);
}
pub fn sample_clock(&self) -> u64 {
self.sample_clock
}
pub fn seek_frame(&mut self, frame: u64) {
self.sample_clock = frame;
}
pub fn samples_per_beat(&self) -> f64 {
self.sample_rate * 60.0 / self.bpm
}
pub fn beat_to_frame(&self, beat: f64) -> u64 {
(beat * self.samples_per_beat()).round().max(0.0) as u64
}
pub fn frame_to_beat(&self, frame: u64) -> f64 {
frame as f64 / self.samples_per_beat()
}
pub fn advance_block(&mut self, frames: usize) -> BlockEvents {
let start = self.sample_clock;
let end = start + frames as u64;
let mut out = BlockEvents::default();
for clip in &self.clips {
for (beat, event) in &clip.events {
let frame = self.beat_to_frame(*beat);
if frame >= start && frame < end {
out.midi.push((*event, (frame - start) as i32));
}
}
}
out.midi.sort_by_key(|(_, offset)| *offset);
if frames > 0 {
let start_beats = self.frame_to_beat(start);
let spb = self.samples_per_beat();
for lane in &self.lanes {
for (offset, value) in lane.automation.points_for_block(
start_beats,
frames,
spb,
lane.points_per_block,
) {
out.params.push((lane.param_id, offset, value));
}
}
}
self.sample_clock = end;
out
}
pub fn drive_block(&mut self, plugin: &mut Plugin, buffers: &mut AudioBuffers) -> Result<()> {
let frames = buffers.block_size;
let events = self.advance_block(frames);
for (event, offset) in events.midi {
plugin.send_midi_event_at(event, offset)?;
}
for (id, offset, value) in events.params {
plugin.set_parameter_at(id, value, offset)?;
}
plugin.process_audio(buffers)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::midi::MidiChannel;
fn note_on(n: u8) -> MidiEvent {
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: n,
velocity: 100,
}
}
fn note_off(n: u8) -> MidiEvent {
MidiEvent::NoteOff {
channel: MidiChannel::Ch1,
note: n,
velocity: 0,
}
}
#[test]
fn beat_frame_round_trip_at_120_and_140_bpm() {
let t = Timeline::new(48_000.0, 120.0);
assert_eq!(t.beat_to_frame(0.0), 0);
assert_eq!(t.beat_to_frame(1.0), 24_000);
assert_eq!(t.beat_to_frame(0.5), 12_000);
assert_eq!(t.frame_to_beat(24_000), 1.0);
let t = Timeline::new(48_000.0, 140.0);
assert_eq!(t.beat_to_frame(1.0), 20_571);
}
#[test]
fn invalid_bpm_falls_back_to_120() {
for bad in [0.0, -10.0, f64::NAN, f64::INFINITY] {
let t = Timeline::new(48_000.0, bad);
assert_eq!(
t.beat_to_frame(1.0),
24_000,
"bpm {bad} should fall back to 120"
);
}
}
#[test]
fn slices_clip_and_lane_into_block_offsets() {
let clip = MidiClip::new()
.with(0.0, note_on(60))
.with(0.02, note_off(60));
let lane = AutomationLane::new(
7,
ParameterAutomation::new()
.add_point(0.0, 0.0)
.add_point(4.0, 1.0),
1,
);
let mut t = Timeline::new(48_000.0, 120.0)
.with_clip(clip)
.with_lane(lane);
let b0 = t.advance_block(512);
assert_eq!(b0.midi, vec![(note_on(60), 0), (note_off(60), 480)]);
assert_eq!(b0.params.len(), 1);
assert_eq!(b0.params[0].0, 7);
assert_eq!(b0.params[0].1, 0);
assert_eq!(t.sample_clock(), 512);
let b1 = t.advance_block(512);
assert!(b1.midi.is_empty());
assert_eq!(b1.params.len(), 1);
assert_eq!(t.sample_clock(), 1024);
}
#[test]
fn event_on_block_boundary_lands_in_the_next_block() {
let boundary_beat = 512.0 / (48_000.0 * 60.0 / 120.0);
let clip = MidiClip::new().with(boundary_beat, note_on(60));
let mut t = Timeline::new(48_000.0, 120.0).with_clip(clip);
let b0 = t.advance_block(512); assert!(b0.midi.is_empty(), "frame 512 must not be in block [0,512)");
let b1 = t.advance_block(512); assert_eq!(
b1.midi,
vec![(note_on(60), 0)],
"lands at offset 0 of the next block"
);
}
}