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)>,
}
fn rendered_frames(buffers: &AudioBuffers) -> usize {
buffers
.outputs
.iter()
.chain(buffers.inputs.iter())
.map(|channel| channel.len())
.next()
.unwrap_or(buffers.block_size)
}
#[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.saturating_add(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 = rendered_frames(buffers);
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::*;
#[test]
fn advance_block_near_the_end_of_the_clock_does_not_overflow() {
let mut t = Timeline::new(48_000.0, 120.0);
t.seek_frame(u64::MAX);
let _ = t.advance_block(1);
let _ = t.advance_block(4096);
}
#[test]
fn rendered_frames_follows_the_channel_buffers_not_block_size() {
let mut buffers = AudioBuffers::new(0, 2, 512, 48_000.0);
assert_eq!(rendered_frames(&buffers), 512);
for channel in &mut buffers.outputs {
channel.resize(128, 0.0);
}
assert_eq!(rendered_frames(&buffers), 128);
let mut input_only = AudioBuffers::new(1, 0, 512, 48_000.0);
input_only.inputs[0].resize(64, 0.0);
assert_eq!(rendered_frames(&input_only), 64);
let empty = AudioBuffers::new(0, 0, 256, 48_000.0);
assert_eq!(rendered_frames(&empty), 256);
}
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"
);
}
}