Skip to main content

vst3_host/
transport.rs

1//! A sample-accurate musical timeline: schedule MIDI clips and parameter-automation lanes on
2//! a beat grid and drive them into a plugin block by block.
3//!
4//! [`Timeline`] owns a tempo (BPM) and sample rate and a sample clock. Each call to
5//! [`Timeline::advance_block`] returns the events that fall in the next block as
6//! sample-accurate offsets (`(event, offset)` / `(param_id, offset, value)`), then advances the
7//! clock. [`Timeline::drive_block`] is the convenience that pushes those into a [`Plugin`] and
8//! renders one block.
9//!
10//! **Timebase:** clips and lanes are authored in **beats**. Slice 1 uses a single constant
11//! tempo (`bpm`); a varying tempo curve is future work, so beat↔sample conversion here is the
12//! constant-tempo `samples_per_beat = sample_rate * 60 / bpm`.
13//!
14//! ```no_run
15//! use vst3_host::{simple, transport::{Timeline, MidiClip}, midi::{MidiEvent, MidiChannel}};
16//! # fn main() -> vst3_host::Result<()> {
17//! let mut plugin = simple::load_plugin("/path/synth.vst3")?;
18//! plugin.start_processing()?;
19//!
20//! let clip = MidiClip::new()
21//!     .with(0.0, MidiEvent::NoteOn { channel: MidiChannel::Ch1, note: 60, velocity: 100 })
22//!     .with(2.0, MidiEvent::NoteOff { channel: MidiChannel::Ch1, note: 60, velocity: 0 });
23//! let mut timeline = Timeline::new(48_000.0, 120.0).with_clip(clip);
24//!
25//! let mut buffers = vst3_host::audio::AudioBuffers::new(0, 2, 512, 48_000.0);
26//! for _ in 0..96 {
27//!     timeline.drive_block(&mut plugin, &mut buffers)?;
28//! }
29//! # Ok(())
30//! # }
31//! ```
32
33use crate::audio::AudioBuffers;
34use crate::error::Result;
35use crate::midi::MidiEvent;
36use crate::parameters::ParameterAutomation;
37use crate::plugin::Plugin;
38
39/// A clip of MIDI events placed at beat positions on the timeline.
40#[derive(Debug, Clone, Default)]
41pub struct MidiClip {
42    /// `(beat, event)`, not required to be sorted — [`Timeline::advance_block`] windows by frame.
43    events: Vec<(f64, MidiEvent)>,
44}
45
46impl MidiClip {
47    /// An empty clip.
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Add an event at `beat` (fluent).
53    pub fn with(mut self, beat: f64, event: MidiEvent) -> Self {
54        self.events.push((beat, event));
55        self
56    }
57
58    /// Add an event at `beat`.
59    pub fn add(&mut self, beat: f64, event: MidiEvent) {
60        self.events.push((beat, event));
61    }
62}
63
64/// A parameter-automation lane: a parameter id plus its [`ParameterAutomation`] curve, whose
65/// point times are interpreted in **beats** (so the lane follows the timeline's tempo).
66#[derive(Debug, Clone)]
67pub struct AutomationLane {
68    /// Target parameter id.
69    pub param_id: u32,
70    /// The automation curve; point times are in beats.
71    pub automation: ParameterAutomation,
72    /// How many automation points to emit per block (denser = smoother, more events).
73    pub points_per_block: usize,
74}
75
76impl AutomationLane {
77    /// A lane targeting `param_id` driven by `automation` (point times in beats), emitting
78    /// `points_per_block` points per processed block.
79    pub fn new(param_id: u32, automation: ParameterAutomation, points_per_block: usize) -> Self {
80        Self {
81            param_id,
82            automation,
83            points_per_block,
84        }
85    }
86}
87
88/// The events a single block should deliver, as sample offsets within that block.
89#[derive(Debug, Clone, Default, PartialEq)]
90pub struct BlockEvents {
91    /// MIDI events with their sample offset within the block, in scheduled order.
92    pub midi: Vec<(MidiEvent, i32)>,
93    /// Parameter changes as `(param_id, sample_offset, value)`.
94    pub params: Vec<(u32, i32, f64)>,
95}
96
97/// How many frames [`Plugin::process_audio`] will render for `buffers`: the length of the first
98/// channel buffer (outputs first, then inputs), falling back to `block_size` when the buffers
99/// carry no channels at all. Mirrors the plugin's own derivation so the timeline windows events
100/// over exactly the span the plugin advances.
101fn rendered_frames(buffers: &AudioBuffers) -> usize {
102    buffers
103        .outputs
104        .iter()
105        .chain(buffers.inputs.iter())
106        .map(|channel| channel.len())
107        .next()
108        .unwrap_or(buffers.block_size)
109}
110
111/// A sample-accurate musical timeline driving MIDI clips and automation lanes into a plugin.
112#[derive(Debug, Clone)]
113pub struct Timeline {
114    sample_rate: f64,
115    bpm: f64,
116    sample_clock: u64,
117    clips: Vec<MidiClip>,
118    lanes: Vec<AutomationLane>,
119}
120
121impl Timeline {
122    /// A timeline at `sample_rate` and constant tempo `bpm`. `bpm` must be finite and `> 0`;
123    /// an invalid value falls back to `120.0` so beat↔sample conversion can't produce NaN.
124    pub fn new(sample_rate: f64, bpm: f64) -> Self {
125        let bpm = if bpm.is_finite() && bpm > 0.0 {
126            bpm
127        } else {
128            120.0
129        };
130        Self {
131            sample_rate,
132            bpm,
133            sample_clock: 0,
134            clips: Vec::new(),
135            lanes: Vec::new(),
136        }
137    }
138
139    /// Add a MIDI clip (fluent).
140    pub fn with_clip(mut self, clip: MidiClip) -> Self {
141        self.clips.push(clip);
142        self
143    }
144
145    /// Add an automation lane (fluent).
146    pub fn with_lane(mut self, lane: AutomationLane) -> Self {
147        self.lanes.push(lane);
148        self
149    }
150
151    /// Add a MIDI clip.
152    pub fn add_clip(&mut self, clip: MidiClip) {
153        self.clips.push(clip);
154    }
155
156    /// Add an automation lane.
157    pub fn add_lane(&mut self, lane: AutomationLane) {
158        self.lanes.push(lane);
159    }
160
161    /// The current playhead position in frames since the start.
162    pub fn sample_clock(&self) -> u64 {
163        self.sample_clock
164    }
165
166    /// Move the playhead to `frame` (e.g. to loop or seek). Does not emit events.
167    pub fn seek_frame(&mut self, frame: u64) {
168        self.sample_clock = frame;
169    }
170
171    /// Samples per beat at the current constant tempo.
172    pub fn samples_per_beat(&self) -> f64 {
173        self.sample_rate * 60.0 / self.bpm
174    }
175
176    /// Convert a beat position to an absolute frame index.
177    pub fn beat_to_frame(&self, beat: f64) -> u64 {
178        (beat * self.samples_per_beat()).round().max(0.0) as u64
179    }
180
181    /// Convert an absolute frame index to a beat position.
182    pub fn frame_to_beat(&self, frame: u64) -> f64 {
183        frame as f64 / self.samples_per_beat()
184    }
185
186    /// Collect the events that fall in the next `frames`-sample block as sample offsets, then
187    /// advance the playhead by `frames`. Clip events are windowed by frame index against the
188    /// half-open block `[clock, clock + frames)`; automation lanes emit their per-block points
189    /// (evaluated in the beat domain) tagged with the lane's parameter id.
190    pub fn advance_block(&mut self, frames: usize) -> BlockEvents {
191        let start = self.sample_clock;
192        // Saturating: `seek_frame` is public and takes any `u64`, so a seek near the top of the
193        // range followed by a normal block would otherwise overflow (panic in debug, wrap in
194        // release — wrapping would make the window run backwards and re-fire events).
195        let end = start.saturating_add(frames as u64);
196        let mut out = BlockEvents::default();
197
198        for clip in &self.clips {
199            for (beat, event) in &clip.events {
200                let frame = self.beat_to_frame(*beat);
201                if frame >= start && frame < end {
202                    out.midi.push((*event, (frame - start) as i32));
203                }
204            }
205        }
206        // Deliver scheduled events in time order so a NoteOff never precedes its NoteOn within a
207        // block when two clips overlap.
208        out.midi.sort_by_key(|(_, offset)| *offset);
209
210        if frames > 0 {
211            // Drive points_for_block in the beat domain: passing `samples_per_beat` as the
212            // "sample rate" makes its internal `offset / rate` term read as beats, so a
213            // beat-authored curve is evaluated correctly with sample-accurate offsets.
214            let start_beats = self.frame_to_beat(start);
215            let spb = self.samples_per_beat();
216            for lane in &self.lanes {
217                for (offset, value) in lane.automation.points_for_block(
218                    start_beats,
219                    frames,
220                    spb,
221                    lane.points_per_block,
222                ) {
223                    out.params.push((lane.param_id, offset, value));
224                }
225            }
226        }
227
228        self.sample_clock = end;
229        out
230    }
231
232    /// Advance one block and drive it into `plugin`: schedule its MIDI and parameter changes at
233    /// their sample offsets, then render `buffers`.
234    ///
235    /// The block length is the number of frames [`Plugin::process_audio`] will actually render
236    /// — the length of `buffers`' first channel buffer, not its `block_size` field. The two are
237    /// independent (both are public), and windowing by a different count than the plugin renders
238    /// would slip the timeline clock against the plugin's transport by the difference every block.
239    pub fn drive_block(&mut self, plugin: &mut Plugin, buffers: &mut AudioBuffers) -> Result<()> {
240        let frames = rendered_frames(buffers);
241        let events = self.advance_block(frames);
242        for (event, offset) in events.midi {
243            plugin.send_midi_event_at(event, offset)?;
244        }
245        for (id, offset, value) in events.params {
246            plugin.set_parameter_at(id, value, offset)?;
247        }
248        plugin.process_audio(buffers)
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    /// `seek_frame` is public and takes any `u64`, so the block window must not overflow: a panic
257    /// in debug, and in release a wrapped `end` that runs the window backwards and re-fires events.
258    #[test]
259    fn advance_block_near_the_end_of_the_clock_does_not_overflow() {
260        let mut t = Timeline::new(48_000.0, 120.0);
261        t.seek_frame(u64::MAX);
262        let _ = t.advance_block(1);
263        let _ = t.advance_block(4096);
264    }
265
266    /// `AudioBuffers`' fields are public, so `block_size` can disagree with the channel buffers
267    /// the plugin actually renders. `drive_block` must window by the latter, or the timeline
268    /// clock and the plugin's transport drift apart by the difference every block.
269    #[test]
270    fn rendered_frames_follows_the_channel_buffers_not_block_size() {
271        let mut buffers = AudioBuffers::new(0, 2, 512, 48_000.0);
272        assert_eq!(rendered_frames(&buffers), 512);
273
274        // A caller (or a resizing bridge) hands over shorter channel buffers than `block_size`.
275        for channel in &mut buffers.outputs {
276            channel.resize(128, 0.0);
277        }
278        assert_eq!(rendered_frames(&buffers), 128);
279
280        // Input-only buffers fall through to the input channels.
281        let mut input_only = AudioBuffers::new(1, 0, 512, 48_000.0);
282        input_only.inputs[0].resize(64, 0.0);
283        assert_eq!(rendered_frames(&input_only), 64);
284
285        // No channels at all: nothing to measure, so `block_size` is the only answer.
286        let empty = AudioBuffers::new(0, 0, 256, 48_000.0);
287        assert_eq!(rendered_frames(&empty), 256);
288    }
289
290    use crate::midi::MidiChannel;
291
292    fn note_on(n: u8) -> MidiEvent {
293        MidiEvent::NoteOn {
294            channel: MidiChannel::Ch1,
295            note: n,
296            velocity: 100,
297        }
298    }
299    fn note_off(n: u8) -> MidiEvent {
300        MidiEvent::NoteOff {
301            channel: MidiChannel::Ch1,
302            note: n,
303            velocity: 0,
304        }
305    }
306
307    #[test]
308    fn beat_frame_round_trip_at_120_and_140_bpm() {
309        // 120 bpm @ 48k: 1 beat = 0.5s = 24000 frames.
310        let t = Timeline::new(48_000.0, 120.0);
311        assert_eq!(t.beat_to_frame(0.0), 0);
312        assert_eq!(t.beat_to_frame(1.0), 24_000);
313        assert_eq!(t.beat_to_frame(0.5), 12_000);
314        assert_eq!(t.frame_to_beat(24_000), 1.0);
315
316        // 140 bpm @ 48k: 1 beat = 60/140 s ≈ 20571.43 frames → rounds to 20571.
317        let t = Timeline::new(48_000.0, 140.0);
318        assert_eq!(t.beat_to_frame(1.0), 20_571);
319    }
320
321    #[test]
322    fn invalid_bpm_falls_back_to_120() {
323        for bad in [0.0, -10.0, f64::NAN, f64::INFINITY] {
324            let t = Timeline::new(48_000.0, bad);
325            assert_eq!(
326                t.beat_to_frame(1.0),
327                24_000,
328                "bpm {bad} should fall back to 120"
329            );
330        }
331    }
332
333    #[test]
334    fn slices_clip_and_lane_into_block_offsets() {
335        // 120 bpm @ 48k. NoteOn @ beat 0 (frame 0); NoteOff @ beat 0.02 (=0.01s=480 frames).
336        let clip = MidiClip::new()
337            .with(0.0, note_on(60))
338            .with(0.02, note_off(60));
339        let lane = AutomationLane::new(
340            7,
341            ParameterAutomation::new()
342                .add_point(0.0, 0.0)
343                .add_point(4.0, 1.0),
344            1,
345        );
346        let mut t = Timeline::new(48_000.0, 120.0)
347            .with_clip(clip)
348            .with_lane(lane);
349
350        // Block 0: [0, 512). NoteOn @ offset 0, NoteOff @ offset 480; one lane point @ offset 0.
351        let b0 = t.advance_block(512);
352        assert_eq!(b0.midi, vec![(note_on(60), 0), (note_off(60), 480)]);
353        assert_eq!(b0.params.len(), 1);
354        assert_eq!(b0.params[0].0, 7);
355        assert_eq!(b0.params[0].1, 0);
356        assert_eq!(t.sample_clock(), 512);
357
358        // Block 1: [512, 1024). No MIDI (both events were before 512); lane still emits a point.
359        let b1 = t.advance_block(512);
360        assert!(b1.midi.is_empty());
361        assert_eq!(b1.params.len(), 1);
362        assert_eq!(t.sample_clock(), 1024);
363    }
364
365    #[test]
366    fn event_on_block_boundary_lands_in_the_next_block() {
367        // An event whose frame index == clock + frames must fall in the NEXT block (the window
368        // is half-open `[clock, clock + frames)`), guarding the off-by-one.
369        // 120 bpm @ 48k: beat 0.0213333.. → frame 512 exactly.
370        let boundary_beat = 512.0 / (48_000.0 * 60.0 / 120.0);
371        let clip = MidiClip::new().with(boundary_beat, note_on(60));
372        let mut t = Timeline::new(48_000.0, 120.0).with_clip(clip);
373
374        let b0 = t.advance_block(512); // [0, 512)
375        assert!(b0.midi.is_empty(), "frame 512 must not be in block [0,512)");
376        let b1 = t.advance_block(512); // [512, 1024)
377        assert_eq!(
378            b1.midi,
379            vec![(note_on(60), 0)],
380            "lands at offset 0 of the next block"
381        );
382    }
383}