truce_core/buffer.rs
1/// Non-interleaved audio buffer. Zero-copy — borrows host memory
2/// through the format wrapper.
3pub struct AudioBuffer<'a> {
4 inputs: &'a [&'a [f32]],
5 outputs: &'a mut [&'a mut [f32]],
6 offset: usize,
7 num_samples: usize,
8}
9
10impl<'a> AudioBuffer<'a> {
11 /// Create a buffer from pre-split channel slices.
12 /// Used by format wrappers after converting from host-specific buffer types.
13 ///
14 /// # Safety
15 /// The caller must ensure the slices are valid for the lifetime `'a`
16 /// and that `num_samples` does not exceed any slice's length.
17 pub unsafe fn from_slices(
18 inputs: &'a [&'a [f32]],
19 outputs: &'a mut [&'a mut [f32]],
20 num_samples: usize,
21 ) -> Self {
22 #[cfg(debug_assertions)]
23 {
24 // Verify no input channel aliases any output channel.
25 for (i, inp) in inputs.iter().enumerate() {
26 let i_start = inp.as_ptr() as usize;
27 let i_end = i_start + inp.len() * std::mem::size_of::<f32>();
28 for (o, out) in outputs.iter().enumerate() {
29 let o_start = out.as_ptr() as usize;
30 let o_end = o_start + out.len() * std::mem::size_of::<f32>();
31 assert!(
32 i_end <= o_start || o_end <= i_start,
33 "AudioBuffer: input channel {i} and output channel {o} alias \
34 (input: {i_start:#x}..{i_end:#x}, output: {o_start:#x}..{o_end:#x})"
35 );
36 }
37 }
38 // Verify num_samples doesn't exceed any slice.
39 for (i, inp) in inputs.iter().enumerate() {
40 assert!(
41 num_samples <= inp.len(),
42 "AudioBuffer: num_samples ({num_samples}) exceeds input channel {i} length ({})",
43 inp.len()
44 );
45 }
46 for (o, out) in outputs.iter().enumerate() {
47 assert!(
48 num_samples <= out.len(),
49 "AudioBuffer: num_samples ({num_samples}) exceeds output channel {o} length ({})",
50 out.len()
51 );
52 }
53 }
54 Self {
55 inputs,
56 outputs,
57 offset: 0,
58 num_samples,
59 }
60 }
61
62 pub fn num_samples(&self) -> usize {
63 self.num_samples
64 }
65
66 pub fn num_input_channels(&self) -> usize {
67 self.inputs.len()
68 }
69
70 pub fn num_output_channels(&self) -> usize {
71 self.outputs.len()
72 }
73
74 pub fn input(&self, channel: usize) -> &[f32] {
75 let end = self.offset + self.num_samples;
76 &self.inputs[channel][self.offset..end]
77 }
78
79 pub fn output(&mut self, channel: usize) -> &mut [f32] {
80 let end = self.offset + self.num_samples;
81 &mut self.outputs[channel][self.offset..end]
82 }
83
84 /// Number of channels (min of input and output).
85 pub fn channels(&self) -> usize {
86 self.inputs.len().min(self.outputs.len())
87 }
88
89 /// Get an input/output pair for a channel. Useful for in-place processing.
90 pub fn io_pair(&mut self, in_ch: usize, out_ch: usize) -> (&[f32], &mut [f32]) {
91 let end = self.offset + self.num_samples;
92 let input = &self.inputs[in_ch][self.offset..end];
93 let output = &mut self.outputs[out_ch][self.offset..end];
94 (input, output)
95 }
96
97 /// Get an input/output pair for the same channel index. Shorthand for `io_pair(ch, ch)`.
98 pub fn io(&mut self, ch: usize) -> (&[f32], &mut [f32]) {
99 self.io_pair(ch, ch)
100 }
101
102 /// Peak absolute value across an output channel.
103 pub fn output_peak(&self, ch: usize) -> f32 {
104 let end = self.offset + self.num_samples;
105 self.outputs[ch][self.offset..end]
106 .iter()
107 .fold(0.0f32, |a, &b| a.max(b.abs()))
108 }
109
110 /// Return a sub-block view covering samples `start..start+len`.
111 ///
112 /// The returned buffer borrows `self` exclusively — you cannot use
113 /// the original buffer while the slice is alive.
114 ///
115 /// # Panics
116 /// Panics if `start + len > self.num_samples()`.
117 ///
118 /// # Example
119 /// ```ignore
120 /// let mut offset = 0;
121 /// for event in events.iter() {
122 /// let at = event.sample_offset as usize;
123 /// if at > offset {
124 /// let mut sub = buffer.slice(offset, at - offset);
125 /// process_sub_block(&mut sub);
126 /// }
127 /// handle_event(&event.body);
128 /// offset = at;
129 /// }
130 /// if offset < buffer.num_samples() {
131 /// let mut sub = buffer.slice(offset, buffer.num_samples() - offset);
132 /// process_sub_block(&mut sub);
133 /// }
134 /// ```
135 pub fn slice(&mut self, start: usize, len: usize) -> AudioBuffer<'_> {
136 assert!(
137 start + len <= self.num_samples,
138 "slice({start}, {len}) out of bounds for buffer of {} samples",
139 self.num_samples,
140 );
141 let new_offset = self.offset + start;
142 // SAFETY: We construct an AudioBuffer<'a> and transmute to AudioBuffer<'_>.
143 // These have identical memory layout (lifetimes are erased at runtime).
144 // This is sound because:
145 // 1. &mut self prevents the caller from using self while the slice exists
146 // 2. The underlying channel memory lives for 'a which outlives '_
147 // 3. Bounds are checked by the assert above
148 let self_ptr: *mut Self = self;
149 unsafe {
150 let s = &mut *self_ptr;
151 std::mem::transmute::<AudioBuffer<'a>, AudioBuffer<'_>>(AudioBuffer {
152 inputs: s.inputs,
153 outputs: &mut *s.outputs,
154 offset: new_offset,
155 num_samples: len,
156 })
157 }
158 }
159}
160
161/// Scratch space for `AudioBuffer::from_raw_ptrs`.
162///
163/// Callers allocate this on the stack and pass it to `from_raw_ptrs`.
164/// The buffer borrows the slices stored here, so this struct must
165/// outlive the returned `AudioBuffer`.
166pub struct RawBufferScratch {
167 pub input_slices: Vec<&'static [f32]>,
168 pub output_slices: Vec<&'static mut [f32]>,
169}
170
171impl RawBufferScratch {
172 /// Build an `AudioBuffer` from raw C pointers.
173 ///
174 /// This is the common FFI pattern used by VST3, VST2, AU, and AAX
175 /// wrappers. It:
176 /// 1. Converts raw `*const f32` / `*mut f32` channel pointers to slices
177 /// 2. Copies input channels to output channels (in-place effect processing)
178 /// 3. Returns an `AudioBuffer` borrowing the scratch slices
179 ///
180 /// # Safety
181 /// - `inputs` must point to `num_in` valid `*const f32` pointers,
182 /// each pointing to `num_frames` samples.
183 /// - `outputs` must point to `num_out` valid `*mut f32` pointers,
184 /// each pointing to `num_frames` samples.
185 /// - The pointed-to memory must remain valid for the lifetime of
186 /// the returned `AudioBuffer`.
187 pub unsafe fn build<'a>(
188 &'a mut self,
189 inputs: *const *const f32,
190 outputs: *mut *mut f32,
191 num_in: u32,
192 num_out: u32,
193 num_frames: u32,
194 ) -> AudioBuffer<'a> {
195 let nf = num_frames as usize;
196
197 self.input_slices.clear();
198 for ch in 0..num_in as usize {
199 let ptr = *inputs.add(ch);
200 if !ptr.is_null() {
201 self.input_slices
202 .push(std::slice::from_raw_parts(ptr, nf));
203 }
204 }
205
206 self.output_slices.clear();
207 for ch in 0..num_out as usize {
208 let ptr = *outputs.add(ch);
209 if !ptr.is_null() {
210 self.output_slices
211 .push(std::slice::from_raw_parts_mut(ptr, nf));
212 }
213 }
214
215 // Copy input to output for in-place effect processing.
216 let copy_ch = self.input_slices.len().min(self.output_slices.len());
217 for ch in 0..copy_ch {
218 self.output_slices[ch][..nf]
219 .copy_from_slice(&self.input_slices[ch][..nf]);
220 }
221
222 // SAFETY: Same transmute pattern as AudioBuffer::slice().
223 // RawBufferScratch stores 'static slices but we return AudioBuffer<'a>.
224 // Sound because the caller's raw pointers must outlive 'a, and
225 // &'a mut self prevents aliasing.
226 let self_ptr: *mut Self = self;
227 let s = &mut *self_ptr;
228 std::mem::transmute::<AudioBuffer<'static>, AudioBuffer<'a>>(
229 AudioBuffer::from_slices(&s.input_slices, &mut s.output_slices, nf)
230 )
231 }
232}
233
234impl Default for RawBufferScratch {
235 fn default() -> Self {
236 Self {
237 input_slices: Vec::with_capacity(2),
238 output_slices: Vec::with_capacity(2),
239 }
240 }
241}