1
0
Fork 0
nih-plug/plugins/examples/stft/src/lib.rs

176 lines
6.2 KiB
Rust
Raw Normal View History

use nih_plug::prelude::*;
use realfft::num_complex::Complex32;
use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
use std::f32;
use std::sync::Arc;
/// The size of the windows we'll process at a time.
const WINDOW_SIZE: usize = 64;
/// The length of the filter's impulse response.
const FILTER_WINDOW_SIZE: usize = 33;
/// The length of the FFT window we will use to perform FFT convolution. This includes padding to
/// prevent time domain aliasing as a result of cyclic convolution.
const FFT_WINDOW_SIZE: usize = WINDOW_SIZE + FILTER_WINDOW_SIZE - 1;
/// The gain compensation we need to apply for the STFT process.
const GAIN_COMPENSATION: f32 = 1.0 / WINDOW_SIZE as f32;
struct Stft {
params: Arc<StftParams>,
2022-03-07 03:54:23 +11:00
/// An adapter that performs most of the overlap-add algorithm for us.
stft: util::StftHelper,
2022-03-07 03:54:23 +11:00
/// The FFT of a simple low-pass FIR filter.
lp_filter_spectrum: Vec<Complex32>,
/// The algorithm for the FFT operation.
r2c_plan: Arc<dyn RealToComplex<f32>>,
/// The algorithm for the IFFT operation.
c2r_plan: Arc<dyn ComplexToReal<f32>>,
/// The output of our real->complex FFT.
complex_fft_buffer: Vec<Complex32>,
}
#[derive(Params)]
struct StftParams {}
impl Default for Stft {
fn default() -> Self {
let mut planner = RealFftPlanner::new();
let r2c_plan = planner.plan_fft_forward(FFT_WINDOW_SIZE);
let c2r_plan = planner.plan_fft_inverse(FFT_WINDOW_SIZE);
let mut real_fft_buffer = r2c_plan.make_input_vec();
let mut complex_fft_buffer = r2c_plan.make_output_vec();
2022-03-07 03:54:23 +11:00
// Build a super simple low-pass filter from one of the built in window functions
let mut filter_window = util::window::hann(FILTER_WINDOW_SIZE);
2022-03-07 12:02:46 +11:00
// And make sure to normalize this so convolution sums to 1
let filter_normalization_factor = filter_window.iter().sum::<f32>().recip();
for sample in &mut filter_window {
2022-03-07 04:54:18 +11:00
*sample *= filter_normalization_factor;
2022-03-07 03:54:23 +11:00
}
real_fft_buffer[0..FILTER_WINDOW_SIZE].copy_from_slice(&filter_window);
2022-03-07 03:54:23 +11:00
// RustFFT doesn't actually need a scratch buffer here, so we'll pass an empty buffer
// instead
2022-03-07 03:54:23 +11:00
r2c_plan
.process_with_scratch(&mut real_fft_buffer, &mut complex_fft_buffer, &mut [])
2022-03-07 03:54:23 +11:00
.unwrap();
Self {
params: Arc::new(StftParams::default()),
// We'll process the input in `WINDOW_SIZE` chunks, but our FFT window is slightly
// larger to account for time domain aliasing so we'll need to add some padding ot each
// block.
stft: util::StftHelper::new(2, WINDOW_SIZE, FFT_WINDOW_SIZE - WINDOW_SIZE),
2022-03-07 03:54:23 +11:00
lp_filter_spectrum: complex_fft_buffer.clone(),
2022-03-07 03:54:23 +11:00
r2c_plan,
c2r_plan,
complex_fft_buffer,
}
}
}
2022-03-07 03:54:23 +11:00
#[allow(clippy::derivable_impls)]
impl Default for StftParams {
fn default() -> Self {
Self {}
}
}
impl Plugin for Stft {
const NAME: &'static str = "STFT Example";
const VENDOR: &'static str = "Moist Plugins GmbH";
const URL: &'static str = "https://youtu.be/dQw4w9WgXcQ";
const EMAIL: &'static str = "info@example.com";
const VERSION: &'static str = "0.0.1";
const DEFAULT_NUM_INPUTS: u32 = 2;
const DEFAULT_NUM_OUTPUTS: u32 = 2;
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
fn params(&self) -> Arc<dyn Params> {
// The explicit cast is not needed, but Rust Analyzer gets very upset when you don't do it
self.params.clone() as Arc<dyn Params>
}
fn accepts_bus_config(&self, config: &BusConfig) -> bool {
// We'll only do stereo for simplicity's sake
config.num_input_channels == config.num_output_channels && config.num_input_channels == 2
}
fn initialize(
&mut self,
_bus_config: &BusConfig,
_buffer_config: &BufferConfig,
context: &mut impl ProcessContext,
) -> bool {
context.set_latency_samples(self.stft.latency_samples());
true
}
fn reset(&mut self) {
// Normally we'd also initialize the STFT helper for the correct channel count here, but we
// only do stereo so that's not necessary. Setting the block size also zeroes out the
// buffers.
self.stft.set_block_size(WINDOW_SIZE);
}
fn process(
&mut self,
buffer: &mut Buffer,
_context: &mut impl ProcessContext,
) -> ProcessStatus {
self.stft
.process_overlap_add(buffer, 1, |_channel_idx, real_fft_buffer| {
// Forward FFT, `real_fft_buffer` already is already padded with zeroes, and the
// padding from the last iteration will have already been added back to the start of
// the buffer
self.r2c_plan
.process_with_scratch(real_fft_buffer, &mut self.complex_fft_buffer, &mut [])
2022-03-07 03:54:23 +11:00
.unwrap();
// As per the convolution theorem we can simply multiply these two buffers. We'll
// also apply the gain compensation at this point.
for (fft_bin, kernel_bin) in self
.complex_fft_buffer
2022-03-07 03:54:23 +11:00
.iter_mut()
.zip(&self.lp_filter_spectrum)
2022-03-07 03:54:23 +11:00
{
*fft_bin *= *kernel_bin * GAIN_COMPENSATION;
}
2022-03-07 03:54:23 +11:00
// Inverse FFT back into the scratch buffer. This will be added to a ring buffer
// which gets written back to the host at a one block delay.
self.c2r_plan
.process_with_scratch(&mut self.complex_fft_buffer, real_fft_buffer, &mut [])
2022-03-07 03:54:23 +11:00
.unwrap();
});
ProcessStatus::Normal
}
}
impl ClapPlugin for Stft {
const CLAP_ID: &'static str = "com.moist-plugins-gmbh.stft";
const CLAP_DESCRIPTION: &'static str = "An example plugin using the STFT helper";
const CLAP_FEATURES: &'static [&'static str] = &["audio_effect", "stereo", "tool"];
const CLAP_MANUAL_URL: &'static str = Self::URL;
const CLAP_SUPPORT_URL: &'static str = Self::URL;
}
impl Vst3Plugin for Stft {
const VST3_CLASS_ID: [u8; 16] = *b"StftMoistestPlug";
const VST3_CATEGORIES: &'static str = "Fx|Tools";
}
nih_export_clap!(Stft);
nih_export_vst3!(Stft);