Skip to main content

vst3_host/
playback.rs

1//! Batteries-included audio playback: drive a [`Plugin`] from an [`AudioBackend`].
2//!
3//! This is the glue that turns a loaded plugin into sound. [`play_with_backend`]
4//! opens the backend's default output device and pumps the plugin's
5//! [`Plugin::process_audio`] from the device callback, returning an [`AudioHandle`]
6//! that keeps the stream alive and lets you keep controlling the plugin (send MIDI,
7//! change parameters) while it plays.
8//!
9//! For the common case, prefer [`crate::simple::play`] or [`crate::Vst3Host::play`].
10
11use std::sync::{Arc, Mutex, MutexGuard};
12
13use crate::{
14    audio::{AudioBackend, AudioBuffers, AudioConfig, AudioStream},
15    error::{Error, Result},
16    plugin::Plugin,
17    realtime::{RealtimePluginRunner, RtControl},
18};
19
20/// A running audio stream driving a [`Plugin`].
21///
22/// Dropping the handle stops playback (the underlying device stream is released).
23/// While it lives, the plugin keeps running on the audio thread; use [`Self::lock`]
24/// to send MIDI or change parameters from your control thread.
25pub struct AudioHandle {
26    // Boxed as a trait object so `AudioHandle` is not generic over the backend.
27    // Kept solely to hold the stream open — dropping it stops audio.
28    _stream: Box<dyn AudioStream>,
29    // The capture stream for the duplex (effect-hosting) path; `None` for output-only play.
30    // Kept alive alongside `_stream`.
31    _input_stream: Option<Box<dyn AudioStream>>,
32    plugin: Arc<Mutex<Plugin>>,
33}
34
35impl AudioHandle {
36    /// Lock the running plugin to send MIDI, change parameters, etc.
37    ///
38    /// Recovers automatically if the audio thread previously panicked while holding
39    /// the lock (poisoned mutex), so control calls keep working.
40    pub fn lock(&self) -> MutexGuard<'_, Plugin> {
41        self.plugin
42            .lock()
43            .unwrap_or_else(|poisoned| poisoned.into_inner())
44    }
45
46    /// A shared handle to the plugin, e.g. to move into another thread.
47    pub fn plugin(&self) -> Arc<Mutex<Plugin>> {
48        Arc::clone(&self.plugin)
49    }
50
51    /// Stop playback now (equivalent to dropping the handle).
52    pub fn stop(self) {}
53}
54
55/// Interleave per-channel plugin output into a device's interleaved buffer.
56///
57/// `out` is laid out as `[frame0_ch0, frame0_ch1, ..., frame1_ch0, ...]` with
58/// `out.len() == frames * channels`. Channels the plugin didn't produce are left
59/// untouched (callers should pre-fill `out` with silence); plugin channels beyond
60/// `channels` are ignored.
61pub(crate) fn interleave_outputs(outputs: &[Vec<f32>], out: &mut [f32], channels: usize) {
62    if channels == 0 {
63        return;
64    }
65    let frames = out.len() / channels;
66    for ch in 0..channels.min(outputs.len()) {
67        let src = &outputs[ch];
68        for frame in 0..frames.min(src.len()) {
69            out[frame * channels + ch] = src[frame];
70        }
71    }
72}
73
74/// Resize a scratch buffer's output channels to exactly `frames`, clearing them.
75fn prepare_scratch(scratch: &mut AudioBuffers, frames: usize) {
76    for ch in &mut scratch.outputs {
77        if ch.len() != frames {
78            ch.resize(frames, 0.0);
79        }
80        ch.fill(0.0);
81    }
82    for ch in &mut scratch.inputs {
83        if ch.len() != frames {
84            ch.resize(frames, 0.0);
85        }
86        ch.fill(0.0);
87    }
88    scratch.block_size = frames;
89}
90
91/// Start streaming `plugin` through `backend`'s default output device.
92///
93/// The plugin is moved behind a shared lock so it can keep being controlled while
94/// the audio thread pulls blocks. Playback starts immediately and continues until
95/// the returned [`AudioHandle`] is dropped.
96///
97/// `config.output_channels` and `config.sample_rate` define the stream; the device
98/// callback may request varying block sizes, which the bridge accommodates.
99pub fn play_with_backend<B: AudioBackend>(
100    backend: &B,
101    plugin: Plugin,
102    config: AudioConfig,
103) -> Result<AudioHandle> {
104    let device = backend
105        .default_output_device()
106        .ok_or_else(|| Error::AudioBackendError("No default output device available".into()))?;
107
108    let channels = config.output_channels;
109    let sample_rate = config.sample_rate;
110
111    let plugin = Arc::new(Mutex::new(plugin));
112    // Ensure the plugin is armed before the first callback fires.
113    plugin
114        .lock()
115        .unwrap_or_else(|p| p.into_inner())
116        .start_processing()?;
117
118    let plugin_cb = Arc::clone(&plugin);
119    // Reusable scratch buffer so the steady-state callback does not allocate.
120    let mut scratch = AudioBuffers::new(0, channels, config.block_size, sample_rate);
121
122    let data_cb = Box::new(move |data: &mut [f32]| {
123        // Start from silence so unproduced channels/frames are quiet.
124        data.fill(0.0);
125        if channels == 0 {
126            return;
127        }
128        let frames = data.len() / channels;
129        prepare_scratch(&mut scratch, frames);
130
131        if let Ok(mut p) = plugin_cb.lock() {
132            if p.process_audio(&mut scratch).is_ok() {
133                interleave_outputs(&scratch.outputs, data, channels);
134            }
135        }
136    });
137
138    let err_cb = Box::new(|e: B::Error| {
139        log::error!("audio stream error: {}", e);
140    });
141
142    let stream = backend
143        .create_output_stream(&device, config, data_cb, err_cb)
144        .map_err(|e| Error::AudioBackendError(format!("Failed to create output stream: {}", e)))?;
145
146    stream
147        .play()
148        .map_err(|e| Error::AudioBackendError(format!("Failed to start stream: {}", e)))?;
149
150    Ok(AudioHandle {
151        _stream: Box::new(stream),
152        _input_stream: None,
153        plugin,
154    })
155}
156
157/// Drive a plugin with **live audio input** (effect hosting): capture from the default input
158/// device, process it through the plugin, and play the result on the default output device.
159///
160/// cpal has no true duplex stream, so this opens a separate input and output stream bridged
161/// by a lock-free ring: the input callback pushes captured frames, the output callback pops
162/// them into the plugin's input buffers, processes, and writes the output. `config`'s
163/// `input_channels`/`output_channels`/`sample_rate` define the streams. Like
164/// [`play_with_backend`], control the plugin via the returned [`AudioHandle`].
165///
166/// Note: the two device clocks are independent; this uses a small bridge buffer and tolerates
167/// drift by dropping/zero-filling at the edges. Suitable for monitoring/auditioning effects.
168pub fn play_with_input_backend<B: AudioBackend>(
169    backend: &B,
170    plugin: Plugin,
171    config: AudioConfig,
172) -> Result<AudioHandle> {
173    let in_device = backend
174        .default_input_device()
175        .ok_or_else(|| Error::AudioBackendError("No default input device available".into()))?;
176    let out_device = backend
177        .default_output_device()
178        .ok_or_else(|| Error::AudioBackendError("No default output device available".into()))?;
179
180    let in_channels = config.input_channels.max(1);
181    let out_channels = config.output_channels;
182    let sample_rate = config.sample_rate;
183
184    let plugin = Arc::new(Mutex::new(plugin));
185    plugin
186        .lock()
187        .unwrap_or_else(|p| p.into_inner())
188        .start_processing()?;
189
190    // SPSC bridge: input callback (producer) -> output callback (consumer). Hold a few
191    // blocks of interleaved input so the independent device clocks don't starve immediately.
192    let ring_cap = (config.block_size * in_channels * 8).max(2048);
193    let (mut producer, mut consumer) = rtrb::RingBuffer::<f32>::new(ring_cap);
194
195    let in_data_cb = Box::new(move |data: &[f32]| {
196        // Drop on full (output side fell behind) rather than block the capture callback.
197        for &s in data {
198            let _ = producer.push(s);
199        }
200    });
201    let in_err_cb = Box::new(|e: B::Error| log::error!("input stream error: {}", e));
202    let input_stream = backend
203        .create_input_stream(&in_device, config, in_data_cb, in_err_cb)
204        .map_err(|e| Error::AudioBackendError(format!("Failed to create input stream: {}", e)))?;
205
206    let plugin_cb = Arc::clone(&plugin);
207    let mut scratch = AudioBuffers::new(in_channels, out_channels, config.block_size, sample_rate);
208    let out_data_cb = Box::new(move |data: &mut [f32]| {
209        data.fill(0.0);
210        if out_channels == 0 {
211            return;
212        }
213        let frames = data.len() / out_channels;
214        prepare_scratch(&mut scratch, frames);
215        // Deinterleave captured input from the ring into the plugin's input buffers
216        // (interleaved frame-major order matches the input callback's push order).
217        for f in 0..frames {
218            for ch in scratch.inputs.iter_mut() {
219                ch[f] = consumer.pop().unwrap_or(0.0);
220            }
221        }
222        if let Ok(mut p) = plugin_cb.lock() {
223            if p.process_audio(&mut scratch).is_ok() {
224                interleave_outputs(&scratch.outputs, data, out_channels);
225            }
226        }
227    });
228    let out_err_cb = Box::new(|e: B::Error| log::error!("output stream error: {}", e));
229    let output_stream = backend
230        .create_output_stream(&out_device, config, out_data_cb, out_err_cb)
231        .map_err(|e| Error::AudioBackendError(format!("Failed to create output stream: {}", e)))?;
232
233    input_stream
234        .play()
235        .map_err(|e| Error::AudioBackendError(format!("Failed to start input stream: {}", e)))?;
236    output_stream
237        .play()
238        .map_err(|e| Error::AudioBackendError(format!("Failed to start output stream: {}", e)))?;
239
240    Ok(AudioHandle {
241        _stream: Box::new(output_stream),
242        _input_stream: Some(Box::new(input_stream)),
243        plugin,
244    })
245}
246
247/// A running real-time audio stream (the [`RealtimePluginRunner`] variant of
248/// [`AudioHandle`]). Holds the device stream open and exposes the lock-free [`RtControl`];
249/// dropping it stops playback.
250pub struct RtAudioHandle {
251    _stream: Box<dyn AudioStream>,
252    control: RtControl,
253}
254
255impl RtAudioHandle {
256    /// The lock-free control handle — queue MIDI and parameter changes without locking the
257    /// audio thread.
258    pub fn control(&mut self) -> &mut RtControl {
259        &mut self.control
260    }
261
262    /// Stop playback now (equivalent to dropping the handle).
263    pub fn stop(self) {}
264}
265
266/// Like [`play_with_backend`], but drives the plugin through a [`RealtimePluginRunner`] so the
267/// audio callback takes **no lock** — control changes flow over a lock-free queue. Returns an
268/// [`RtAudioHandle`] that keeps the stream alive and exposes the [`RtControl`].
269///
270/// `command_capacity` bounds how many MIDI/parameter commands can queue between callbacks.
271pub fn play_realtime_with_backend<B: AudioBackend>(
272    backend: &B,
273    plugin: Plugin,
274    config: AudioConfig,
275    command_capacity: usize,
276) -> Result<RtAudioHandle> {
277    let device = backend
278        .default_output_device()
279        .ok_or_else(|| Error::AudioBackendError("No default output device available".into()))?;
280
281    let channels = config.output_channels;
282    let sample_rate = config.sample_rate;
283
284    let (mut runner, control) = RealtimePluginRunner::new(plugin, command_capacity);
285    runner.start()?;
286
287    // Reusable scratch buffer so the steady-state callback does not allocate.
288    let mut scratch = AudioBuffers::new(0, channels, config.block_size, sample_rate);
289
290    let data_cb = Box::new(move |data: &mut [f32]| {
291        data.fill(0.0);
292        if channels == 0 {
293            return;
294        }
295        let frames = data.len() / channels;
296        prepare_scratch(&mut scratch, frames);
297
298        // No lock: the runner owns the plugin and drains its command queue here.
299        if runner.process(&mut scratch).is_ok() {
300            interleave_outputs(&scratch.outputs, data, channels);
301        }
302    });
303
304    let err_cb = Box::new(|e: B::Error| {
305        log::error!("audio stream error: {}", e);
306    });
307
308    let stream = backend
309        .create_output_stream(&device, config, data_cb, err_cb)
310        .map_err(|e| Error::AudioBackendError(format!("Failed to create output stream: {}", e)))?;
311
312    stream
313        .play()
314        .map_err(|e| Error::AudioBackendError(format!("Failed to start stream: {}", e)))?;
315
316    Ok(RtAudioHandle {
317        _stream: Box::new(stream),
318        control,
319    })
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn interleaves_two_channels() {
328        // outputs[ch][frame]
329        let outputs = vec![vec![1.0, 2.0, 3.0], vec![-1.0, -2.0, -3.0]];
330        let mut out = vec![0.0; 6]; // 3 frames * 2 channels
331        interleave_outputs(&outputs, &mut out, 2);
332        assert_eq!(out, vec![1.0, -1.0, 2.0, -2.0, 3.0, -3.0]);
333    }
334
335    #[test]
336    fn ignores_extra_plugin_channels() {
337        // Plugin produced 3 channels but the device only has 2.
338        let outputs = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![9.0, 9.0]];
339        let mut out = vec![0.0; 4];
340        interleave_outputs(&outputs, &mut out, 2);
341        assert_eq!(out, vec![1.0, 3.0, 2.0, 4.0]);
342    }
343
344    #[test]
345    fn leaves_missing_channels_as_silence() {
346        // Device wants 2 channels but plugin produced only 1 (mono).
347        let outputs = vec![vec![0.5, 0.6]];
348        let mut out = vec![0.0; 4];
349        interleave_outputs(&outputs, &mut out, 2);
350        // ch1 stays at the pre-filled silence.
351        assert_eq!(out, vec![0.5, 0.0, 0.6, 0.0]);
352    }
353
354    #[test]
355    fn zero_channels_is_a_noop() {
356        let outputs = vec![vec![1.0, 2.0]];
357        let mut out = vec![7.0, 7.0];
358        interleave_outputs(&outputs, &mut out, 0);
359        assert_eq!(out, vec![7.0, 7.0]);
360    }
361
362    #[test]
363    fn prepare_scratch_resizes_and_clears() {
364        let mut scratch = AudioBuffers::new(1, 2, 4, 48000.0);
365        scratch.outputs[0][0] = 9.0;
366        prepare_scratch(&mut scratch, 8);
367        assert_eq!(scratch.block_size, 8);
368        assert!(scratch.outputs.iter().all(|c| c.len() == 8));
369        assert!(scratch.inputs.iter().all(|c| c.len() == 8));
370        assert!(scratch.outputs.iter().flatten().all(|&s| s == 0.0));
371    }
372}