1
0
Fork 0

Add a padding option to StftHelper

This commit is contained in:
Robbert van der Helm 2022-05-08 01:18:56 +02:00
parent d0fcc9878e
commit 55eeb689dd
4 changed files with 89 additions and 21 deletions

View file

@ -60,7 +60,7 @@ impl SpectrumInput {
TripleBuffer::new(&[0.0; SPECTRUM_WINDOW_SIZE / 2]).split(); TripleBuffer::new(&[0.0; SPECTRUM_WINDOW_SIZE / 2]).split();
let input = Self { let input = Self {
stft: util::StftHelper::new(num_channels, SPECTRUM_WINDOW_SIZE), stft: util::StftHelper::new(num_channels, SPECTRUM_WINDOW_SIZE, 0),
num_channels, num_channels,
triple_buffer_input, triple_buffer_input,

View file

@ -57,7 +57,7 @@ impl Default for Stft {
Self { Self {
params: Arc::new(StftParams::default()), params: Arc::new(StftParams::default()),
stft: util::StftHelper::new(2, WINDOW_SIZE), stft: util::StftHelper::new(2, WINDOW_SIZE, 0),
window_function: util::window::hann(WINDOW_SIZE), window_function: util::window::hann(WINDOW_SIZE),
lp_filter_kernel: complex_fft_buffer.clone(), lp_filter_kernel: complex_fft_buffer.clone(),

View file

@ -83,7 +83,7 @@ impl Default for PubertySimulator {
Self { Self {
params: Arc::new(PubertySimulatorParams::default()), params: Arc::new(PubertySimulatorParams::default()),
stft: util::StftHelper::new(2, MAX_WINDOW_SIZE), stft: util::StftHelper::new(2, MAX_WINDOW_SIZE, 0),
window_function: Vec::with_capacity(MAX_WINDOW_SIZE), window_function: Vec::with_capacity(MAX_WINDOW_SIZE),
plan_for_order: None, plan_for_order: None,

View file

@ -1,5 +1,7 @@
//! Utilities for buffering audio, likely used as part of a short-term Fourier transform. //! Utilities for buffering audio, likely used as part of a short-term Fourier transform.
use std::cmp;
use crate::buffer::{Block, Buffer}; use crate::buffer::{Block, Buffer};
/// Some buffer that can be used with the [`StftHelper`]. /// Some buffer that can be used with the [`StftHelper`].
@ -29,7 +31,6 @@ pub trait StftInputMut: StftInput {
/// the same number of channels as the main input. /// the same number of channels as the main input.
/// ///
/// TODO: Better name? /// TODO: Better name?
/// TODO: This needs an option that adds padding to the `real_fft_window`
/// TODO: We may need something like this purely for analysis, e.g. for showing spectrums in a GUI. /// TODO: We may need something like this purely for analysis, e.g. for showing spectrums in a GUI.
/// Figure out the cleanest way to adapt this for the non-processing use case. /// Figure out the cleanest way to adapt this for the non-processing use case.
pub struct StftHelper<const NUM_SIDECHAIN_INPUTS: usize = 0> { pub struct StftHelper<const NUM_SIDECHAIN_INPUTS: usize = 0> {
@ -44,10 +45,16 @@ pub struct StftHelper<const NUM_SIDECHAIN_INPUTS: usize = 0> {
/// Results from the ring buffers are copied to this scratch buffer before being passed to the /// Results from the ring buffers are copied to this scratch buffer before being passed to the
/// plugin. Needed to handle overlap. /// plugin. Needed to handle overlap.
scratch_buffer: Vec<f32>, scratch_buffer: Vec<f32>,
/// If padding is used, then this will contain the previous iteration's values from the padding
/// values in `scratch_buffer` (`scratch_buffer[(scratch_buffer.len() - padding -
/// 1)..scratch_buffer.len()]`). This is then added to the ring buffer in the next iteration.
padding_buffers: Vec<Vec<f32>>,
/// The current position in our ring buffers. Whenever this wraps around to 0, we'll process /// The current position in our ring buffers. Whenever this wraps around to 0, we'll process
/// a block. /// a block.
current_pos: usize, current_pos: usize,
/// If padding is used, then this much extra capacity has been added to the buffers.
padding: usize,
} }
/// Marker struct for the version wtihout sidechaining. /// Marker struct for the version wtihout sidechaining.
@ -173,13 +180,17 @@ impl StftInput for NoSidechain {
impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> { impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
/// Initialize the [`StftHelper`] for [`Buffer`]s with the specified number of channels and the /// Initialize the [`StftHelper`] for [`Buffer`]s with the specified number of channels and the
/// given maximum block size. Call [`set_block_size()`][`Self::set_block_size()`] afterwards if /// given maximum block size. When the option is set, then every yielded sample buffer will have
/// you do not need the full capacity upfront. /// this many zero samples appended at the end of the block. Call
/// [`set_block_size()`][`Self::set_block_size()`] afterwards if you do not need the full
/// capacity upfront. If the padding option is non zero, then all yielded blocks will have that
/// many zeroes added to the end of it and the results stored in the padding area will be added
/// to the outputs in the next iteration(s).
/// ///
/// # Panics /// # Panics
/// ///
/// Panics if `num_channels == 0 || max_block_size == 0`. /// Panics if `num_channels == 0 || max_block_size == 0`.
pub fn new(num_channels: usize, max_block_size: usize) -> Self { pub fn new(num_channels: usize, max_block_size: usize, padding: usize) -> Self {
assert_ne!(num_channels, 0); assert_ne!(num_channels, 0);
assert_ne!(max_block_size, 0); assert_ne!(max_block_size, 0);
@ -190,9 +201,13 @@ impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
sidechain_ring_buffers: [(); NUM_SIDECHAIN_INPUTS] sidechain_ring_buffers: [(); NUM_SIDECHAIN_INPUTS]
.map(|_| vec![vec![0.0; max_block_size]; num_channels]), .map(|_| vec![vec![0.0; max_block_size]; num_channels]),
scratch_buffer: vec![0.0; max_block_size], // When padding is used this scratch buffer will have a bunch of zeroes added to it
// after copying a block of audio to it
scratch_buffer: vec![0.0; max_block_size + padding],
padding_buffers: vec![vec![0.0; padding]; num_channels],
current_pos: 0, current_pos: 0,
padding,
} }
} }
@ -213,14 +228,19 @@ impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
main_ring_buffer.resize(block_size, 0.0); main_ring_buffer.resize(block_size, 0.0);
main_ring_buffer.fill(0.0); main_ring_buffer.fill(0.0);
} }
self.scratch_buffer.resize(block_size, 0.0);
self.scratch_buffer.fill(0.0);
for sidechain_ring_buffers in &mut self.sidechain_ring_buffers { for sidechain_ring_buffers in &mut self.sidechain_ring_buffers {
for sidechain_ring_buffer in sidechain_ring_buffers { for sidechain_ring_buffer in sidechain_ring_buffers {
sidechain_ring_buffer.resize(block_size, 0.0); sidechain_ring_buffer.resize(block_size, 0.0);
sidechain_ring_buffer.fill(0.0); sidechain_ring_buffer.fill(0.0);
} }
} }
self.scratch_buffer.resize(block_size + self.padding, 0.0);
self.scratch_buffer.fill(0.0);
// For consistency's sake we'll also clear this here
for padding_buffer in &mut self.padding_buffers {
padding_buffer.fill(0.0);
}
self.current_pos = 0; self.current_pos = 0;
} }
@ -230,19 +250,19 @@ impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
self.main_input_ring_buffers[0].len() as u32 self.main_input_ring_buffers[0].len() as u32
} }
/// Process the audio in `main_buffer` in small overlapping blocks with a window function /// Process the audio in `main_buffer` in small overlapping blocks, adding up the results for
/// applied, adding up the results for the main buffer so they can be written back to the host. /// the main buffer so they can eventually be written back to the host one block later. This
/// Since there are a couple ways to do it, the window function needs to be applied in the /// means that this function will introduce one block of latency. This can be compensated by
/// process callbacks. Check the [`nih_plug::util::window`] module for more information. /// calling
/// Whenever a new block is available, `process_cb()` gets called with a new audio block of the
/// specified size with the windowing function already applied. The summed reults will then be
/// written back to `main_buffer` exactly one block later, which means that this function will
/// introduce one block of latency. This can be compensated by calling
/// [`ProcessContext::set_latency()`][`crate::prelude::ProcessContext::set_latency_samples()`] /// [`ProcessContext::set_latency()`][`crate::prelude::ProcessContext::set_latency_samples()`]
/// in your plugin's initialization function. /// in your plugin's initialization function.
/// ///
/// This function does not apply any gain compensation for the windowing. You will need to do /// If a padding value was specified in [`new()`][Self::new()], then the yielded blocks will
/// that yoruself depending on your window function and the amount of overlap. /// have that many zeroes appended at the end of them. The padding values will be added to the
/// next block before `process_cb()` is called.
///
/// Since there are a couple different ways to do it, any window functions needs to be applied
/// in the callbacks. Check the [`nih_plug::util::window`] module for more information.
/// ///
/// For efficiency's sake this function will reuse the same vector for all calls to /// For efficiency's sake this function will reuse the same vector for all calls to
/// `process_cb`. This means you can only access a single channel's worth of windowed data at a /// `process_cb`. This means you can only access a single channel's worth of windowed data at a
@ -392,14 +412,19 @@ impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
self.current_pos, self.current_pos,
sidechain_ring_buffer, sidechain_ring_buffer,
); );
if self.padding > 0 {
self.scratch_buffer[block_size..].fill(0.0);
}
process_cb(channel_idx, Some(sidechain_idx), &mut self.scratch_buffer); process_cb(channel_idx, Some(sidechain_idx), &mut self.scratch_buffer);
} }
} }
for (channel_idx, (input_ring_buffer, output_ring_buffer)) in self for (channel_idx, ((input_ring_buffer, output_ring_buffer), padding_buffer)) in self
.main_input_ring_buffers .main_input_ring_buffers
.iter() .iter()
.zip(self.main_output_ring_buffers.iter_mut()) .zip(self.main_output_ring_buffers.iter_mut())
.zip(self.padding_buffers.iter_mut())
.enumerate() .enumerate()
{ {
copy_ring_to_scratch_buffer( copy_ring_to_scratch_buffer(
@ -407,14 +432,53 @@ impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
self.current_pos, self.current_pos,
input_ring_buffer, input_ring_buffer,
); );
if self.padding > 0 {
self.scratch_buffer[block_size..].fill(0.0);
}
process_cb(channel_idx, None, &mut self.scratch_buffer); process_cb(channel_idx, None, &mut self.scratch_buffer);
// Add the padding from the last iteration (for this channel) to the scratch
// buffer before it is copied to the output ring buffer. In case the padding is
// longer than the block size, then this will cause everything else to be
// shifted to the left so it can be added in the iteration after this.
if self.padding > 0 {
let padding_to_copy = cmp::min(self.padding, block_size);
for (scratch_sample, padding_sample) in self.scratch_buffer
[..padding_to_copy]
.iter_mut()
.zip(&mut padding_buffer[..padding_to_copy])
{
*scratch_sample += *padding_sample;
}
let remaining_padding = padding_to_copy - self.padding;
if remaining_padding > 0 {
padding_buffer.copy_within(..padding_to_copy, 0);
}
// And we obviously don't want this to feedback
padding_buffer[remaining_padding..].fill(0.0);
}
// The actual overlap-add part of the equation // The actual overlap-add part of the equation
add_scratch_to_ring_buffer( add_scratch_to_ring_buffer(
&self.scratch_buffer, &self.scratch_buffer,
self.current_pos, self.current_pos,
output_ring_buffer, output_ring_buffer,
); );
// And the data from the padding area should be saved so it can be added to next
// iteration's scratch buffer. Like mentioned above, the padding can be larger
// than the block size so we also need to do overlap-add here.
if self.padding > 0 {
for (padding_sample, scratch_sample) in padding_buffer
.iter_mut()
.zip(&mut self.scratch_buffer[block_size..])
{
*padding_sample += *scratch_sample;
}
}
} }
} }
} }
@ -478,6 +542,10 @@ impl<const NUM_SIDECHAIN_INPUTS: usize> StftHelper<NUM_SIDECHAIN_INPUTS> {
self.current_pos, self.current_pos,
input_ring_buffer, input_ring_buffer,
); );
if self.padding > 0 {
self.scratch_buffer[block_size..].fill(0.0);
}
analyze_cb(channel_idx, &mut self.scratch_buffer); analyze_cb(channel_idx, &mut self.scratch_buffer);
} }
} }