truce_core/buffer.rs
1use truce_params::sample::Sample;
2
3/// Non-interleaved audio buffer. Borrows host memory through the
4/// format wrapper.
5///
6/// Generic over the sample type `S` (the plugin's chosen precision,
7/// `f32` or `f64`). The format wrapper bridges between host-buffer
8/// precision and `S` at the block boundary - see
9/// [`RawBufferScratch::build`]. Plugin code under
10/// `use truce::prelude::*;` (f32) or `use truce::prelude64::*;` (f64)
11/// sees `AudioBuffer<S>` with `S` already picked.
12///
13/// **In-place I/O.** Some hosts (Reaper, pluginval) pass the same
14/// buffer for both input and output of a given channel. By default
15/// the wrapper copies the aliased inputs into per-channel scratch so
16/// `input(ch)` and `output(ch)` are disjoint `&[S]` / `&mut [S]` -
17/// no plugin code change required. Plugins that opt into
18/// `Plugin::supports_in_place() = true` skip the copy and must use
19/// [`Self::in_out_mut`] for channels where [`Self::is_in_place`]
20/// returns `true`.
21pub struct AudioBuffer<'a, S: Sample = f32> {
22 inputs: &'a [&'a [S]],
23 outputs: &'a mut [&'a mut [S]],
24 /// Bit `ch` is set when `inputs[ch]` and `outputs[ch]` point to
25 /// the same host memory. Channels ≥ 64 are always reported as
26 /// non-aliased - formats with that many channels are exotic
27 /// enough to be a follow-up.
28 in_place_mask: u64,
29 offset: usize,
30 num_samples: usize,
31}
32
33impl<'a, S: Sample> AudioBuffer<'a, S> {
34 /// Safe wrapper around [`Self::from_slices`] for callers that hold their
35 /// own owned `Vec<Vec<S>>` (e.g. `truce-driver`'s test harness).
36 /// Forwards to the unsafe constructor - the borrow checker proves
37 /// the lifetime invariants the `unsafe fn` requires when both
38 /// slice arrays and the buffer itself live in the same scope.
39 /// `num_samples > slice length` still asserts in debug builds.
40 pub fn from_slices_checked(
41 inputs: &'a [&'a [S]],
42 outputs: &'a mut [&'a mut [S]],
43 num_samples: usize,
44 ) -> Self {
45 // SAFETY: caller hands us references that the borrow checker
46 // already proved valid for `'a`; the debug-mode assertions
47 // inside `from_slices` cover the `num_samples` bound.
48 unsafe { Self::from_slices(inputs, outputs, num_samples) }
49 }
50
51 /// Create a buffer from pre-split channel slices.
52 /// Used by format wrappers after converting from host-specific buffer types.
53 ///
54 /// # Safety
55 /// The caller must ensure the slices are valid for the lifetime `'a`
56 /// and that `num_samples` does not exceed any slice's length.
57 ///
58 /// # Panics
59 ///
60 /// In debug builds only, panics if any input channel aliases an
61 /// output channel or `num_samples` exceeds the length of any
62 /// input/output slice. Release builds skip these checks (they're
63 /// safety preconditions, not runtime invariants).
64 pub unsafe fn from_slices(
65 inputs: &'a [&'a [S]],
66 outputs: &'a mut [&'a mut [S]],
67 num_samples: usize,
68 ) -> Self {
69 #[cfg(debug_assertions)]
70 {
71 // Verify no input channel aliases any output channel.
72 for (i, inp) in inputs.iter().enumerate() {
73 let i_start = inp.as_ptr() as usize;
74 let i_end = i_start + std::mem::size_of_val(*inp);
75 for (o, out) in outputs.iter().enumerate() {
76 let o_start = out.as_ptr() as usize;
77 let o_end = o_start + std::mem::size_of_val(*out);
78 assert!(
79 i_end <= o_start || o_end <= i_start,
80 "AudioBuffer: input channel {i} and output channel {o} alias \
81 - pass disjoint slices or use RawBufferScratch::build which \
82 handles aliasing automatically",
83 );
84 }
85 }
86 // Verify num_samples doesn't exceed any slice length.
87 for (i, inp) in inputs.iter().enumerate() {
88 assert!(
89 num_samples <= inp.len(),
90 "AudioBuffer: num_samples ({num_samples}) exceeds input channel {i} length ({})",
91 inp.len(),
92 );
93 }
94 for (o, out) in outputs.iter().enumerate() {
95 assert!(
96 num_samples <= out.len(),
97 "AudioBuffer: num_samples ({num_samples}) exceeds output channel {o} length ({})",
98 out.len(),
99 );
100 }
101 }
102 AudioBuffer {
103 inputs,
104 outputs,
105 in_place_mask: 0,
106 offset: 0,
107 num_samples,
108 }
109 }
110
111 /// Set the in-place mask. Called by format wrappers (or
112 /// `RawBufferScratch::build`) after construction once they've
113 /// determined which channels alias on the host side.
114 #[inline]
115 pub fn set_in_place_mask(&mut self, mask: u64) {
116 self.in_place_mask = mask;
117 }
118
119 /// `true` when the host passes a single buffer for both input and
120 /// output of `ch` (in-place I/O). Use [`Self::in_out_mut`] to read
121 /// and write that buffer directly when this returns `true`.
122 #[must_use]
123 pub fn is_in_place(&self, ch: usize) -> bool {
124 ch < 64 && (self.in_place_mask >> ch) & 1 == 1
125 }
126
127 /// Read+write slice for an in-place channel - the same memory the
128 /// host gave us for both input and output. Each sample reads as
129 /// the input value before the plugin overwrites it.
130 ///
131 /// Only meaningful when [`Self::is_in_place`] returns `true`. On a
132 /// non-in-place channel this returns the output slice with no
133 /// input data in it; reading is allowed but produces uninitialized
134 /// host-buffer contents.
135 pub fn in_out_mut(&mut self, ch: usize) -> &mut [S] {
136 let end = self.offset + self.num_samples;
137 &mut self.outputs[ch][self.offset..end]
138 }
139
140 #[must_use]
141 pub fn num_samples(&self) -> usize {
142 self.num_samples
143 }
144
145 #[must_use]
146 pub fn num_input_channels(&self) -> usize {
147 self.inputs.len()
148 }
149
150 #[must_use]
151 pub fn num_output_channels(&self) -> usize {
152 self.outputs.len()
153 }
154
155 #[must_use]
156 pub fn input(&self, channel: usize) -> &[S] {
157 let end = self.offset + self.num_samples;
158 &self.inputs[channel][self.offset..end]
159 }
160
161 pub fn output(&mut self, channel: usize) -> &mut [S] {
162 let end = self.offset + self.num_samples;
163 &mut self.outputs[channel][self.offset..end]
164 }
165
166 /// Number of channels (min of input and output).
167 #[must_use]
168 pub fn channels(&self) -> usize {
169 self.inputs.len().min(self.outputs.len())
170 }
171
172 /// Get an input/output pair for a channel. Useful for in-place processing.
173 pub fn io_pair(&mut self, in_ch: usize, out_ch: usize) -> (&[S], &mut [S]) {
174 let end = self.offset + self.num_samples;
175 let input = &self.inputs[in_ch][self.offset..end];
176 let output = &mut self.outputs[out_ch][self.offset..end];
177 (input, output)
178 }
179
180 /// Get an input/output pair for the same channel index. Shorthand for `io_pair(ch, ch)`.
181 pub fn io(&mut self, ch: usize) -> (&[S], &mut [S]) {
182 self.io_pair(ch, ch)
183 }
184
185 /// Iterate per-channel, in fixed-size `N`-sample chunks. The
186 /// last chunk of each channel may be shorter than `N`; it's
187 /// yielded as a [`ChunkItem::Tail`] with the actual remaining
188 /// length, and the caller falls back to scalar for it. Full
189 /// `N`-sample chunks arrive as [`ChunkItem::Full`] carrying
190 /// `&[S; N]` / `&mut [S; N]` stack arrays - exactly the shape
191 /// the per-op SIMD primitives in `truce-simd` expect.
192 ///
193 /// Iteration order is channel-major: all chunks of channel 0,
194 /// then all chunks of channel 1, etc. Matches the natural
195 /// orientation for per-channel state (biquad coefficients,
196 /// per-channel meters) and lets the caller read its smoothed
197 /// params once per chunk instead of once per sample.
198 ///
199 /// The returned object is a "lending iterator" - it doesn't
200 /// implement [`Iterator`] because each yielded item borrows
201 /// from the iterator itself. Use `while let Some(chunk) = …
202 /// .next()`:
203 ///
204 /// ```ignore
205 /// let mut chunks = buffer.chunks_mut::<32>();
206 /// while let Some(chunk) = chunks.next() {
207 /// match chunk {
208 /// ChunkItem::Full { ch, inp, out } => {
209 /// // SIMD-friendly path, inp / out are &[f32; 32]
210 /// }
211 /// ChunkItem::Tail { ch, inp, out } => {
212 /// // scalar fallback for the trailing samples
213 /// }
214 /// }
215 /// }
216 /// ```
217 ///
218 /// Const-generic `N` is the chunk size; pick it to match the
219 /// SIMD width × unroll factor for your inner op (32 / 64 are
220 /// good defaults for current Apple Silicon + `x86_64`).
221 pub fn chunks_mut<const N: usize>(&mut self) -> ChunksMut<'_, 'a, S, N> {
222 ChunksMut {
223 buffer: self,
224 ch: 0,
225 pos: 0,
226 }
227 }
228
229 /// Iterate per-frame and hand a fixed-size `(input, output)`
230 /// stack-array pair to `tick`. Sized at the type level by const
231 /// generic `N`, which must equal [`Self::channels`].
232 ///
233 /// `io()` / `io_pair()` give a per-channel slice view, which is
234 /// the right shape for "process channel `ch` in isolation"
235 /// loops. But libraries that expect a per-frame `(in: &[S],
236 /// out: &mut [S])` callback - `fundsp::AudioUnit::tick`,
237 /// `nih_plug`'s frame iterators, custom per-sample DSP nodes -
238 /// can't take that shape directly without either copying inputs
239 /// into a scratch first (heap allocation on the audio thread)
240 /// or fighting the borrow checker over two simultaneous `&mut`
241 /// borrows of the buffer.
242 ///
243 /// This helper does the per-frame transpose in-place against a
244 /// stack-allocated `[S; N]` pair, calls `tick` `num_samples()`
245 /// times, and writes back. No heap, no borrow gymnastics at the
246 /// call site:
247 ///
248 /// ```ignore
249 /// // Stereo plugin delegating per-frame DSP to fundsp:
250 /// buffer.for_each_frame::<2, _>(|frame_in, frame_out| {
251 /// self.graph.tick(frame_in, frame_out);
252 /// });
253 /// ```
254 ///
255 /// `&[S; N]` deref-coerces to `&[S]` at the call site, so
256 /// callers can pass the arrays straight to slice-taking APIs
257 /// like fundsp's `tick`.
258 ///
259 /// # Panics
260 ///
261 /// Debug builds panic if `N != self.channels()`. Release builds
262 /// rely on the same precondition without checking; reading past
263 /// the actual channel count would index out of bounds anyway.
264 pub fn for_each_frame<const N: usize, F>(&mut self, mut tick: F)
265 where
266 F: FnMut(&[S; N], &mut [S; N]),
267 {
268 debug_assert_eq!(
269 N,
270 self.channels(),
271 "for_each_frame::<{N}> requires the buffer to have exactly {N} channels"
272 );
273 let mut frame_in = [S::default(); N];
274 let mut frame_out = [S::default(); N];
275 let end = self.offset + self.num_samples;
276 for i in self.offset..end {
277 for (ch, slot) in frame_in.iter_mut().enumerate() {
278 *slot = self.inputs[ch][i];
279 }
280 tick(&frame_in, &mut frame_out);
281 for (ch, sample) in frame_out.iter().enumerate() {
282 self.outputs[ch][i] = *sample;
283 }
284 }
285 }
286
287 /// Like [`Self::for_each_frame`] but for a DSP whose frame shape is a
288 /// fixed `(IN, OUT)` that need not match the bus width. Input slot `k`
289 /// reads bus input channel `k`, repeating the last available channel
290 /// when the bus has fewer than `IN` inputs - so a mono source fans into
291 /// both inputs of a stereo graph. Output slot `k` writes bus output
292 /// channel `k` while `k < num_output_channels`; frame outputs past the
293 /// bus width are dropped.
294 ///
295 /// This lets a plugin built around a fixed-shape DSP (a fundsp
296 /// `reverb_stereo`, a dasp graph) run on any declared bus layout -
297 /// `(2, 2)` stereo and `(1, 2)` mono-in/stereo-out alike - through one
298 /// `for_each_frame_io::<2, 2>` call, with no per-width branch. A bus
299 /// with no inputs (an instrument) feeds silence.
300 pub fn for_each_frame_io<const IN: usize, const OUT: usize, F>(&mut self, mut tick: F)
301 where
302 F: FnMut(&[S; IN], &mut [S; OUT]),
303 {
304 let num_in = self.inputs.len();
305 let num_out = self.outputs.len();
306 let mut frame_in = [S::default(); IN];
307 let mut frame_out = [S::default(); OUT];
308 let end = self.offset + self.num_samples;
309 for i in self.offset..end {
310 if num_in > 0 {
311 for (k, slot) in frame_in.iter_mut().enumerate() {
312 *slot = self.inputs[k.min(num_in - 1)][i];
313 }
314 }
315 tick(&frame_in, &mut frame_out);
316 for (k, sample) in frame_out.iter().enumerate().take(num_out) {
317 self.outputs[k][i] = *sample;
318 }
319 }
320 }
321
322 /// [`Self::for_each_frame_io`] specialized to a stereo `(2, 2)` DSP -
323 /// the common case (a `reverb_stereo`, a stereo filter block). Runs the
324 /// 2-in/2-out `tick` over any declared bus: a mono source fans into
325 /// both inputs, a stereo bus maps 1:1, so a stereo effect needs no
326 /// per-width branch and no turbofish.
327 pub fn for_each_stereo_frame<F>(&mut self, tick: F)
328 where
329 F: FnMut(&[S; 2], &mut [S; 2]),
330 {
331 self.for_each_frame_io::<2, 2, F>(tick);
332 }
333
334 /// Peak absolute value across an output channel, returned as `f32`
335 /// because meters / UI display always work in `f32` regardless of
336 /// the plugin's internal precision.
337 ///
338 /// Short-circuits and returns `f32::NAN` on the **first** NaN
339 /// sample seen, so meters can flag runaway plugins instead of
340 /// silently reporting "peaks within range" while NaN poison
341 /// spreads downstream.
342 #[must_use]
343 pub fn output_peak(&self, ch: usize) -> f32 {
344 let end = self.offset + self.num_samples;
345 let mut peak = 0.0f32;
346 for &b in &self.outputs[ch][self.offset..end] {
347 let v = b.to_f32();
348 if v.is_nan() {
349 return f32::NAN;
350 }
351 let abs = v.abs();
352 if abs > peak {
353 peak = abs;
354 }
355 }
356 peak
357 }
358
359 /// Return a sub-block view covering samples `start..start+len`.
360 ///
361 /// The returned buffer borrows `self` exclusively - you cannot use
362 /// the original buffer while the slice is alive.
363 ///
364 /// # Panics
365 /// Panics if `start + len > self.num_samples()`.
366 pub fn slice(&mut self, start: usize, len: usize) -> AudioBuffer<'_, S> {
367 assert!(
368 start + len <= self.num_samples,
369 "slice({start}, {len}) out of bounds for buffer of {} samples",
370 self.num_samples,
371 );
372 let new_offset = self.offset + start;
373 // SAFETY: We construct an AudioBuffer<'a, S> and transmute to AudioBuffer<'_, S>.
374 // These have identical memory layout (lifetimes are erased at runtime).
375 // This is sound because:
376 // 1. &mut self prevents the caller from using self while the slice exists
377 // 2. The underlying channel memory lives for 'a which outlives '_
378 // 3. Bounds are checked by the assert above
379 let self_ptr: *mut Self = self;
380 unsafe {
381 let s = &mut *self_ptr;
382 std::mem::transmute::<AudioBuffer<'a, S>, AudioBuffer<'_, S>>(AudioBuffer {
383 inputs: s.inputs,
384 outputs: &mut *s.outputs,
385 in_place_mask: s.in_place_mask,
386 offset: new_offset,
387 num_samples: len,
388 })
389 }
390 }
391}
392
393/// One yielded chunk from [`AudioBuffer::chunks_mut`].
394///
395/// `Full` is the SIMD-friendly path: `inp` and `out` are stack
396/// arrays of exactly `N` elements, ready to feed `truce-simd`'s
397/// block ops. `Tail` is the trailing fragment when `num_samples()`
398/// isn't a multiple of `N`; fall back to a scalar loop.
399pub enum ChunkItem<'b, S: Sample, const N: usize> {
400 /// Full N-sample chunk. The `&[S; N]` / `&mut [S; N]` are the
401 /// shape `truce-simd` ops are written against - no slice
402 /// length check at the call site.
403 Full {
404 /// Channel index this chunk belongs to.
405 ch: usize,
406 /// Sample offset within the audio block this chunk starts
407 /// at. Use this when indexing into a precomputed envelope
408 /// array - `chunks_mut` iterates channel-major, so the
409 /// envelope (typically read once per audio block via
410 /// `read_into(&mut env[..num_samples])`) is shared across all
411 /// channel passes.
412 sample: usize,
413 /// Read-only N-sample input slice.
414 inp: &'b [S; N],
415 /// Mutable N-sample output slice.
416 out: &'b mut [S; N],
417 },
418 /// Trailing chunk when `num_samples()` isn't a multiple of `N`.
419 /// Length is in `(0, N)`. Fall back to scalar processing.
420 Tail {
421 /// Channel index this chunk belongs to.
422 ch: usize,
423 /// Sample offset within the audio block this chunk starts at.
424 sample: usize,
425 /// Read-only tail input slice; length < N.
426 inp: &'b [S],
427 /// Mutable tail output slice; length < N.
428 out: &'b mut [S],
429 },
430}
431
432/// Lending iterator returned by [`AudioBuffer::chunks_mut`].
433///
434/// Does not implement [`Iterator`] because each yielded
435/// [`ChunkItem`] borrows from the iterator itself - the standard
436/// "GATs would help here" pattern. Drive it with `while let
437/// Some(chunk) = chunks.next()` instead. See
438/// [`AudioBuffer::chunks_mut`] for a worked example.
439pub struct ChunksMut<'b, 'a, S: Sample, const N: usize> {
440 buffer: &'b mut AudioBuffer<'a, S>,
441 /// Current channel being walked.
442 ch: usize,
443 /// Position within the current channel, relative to
444 /// `buffer.offset`. Advances by N each Full chunk, then jumps
445 /// to `num_samples` for the Tail (or directly past it when
446 /// `num_samples` is a multiple of N).
447 pos: usize,
448}
449
450impl<S: Sample, const N: usize> ChunksMut<'_, '_, S, N> {
451 /// Yield the next chunk, or `None` when every channel has been
452 /// fully walked.
453 ///
454 /// Method-on-self rather than `Iterator::next` because each
455 /// yielded [`ChunkItem`] borrows from `self`; GATs would be
456 /// needed to express that through the `Iterator` trait.
457 #[allow(clippy::should_implement_trait, clippy::missing_panics_doc)]
458 pub fn next(&mut self) -> Option<ChunkItem<'_, S, N>> {
459 loop {
460 if self.ch >= self.buffer.outputs.len() {
461 return None;
462 }
463 let ns = self.buffer.num_samples;
464 if self.pos >= ns {
465 self.ch += 1;
466 self.pos = 0;
467 continue;
468 }
469 let abs_start = self.buffer.offset + self.pos;
470 let remaining = ns - self.pos;
471 let take = remaining.min(N);
472 let abs_end = abs_start + take;
473 let ch = self.ch;
474 let sample = self.pos;
475
476 let inp_slice = &self.buffer.inputs[ch][abs_start..abs_end];
477 let out_slice: &mut [S] = &mut self.buffer.outputs[ch][abs_start..abs_end];
478
479 self.pos += take;
480
481 // Full vs Tail by length: full chunks convert to `&[S;
482 // N]` / `&mut [S; N]` for the SIMD-friendly path; tails
483 // fall back to slice form.
484 return Some(if take == N {
485 ChunkItem::Full {
486 ch,
487 sample,
488 // Length-checked above; `try_into` here is a
489 // free reinterpret.
490 inp: inp_slice.try_into().expect("len == N by construction"),
491 out: out_slice.try_into().expect("len == N by construction"),
492 }
493 } else {
494 ChunkItem::Tail {
495 ch,
496 sample,
497 inp: inp_slice,
498 out: out_slice,
499 }
500 });
501 }
502 }
503}
504
505/// Scratch space for [`RawBufferScratch::build`].
506///
507/// Callers allocate this on the stack and pass it to `build`. The
508/// buffer borrows the slices stored here, so this struct must outlive
509/// the returned `AudioBuffer`.
510///
511/// Generic over the plugin's sample type `S`. When the host buffer
512/// matches `S`, slices point into host memory (zero-copy). When the
513/// host buffer is a different precision, the input is widened/narrowed
514/// into per-channel scratch; the output is rendered into scratch and
515/// the wrapper copies + casts it back to the host buffer at the end
516/// of the block via [`Self::finish_widening`].
517pub struct RawBufferScratch<S: Sample = f32> {
518 pub input_slices: Vec<&'static [S]>,
519 pub output_slices: Vec<&'static mut [S]>,
520 /// Per-channel input copies. Used (a) when the host passes the
521 /// same buffer for input and output (in-place processing - VST3
522 /// spec allows this and several real DAWs use it for effects),
523 /// or (b) when the host buffer precision differs from `S` and
524 /// we widen/narrow on the way in. In either case the slice the
525 /// plugin sees points into the matching slot here.
526 input_copies: Vec<Vec<S>>,
527 /// Per-channel output scratch. Only populated by [`Self::build`]
528 /// when the host buffer precision differs from `S`; the wrapper
529 /// copies + casts these back to the host buffer at the end of the
530 /// block via [`Self::finish_widening`].
531 output_buffers: Vec<Vec<S>>,
532}
533
534impl<S: Sample> RawBufferScratch<S> {
535 /// Build an `AudioBuffer<S>` from raw host pointers of wire
536 /// precision `H` - `f32` in the common case (CLAP, LV2, AAX
537 /// always; VST3/VST2/AU 32-bit mode), `f64` when the host
538 /// negotiated a double-precision wire (VST3 `kSample64`, VST2
539 /// `processDoubleReplacing`).
540 ///
541 /// When `S = H`, slices point directly into host memory (modulo
542 /// in-place input copying). Otherwise every channel is converted
543 /// into per-channel scratch and the wrapper must call
544 /// [`Self::finish_widening`] at the end of the block to copy the
545 /// rendered samples back to the host's output pointers.
546 ///
547 /// # Safety
548 /// - `inputs` must point to `num_in` valid `*const H` pointers
549 /// (each non-null pointer must address at least `num_frames`
550 /// readable samples; null is allowed and yields an empty slice).
551 /// - `outputs` must point to `num_out` valid `*mut H` pointers
552 /// (each non-null pointer must address at least `num_frames`
553 /// writable samples; null is allowed and yields an empty slice).
554 /// - The pointed-to memory must remain valid for the lifetime of
555 /// the returned `AudioBuffer`.
556 pub unsafe fn build<H: Sample>(
557 &mut self,
558 inputs: *const *const H,
559 outputs: *mut *mut H,
560 num_in: u32,
561 num_out: u32,
562 num_frames: u32,
563 supports_in_place: bool,
564 ) -> AudioBuffer<'_, S> {
565 // SAFETY: forwarded - caller's contract is the same.
566 unsafe {
567 self.build_inner(
568 inputs,
569 outputs,
570 num_in,
571 num_out,
572 num_frames,
573 supports_in_place,
574 )
575 }
576 }
577
578 /// Copy + convert the rendered `S` output back to the host's `H`
579 /// output pointers. No-op when `S = H` (the slices the plugin
580 /// wrote already point directly at host memory).
581 ///
582 /// # Safety
583 /// `outputs` and `num_out` / `num_frames` must match the values
584 /// passed to the prior [`Self::build`] call on this scratch.
585 pub unsafe fn finish_widening<H: Sample>(
586 &self,
587 outputs: *mut *mut H,
588 num_out: u32,
589 num_frames: u32,
590 ) {
591 // Same precision: the plugin wrote straight into host memory.
592 if S::IS_F64 == H::IS_F64 {
593 return;
594 }
595 unsafe {
596 let nf = num_frames as usize;
597 for ch in 0..(num_out as usize) {
598 let ptr = *outputs.add(ch);
599 if ptr.is_null() {
600 continue;
601 }
602 let host = std::slice::from_raw_parts_mut(ptr, nf);
603 let plugin_out = &self.output_buffers[ch];
604 for (h, &p) in host.iter_mut().zip(plugin_out.iter()) {
605 *h = H::from_f64(p.to_f64());
606 }
607 }
608 }
609 }
610
611 unsafe fn build_inner<'a, H: Sample>(
612 &'a mut self,
613 inputs: *const *const H,
614 outputs: *mut *mut H,
615 num_in: u32,
616 num_out: u32,
617 num_frames: u32,
618 supports_in_place: bool,
619 ) -> AudioBuffer<'a, S> {
620 const MAX_CHANNELS_TRACKED: usize = 64;
621 // Whether the plugin's chosen precision matches the host's.
622 // When matched, we zero-copy host pointers into the slice
623 // arrays; when not, we convert through input_copies and
624 // output_buffers. The traits are sealed at f32/f64, so equal
625 // IS_F64 flags mean S and H are the same type.
626 let same_precision = S::IS_F64 == H::IS_F64;
627
628 unsafe {
629 let nf = num_frames as usize;
630 let num_out_u = num_out as usize;
631 let num_in_u = num_in as usize;
632 debug_assert!(
633 num_out_u <= MAX_CHANNELS_TRACKED,
634 "RawBufferScratch::build: alias detection only covers up to {MAX_CHANNELS_TRACKED} \
635 output channels; got {num_out_u}. Channels beyond the cap won't be \
636 detected as aliased.",
637 );
638 let out_ptrs: [Option<*mut H>; MAX_CHANNELS_TRACKED] = std::array::from_fn(|ch| {
639 if ch < num_out_u {
640 let p = *outputs.add(ch);
641 if p.is_null() { None } else { Some(p) }
642 } else {
643 None
644 }
645 });
646 let aliases_any_output = |in_ptr: *const H| -> bool {
647 let in_start = in_ptr as usize;
648 let in_end = in_start + nf * std::mem::size_of::<H>();
649 out_ptrs
650 .iter()
651 .take(num_out_u.min(MAX_CHANNELS_TRACKED))
652 .any(|o| {
653 o.is_some_and(|op| {
654 let o_start = op as usize;
655 let o_end = o_start + nf * std::mem::size_of::<H>();
656 !(in_end <= o_start || o_end <= in_start)
657 })
658 })
659 };
660
661 // Grow per-channel scratch slots if the bus widened or
662 // we're widening precision and need every channel copied.
663 while self.input_copies.len() < num_in_u {
664 self.input_copies.push(Vec::new());
665 }
666 if !same_precision {
667 while self.output_buffers.len() < num_out_u {
668 self.output_buffers.push(Vec::new());
669 }
670 }
671
672 self.input_slices.clear();
673 self.input_slices.reserve(num_in_u);
674 let mut in_place_mask: u64 = 0;
675 for ch in 0..num_in_u {
676 let ptr = *inputs.add(ch);
677 let slice: &[S] = if ptr.is_null() {
678 &[]
679 } else if aliases_any_output(ptr) {
680 if ch < 64 {
681 in_place_mask |= 1 << ch;
682 }
683 if supports_in_place && same_precision {
684 // Plugin opted in: hand it nothing through
685 // input(ch); it must read+write via in_out_mut.
686 // Only supported in the same-precision case;
687 // the cross-precision path always copies.
688 &[]
689 } else {
690 // Snapshot the input (converting precision if
691 // needed) before the plugin overwrites the
692 // shared buffer. Routing through f64 is
693 // lossless in the widening direction.
694 let host = std::slice::from_raw_parts(ptr, nf);
695 let copy = &mut self.input_copies[ch];
696 copy.clear();
697 copy.reserve(nf);
698 for &h in host {
699 copy.push(S::from_f64(h.to_f64()));
700 }
701 let p = copy.as_ptr();
702 let l = copy.len();
703 // SAFETY: `copy` lives as long as `self`, which
704 // outlives the returned `AudioBuffer<'a>`.
705 std::slice::from_raw_parts(p, l)
706 }
707 } else if same_precision {
708 // SAFETY: same-precision branch - host pointer is
709 // already `*const S` modulo runtime type identity;
710 // the cast reinterprets `*const H` as `*const S`.
711 let raw = ptr.cast::<S>();
712 std::slice::from_raw_parts(raw, nf)
713 } else {
714 // Different precision, no aliasing: convert into
715 // scratch (f64 round-trip, lossless when widening).
716 let host = std::slice::from_raw_parts(ptr, nf);
717 let copy = &mut self.input_copies[ch];
718 copy.clear();
719 copy.reserve(nf);
720 for &h in host {
721 copy.push(S::from_f64(h.to_f64()));
722 }
723 let p = copy.as_ptr();
724 let l = copy.len();
725 std::slice::from_raw_parts(p, l)
726 };
727 self.input_slices.push(slice);
728 }
729
730 self.output_slices.clear();
731 self.output_slices.reserve(num_out_u);
732 for ch in 0..num_out_u {
733 let ptr = *outputs.add(ch);
734 let slice: &mut [S] = if ptr.is_null() {
735 &mut []
736 } else if same_precision {
737 // SAFETY: same-precision branch - host pointer is
738 // already `*mut S` modulo runtime type identity.
739 let raw = ptr.cast::<S>();
740 std::slice::from_raw_parts_mut(raw, nf)
741 } else {
742 // Different precision: render into per-channel
743 // scratch; finish_widening copies+converts back.
744 let buf = &mut self.output_buffers[ch];
745 buf.clear();
746 buf.resize(nf, S::default());
747 let p = buf.as_mut_ptr();
748 let l = buf.len();
749 std::slice::from_raw_parts_mut(p, l)
750 };
751 self.output_slices.push(slice);
752 }
753
754 // SAFETY: Same transmute pattern as AudioBuffer::slice().
755 // RawBufferScratch stores 'static slices but we return AudioBuffer<'a>.
756 let self_ptr: *mut Self = self;
757 let s = &mut *self_ptr;
758 let mut buf = std::mem::transmute::<AudioBuffer<'static, S>, AudioBuffer<'a, S>>(
759 AudioBuffer::from_slices(&s.input_slices, &mut s.output_slices, nf),
760 );
761 buf.set_in_place_mask(in_place_mask);
762 buf
763 }
764 }
765
766 /// Pre-allocate the per-channel scratch vectors so `build` runs
767 /// allocation-free for buses up to `num_in` × `num_out` channels
768 /// and blocks up to `max_frames`. Idempotent and growth-only.
769 pub fn ensure_capacity(&mut self, num_in: usize, num_out: usize, max_frames: usize) {
770 if self.input_slices.capacity() < num_in {
771 self.input_slices
772 .reserve_exact(num_in - self.input_slices.capacity());
773 }
774 if self.output_slices.capacity() < num_out {
775 self.output_slices
776 .reserve_exact(num_out - self.output_slices.capacity());
777 }
778 while self.input_copies.len() < num_in {
779 self.input_copies.push(Vec::with_capacity(max_frames));
780 }
781 for buf in &mut self.input_copies {
782 if buf.capacity() < max_frames {
783 buf.reserve_exact(max_frames - buf.capacity());
784 }
785 }
786 while self.output_buffers.len() < num_out {
787 self.output_buffers.push(Vec::with_capacity(max_frames));
788 }
789 for buf in &mut self.output_buffers {
790 if buf.capacity() < max_frames {
791 buf.reserve_exact(max_frames - buf.capacity());
792 }
793 }
794 }
795}
796
797impl<S: Sample> Default for RawBufferScratch<S> {
798 fn default() -> Self {
799 Self {
800 input_slices: Vec::with_capacity(2),
801 output_slices: Vec::with_capacity(2),
802 input_copies: Vec::with_capacity(2),
803 output_buffers: Vec::with_capacity(2),
804 }
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811
812 /// Drive one block through `build` / `finish_widening` with
813 /// plugin precision `S` on host wire `H`: the plugin doubles a
814 /// `[1, 2, 3, 4]` input ramp into the output.
815 fn double_one_block<S: Sample, H: Sample>() -> Vec<H> {
816 let input: Vec<H> = (1..=4).map(|v| H::from_f64(f64::from(v))).collect();
817 let mut output: Vec<H> = vec![H::default(); 4];
818 let in_ptrs = [input.as_ptr()];
819 let mut out_ptrs = [output.as_mut_ptr()];
820 let mut scratch = RawBufferScratch::<S>::default();
821 // SAFETY: both pointers address 4 valid samples that outlive
822 // the buffer; the finish call reuses the same layout.
823 unsafe {
824 let mut buf = scratch.build(in_ptrs.as_ptr(), out_ptrs.as_mut_ptr(), 1, 1, 4, false);
825 for i in 0..4 {
826 let v = buf.input(0)[i];
827 buf.output(0)[i] = v + v;
828 }
829 scratch.finish_widening(out_ptrs.as_mut_ptr(), 1, 4);
830 }
831 output
832 }
833
834 fn assert_doubled<H: Sample>(output: &[H]) {
835 let got: Vec<f64> = output.iter().map(|v| v.to_f64()).collect();
836 assert_eq!(got, vec![2.0, 4.0, 6.0, 8.0]);
837 }
838
839 // Passthrough, so the outputs are bit-identical to the input - exact
840 // float equality is the contract being checked.
841 #[allow(clippy::float_cmp)]
842 #[test]
843 fn for_each_frame_io_fans_mono_input_to_a_stereo_graph() {
844 // Mono-in (1) / stereo-out (2) bus fed through a 2-in/2-out identity
845 // "graph": the single input must fan into both frame slots, so both
846 // outputs receive the mono signal, with no per-width branch.
847 let input: [f32; 3] = [0.1, 0.2, 0.3];
848 let mut out_l = [0.0f32; 3];
849 let mut out_r = [0.0f32; 3];
850 let inputs: [&[f32]; 1] = [&input];
851 let mut outputs: [&mut [f32]; 2] = [&mut out_l, &mut out_r];
852 let mut buf = AudioBuffer::<f32>::from_slices_checked(&inputs, &mut outputs, 3);
853
854 buf.for_each_frame_io::<2, 2, _>(|frame_in, frame_out| {
855 // Identity graph: both channels pass through.
856 frame_out[0] = frame_in[0];
857 frame_out[1] = frame_in[1];
858 });
859
860 // frame_in[1] repeated the last (only) input channel, so both
861 // outputs equal the mono input.
862 assert_eq!(out_l, input);
863 assert_eq!(out_r, input);
864 }
865
866 #[test]
867 fn f32_wire_f32_plugin_zero_copy() {
868 assert_doubled(&double_one_block::<f32, f32>());
869 }
870
871 #[test]
872 fn f32_wire_f64_plugin_widens() {
873 assert_doubled(&double_one_block::<f64, f32>());
874 }
875
876 #[test]
877 fn f64_wire_f64_plugin_zero_copy() {
878 assert_doubled(&double_one_block::<f64, f64>());
879 }
880
881 #[test]
882 fn f64_wire_f32_plugin_narrows() {
883 assert_doubled(&double_one_block::<f32, f64>());
884 }
885
886 #[test]
887 #[allow(clippy::float_cmp)]
888 fn f64_wire_in_place_snapshots_input() {
889 // Host hands the same f64 buffer for input and output; the
890 // input reads must see the pre-write values.
891 let mut io: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
892 let in_ptrs = [io.as_ptr()];
893 let mut out_ptrs = [io.as_mut_ptr()];
894 let mut scratch = RawBufferScratch::<f64>::default();
895 // SAFETY: the aliased pointer addresses 4 valid samples that
896 // outlive the buffer.
897 unsafe {
898 let mut buf = scratch.build(in_ptrs.as_ptr(), out_ptrs.as_mut_ptr(), 1, 1, 4, false);
899 assert!(buf.is_in_place(0));
900 for i in 0..4 {
901 let v = buf.input(0)[i];
902 buf.output(0)[i] = v * 10.0;
903 }
904 }
905 assert_eq!(io, vec![10.0, 20.0, 30.0, 40.0]);
906 }
907}