// Puberty Simulator: the next generation in voice change simulation technology // Copyright (C) 2022 Robbert van der Helm // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use fftw::array::AlignedVec; use fftw::plan::{C2RPlan, C2RPlan32, R2CPlan, R2CPlan32}; use fftw::types::{c32, Flag}; use nih_plug::prelude::*; use std::f32; use std::pin::Pin; const WINDOW_SIZE: usize = 1024; const OVERLAP_TIMES: usize = 4; struct PubertySimulator { params: Pin>, /// An adapter that performs most of the overlap-add algorithm for us. stft: util::StftHelper, /// A Hann window window, passed to the overlap-add helper. window_function: Vec, /// The algorithms for the FFT and IFFT operations. plan: Plan, /// Scratch buffers for computing our FFT. The [`StftHelper`] already contains a buffer for the /// real values. complex_fft_scratch_buffer: AlignedVec, } /// FFTW uses raw pointers which aren't Send+Sync, so we'll wrap this in a separate struct. struct Plan { r2c_plan: R2CPlan32, c2r_plan: C2RPlan32, } unsafe impl Send for Plan {} unsafe impl Sync for Plan {} #[derive(Params)] struct PubertySimulatorParams {} impl Default for PubertySimulator { fn default() -> Self { Self { params: Box::pin(PubertySimulatorParams::default()), stft: util::StftHelper::new(2, WINDOW_SIZE), window_function: util::window::hann(WINDOW_SIZE), plan: Plan { r2c_plan: R2CPlan32::aligned(&[WINDOW_SIZE], Flag::MEASURE).unwrap(), c2r_plan: C2RPlan32::aligned(&[WINDOW_SIZE], Flag::MEASURE).unwrap(), }, complex_fft_scratch_buffer: AlignedVec::new(WINDOW_SIZE / 2 + 1), } } } #[allow(clippy::derivable_impls)] impl Default for PubertySimulatorParams { fn default() -> Self { Self {} } } impl Plugin for PubertySimulator { const NAME: &'static str = "Puberty Simulator"; const VENDOR: &'static str = "Robbert van der Helm"; const URL: &'static str = "https://github.com/robbert-vdh/nih-plug"; const EMAIL: &'static str = "mail@robbertvanderhelm.nl"; const VERSION: &'static str = "0.1.0"; const DEFAULT_NUM_INPUTS: u32 = 2; const DEFAULT_NUM_OUTPUTS: u32 = 2; const ACCEPTS_MIDI: bool = false; fn params(&self) -> Pin<&dyn Params> { self.params.as_ref() } 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 { // Normally we'd also initialize the STFT helper for the correct channel count here, but we // only do stereo so that's not necessary self.stft.set_block_size(WINDOW_SIZE); context.set_latency_samples(self.stft.latency_samples()); true } fn process( &mut self, buffer: &mut Buffer, _context: &mut impl ProcessContext, ) -> ProcessStatus { // Compensate for the window function, the overlap, and the extra gain introduced by the // IDFT operation const GAIN_COMPENSATION: f32 = 1.0 / OVERLAP_TIMES as f32 / WINDOW_SIZE as f32; self.stft.process_overlap_add( buffer, &self.window_function, OVERLAP_TIMES, |_channel_idx, real_fft_scratch_buffer| { // Forward FFT, the helper has already applied window function self.plan .r2c_plan .r2c( real_fft_scratch_buffer, &mut self.complex_fft_scratch_buffer, ) .unwrap(); // This simply takes the complex sinusoid from the frequency bin for double this // bin's frequency for bin_idx in 0..self.complex_fft_scratch_buffer.len() { // TODO: Since we're always doubling now this can be a lot simpler, but it may // be interesting to add some more options here later // let frequency = bin_idx as f32 / WINDOW_SIZE as f32 * sample_rate; // let target_frequency = frequency * 2.0; // // Simple linear interpolation // let target_bin = target_frequency / sample_rate * WINDOW_SIZE as f32; // let target_bin_low = target_bin.floor() as usize; // let target_bin_high = target_bin.ceil() as usize; // let target_low_t = target_bin % 1.0; // let target_high_t = 1.0 - target_low_t; // let target_low = self // .complex_fft_scratch_buffer // .get(target_bin_low) // .copied() // .unwrap_or_default(); // let target_high = self // .complex_fft_scratch_buffer // .get(target_bin_high) // .copied() // .unwrap_or_default(); // self.complex_fft_scratch_buffer[bin_idx] = (target_low * target_low_t // + target_high * target_high_t) // * 4.0 // Random extra gain, not sure // * GAIN_COMPENSATION; let target_bin = bin_idx * 2; let target = self .complex_fft_scratch_buffer .get(target_bin) .copied() .unwrap_or_default(); self.complex_fft_scratch_buffer[bin_idx] = target * 6.0 // Random extra gain, not sure * GAIN_COMPENSATION; } // 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.plan .c2r_plan .c2r( &mut self.complex_fft_scratch_buffer, real_fft_scratch_buffer, ) .unwrap(); }, ); ProcessStatus::Normal } } impl ClapPlugin for PubertySimulator { const CLAP_ID: &'static str = "nl.robbertvanderhelm.puberty-simulator"; const CLAP_DESCRIPTION: &'static str = "Simulates a pitched down cracking voice"; const CLAP_FEATURES: &'static [&'static str] = &["audio_effect", "stereo", "glitch", "pitch_shifter"]; const CLAP_MANUAL_URL: &'static str = Self::URL; const CLAP_SUPPORT_URL: &'static str = Self::URL; } impl Vst3Plugin for PubertySimulator { const VST3_CLASS_ID: [u8; 16] = *b"PubertySim..RvdH"; const VST3_CATEGORIES: &'static str = "Fx|Pitch Shift"; } nih_export_clap!(PubertySimulator); nih_export_vst3!(PubertySimulator);