1
0
Fork 0

Add a standalone buffer management abstraction

The idea is that all backends are refactored to use this. This greatly
reduces the need for backend-specific code when it comes to buffer
management, and thus also bugs. It also overwrites main output channels
that don't have a corresponding input channel with zeroes, which the
current backends don't do.
This commit is contained in:
Robbert van der Helm 2023-03-31 16:02:00 +02:00
parent a33569bff6
commit 83dd585c40
2 changed files with 367 additions and 0 deletions

View file

@ -5,6 +5,7 @@ use std::os::raw::c_char;
use crate::util::permit_alloc;
pub(crate) mod buffer_management;
#[cfg(debug_assertions)]
pub(crate) mod context_checks;

View file

@ -0,0 +1,366 @@
//! Helpers for safely constructing [`Buffer`]s from a plugin host's audio buffers.
use std::num::NonZeroU32;
use std::ptr::NonNull;
use crate::audio_setup::AudioIOLayout;
use crate::buffer::Buffer;
/// Buffers created using [`create_buffers`]. At some point the main `Plugin::process()` should
/// probably also take an argument like this instead of main+aux buffers if we also want to provide
/// access to overflowing input channels for e.g. stereo to mono plugins.
pub struct Buffers<'a, 'buffer: 'a> {
pub main_buffer: &'a mut Buffer<'buffer>,
// We can't use `AuxiliaryBuffers` here directly because we need different lifetimes for `'a`
// and `'buffer` while `AuxiliaryBuffers` uses the same lifetime for both.
pub aux_inputs: &'a mut [Buffer<'buffer>],
pub aux_outputs: &'a mut [Buffer<'buffer>],
}
/// A helper for safely creating and initializing [`Buffer`]s based on the host's input and output
/// buffers.
pub struct BufferManager {
// These are the storage backing the fields in `BufferSource`. The wrapper needs to set these
// values to match the channel pointers provided by the host. If audio buffers are not provided
// for a bus, then they should be set to `None`. This helper will then copy data to the buffers
// or fill them with zeroes if there is no data, while also accounting for in-place main IO
// buffers.
main_input_channel_pointers: Option<ChannelPointers>,
main_output_channel_pointers: Option<ChannelPointers>,
aux_input_channel_pointers: Vec<Option<ChannelPointers>>,
aux_output_channel_pointers: Vec<Option<ChannelPointers>>,
/// The backing buffers that will be filled during `create_buffers`. This `'static` lifetime
/// will be shortened when returning a reference to these buffers in `create_buffers` to match
/// the function's lifetime.
main_buffer: Buffer<'static>,
aux_input_buffers: Vec<Buffer<'static>>,
/// Stores the data to back `aux_input_buffers`. We need to copy the host's auxiliary input
/// buffers to our own first because the `Buffer` API is designed around mutable buffers, and
/// the host may reuse its input buffers between plugins.
aux_input_storage: Vec<Vec<Vec<f32>>>,
aux_output_buffers: Vec<Buffer<'static>>,
}
// SAFETY: The raw pointers in the `ChannelPointers` fields/vectors are only used as scratch storage
// inside of the `create_buffers()` function.
unsafe impl Send for BufferManager {}
unsafe impl Sync for BufferManager {}
/// Host data that the plugin's [`Buffer`]s should be created from. Leave these fields as `None`
/// values
pub struct BufferSource<'a> {
pub main_input_channel_pointers: &'a mut Option<ChannelPointers>,
pub main_output_channel_pointers: &'a mut Option<ChannelPointers>,
pub aux_input_channel_pointers: &'a mut [Option<ChannelPointers>],
pub aux_output_channel_pointers: &'a mut [Option<ChannelPointers>],
}
/// Pointers to raw multichannel audio data for this port.
#[derive(Debug, Clone, Copy)]
pub struct ChannelPointers {
/// A raw pointer to an array of f32 arrays, containing one array for each channel. `ptrs` must
/// contain (at least) `num_channel` `*const f32`s, and each of those inner arrays must contain
/// (at least) `num_samples` `f32` values.
pub ptrs: NonNull<*mut f32>,
/// The number of audio channels used for this port.
pub num_channels: usize,
}
impl BufferManager {
/// Initialize managed buffers for a specific audio IO layout. The actual buffers can be set up
/// using channel pointer data using [`create_buffers()`][Self::create_buffers()].
pub fn for_audio_io_layout(max_buffer_size: usize, audio_io_layout: AudioIOLayout) -> Self {
nih_debug_assert!(
audio_io_layout
.main_input_channels
.map(NonZeroU32::get)
.unwrap_or(0)
<= audio_io_layout
.main_output_channels
.map(NonZeroU32::get)
.unwrap_or(0),
"Stereo-to-mono and other many-to-few audio channel configurations are currently not \
supported"
);
// The buffers are preallocated so that `create_buffers()` can be called without having to
// allocate
let mut main_buffer = Buffer::default();
unsafe {
main_buffer.set_slices(0, |output_slices| {
output_slices.resize_with(
audio_io_layout
.main_output_channels
.map(NonZeroU32::get)
.unwrap_or(0) as usize,
|| &mut [],
);
})
};
let mut aux_input_buffers = Vec::with_capacity(audio_io_layout.aux_input_ports.len());
let mut aux_input_storage = Vec::with_capacity(audio_io_layout.aux_input_ports.len());
for num_channels in audio_io_layout.aux_input_ports {
let mut buffer = Buffer::default();
unsafe {
buffer.set_slices(0, |slices| {
slices.resize_with(num_channels.get() as usize, || &mut []);
})
};
aux_input_buffers.push(buffer);
aux_input_storage.push(vec![
vec![0.0; max_buffer_size];
num_channels.get() as usize
]);
}
let mut aux_output_buffers = Vec::with_capacity(audio_io_layout.aux_output_ports.len());
for num_channels in audio_io_layout.aux_output_ports {
let mut buffer = Buffer::default();
unsafe {
buffer.set_slices(0, |slices| {
slices.resize_with(num_channels.get() as usize, || &mut []);
})
};
aux_output_buffers.push(buffer);
}
Self {
main_input_channel_pointers: None,
main_output_channel_pointers: None,
aux_input_channel_pointers: vec![None; audio_io_layout.aux_input_ports.len()],
aux_output_channel_pointers: vec![None; audio_io_layout.aux_output_ports.len()],
main_buffer,
aux_input_buffers,
aux_input_storage,
aux_output_buffers,
}
}
/// Initialize the buffers using the host provided buffer pointers and return a reference to the
/// created buffers that can be passed to `Plugin::process()`. This accounts for in-place main
/// IO, missing channel pointers, null pointers, and mismatching channel counts. All
/// uninitialized buffer data (aux outputs, and main output channels with no matching input
/// channel) are filled with zeroes.
///
/// If any of the output
///
/// # Panics
///
/// May panic if one of the inner channel pointers is a null pointer.
///
/// # Safety
///
/// Any provided `ChannelPointers` must point to memory regions that remain valid to read from
/// or write to for the lifetime of the returned [`Buffers`].
pub unsafe fn create_buffers<'a, 'buffer: 'a>(
&'a mut self,
num_samples: usize,
set_buffer_sources: impl FnOnce(&mut BufferSource),
) -> Buffers<'a, 'buffer> {
// Make sure the caller can't forget to unset previously set values
self.main_input_channel_pointers = None;
self.main_output_channel_pointers = None;
self.aux_input_channel_pointers.fill(None);
self.aux_output_channel_pointers.fill(None);
set_buffer_sources(&mut BufferSource {
main_input_channel_pointers: &mut self.main_input_channel_pointers,
main_output_channel_pointers: &mut self.main_output_channel_pointers,
aux_input_channel_pointers: &mut self.aux_input_channel_pointers,
aux_output_channel_pointers: &mut self.aux_output_channel_pointers,
});
// The main buffer points directly to the main output pointers
self.main_buffer.set_slices(num_samples, |output_slices| {
match self.main_output_channel_pointers {
Some(output_channel_pointers) => {
nih_debug_assert_eq!(output_slices.len(), output_channel_pointers.num_channels);
for (channel_idx, output_slice) in output_slices
.iter_mut()
.enumerate()
.take(output_channel_pointers.num_channels)
{
let output_channel_pointer =
output_channel_pointers.ptrs.as_ptr().add(channel_idx);
assert!(!output_channel_pointer.is_null());
*output_slice =
std::slice::from_raw_parts_mut(*output_channel_pointer, num_samples);
}
// If the caller/host should have provided buffer pointers but didn't then we
// must get rid of any dangling slices
output_slices[output_channel_pointers.num_channels..].fill_with(|| &mut [])
}
None => {
nih_debug_assert_eq!(output_slices.len(), 0);
// Same as above
output_slices.fill_with(|| &mut [])
}
}
});
// Since NIH-plug processes audio in-place, main input data needs to be copied to the main
// output buffers
if let (Some(input_channel_pointers), Some(output_channel_pointers)) = (
self.main_input_channel_pointers,
self.main_output_channel_pointers,
) {
// If the host processes the main IO out of place then the inputs need to be copied to
// the output buffers. Otherwise the input should already be there.
if input_channel_pointers.ptrs != output_channel_pointers.ptrs {
self.main_buffer.set_slices(num_samples, |output_slices| {
for (channel_idx, output_slice) in output_slices
.iter_mut()
.enumerate()
.take(input_channel_pointers.num_channels)
{
let input_channel_pointer =
input_channel_pointers.ptrs.as_ptr().add(channel_idx);
assert!(!input_channel_pointer.is_null());
output_slice.copy_from_slice(std::slice::from_raw_parts_mut(
*input_channel_pointer,
num_samples,
))
}
});
}
// Any excess channels will need to be filled with zeroes since they'd otherwise point
// to whatever was left in the buffer
if input_channel_pointers.num_channels < output_channel_pointers.num_channels {
self.main_buffer.set_slices(num_samples, |output_slices| {
for slice in &mut output_slices[input_channel_pointers.num_channels..] {
slice.fill(0.0);
}
});
}
}
// Because NIH-plug's `Buffer` type is geared around in-place processing, auxiliary inputs
// need to be copied to our own buffers first (backed by the 'storage' vectors on this
// object). That way the plugin can modify those buffers like any other buffers.
for (input_channel_pointers, (input_storage, input_buffer)) in
self.aux_input_channel_pointers.iter().zip(
self.aux_input_storage
.iter_mut()
.zip(self.aux_input_buffers.iter_mut()),
)
{
// Since these buffers are backed by our own storage, we can fill them with zeroes if
// the pointers are missing for whatever reason that might be
nih_debug_assert!(input_channel_pointers.is_some());
match input_channel_pointers {
Some(input_channel_pointers) => {
nih_debug_assert_eq!(input_channel_pointers.num_channels, input_storage.len());
for (channel_idx, channel) in input_storage
.iter_mut()
.enumerate()
.take(input_channel_pointers.num_channels)
{
let input_channel_pointer =
input_channel_pointers.ptrs.as_ptr().add(channel_idx);
assert!(!input_channel_pointer.is_null());
nih_debug_assert!(num_samples <= channel.capacity());
channel.resize(num_samples, 0.0);
channel.copy_from_slice(std::slice::from_raw_parts_mut(
*input_channel_pointer,
num_samples,
))
}
// In case we were provided too few channels we'll fill the rest with zeroes to
// avoid unexpected situations
for channel in input_storage
.iter_mut()
.skip(input_channel_pointers.num_channels)
{
channel.fill(0.0);
}
}
None => {
for channel in input_storage.iter_mut() {
channel.fill(0.0);
}
}
}
input_buffer.set_slices(num_samples, |input_slices| {
// Since we initialized both `input_buffer` and `input_storage` this invariant
// should never fail unless we made an error ourselves
debug_assert_eq!(input_slices.len(), input_storage.len());
for (channel_slice, channel_storage) in
input_slices.iter_mut().zip(input_storage.iter_mut())
{
// SAFETY: `channel_storage` is no longer used accessed directly after this
*channel_slice = &mut *(channel_storage.as_mut_slice() as *mut [f32]);
}
});
}
// The auxiliary output buffers can point directly to the host's buffers. This logic is the
// same as the main outputs, minus the copying of input cdata
for (output_channel_pointers, output_buffer) in self
.aux_output_channel_pointers
.iter()
.zip(self.aux_output_buffers.iter_mut())
{
output_buffer.set_slices(num_samples, |output_slices| {
match output_channel_pointers {
Some(output_channel_pointers) => {
nih_debug_assert_eq!(
output_slices.len(),
output_channel_pointers.num_channels
);
for (channel_idx, output_slice) in output_slices
.iter_mut()
.enumerate()
.take(output_channel_pointers.num_channels)
{
let output_channel_pointer =
output_channel_pointers.ptrs.as_ptr().add(channel_idx);
assert!(!output_channel_pointer.is_null());
*output_slice = std::slice::from_raw_parts_mut(
*output_channel_pointer,
num_samples,
);
}
// If the caller/host should have provided buffer pointers but didn't then
// we must get rid of any dangling slices
output_slices[output_channel_pointers.num_channels..].fill_with(|| &mut [])
}
None => {
nih_debug_assert_eq!(output_slices.len(), 0);
// Same as above
output_slices.fill_with(|| &mut [])
}
}
});
}
// SAFETY: The 'static lifetimes on the objects are needed so we can store the buffers.
// Their actual lifetimes are `'a`, so we need to shrink them here. The contents are
// valid for as long as the returned object is borrowed.
std::mem::transmute::<Buffers<'a, 'static>, Buffers<'a, 'buffer>>(Buffers {
main_buffer: &mut self.main_buffer,
aux_inputs: &mut self.aux_input_buffers,
aux_outputs: &mut self.aux_output_buffers,
})
}
}