From 5f45d8cc63999daa0ff9299a478317c75ae7b705 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 11:40:46 +0200 Subject: [PATCH 01/67] wip: working polysynth example --- .idea/valib.iml | 1 + Cargo.lock | 9 + Cargo.toml | 1 + Makefile.plugins.toml | 6 +- crates/valib-core/src/dsp/mod.rs | 3 +- crates/valib-oscillators/src/lib.rs | 21 +- crates/valib-oscillators/src/polyblep.rs | 196 +++++++++++++++++ crates/valib-oversample/src/lib.rs | 29 ++- crates/valib-voice/src/lib.rs | 227 +++++++++++++++++++- crates/valib-voice/src/monophonic.rs | 7 +- crates/valib-voice/src/polyphonic.rs | 39 +++- crates/valib-voice/src/upsample.rs | 25 ++- examples/polysynth/Makefile.toml | 1 + examples/polysynth/src/dsp.rs | 255 +++++++++++++++++++++++ examples/polysynth/src/main.rs | 6 + examples/polysynth/src/params.rs | 162 ++++++++++++++ 16 files changed, 963 insertions(+), 25 deletions(-) create mode 100644 crates/valib-oscillators/src/polyblep.rs create mode 100644 examples/polysynth/Makefile.toml create mode 100644 examples/polysynth/src/dsp.rs create mode 100644 examples/polysynth/src/main.rs create mode 100644 examples/polysynth/src/params.rs diff --git a/.idea/valib.iml b/.idea/valib.iml index f19c036..7c1daf9 100644 --- a/.idea/valib.iml +++ b/.idea/valib.iml @@ -31,6 +31,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 70899ad..b152402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3555,6 +3555,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polysynth" +version = "0.1.0" +dependencies = [ + "nih_plug", + "num-traits", + "valib", +] + [[package]] name = "portable-atomic" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 54b15f3..b70ff53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ filters = ["saturators", "dep:valib-filters"] oscillators = ["dep:valib-oscillators"] oversample = ["filters", "dep:valib-oversample"] voice = ["dep:valib-voice"] +voice-upsampled = ["voice", "valib-voice/resampled"] wdf = ["filters", "dep:valib-wdf"] fundsp = ["dep:valib-fundsp"] nih-plug = ["dep:valib-nih-plug"] diff --git a/Makefile.plugins.toml b/Makefile.plugins.toml index f52090c..eea98bc 100644 --- a/Makefile.plugins.toml +++ b/Makefile.plugins.toml @@ -7,8 +7,12 @@ dependencies = ["xtask-build"] command = "cargo" args = ["xtask", "bundle", "${CARGO_MAKE_CRATE_NAME}", "${@}"] +[tasks.install-target-x86_64-darwin] +command = "rustup" +args = ["target", "add", "x86_64-apple-darwin"] + [tasks.bundle-universal] -dependencies = ["xtask-build"] +dependencies = ["xtask-build", "install-target-x86_64-darwin"] command = "cargo" args = ["xtask", "bundle-universal", "${CARGO_MAKE_CRATE_NAME}", "${@}"] diff --git a/crates/valib-core/src/dsp/mod.rs b/crates/valib-core/src/dsp/mod.rs index f504245..477cf9e 100644 --- a/crates/valib-core/src/dsp/mod.rs +++ b/crates/valib-core/src/dsp/mod.rs @@ -123,13 +123,14 @@ pub struct SampleAdapter where P: DSPProcessBlock, { + /// Inner block processor + pub inner: P, /// Size of the buffers passed into the inner block processor. pub buffer_size: usize, input_buffer: AudioBufferBox, input_filled: usize, output_buffer: AudioBufferBox, output_filled: usize, - inner: P, } impl std::ops::Deref for SampleAdapter diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 76c665b..9aa04b2 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -8,18 +8,29 @@ use valib_core::dsp::DSPProcess; use valib_core::Scalar; pub mod blit; +pub mod polyblep; pub mod wavetable; /// Tracks normalized phase for a given frequency. Phase is smooth even when frequency changes, so /// it is suitable for driving oscillators. #[derive(Debug, Clone, Copy)] pub struct Phasor { + samplerate: T, + frequency: T, phase: T, step: T, } impl DSPMeta for Phasor { type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = T::from_f64(samplerate as _); + } + + fn reset(&mut self) { + self.phase = T::zero(); + } } #[profiling::all_functions] @@ -43,10 +54,12 @@ impl Phasor { /// /// returns: Phasor #[replace_float_literals(T::from_f64(literal))] - pub fn new(samplerate: T, freq: T) -> Self { + pub fn new(samplerate: T, frequency: T) -> Self { Self { + samplerate, + frequency, phase: 0.0, - step: freq / samplerate, + step: frequency / samplerate, } } @@ -58,7 +71,7 @@ impl Phasor { /// * `freq`: New frequency /// /// returns: () - pub fn set_frequency(&mut self, samplerate: T, freq: T) { - self.step = freq / samplerate; + pub fn set_frequency(&mut self, freq: T) { + self.step = freq / self.samplerate; } } diff --git a/crates/valib-oscillators/src/polyblep.rs b/crates/valib-oscillators/src/polyblep.rs new file mode 100644 index 0000000..2495c6e --- /dev/null +++ b/crates/valib-oscillators/src/polyblep.rs @@ -0,0 +1,196 @@ +use crate::Phasor; +use num_traits::{one, zero, ConstOne, ConstZero}; +use std::marker::PhantomData; +use valib_core::dsp::blocks::P1; +use valib_core::dsp::{DSPMeta, DSPProcess}; +use valib_core::simd::SimdBool; +use valib_core::Scalar; + +pub struct PolyBLEP { + pub amplitude: T, + pub phase: T, +} + +impl PolyBLEP { + pub fn eval(&self, dt: T, phase: T) -> T { + let t = T::simd_fract(phase + self.phase); + let ret = t.simd_lt(dt).if_else( + || { + let t = t / dt; + t + t - t * t - one() + }, + || { + t.simd_gt(one::() - dt).if_else( + || { + let t = (t - one()) / dt; + t * t + t + t + one() + }, + || zero(), + ) + }, + ); + self.amplitude * ret + } +} + +pub trait PolyBLEPOscillator: DSPMeta { + fn bleps() -> impl IntoIterator>; + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample; +} + +pub struct PolyBLEPDriver { + pub phasor: Phasor, + pub blep: Osc, + samplerate: Osc::Sample, +} + +impl PolyBLEPDriver { + pub fn new(samplerate: Osc::Sample, frequency: Osc::Sample, blep: Osc) -> Self { + Self { + phasor: Phasor::new(samplerate, frequency), + blep, + samplerate, + } + } + + pub fn set_frequency(&mut self, frequency: Osc::Sample) { + self.phasor.set_frequency(frequency); + } +} + +impl DSPProcess<0, 1> for PolyBLEPDriver { + fn process(&mut self, _: [Self::Sample; 0]) -> [Self::Sample; 1] { + let [phase] = self.phasor.process([]); + let mut y = self.blep.naive_eval(phase); + for blep in Osc::bleps() { + y += blep.eval(self.phasor.step, phase); + } + [y] + } +} + +impl DSPMeta for PolyBLEPDriver { + type Sample = Osc::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + self.phasor.set_samplerate(samplerate); + self.blep.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.blep.latency() + } + + fn reset(&mut self) { + self.phasor.reset(); + self.blep.reset(); + } +} + +#[derive(Debug, Copy, Clone)] +pub struct SawBLEP(PhantomData); + +impl Default for SawBLEP { + fn default() -> Self { + Self(PhantomData) + } +} + +impl DSPMeta for SawBLEP { + type Sample = T; +} + +impl PolyBLEPOscillator for SawBLEP { + fn bleps() -> impl IntoIterator> { + [PolyBLEP { + amplitude: -T::ONE, + phase: T::ZERO, + }] + } + + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { + T::from_f64(2.0) * phase - T::one() + } +} + +pub type Sawtooth = PolyBLEPDriver>; + +#[derive(Debug, Copy, Clone)] +pub struct SquareBLEP { + pw: T, +} + +impl SquareBLEP { + pub fn new(pulse_width: T) -> Self { + Self { + pw: pulse_width.simd_clamp(zero(), one()), + } + } +} + +impl SquareBLEP { + pub fn set_pulse_width(&mut self, pw: T) { + self.pw = pw.simd_clamp(zero(), one()); + } +} + +impl DSPMeta for SquareBLEP { + type Sample = T; +} + +impl PolyBLEPOscillator for SquareBLEP { + fn bleps() -> impl IntoIterator> { + [ + PolyBLEP { + amplitude: T::ONE, + phase: T::ZERO, + }, + PolyBLEP { + amplitude: -T::ONE, + phase: T::from_f64(0.5), + }, + ] + } + + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { + T::from_f64(2.0) * phase - T::one() + } +} + +pub type Square = PolyBLEPDriver>; + +pub struct Triangle { + square: Square, + integrator: P1, +} + +impl DSPMeta for Triangle { + type Sample = T; + fn set_samplerate(&mut self, samplerate: f32) { + self.square.set_samplerate(samplerate); + self.integrator.set_samplerate(samplerate); + } + fn reset(&mut self) { + self.square.reset(); + self.integrator.reset(); + } +} + +impl DSPProcess<0, 1> for Triangle { + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + self.integrator.process(self.square.process([])) + } +} + +impl Triangle { + pub fn new(samplerate: T, frequency: T) -> Self { + let square = PolyBLEPDriver::new(samplerate, frequency, SquareBLEP::new(T::from_f64(0.5))); + let integrator = P1::new(samplerate, frequency); + Self { square, integrator } + } + + pub fn set_frequency(&mut self, frequency: T) { + self.square.set_frequency(frequency); + self.integrator.set_fc(frequency); + } +} diff --git a/crates/valib-oversample/src/lib.rs b/crates/valib-oversample/src/lib.rs index b2562e4..59b93be 100644 --- a/crates/valib-oversample/src/lib.rs +++ b/crates/valib-oversample/src/lib.rs @@ -169,6 +169,19 @@ impl ResampleStage { } impl ResampleStage { + /// Upsample a single sample of audio + /// + /// # Arguments + /// + /// * `s`: Input sample + /// + /// returns: [T; 2] + pub fn process(&mut self, s: T) -> [T; 2] { + let [x0] = self.filter.process([s + s]); + let [x1] = self.filter.process([T::zero()]); + [x0, x1] + } + /// Upsample the input buffer by a factor of 2. /// /// The output slice should be twice the length of the input slice. @@ -176,8 +189,7 @@ impl ResampleStage { pub fn process_block(&mut self, input: &[T], output: &mut [T]) { assert_eq!(input.len() * 2, output.len()); for (i, s) in input.iter().copied().enumerate() { - let [x0] = self.filter.process([s + s]); - let [x1] = self.filter.process([T::zero()]); + let [x0, x1] = self.process(s); output[2 * i + 0] = x0; output[2 * i + 1] = x1; } @@ -185,6 +197,19 @@ impl ResampleStage { } impl ResampleStage { + /// Downsample 2 samples of input audio, and output a single sample of audio. + /// + /// # Arguments + /// + /// * `[x0, x1]`: Inputs samples + /// + /// returns: T + pub fn process(&mut self, [x0, x1]: [T; 2]) -> T { + let [y] = self.filter.process([x0]); + let _ = self.filter.process([x1]); + y + } + /// Downsample the input buffer by a factor of 2. /// /// The output slice should be twice the length of the input slice. diff --git a/crates/valib-voice/src/lib.rs b/crates/valib-voice/src/lib.rs index 475af22..62fbbf6 100644 --- a/crates/valib-voice/src/lib.rs +++ b/crates/valib-voice/src/lib.rs @@ -2,8 +2,9 @@ //! # Voice abstractions //! //! This crate provides abstractions around voice processing and voice management. -use valib_core::dsp::DSPMeta; +use valib_core::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock, SampleAdapter}; use valib_core::simd::SimdRealField; +use valib_core::util::midi_to_freq; use valib_core::Scalar; pub mod monophonic; @@ -20,11 +21,57 @@ pub trait Voice: DSPMeta { /// Return a mutable reference to the voice's note data fn note_data_mut(&mut self) -> &mut NoteData; /// Release the note (corresponding to a note off) - fn release(&mut self); + fn release(&mut self, release_velocity: f32); /// Reuse the note (corresponding to a soft reset) fn reuse(&mut self); } +impl + Voice, const I: usize, const O: usize> Voice + for SampleAdapter +{ + fn active(&self) -> bool { + self.inner.active() + } + + fn note_data(&self) -> &NoteData { + self.inner.note_data() + } + + fn note_data_mut(&mut self) -> &mut NoteData { + self.inner.note_data_mut() + } + + fn release(&mut self, release_velocity: f32) { + self.inner.release(release_velocity); + } + + fn reuse(&mut self) { + self.inner.reuse(); + } +} + +impl Voice for BlockAdapter { + fn active(&self) -> bool { + self.0.active() + } + + fn note_data(&self) -> &NoteData { + self.0.note_data() + } + + fn note_data_mut(&mut self) -> &mut NoteData { + self.0.note_data_mut() + } + + fn release(&mut self, release_velocity: f32) { + self.0.release(release_velocity); + } + + fn reuse(&mut self) { + self.0.reuse(); + } +} + /// Value representing velocity. The square root is precomputed to be used in voices directly. #[derive(Debug, Copy, Clone)] pub struct Velocity { @@ -124,9 +171,30 @@ pub struct NoteData { pub pressure: T, } +impl NoteData { + pub fn from_midi(midi_note: u8, velocity: f32) -> Self { + let frequency = midi_to_freq(midi_note); + let velocity = Velocity::new(T::from_f64(velocity as _)); + let gain = Gain::from_linear(T::one()); + let pan = T::zero(); + let pressure = T::zero(); + Self { + frequency, + velocity, + gain, + pan, + pressure, + } + } +} + /// Trait for types which manage voices. #[allow(unused_variables)] -pub trait VoiceManager: DSPMeta { +pub trait VoiceManager: + DSPMeta::Voice as DSPMeta>::Sample> +{ + /// Type of the inner voice. + type Voice: Voice; /// Type for the voice ID. type ID: Copy; @@ -134,9 +202,9 @@ pub trait VoiceManager: DSPMeta { fn capacity(&self) -> usize; /// Get the voice by its ID - fn get_voice(&self, id: Self::ID) -> Option<&V>; + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice>; /// Get the voice mutably by its ID - fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut V>; + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice>; /// Return true if the voice referred by the given ID is currently active fn is_voice_active(&self, id: Self::ID) -> bool { @@ -153,9 +221,9 @@ pub trait VoiceManager: DSPMeta { } /// Indicate a note on event, with the given note data to instanciate the voice. - fn note_on(&mut self, note_data: NoteData) -> Self::ID; + fn note_on(&mut self, note_data: NoteData) -> Self::ID; /// Indicate a note off event on the given voice ID. - fn note_off(&mut self, id: Self::ID); + fn note_off(&mut self, id: Self::ID, release_velocity: f32); /// Choke the voice, causing all processing on that voice to stop. fn choke(&mut self, id: Self::ID); /// Choke all the notes. @@ -177,3 +245,148 @@ pub trait VoiceManager: DSPMeta { /// Note gain fn gain(&mut self, id: Self::ID, gain: f32) {} } + +impl VoiceManager for BlockAdapter { + type Voice = V::Voice; + type ID = V::ID; + + fn capacity(&self) -> usize { + self.0.capacity() + } + + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice> { + self.0.get_voice(id) + } + + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice> { + self.0.get_voice_mut(id) + } + + fn is_voice_active(&self, id: Self::ID) -> bool { + self.0.is_voice_active(id) + } + + fn all_voices(&self) -> impl Iterator { + self.0.all_voices() + } + + fn active(&self) -> usize { + self.0.active() + } + + fn note_on(&mut self, note_data: NoteData) -> Self::ID { + self.0.note_on(note_data) + } + + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { + self.0.note_off(id, release_velocity) + } + + fn choke(&mut self, id: Self::ID) { + self.0.choke(id) + } + + fn panic(&mut self) { + self.0.panic() + } + + fn pitch_bend(&mut self, amount: f64) { + self.0.pitch_bend(amount) + } + + fn aftertouch(&mut self, amount: f64) { + self.0.aftertouch(amount) + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + self.0.pressure(id, pressure) + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + self.0.glide(id, semitones) + } + + fn pan(&mut self, id: Self::ID, pan: f32) { + self.0.pan(id, pan) + } + + fn gain(&mut self, id: Self::ID, gain: f32) { + self.0.gain(id, gain) + } +} + +impl + VoiceManager, const I: usize, const O: usize> VoiceManager + for SampleAdapter +{ + type Voice = V::Voice; + type ID = V::ID; + + fn capacity(&self) -> usize { + self.inner.capacity() + } + + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice> { + self.inner.get_voice(id) + } + + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice> { + self.inner.get_voice_mut(id) + } + + fn is_voice_active(&self, id: Self::ID) -> bool { + self.inner.is_voice_active(id) + } + + fn all_voices(&self) -> impl Iterator { + self.inner.all_voices() + } + + fn active(&self) -> usize { + self.inner.active() + } + + fn note_on(&mut self, note_data: NoteData) -> Self::ID { + self.inner.note_on(note_data) + } + + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { + self.inner.note_off(id, release_velocity) + } + + fn choke(&mut self, id: Self::ID) { + self.inner.choke(id) + } + + fn panic(&mut self) { + self.inner.panic() + } + + fn pitch_bend(&mut self, amount: f64) { + self.inner.pitch_bend(amount) + } + + fn aftertouch(&mut self, amount: f64) { + self.inner.aftertouch(amount) + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + self.inner.pressure(id, pressure) + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + self.inner.glide(id, semitones) + } + + fn pan(&mut self, id: Self::ID, pan: f32) { + self.inner.pan(id, pan) + } + + fn gain(&mut self, id: Self::ID, gain: f32) { + self.inner.gain(id, gain) + } +} + +/// Inner voice of the voice manager. +pub type InnerVoice = ::Voice; +/// Inner voice ID of the voice manager. +pub type VoiceId = ::ID; diff --git a/crates/valib-voice/src/monophonic.rs b/crates/valib-voice/src/monophonic.rs index 86f43f5..0f16ad0 100644 --- a/crates/valib-voice/src/monophonic.rs +++ b/crates/valib-voice/src/monophonic.rs @@ -82,7 +82,8 @@ impl Monophonic { } } -impl VoiceManager for Monophonic { +impl VoiceManager for Monophonic { + type Voice = V; type ID = (); fn capacity(&self) -> usize { @@ -122,9 +123,9 @@ impl VoiceManager for Monophonic { } } - fn note_off(&mut self, _id: Self::ID) { + fn note_off(&mut self, _: Self::ID, release_velocity: f32) { if let Some(voice) = &mut self.voice { - voice.release(); + voice.release(release_velocity); } } diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index b9b1f89..61d4f95 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -1,18 +1,35 @@ //! # Polyphonic voice manager //! //! Provides a polyphonic voice manager with rotating voice allocation. + use crate::{NoteData, Voice, VoiceManager}; use num_traits::zero; +use std::fmt; +use std::fmt::Formatter; use valib_core::dsp::{DSPMeta, DSPProcess}; /// Polyphonic voice manager with rotating voice allocation pub struct Polyphonic { - create_voice: Box) -> V>, + create_voice: Box) -> V>, voice_pool: Box<[Option]>, next_voice: usize, samplerate: f32, } +impl fmt::Debug for Polyphonic { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Polyphonic") + .field( + "create_voice", + &"Box) -> V>", + ) + .field("voice_pool", &"Box<[Option]>") + .field("next_voice", &self.next_voice) + .field("samplerate", &self.samplerate) + .finish() + } +} + impl Polyphonic { /// Create a new polyphonice voice manager. /// @@ -26,7 +43,7 @@ impl Polyphonic { pub fn new( samplerate: f32, voice_capacity: usize, - create_voice: impl Fn(f32, NoteData) -> V + 'static, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V + 'static, ) -> Self { Self { create_voice: Box::new(create_voice), @@ -35,6 +52,15 @@ impl Polyphonic { samplerate, } } + + /// Clean inactive voices to prevent them being processed for nothing. + pub fn clean_inactive_voices(&mut self) { + for slot in &mut self.voice_pool { + if slot.as_ref().is_some_and(|v| !v.active()) { + slot.take(); + } + } + } } impl DSPMeta for Polyphonic { @@ -61,7 +87,8 @@ impl DSPMeta for Polyphonic { } } -impl VoiceManager for Polyphonic { +impl VoiceManager for Polyphonic { + type Voice = V; type ID = usize; fn capacity(&self) -> usize { @@ -82,7 +109,7 @@ impl VoiceManager for Polyphonic { fn note_on(&mut self, note_data: NoteData) -> Self::ID { let id = self.next_voice; - self.next_voice += 1; + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); if let Some(voice) = &mut self.voice_pool[id] { *voice.note_data_mut() = note_data; @@ -94,9 +121,9 @@ impl VoiceManager for Polyphonic { id } - fn note_off(&mut self, id: Self::ID) { + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { if let Some(voice) = &mut self.voice_pool[id] { - voice.release(); + voice.release(release_velocity); } } diff --git a/crates/valib-voice/src/upsample.rs b/crates/valib-voice/src/upsample.rs index 71384b4..e3d26b8 100644 --- a/crates/valib-voice/src/upsample.rs +++ b/crates/valib-voice/src/upsample.rs @@ -1,6 +1,7 @@ //! # Upsampled voices //! //! Provides upsampling for DSP process which are generators (0 input channels). +use crate::{NoteData, Voice}; use num_traits::zero; use valib_core::dsp::buffer::{AudioBufferBox, AudioBufferMut, AudioBufferRef}; use valib_core::dsp::{DSPMeta, DSPProcessBlock}; @@ -15,6 +16,28 @@ pub struct UpsampledVoice { num_active_stages: usize, } +impl Voice for UpsampledVoice

{ + fn active(&self) -> bool { + self.inner.active() + } + + fn note_data(&self) -> &NoteData { + self.inner.note_data() + } + + fn note_data_mut(&mut self) -> &mut NoteData { + self.inner.note_data_mut() + } + + fn release(&mut self, release_velocity: f32) { + self.inner.release(release_velocity); + } + + fn reuse(&mut self) { + self.inner.reuse() + } +} + impl> DSPProcessBlock<0, 1> for UpsampledVoice

{ fn process_block( &mut self, @@ -32,7 +55,7 @@ impl> DSPProcessBlock<0, 1> for UpsampledVoice

{ let mut length = inner_len; for stage in &mut self.downsample_stages[..self.num_active_stages] { let (input, output) = self.ping_pong_buffer.get_io_buffers(..length); - stage.process_block(input, output); + stage.process_block(input, &mut output[..length / 2]); self.ping_pong_buffer.switch(); length /= 2; } diff --git a/examples/polysynth/Makefile.toml b/examples/polysynth/Makefile.toml new file mode 100644 index 0000000..1243632 --- /dev/null +++ b/examples/polysynth/Makefile.toml @@ -0,0 +1 @@ +extend = "../../Makefile.plugins.toml" \ No newline at end of file diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs new file mode 100644 index 0000000..2240c5d --- /dev/null +++ b/examples/polysynth/src/dsp.rs @@ -0,0 +1,255 @@ +use crate::params::{FilterParams, OscParams, OscShape, PolysynthParams}; +use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use nih_plug::nih_log; +use nih_plug::util::db_to_gain_fast; +use num_traits::{ConstOne, ConstZero}; +use std::sync::Arc; +use valib::dsp::parameter::SmoothedParam; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; +use valib::filters::ladder::{Ladder, OTA}; +use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; +use valib::oscillators::Phasor; +use valib::saturators::{bjt, Tanh}; +use valib::util::semitone_to_ratio; +use valib::voice::polyphonic::Polyphonic; +use valib::voice::upsample::UpsampledVoice; +use valib::voice::{NoteData, Voice}; +use valib::Scalar; + +pub enum PolyOsc { + Sine(Phasor), + Triangle(Triangle), + Square(Square), + Sawtooth(Sawtooth), +} + +impl PolyOsc { + fn new(samplerate: T, shape: OscShape, note_data: NoteData, pulse_width: T) -> Self { + match shape { + OscShape::Sine => Self::Sine(Phasor::new(samplerate, note_data.frequency)), + OscShape::Triangle => Self::Triangle(Triangle::new(samplerate, note_data.frequency)), + OscShape::Square => Self::Square(Square::new( + samplerate, + note_data.frequency, + SquareBLEP::new(pulse_width), + )), + OscShape::Saw => Self::Sawtooth(Sawtooth::new( + samplerate, + note_data.frequency, + SawBLEP::default(), + )), + } + } + + fn is_osc_shape(&self, osc_shape: OscShape) -> bool { + match self { + Self::Sine(..) if matches!(osc_shape, OscShape::Sine) => true, + Self::Triangle(..) if matches!(osc_shape, OscShape::Triangle) => true, + Self::Square(..) if matches!(osc_shape, OscShape::Square) => true, + Self::Sawtooth(..) if matches!(osc_shape, OscShape::Saw) => true, + _ => false, + } + } +} + +impl DSPMeta for PolyOsc { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + match self { + Self::Sine(p) => p.set_samplerate(samplerate), + Self::Triangle(tri) => tri.set_samplerate(samplerate), + Self::Square(sq) => sq.set_samplerate(samplerate), + Self::Sawtooth(sw) => sw.set_samplerate(samplerate), + } + } + + fn reset(&mut self) { + match self { + PolyOsc::Sine(p) => p.reset(), + PolyOsc::Triangle(tri) => tri.reset(), + PolyOsc::Square(sqr) => sqr.reset(), + PolyOsc::Sawtooth(saw) => saw.reset(), + } + } +} + +impl DSPProcess<1, 1> for PolyOsc { + fn process(&mut self, [freq]: [Self::Sample; 1]) -> [Self::Sample; 1] { + match self { + Self::Sine(p) => { + p.set_frequency(freq); + p.process([]).map(|x| (T::simd_two_pi() * x).simd_sin()) + } + Self::Triangle(tri) => { + tri.set_frequency(freq); + tri.process([]) + } + Self::Square(sq) => { + sq.set_frequency(freq); + sq.process([]) + } + Self::Sawtooth(sw) => { + sw.set_frequency(freq); + sw.process([]) + } + } + } +} + +pub struct RawVoice { + osc: [PolyOsc; 2], + osc_out_sat: bjt::CommonCollector, + filter: Ladder>, + osc_params: [Arc; 2], + filter_params: Arc, + gate: SmoothedParam, + note_data: NoteData, + samplerate: T, +} + +impl RawVoice { + pub(crate) fn update_osc_types(&mut self) { + for i in 0..2 { + let params = &self.osc_params[i]; + let shape = params.shape.value(); + let osc = &mut self.osc[i]; + if !osc.is_osc_shape(shape) { + let pulse_width = T::from_f64(params.pulse_width.value() as _); + *osc = PolyOsc::new(self.samplerate, shape, self.note_data, pulse_width); + } + } + } +} + +impl Voice for RawVoice { + fn active(&self) -> bool { + self.gate.current_value() > 0.5 + } + + fn note_data(&self) -> &NoteData { + &self.note_data + } + + fn note_data_mut(&mut self) -> &mut NoteData { + &mut self.note_data + } + + fn release(&mut self, _: f32) { + nih_log!("RawVoice: release(_)"); + self.gate.param = 0.; + } + + fn reuse(&mut self) { + self.gate.param = 1.; + } +} + +impl DSPMeta for RawVoice { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = T::from_f64(samplerate as _); + for osc in &mut self.osc { + osc.set_samplerate(samplerate); + } + self.filter.set_samplerate(samplerate); + } + + fn reset(&mut self) { + for osc in &mut self.osc { + osc.reset(); + } + self.filter.reset(); + } +} + +impl DSPProcess<0, 1> for RawVoice { + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + let frequency = self.note_data.frequency; + self.update_osc_types(); + let osc1_freq = frequency + * T::from_f64(semitone_to_ratio( + self.osc_params[0].pitch_coarse.value() + self.osc_params[0].pitch_fine.value(), + ) as _); + let osc2_freq = frequency + * T::from_f64(semitone_to_ratio( + self.osc_params[1].pitch_coarse.value() + self.osc_params[1].pitch_fine.value(), + ) as _); + let [osc1] = self.osc[0].process([osc1_freq]); + let [osc2] = self.osc[1].process([osc2_freq]); + let osc_mixer = osc1 * T::from_f64(self.osc_params[0].amplitude.smoothed.next() as _) + + osc2 * T::from_f64(self.osc_params[1].amplitude.smoothed.next() as _); + let filter_in = self + .osc_out_sat + .process([osc_mixer]) + .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); + self.filter + .set_cutoff(T::from_f64(self.filter_params.cutoff.smoothed.next() as _)); + self.filter.set_resonance(T::from_f64( + 4f64 * self.filter_params.resonance.smoothed.next() as f64, + )); + let vca = self.gate.next_sample_as::(); + self.filter.process(filter_in).map(|x| vca * x) + } +} + +type SynthVoice = SampleAdapter>>, 0, 1>; + +pub type VoiceManager = Polyphonic>; + +pub fn create_voice_manager( + samplerate: f32, + osc_params: [Arc; 2], + filter_params: Arc, +) -> VoiceManager { + let target_samplerate_f64 = OVERSAMPLE as f64 * samplerate as f64; + let target_samplerate = T::from_f64(target_samplerate_f64); + Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { + SampleAdapter::new(UpsampledVoice::new( + 2, + MAX_BUFFER_SIZE, + BlockAdapter(RawVoice { + osc: std::array::from_fn(|i| { + let osc_param = &osc_params[i]; + let pulse_width = T::from_f64(osc_param.pulse_width.value() as _); + PolyOsc::new( + target_samplerate, + osc_param.shape.value(), + note_data, + pulse_width, + ) + }), + osc_params: osc_params.clone(), + filter: Ladder::new( + target_samplerate_f64, + T::from_f64(filter_params.cutoff.value() as _), + T::from_f64(filter_params.resonance.value() as _), + ), + filter_params: filter_params.clone(), + osc_out_sat: bjt::CommonCollector { + vee: -T::ONE, + vcc: T::ONE, + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, + gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + note_data, + samplerate: target_samplerate, + }), + )) + }) +} + +pub type Dsp = VoiceManager; + +pub fn create( + samplerate: f32, + params: &PolysynthParams, +) -> Dsp { + create_voice_manager( + samplerate, + params.osc_params.clone(), + params.filter_params.clone(), + ) +} diff --git a/examples/polysynth/src/main.rs b/examples/polysynth/src/main.rs new file mode 100644 index 0000000..b95b123 --- /dev/null +++ b/examples/polysynth/src/main.rs @@ -0,0 +1,6 @@ +use nih_plug::nih_export_standalone; +use polysynth::PolysynthPlugin; + +fn main() { + nih_export_standalone::(); +} diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs new file mode 100644 index 0000000..e2fad43 --- /dev/null +++ b/examples/polysynth/src/params.rs @@ -0,0 +1,162 @@ +use crate::{ + OVERSAMPLE, POLYMOD_FILTER_CUTOFF, POLYMOD_OSC_AMP, POLYMOD_OSC_PITCH_COARSE, + POLYMOD_OSC_PITCH_FINE, +}; +use nih_plug::prelude::*; +use nih_plug::util::{db_to_gain, MINUS_INFINITY_DB}; +use std::sync::Arc; +use valib::dsp::parameter::{ParamId, ParamName}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, ParamName, Enum)] +pub enum OscShape { + Sine, + Triangle, + Square, + Saw, +} + +#[derive(Debug, Params)] +pub struct OscParams { + #[id = "shp"] + pub shape: EnumParam, + #[id = "amp"] + pub amplitude: FloatParam, + #[id = "pco"] + pub pitch_coarse: FloatParam, + #[id = "pfi"] + pub pitch_fine: FloatParam, + #[id = "pw"] + pub pulse_width: FloatParam, +} + +impl OscParams { + fn new(osc_index: usize, oversample: Arc) -> Self { + Self { + shape: EnumParam::new("Shape", OscShape::Saw), + amplitude: FloatParam::new( + "Amplitude", + 0.8, + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_AMP[osc_index]), + pitch_coarse: FloatParam::new( + "Pitch (Coarse)", + 0.0, + FloatRange::Linear { + min: -24., + max: 24., + }, + ) + .with_step_size(1.) + .with_unit(" st") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_PITCH_COARSE[osc_index]), + pitch_fine: FloatParam::new( + "Pitch (Fine)", + 0.0, + FloatRange::Linear { + min: -0.5, + max: 0.5, + }, + ) + .with_value_to_string(formatters::v2s_f32_rounded(3)) + .with_unit(" st") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_PITCH_FINE[osc_index]), + pulse_width: FloatParam::new( + "Pulse Width", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ) + .with_unit(" %") + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), + } + } +} + +#[derive(Debug, Params)] +pub struct FilterParams { + #[id = "fc"] + pub cutoff: FloatParam, + #[id = "res"] + pub resonance: FloatParam, +} + +impl FilterParams { + fn new(oversample: Arc) -> Self { + Self { + cutoff: FloatParam::new( + "Cutoff", + 3000., + FloatRange::Skewed { + min: 20., + max: 20e3, + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, true)) + .with_string_to_value(formatters::s2v_f32_hz_then_khz()) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_FILTER_CUTOFF), + resonance: FloatParam::new( + "Resonance", + 0.1, + FloatRange::Linear { + min: 0.0, + max: 1.25, + }, + ) + .with_value_to_string(formatters::v2s_f32_percentage(1)) + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_unit(" %") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), + } + } +} + +#[derive(Debug, Params)] +pub struct PolysynthParams { + #[nested(array)] + pub osc_params: [Arc; 2], + #[nested] + pub filter_params: Arc, + pub oversample: Arc, +} + +impl Default for PolysynthParams { + fn default() -> Self { + let oversample = Arc::new(AtomicF32::new(OVERSAMPLE as _)); + Self { + osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), + filter_params: Arc::new(FilterParams::new(oversample.clone())), + oversample, + } + } +} From 4f2ba980b23ac37523629d3ca30888718eee26e3 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 13:14:40 +0200 Subject: [PATCH 02/67] fix: add missing files --- examples/polysynth/Cargo.toml | 18 ++ examples/polysynth/src/lib.rs | 328 ++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 examples/polysynth/Cargo.toml create mode 100644 examples/polysynth/src/lib.rs diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml new file mode 100644 index 0000000..17c16da --- /dev/null +++ b/examples/polysynth/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "polysynth" +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] + +[dependencies] +valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} +nih_plug = { workspace = true, features = ["standalone"] } +num-traits.workspace = true \ No newline at end of file diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs new file mode 100644 index 0000000..0d5eb58 --- /dev/null +++ b/examples/polysynth/src/lib.rs @@ -0,0 +1,328 @@ +use crate::params::PolysynthParams; +use nih_plug::audio_setup::{AudioIOLayout, AuxiliaryBuffers}; +use nih_plug::buffer::Buffer; +use nih_plug::params::Params; +use nih_plug::plugin::ProcessStatus; +use nih_plug::prelude::*; +use std::cmp::Ordering; +use std::sync::Arc; +use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock}; +use valib::voice::{NoteData, VoiceId, VoiceManager}; + +mod dsp; +mod params; + +const NUM_VOICES: usize = 16; +const OVERSAMPLE: usize = 4; +const MAX_BUFFER_SIZE: usize = 64; + +const POLYMOD_OSC_AMP: [u32; 2] = [0, 1]; +const POLYMOD_OSC_PITCH_COARSE: [u32; 2] = [2, 3]; +const POLYMOD_OSC_PITCH_FINE: [u32; 2] = [4, 5]; +const POLYMOD_FILTER_CUTOFF: u32 = 6; + +#[derive(Debug, Copy, Clone)] +struct VoiceKey { + voice_id: Option, + channel: u8, + note: u8, +} + +impl PartialEq for VoiceKey { + fn eq(&self, other: &Self) -> bool { + match (self.voice_id, other.voice_id) { + (Some(a), Some(b)) => a == b, + _ => self.channel == other.channel && self.note == other.note, + } + } +} + +impl Eq for VoiceKey {} + +impl Ord for VoiceKey { + fn cmp(&self, other: &Self) -> Ordering { + match (self.voice_id, other.voice_id) { + (Some(a), Some(b)) => a.cmp(&b), + _ => self + .channel + .cmp(&other.channel) + .then(self.note.cmp(&other.note)), + } + } +} + +impl PartialOrd for VoiceKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl VoiceKey { + fn new(voice_id: Option, channel: u8, note: u8) -> Self { + Self { + voice_id, + channel, + note, + } + } +} + +#[derive(Debug)] +struct VoiceIdMap { + data: [Option<(VoiceKey, VoiceId>)>; NUM_VOICES], +} + +impl Default for VoiceIdMap { + fn default() -> Self { + Self { + data: [None; NUM_VOICES], + } + } +} + +impl VoiceIdMap { + fn add_voice(&mut self, key: VoiceKey, v: VoiceId>) -> bool { + let Some(position) = self.data.iter().position(|x| x.is_none()) else { + return false; + }; + self.data[position] = Some((key, v)); + true + } + + fn get_voice(&self, key: VoiceKey) -> Option>> { + self.data.iter().find_map(|x| { + x.as_ref() + .and_then(|(vkey, id)| (*vkey == key).then_some(*id)) + }) + } + + fn get_voice_by_poly_id(&self, voice_id: i32) -> Option>> { + self.data + .iter() + .flatten() + .find_map(|(vkey, id)| (vkey.voice_id == Some(voice_id)).then_some(*id)) + } + + fn remove_voice(&mut self, key: VoiceKey) -> Option<(VoiceKey, VoiceId>)> { + let position = self + .data + .iter() + .position(|x| x.as_ref().is_some_and(|(vkey, _)| *vkey == key))?; + self.data[position].take() + } +} + +type SynthSample = f32; + +#[derive(Debug)] +pub struct PolysynthPlugin { + dsp: BlockAdapter>, + params: Arc, + voice_id_map: VoiceIdMap, +} + +impl Default for PolysynthPlugin { + fn default() -> Self { + const DEFAULT_SAMPLERATE: f32 = 44100.; + let params = Arc::new(PolysynthParams::default()); + Self { + dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, ¶ms)), + params, + voice_id_map: VoiceIdMap::default(), + } + } +} + +impl Plugin for PolysynthPlugin { + const NAME: &'static str = "Polysynth"; + const VENDOR: &'static str = "SolarLiner"; + const URL: &'static str = "https://github.com/SolarLiner/valib"; + const EMAIL: &'static str = "me@solarliner.dev"; + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(0), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }]; + const MIDI_INPUT: MidiConfig = MidiConfig::Basic; + const SAMPLE_ACCURATE_AUTOMATION: bool = true; + type SysExMessage = (); + type BackgroundTask = (); + + fn params(&self) -> Arc { + self.params.clone() + } + + fn reset(&mut self) { + self.dsp.reset(); + } + + fn process( + &mut self, + buffer: &mut Buffer, + _: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + let num_samples = buffer.samples(); + let sample_rate = context.transport().sample_rate; + let output = buffer.as_slice(); + + let mut next_event = context.next_event(); + let mut block_start: usize = 0; + let mut block_end: usize = MAX_BUFFER_SIZE.min(num_samples); + while block_start < num_samples { + 'events: loop { + match next_event { + Some(event) if (event.timing() as usize) <= block_start => match event { + NoteEvent::NoteOn { + voice_id, + channel, + note, + velocity, + .. + } => { + let key = VoiceKey::new(voice_id, channel, note); + let note_data = NoteData::from_midi(note, velocity); + let id = self.dsp.note_on(note_data); + nih_log!("Note on {id} <- {key:?}"); + self.voice_id_map.add_voice(key, id); + } + NoteEvent::NoteOff { + voice_id, + channel, + note, + velocity, + .. + } => { + let key = VoiceKey::new(voice_id, channel, note); + if let Some((_, id)) = self.voice_id_map.remove_voice(key) { + nih_log!("Note off {id} <- {key:?}"); + self.dsp.note_off(id, velocity); + } else { + nih_log!("Note off {key:?}: ID not found"); + } + } + NoteEvent::Choke { + voice_id, + channel, + note, + .. + } => { + let key = VoiceKey::new(voice_id, channel, note); + if let Some((_, id)) = self.voice_id_map.remove_voice(key) { + self.dsp.choke(id); + } + } + NoteEvent::PolyModulation { voice_id, .. } => { + if let Some(id) = self.voice_id_map.get_voice_by_poly_id(voice_id) { + nih_log!("TODO: Poly modulation ({id})"); + } + } + NoteEvent::MonoAutomation { + poly_modulation_id, + normalized_value, + .. + } => match poly_modulation_id { + POLYMOD_FILTER_CUTOFF => { + let target_plain_value = self + .params + .filter_params + .cutoff + .preview_plain(normalized_value); + self.params + .filter_params + .cutoff + .smoothed + .set_target(sample_rate, target_plain_value); + } + _ => { + for i in 0..2 { + match poly_modulation_id { + id if id == POLYMOD_OSC_PITCH_COARSE[i] => { + let target_plain_value = self.params.osc_params[i] + .pitch_coarse + .preview_plain(normalized_value); + self.params.osc_params[i] + .pitch_coarse + .smoothed + .set_target(sample_rate, target_plain_value); + } + id if id == POLYMOD_OSC_PITCH_FINE[i] => { + let target_plain_value = self.params.osc_params[i] + .pitch_fine + .preview_plain(normalized_value); + self.params.osc_params[i] + .pitch_fine + .smoothed + .set_target(sample_rate, target_plain_value); + } + id if id == POLYMOD_OSC_AMP[i] => { + let target_plain_value = self.params.osc_params[i] + .amplitude + .preview_plain(normalized_value); + self.params.osc_params[i] + .amplitude + .smoothed + .set_target(sample_rate, target_plain_value); + } + id => nih_error!("Unknown poly ID {id}"), + } + } + } + }, + _ => {} + }, + Some(event) if (event.timing() as usize) < block_end => { + block_end = event.timing() as usize; + break 'events; + } + _ => break 'events, + } + next_event = context.next_event(); + } + let dsp_block = AudioBufferMut::from(&mut output[0][block_start..block_end]); + let input = AudioBufferRef::::empty(dsp_block.samples()); + self.dsp.process_block(input, dsp_block); + + block_start = block_end; + block_end = (block_start + MAX_BUFFER_SIZE).min(num_samples); + } + + self.dsp.0.clean_inactive_voices(); + ProcessStatus::Normal + } +} + +impl Vst3Plugin for PolysynthPlugin { + const VST3_CLASS_ID: [u8; 16] = *b"VaLibPlySynTHSLN"; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Synth, + Vst3SubCategory::Instrument, + Vst3SubCategory::Mono, + ]; +} + +impl ClapPlugin for PolysynthPlugin { + const CLAP_ID: &'static str = "dev.solarliner.valib.polysynth"; + const CLAP_DESCRIPTION: Option<&'static str> = option_env!("CARGO_PKG_DESCRIPTION"); + const CLAP_MANUAL_URL: Option<&'static str> = option_env!("CARGO_PKG_MANIFEST_URL"); + const CLAP_SUPPORT_URL: Option<&'static str> = None; + const CLAP_FEATURES: &'static [ClapFeature] = &[ + ClapFeature::Synthesizer, + ClapFeature::Instrument, + ClapFeature::Mono, + ]; + const CLAP_POLY_MODULATION_CONFIG: Option = Some(PolyModulationConfig { + // If the plugin's voice capacity changes at runtime (for instance, when switching to a + // monophonic mode), then the plugin should inform the host in the `initialize()` function + // as well as in the `process()` function if it changes at runtime using + // `context.set_current_voice_capacity()` + max_voice_capacity: NUM_VOICES as _, + // This enables voice stacking in Bitwig. + supports_overlapping_voices: true, + }); +} + +nih_export_clap!(PolysynthPlugin); +nih_export_vst3!(PolysynthPlugin); From 809dd3d6745516a43c7ffcb504e4f7ba93b225fa Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 14:10:08 +0200 Subject: [PATCH 03/67] refactor: move RMS type to valib-core --- crates/valib-core/src/util.rs | 32 ++++++++++++++++++++++++++++++- plugins/ts404/src/dsp/clipping.rs | 3 ++- plugins/ts404/src/util.rs | 32 ------------------------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/crates/valib-core/src/util.rs b/crates/valib-core/src/util.rs index 8a9a151..596b5e2 100644 --- a/crates/valib-core/src/util.rs +++ b/crates/valib-core/src/util.rs @@ -3,7 +3,8 @@ use crate::Scalar; use num_traits::{AsPrimitive, Float, Zero}; use numeric_literals::replace_float_literals; -use simba::simd::SimdValue; +use simba::simd::{SimdComplexField, SimdValue}; +use std::collections::VecDeque; /// Transmutes a slice into a slice of static arrays, putting the remainder of the slice not fitting /// as a separate slice. @@ -200,3 +201,32 @@ pub fn semitone_to_ratio(semi: T) -> T { #[cfg(feature = "test-utils")] pub mod tests; + +#[derive(Debug, Clone)] +pub struct Rms { + data: VecDeque, + summed_squared: T, +} + +impl Rms { + pub fn new(size: usize) -> Self { + Self { + data: (0..size).map(|_| T::zero()).collect(), + summed_squared: T::zero(), + } + } +} + +impl Rms { + pub fn add_element(&mut self, value: T) -> T { + let v2 = value.simd_powi(2); + self.summed_squared -= self.data.pop_front().unwrap(); + self.summed_squared += v2; + self.data.push_back(v2); + self.get_rms() + } + + pub fn get_rms(&self) -> T { + self.summed_squared.simd_sqrt() + } +} diff --git a/plugins/ts404/src/dsp/clipping.rs b/plugins/ts404/src/dsp/clipping.rs index 3605662..a0fbb47 100644 --- a/plugins/ts404/src/dsp/clipping.rs +++ b/plugins/ts404/src/dsp/clipping.rs @@ -1,4 +1,4 @@ -use crate::{util::Rms, TARGET_SAMPLERATE}; +use crate::TARGET_SAMPLERATE; use nih_plug::prelude::AtomicF32; use nih_plug::util::db_to_gain_fast; use num_traits::{Float, ToPrimitive}; @@ -8,6 +8,7 @@ use valib::math::smooth_clamp; use valib::saturators::clippers::DiodeClipper; use valib::saturators::{Saturator, Slew}; use valib::simd::SimdValue; +use valib::util::Rms; use valib::wdf::dsl::*; use valib::{ dsp::{DSPMeta, DSPProcess}, diff --git a/plugins/ts404/src/util.rs b/plugins/ts404/src/util.rs index 0e10379..8b13789 100644 --- a/plugins/ts404/src/util.rs +++ b/plugins/ts404/src/util.rs @@ -1,33 +1 @@ -use std::collections::VecDeque; -use num_traits::Zero; -use valib::Scalar; - -#[derive(Debug, Clone)] -pub struct Rms { - data: VecDeque, - summed_squared: T, -} - -impl Rms { - pub fn new(size: usize) -> Self { - Self { - data: (0..size).map(|_| T::zero()).collect(), - summed_squared: T::zero(), - } - } -} - -impl Rms { - pub fn add_element(&mut self, value: T) -> T { - let v2 = value.simd_powi(2); - self.summed_squared -= self.data.pop_front().unwrap(); - self.summed_squared += v2; - self.data.push_back(v2); - self.get_rms() - } - - pub fn get_rms(&self) -> T { - self.summed_squared.simd_sqrt() - } -} From 1aab7fa8c4420e111022e3bdb021189158773eaa Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 14:10:52 +0200 Subject: [PATCH 04/67] feat(examples): polysynth prototype gui --- Cargo.lock | 1 + examples/polysynth/Cargo.toml | 1 + examples/polysynth/src/dsp.rs | 3 +- examples/polysynth/src/editor.rs | 72 ++++++++++++++++++++++++++++++++ examples/polysynth/src/lib.rs | 21 +++++++++- examples/polysynth/src/params.rs | 4 ++ 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 examples/polysynth/src/editor.rs diff --git a/Cargo.lock b/Cargo.lock index b152402..747259f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3560,6 +3560,7 @@ name = "polysynth" version = "0.1.0" dependencies = [ "nih_plug", + "nih_plug_vizia", "num-traits", "valib", ] diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml index 17c16da..473096e 100644 --- a/examples/polysynth/Cargo.toml +++ b/examples/polysynth/Cargo.toml @@ -15,4 +15,5 @@ crate-type = ["lib", "cdylib"] [dependencies] valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} nih_plug = { workspace = true, features = ["standalone"] } +nih_plug_vizia.workspace = true num-traits.workspace = true \ No newline at end of file diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 2240c5d..fef3d53 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -190,7 +190,8 @@ impl DSPProcess<0, 1> for RawVoice { 4f64 * self.filter_params.resonance.smoothed.next() as f64, )); let vca = self.gate.next_sample_as::(); - self.filter.process(filter_in).map(|x| vca * x) + let static_amp = T::from_f64(0.25); + self.filter.process(filter_in).map(|x| static_amp * vca * x) } } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs new file mode 100644 index 0000000..5462092 --- /dev/null +++ b/examples/polysynth/src/editor.rs @@ -0,0 +1,72 @@ +use crate::params::PolysynthParams; +use nih_plug::prelude::{util, AtomicF32, Editor}; +use nih_plug_vizia::vizia::prelude::*; +use nih_plug_vizia::widgets::*; +use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Lens)] +struct Data { + params: Arc, +} + +impl Model for Data {} + +// Makes sense to also define this here, makes it a bit easier to keep track of +pub(crate) fn default_state() -> Arc { + ViziaState::new(|| (750, 550)) +} + +pub(crate) fn create( + params: Arc, + editor_state: Arc, +) -> Option> { + create_vizia_editor(editor_state, ViziaTheming::Custom, move |cx, _| { + assets::register_noto_sans_light(cx); + assets::register_noto_sans_thin(cx); + + Data { + params: params.clone(), + } + .build(cx); + + VStack::new(cx, |cx| { + Label::new(cx, "Polysynth") + .font_weight(FontWeightKeyword::Thin) + .font_size(30.0) + .height(Pixels(50.0)) + .child_top(Stretch(1.0)) + .child_bottom(Pixels(0.0)); + HStack::new(cx, |cx| { + for ix in 0..2 { + let p = Data::params.map(move |p| p.osc_params[ix].clone()); + VStack::new(cx, |cx| { + Label::new(cx, &format!("Oscillator {}", ix + 1)) + .font_size(22.) + .height(Pixels(30.)) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, p).width(Percentage(100.)); + }) + .width(Stretch(1.0)); + } + }) + .row_between(Pixels(0.0)); + + VStack::new(cx, |cx| { + Label::new(cx, "Filter") + .font_size(22.) + .height(Pixels(30.)) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())) + .width(Percentage(100.)); + }); + }) + .width(Percentage(100.)) + .height(Percentage(100.)) + .row_between(Pixels(0.0)); + + ResizeHandle::new(cx); + }) +} diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 0d5eb58..3775170 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -5,12 +5,14 @@ use nih_plug::params::Params; use nih_plug::plugin::ProcessStatus; use nih_plug::prelude::*; use std::cmp::Ordering; -use std::sync::Arc; +use std::sync::{atomic, Arc}; use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock}; +use valib::util::Rms; use valib::voice::{NoteData, VoiceId, VoiceManager}; mod dsp; +mod editor; mod params; const NUM_VOICES: usize = 16; @@ -158,6 +160,21 @@ impl Plugin for PolysynthPlugin { self.dsp.reset(); } + fn initialize( + &mut self, + _: &AudioIOLayout, + buffer_config: &BufferConfig, + _: &mut impl InitContext, + ) -> bool { + let sample_rate = buffer_config.sample_rate; + self.dsp.set_samplerate(sample_rate); + true + } + + fn editor(&mut self, _: AsyncExecutor) -> Option> { + editor::create(self.params.clone(), self.params.editor_state.clone()) + } + fn process( &mut self, buffer: &mut Buffer, @@ -266,7 +283,7 @@ impl Plugin for PolysynthPlugin { .smoothed .set_target(sample_rate, target_plain_value); } - id => nih_error!("Unknown poly ID {id}"), + _ => {} } } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index e2fad43..bb8b017 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -4,6 +4,7 @@ use crate::{ }; use nih_plug::prelude::*; use nih_plug::util::{db_to_gain, MINUS_INFINITY_DB}; +use nih_plug_vizia::ViziaState; use std::sync::Arc; use valib::dsp::parameter::{ParamId, ParamName}; @@ -148,6 +149,8 @@ pub struct PolysynthParams { #[nested] pub filter_params: Arc, pub oversample: Arc, + #[persist = "editor"] + pub editor_state: Arc, } impl Default for PolysynthParams { @@ -157,6 +160,7 @@ impl Default for PolysynthParams { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), oversample, + editor_state: crate::editor::default_state(), } } } From 3e4fe38c256ebcd8fa1f2e20a10a351e23d07730 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 15:26:42 +0200 Subject: [PATCH 05/67] fix(oscillators): square polyblep correct implementation --- crates/valib-oscillators/src/polyblep.rs | 14 +++++----- crates/valib-voice/src/polyphonic.rs | 33 +++++++++++++++++++----- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/valib-oscillators/src/polyblep.rs b/crates/valib-oscillators/src/polyblep.rs index 2495c6e..d99dd5b 100644 --- a/crates/valib-oscillators/src/polyblep.rs +++ b/crates/valib-oscillators/src/polyblep.rs @@ -4,6 +4,7 @@ use std::marker::PhantomData; use valib_core::dsp::blocks::P1; use valib_core::dsp::{DSPMeta, DSPProcess}; use valib_core::simd::SimdBool; +use valib_core::util::lerp; use valib_core::Scalar; pub struct PolyBLEP { @@ -34,7 +35,7 @@ impl PolyBLEP { } pub trait PolyBLEPOscillator: DSPMeta { - fn bleps() -> impl IntoIterator>; + fn bleps(&self) -> impl IntoIterator>; fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample; } @@ -62,7 +63,7 @@ impl DSPProcess<0, 1> for PolyBLEPDriver { fn process(&mut self, _: [Self::Sample; 0]) -> [Self::Sample; 1] { let [phase] = self.phasor.process([]); let mut y = self.blep.naive_eval(phase); - for blep in Osc::bleps() { + for blep in self.blep.bleps() { y += blep.eval(self.phasor.step, phase); } [y] @@ -101,7 +102,7 @@ impl DSPMeta for SawBLEP { } impl PolyBLEPOscillator for SawBLEP { - fn bleps() -> impl IntoIterator> { + fn bleps(&self) -> impl IntoIterator> { [PolyBLEP { amplitude: -T::ONE, phase: T::ZERO, @@ -139,7 +140,7 @@ impl DSPMeta for SquareBLEP { } impl PolyBLEPOscillator for SquareBLEP { - fn bleps() -> impl IntoIterator> { + fn bleps(&self) -> impl IntoIterator> { [ PolyBLEP { amplitude: T::ONE, @@ -147,13 +148,14 @@ impl PolyBLEPOscillator for SquareBLEP { }, PolyBLEP { amplitude: -T::ONE, - phase: T::from_f64(0.5), + phase: T::one() - self.pw, }, ] } fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { - T::from_f64(2.0) * phase - T::one() + let dc_offset = lerp(self.pw, -T::ONE, T::ONE); + phase.simd_gt(self.pw).if_else(T::one, || -T::one()) + dc_offset } } diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index 61d4f95..afccb4b 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -12,6 +12,7 @@ use valib_core::dsp::{DSPMeta, DSPProcess}; pub struct Polyphonic { create_voice: Box) -> V>, voice_pool: Box<[Option]>, + active_voices: usize, next_voice: usize, samplerate: f32, } @@ -49,6 +50,7 @@ impl Polyphonic { create_voice: Box::new(create_voice), next_voice: 0, voice_pool: (0..voice_capacity).map(|_| None).collect(), + active_voices: 0, samplerate, } } @@ -58,6 +60,7 @@ impl Polyphonic { for slot in &mut self.voice_pool { if slot.as_ref().is_some_and(|v| !v.active()) { slot.take(); + self.active_voices -= 1; } } } @@ -108,17 +111,31 @@ impl VoiceManager for Polyphonic { } fn note_on(&mut self, note_data: NoteData) -> Self::ID { - let id = self.next_voice; - self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); + if self.active_voices == self.capacity() { + // At capacity, we must steal a voice + let id = self.next_voice; + + if let Some(voice) = &mut self.voice_pool[id] { + *voice.note_data_mut() = note_data; + voice.reuse(); + } else { + self.voice_pool[id] = Some((self.create_voice)(self.samplerate, note_data)); + } - if let Some(voice) = &mut self.voice_pool[id] { - *voice.note_data_mut() = note_data; - voice.reuse(); + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); + id } else { + // Find first available slot + while self.voice_pool[self.next_voice].is_some() { + self.next_voice += 1; + } + + let id = self.next_voice; self.voice_pool[id] = Some((self.create_voice)(self.samplerate, note_data)); + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); + self.active_voices += 1; + id } - - id } fn note_off(&mut self, id: Self::ID, release_velocity: f32) { @@ -129,10 +146,12 @@ impl VoiceManager for Polyphonic { fn choke(&mut self, id: Self::ID) { self.voice_pool[id] = None; + self.active_voices -= 1; } fn panic(&mut self) { self.voice_pool.fill_with(|| None); + self.active_voices = 0; } } From b5dec36d28a126839ca24159bdc1215f47500187 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 15:27:48 +0200 Subject: [PATCH 06/67] feat(examples): polysynth: output level and filter keyboard tracking --- examples/polysynth/src/dsp.rs | 74 +++++++++++++++++++------------- examples/polysynth/src/editor.rs | 53 +++++++++++++---------- examples/polysynth/src/lib.rs | 10 ++--- examples/polysynth/src/params.rs | 31 +++++++++++++ 4 files changed, 110 insertions(+), 58 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index fef3d53..c6bf7bd 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -50,6 +50,12 @@ impl PolyOsc { _ => false, } } + + pub fn set_pulse_width(&mut self, pw: T) { + if let Self::Square(sq) = self { + sq.blep.set_pulse_width(pw) + } + } } impl DSPMeta for PolyOsc { @@ -101,8 +107,7 @@ pub struct RawVoice { osc: [PolyOsc; 2], osc_out_sat: bjt::CommonCollector, filter: Ladder>, - osc_params: [Arc; 2], - filter_params: Arc, + params: Arc, gate: SmoothedParam, note_data: NoteData, samplerate: T, @@ -111,7 +116,7 @@ pub struct RawVoice { impl RawVoice { pub(crate) fn update_osc_types(&mut self) { for i in 0..2 { - let params = &self.osc_params[i]; + let params = &self.params.osc_params[i]; let shape = params.shape.value(); let osc = &mut self.osc[i]; if !osc.is_osc_shape(shape) { @@ -124,7 +129,7 @@ impl RawVoice { impl Voice for RawVoice { fn active(&self) -> bool { - self.gate.current_value() > 0.5 + self.gate.current_value() > 1e-4 } fn note_data(&self) -> &NoteData { @@ -166,31 +171,44 @@ impl DSPMeta for RawVoice { impl DSPProcess<0, 1> for RawVoice { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + // Process oscillators let frequency = self.note_data.frequency; + let osc_params = self.params.osc_params.clone(); + let filter_params = self.params.filter_params.clone(); self.update_osc_types(); - let osc1_freq = frequency - * T::from_f64(semitone_to_ratio( - self.osc_params[0].pitch_coarse.value() + self.osc_params[0].pitch_fine.value(), - ) as _); - let osc2_freq = frequency - * T::from_f64(semitone_to_ratio( - self.osc_params[1].pitch_coarse.value() + self.osc_params[1].pitch_fine.value(), - ) as _); - let [osc1] = self.osc[0].process([osc1_freq]); - let [osc2] = self.osc[1].process([osc2_freq]); - let osc_mixer = osc1 * T::from_f64(self.osc_params[0].amplitude.smoothed.next() as _) - + osc2 * T::from_f64(self.osc_params[1].amplitude.smoothed.next() as _); + let [osc1, osc2] = std::array::from_fn(|i| { + let osc = &mut self.osc[i]; + let params = &self.params.osc_params[i]; + let osc_freq = frequency + * T::from_f64(semitone_to_ratio( + params.pitch_coarse.value() + params.pitch_fine.value(), + ) as _); + osc.set_pulse_width(T::from_f64(params.pulse_width.smoothed.next() as _)); + let [osc] = osc.process([osc_freq]); + osc + }); + + // Process filter input + let osc_mixer = osc1 * T::from_f64(osc_params[0].amplitude.smoothed.next() as _) + + osc2 * T::from_f64(osc_params[1].amplitude.smoothed.next() as _); let filter_in = self .osc_out_sat .process([osc_mixer]) .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); - self.filter - .set_cutoff(T::from_f64(self.filter_params.cutoff.smoothed.next() as _)); + + let freq_ratio = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) + * frequency + / T::from_f64(440.); + let filter_freq = + (T::one() + freq_ratio) * T::from_f64(filter_params.cutoff.smoothed.next() as _); + + // Process filter + self.filter.set_cutoff(filter_freq); self.filter.set_resonance(T::from_f64( - 4f64 * self.filter_params.resonance.smoothed.next() as f64, + 4f64 * filter_params.resonance.smoothed.next() as f64, )); let vca = self.gate.next_sample_as::(); - let static_amp = T::from_f64(0.25); + let static_amp = T::from_f64(self.params.output_level.smoothed.next() as _); self.filter.process(filter_in).map(|x| static_amp * vca * x) } } @@ -201,11 +219,12 @@ pub type VoiceManager = Polyphonic>; pub fn create_voice_manager( samplerate: f32, - osc_params: [Arc; 2], - filter_params: Arc, + params: Arc, ) -> VoiceManager { let target_samplerate_f64 = OVERSAMPLE as f64 * samplerate as f64; let target_samplerate = T::from_f64(target_samplerate_f64); + let osc_params = params.osc_params.clone(); + let filter_params = params.filter_params.clone(); Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { SampleAdapter::new(UpsampledVoice::new( 2, @@ -221,19 +240,18 @@ pub fn create_voice_manager( pulse_width, ) }), - osc_params: osc_params.clone(), filter: Ladder::new( target_samplerate_f64, T::from_f64(filter_params.cutoff.value() as _), T::from_f64(filter_params.resonance.value() as _), ), - filter_params: filter_params.clone(), osc_out_sat: bjt::CommonCollector { vee: -T::ONE, vcc: T::ONE, xbias: T::from_f64(0.1), ybias: T::from_f64(-0.1), }, + params: params.clone(), gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), note_data, samplerate: target_samplerate, @@ -246,11 +264,7 @@ pub type Dsp = VoiceManager; pub fn create( samplerate: f32, - params: &PolysynthParams, + params: Arc, ) -> Dsp { - create_voice_manager( - samplerate, - params.osc_params.clone(), - params.filter_params.clone(), - ) + create_voice_manager(samplerate, params) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index 5462092..c950de8 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -1,11 +1,9 @@ use crate::params::PolysynthParams; -use nih_plug::prelude::{util, AtomicF32, Editor}; +use nih_plug::prelude::Editor; use nih_plug_vizia::vizia::prelude::*; use nih_plug_vizia::widgets::*; use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; -use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::Duration; #[derive(Lens)] struct Data { @@ -33,26 +31,28 @@ pub(crate) fn create( .build(cx); VStack::new(cx, |cx| { - Label::new(cx, "Polysynth") - .font_weight(FontWeightKeyword::Thin) - .font_size(30.0) - .height(Pixels(50.0)) - .child_top(Stretch(1.0)) - .child_bottom(Pixels(0.0)); - HStack::new(cx, |cx| { - for ix in 0..2 { - let p = Data::params.map(move |p| p.osc_params[ix].clone()); - VStack::new(cx, |cx| { - Label::new(cx, &format!("Oscillator {}", ix + 1)) - .font_size(22.) - .height(Pixels(30.)) - .child_bottom(Pixels(8.)); - GenericUi::new(cx, p).width(Percentage(100.)); - }) - .width(Stretch(1.0)); - } - }) - .row_between(Pixels(0.0)); + VStack::new(cx, |cx| { + Label::new(cx, "Polysynth") + .font_weight(FontWeightKeyword::Thin) + .font_size(30.0) + .height(Pixels(50.0)) + .child_top(Stretch(1.0)) + .child_bottom(Pixels(0.0)); + HStack::new(cx, |cx| { + for ix in 0..2 { + let p = Data::params.map(move |p| p.osc_params[ix].clone()); + VStack::new(cx, |cx| { + Label::new(cx, &format!("Oscillator {}", ix + 1)) + .font_size(22.) + .height(Pixels(30.)) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, p).width(Percentage(100.)); + }) + .width(Stretch(1.0)); + } + }) + .row_between(Pixels(0.0)); + }); VStack::new(cx, |cx| { Label::new(cx, "Filter") @@ -62,6 +62,13 @@ pub(crate) fn create( GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())) .width(Percentage(100.)); }); + + VStack::new(cx, |cx| { + HStack::new(cx, |cx| { + Label::new(cx, "Output Level").width(Stretch(1.)); + ParamSlider::new(cx, Data::params, |p| &p.output_level).width(Stretch(1.)); + }); + }); }) .width(Percentage(100.)) .height(Percentage(100.)) diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 3775170..9fe44c9 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -129,7 +129,7 @@ impl Default for PolysynthPlugin { const DEFAULT_SAMPLERATE: f32 = 44100.; let params = Arc::new(PolysynthParams::default()); Self { - dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, ¶ms)), + dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, params.clone())), params, voice_id_map: VoiceIdMap::default(), } @@ -156,8 +156,8 @@ impl Plugin for PolysynthPlugin { self.params.clone() } - fn reset(&mut self) { - self.dsp.reset(); + fn editor(&mut self, _: AsyncExecutor) -> Option> { + editor::create(self.params.clone(), self.params.editor_state.clone()) } fn initialize( @@ -171,8 +171,8 @@ impl Plugin for PolysynthPlugin { true } - fn editor(&mut self, _: AsyncExecutor) -> Option> { - editor::create(self.params.clone(), self.params.editor_state.clone()) + fn reset(&mut self) { + self.dsp.reset(); } fn process( diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index bb8b017..eff006f 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -102,6 +102,8 @@ pub struct FilterParams { pub cutoff: FloatParam, #[id = "res"] pub resonance: FloatParam, + #[id = "kt"] + pub keyboard_tracking: FloatParam, } impl FilterParams { @@ -138,6 +140,18 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + keyboard_tracking: FloatParam::new( + "Keyboard Tracking", + 0.5, + FloatRange::Linear { min: 0., max: 2. }, + ) + .with_unit(" %") + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), } } } @@ -148,6 +162,8 @@ pub struct PolysynthParams { pub osc_params: [Arc; 2], #[nested] pub filter_params: Arc, + #[id = "out"] + pub output_level: FloatParam, pub oversample: Arc, #[persist = "editor"] pub editor_state: Arc, @@ -159,6 +175,21 @@ impl Default for PolysynthParams { Self { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), + output_level: FloatParam::new( + "Output Level", + 0.25, + FloatRange::Skewed { + min: 0.0, + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.), + }, + ) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(50.), + )), oversample, editor_state: crate::editor::default_state(), } From dc8bca6bd783a4f3e1ee339f58d95874306dc984 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 13:23:28 +0200 Subject: [PATCH 07/67] feat(examples): polysynth: phase retriggering option --- Cargo.lock | 15 ++- crates/valib-oscillators/src/lib.rs | 13 +++ crates/valib-oscillators/src/polyblep.rs | 6 +- examples/polysynth/Cargo.toml | 2 + examples/polysynth/src/dsp.rs | 134 +++++++++++++++-------- examples/polysynth/src/params.rs | 3 + 6 files changed, 123 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 747259f..1859add 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1577,6 +1577,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fastrand-contrib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6c045880cda8f657f4859baf534963ff0595e2dcce0de5f52dcdf3076c290b" +dependencies = [ + "fastrand 2.1.1", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -3064,7 +3073,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.77", @@ -3559,6 +3568,8 @@ dependencies = [ name = "polysynth" version = "0.1.0" dependencies = [ + "fastrand 2.1.1", + "fastrand-contrib", "nih_plug", "nih_plug_vizia", "num-traits", @@ -5496,7 +5507,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 9aa04b2..4427dc1 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -63,6 +63,19 @@ impl Phasor { } } + pub fn phase(&self) -> T { + self.phase + } + + pub fn set_phase(&mut self, phase: T) { + self.phase = phase.simd_fract(); + } + + pub fn with_phase(mut self, phase: T) -> Self { + self.set_phase(phase); + self + } + /// Sets the frequency of this phasor. Phase is not reset, which means the phase remains /// continuous. /// # Arguments diff --git a/crates/valib-oscillators/src/polyblep.rs b/crates/valib-oscillators/src/polyblep.rs index d99dd5b..eb132e9 100644 --- a/crates/valib-oscillators/src/polyblep.rs +++ b/crates/valib-oscillators/src/polyblep.rs @@ -185,8 +185,10 @@ impl DSPProcess<0, 1> for Triangle { } impl Triangle { - pub fn new(samplerate: T, frequency: T) -> Self { - let square = PolyBLEPDriver::new(samplerate, frequency, SquareBLEP::new(T::from_f64(0.5))); + pub fn new(samplerate: T, frequency: T, phase: T) -> Self { + let mut square = + PolyBLEPDriver::new(samplerate, frequency, SquareBLEP::new(T::from_f64(0.5))); + square.phasor.phase = phase; let integrator = P1::new(samplerate, frequency); Self { square, integrator } } diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml index 473096e..994d380 100644 --- a/examples/polysynth/Cargo.toml +++ b/examples/polysynth/Cargo.toml @@ -13,6 +13,8 @@ keywords.workspace = true crate-type = ["lib", "cdylib"] [dependencies] +fastrand = { version = "2.1.1", default-features = false } +fastrand-contrib = { version = "0.1.0", default-features = false } valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} nih_plug = { workspace = true, features = ["standalone"] } nih_plug_vizia.workspace = true diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index c6bf7bd..3998a2e 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,8 +1,11 @@ -use crate::params::{FilterParams, OscParams, OscShape, PolysynthParams}; +use crate::params::{OscShape, PolysynthParams}; use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use fastrand::Rng; +use fastrand_contrib::RngExt; use nih_plug::nih_log; use nih_plug::util::db_to_gain_fast; use num_traits::{ConstOne, ConstZero}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::parameter::SmoothedParam; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; @@ -24,20 +27,35 @@ pub enum PolyOsc { } impl PolyOsc { - fn new(samplerate: T, shape: OscShape, note_data: NoteData, pulse_width: T) -> Self { + fn new( + samplerate: T, + shape: OscShape, + note_data: NoteData, + pulse_width: T, + phase: T, + ) -> Self { match shape { - OscShape::Sine => Self::Sine(Phasor::new(samplerate, note_data.frequency)), - OscShape::Triangle => Self::Triangle(Triangle::new(samplerate, note_data.frequency)), - OscShape::Square => Self::Square(Square::new( - samplerate, - note_data.frequency, - SquareBLEP::new(pulse_width), - )), - OscShape::Saw => Self::Sawtooth(Sawtooth::new( - samplerate, - note_data.frequency, - SawBLEP::default(), - )), + OscShape::Sine => { + Self::Sine(Phasor::new(samplerate, note_data.frequency).with_phase(phase)) + } + OscShape::Triangle => { + Self::Triangle(Triangle::new(samplerate, note_data.frequency, phase)) + } + OscShape::Square => { + let mut square = Square::new( + samplerate, + note_data.frequency, + SquareBLEP::new(pulse_width), + ); + square.phasor.set_phase(phase); + Self::Square(square) + } + OscShape::Saw => { + let mut sawtooth = + Sawtooth::new(samplerate, note_data.frequency, SawBLEP::default()); + sawtooth.phasor.set_phase(phase); + Self::Sawtooth(sawtooth) + } } } @@ -111,17 +129,66 @@ pub struct RawVoice { gate: SmoothedParam, note_data: NoteData, samplerate: T, + rng: Rng, } impl RawVoice { - pub(crate) fn update_osc_types(&mut self) { + fn create_voice( + target_samplerate_f64: f64, + params: Arc, + note_data: NoteData, + ) -> Self { + static VOICE_SEED: AtomicU64 = AtomicU64::new(0); + let target_samplerate = T::from_f64(target_samplerate_f64); + let mut rng = Rng::with_seed(VOICE_SEED.fetch_add(1, Ordering::SeqCst)); + RawVoice { + osc: std::array::from_fn(|i| { + let osc_param = ¶ms.osc_params[i]; + let pulse_width = T::from_f64(osc_param.pulse_width.value() as _); + PolyOsc::new( + target_samplerate, + osc_param.shape.value(), + note_data, + pulse_width, + if osc_param.retrigger.value() { + T::zero() + } else { + T::from_f64(rng.f64_range(0.0..1.0)) + }, + ) + }), + filter: Ladder::new( + target_samplerate_f64, + T::from_f64(params.filter_params.cutoff.value() as _), + T::from_f64(params.filter_params.resonance.value() as _), + ), + osc_out_sat: bjt::CommonCollector { + vee: -T::ONE, + vcc: T::ONE, + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, + params: params.clone(), + gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + note_data, + samplerate: target_samplerate, + rng, + } + } + + fn update_osc_types(&mut self) { for i in 0..2 { let params = &self.params.osc_params[i]; let shape = params.shape.value(); let osc = &mut self.osc[i]; if !osc.is_osc_shape(shape) { let pulse_width = T::from_f64(params.pulse_width.value() as _); - *osc = PolyOsc::new(self.samplerate, shape, self.note_data, pulse_width); + let phase = if params.retrigger.value() { + T::zero() + } else { + T::from_f64(self.rng.f64_range(0.0..1.0)) + }; + *osc = PolyOsc::new(self.samplerate, shape, self.note_data, pulse_width, phase); } } } @@ -221,41 +288,16 @@ pub fn create_voice_manager( samplerate: f32, params: Arc, ) -> VoiceManager { - let target_samplerate_f64 = OVERSAMPLE as f64 * samplerate as f64; - let target_samplerate = T::from_f64(target_samplerate_f64); - let osc_params = params.osc_params.clone(); - let filter_params = params.filter_params.clone(); + let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { SampleAdapter::new(UpsampledVoice::new( 2, MAX_BUFFER_SIZE, - BlockAdapter(RawVoice { - osc: std::array::from_fn(|i| { - let osc_param = &osc_params[i]; - let pulse_width = T::from_f64(osc_param.pulse_width.value() as _); - PolyOsc::new( - target_samplerate, - osc_param.shape.value(), - note_data, - pulse_width, - ) - }), - filter: Ladder::new( - target_samplerate_f64, - T::from_f64(filter_params.cutoff.value() as _), - T::from_f64(filter_params.resonance.value() as _), - ), - osc_out_sat: bjt::CommonCollector { - vee: -T::ONE, - vcc: T::ONE, - xbias: T::from_f64(0.1), - ybias: T::from_f64(-0.1), - }, - params: params.clone(), - gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + BlockAdapter(RawVoice::create_voice( + target_samplerate, + params.clone(), note_data, - samplerate: target_samplerate, - }), + )), )) }) } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index eff006f..f08dfaf 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -28,6 +28,8 @@ pub struct OscParams { pub pitch_fine: FloatParam, #[id = "pw"] pub pulse_width: FloatParam, + #[id = "rtrg"] + pub retrigger: BoolParam, } impl OscParams { @@ -92,6 +94,7 @@ impl OscParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + retrigger: BoolParam::new("Retrigger", false), } } } From 5c8affb35b33f663a5b1aecedb02558be0fc121c Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 15:23:49 +0200 Subject: [PATCH 08/67] feat(examples): polysynth: use oversample const everywhere and increase to 8x for voices --- examples/polysynth/src/dsp.rs | 2 +- examples/polysynth/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 3998a2e..d5ad4a6 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -291,7 +291,7 @@ pub fn create_voice_manager( let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { SampleAdapter::new(UpsampledVoice::new( - 2, + OVERSAMPLE, MAX_BUFFER_SIZE, BlockAdapter(RawVoice::create_voice( target_samplerate, diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 9fe44c9..0c86d28 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -16,7 +16,7 @@ mod editor; mod params; const NUM_VOICES: usize = 16; -const OVERSAMPLE: usize = 4; +const OVERSAMPLE: usize = 8; const MAX_BUFFER_SIZE: usize = 64; const POLYMOD_OSC_AMP: [u32; 2] = [0, 1]; From 70a717640a32ed5d1e82e03f10c00448eb9db39a Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 19:02:50 +0200 Subject: [PATCH 09/67] chore(core): provide type alias for sine interpolation --- crates/valib-core/src/math/interpolation.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/valib-core/src/math/interpolation.rs b/crates/valib-core/src/math/interpolation.rs index 14b6107..2cf2a0c 100644 --- a/crates/valib-core/src/math/interpolation.rs +++ b/crates/valib-core/src/math/interpolation.rs @@ -133,9 +133,12 @@ impl Interpolate for Linear { #[derive(Debug, Copy, Clone)] pub struct MappedLinear(pub F); +pub type Sine = MappedLinear T>; + /// Returns an interpolator that performs sine interpolation. -pub fn sine_interpolation() -> MappedLinear T> { - MappedLinear(|t| T::simd_cos(t * T::simd_pi())) +#[replace_float_literals(T::from_f64(literal))] +pub fn sine_interpolation() -> Sine { + MappedLinear(|t| 0.5 - 0.5 * T::simd_cos(t * T::simd_pi())) } impl T> Interpolate for MappedLinear From 70e20bc9a07769eade5fe3230410455b1b25a98f Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 19:03:11 +0200 Subject: [PATCH 10/67] fix(oscillators): make phasor phase reset consistent --- crates/valib-oscillators/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 4427dc1..20b8847 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -38,7 +38,7 @@ impl DSPProcess<0, 1> for Phasor { fn process(&mut self, _: [Self::Sample; 0]) -> [Self::Sample; 1] { let p = self.phase; let new_phase = self.phase + self.step; - let gt = new_phase.simd_gt(T::one()); + let gt = new_phase.simd_ge(T::one()); self.phase = (new_phase - T::one()).select(gt, new_phase); [p] } @@ -76,6 +76,10 @@ impl Phasor { self } + pub fn next_sample_resets(&self) -> T::SimdBool { + (self.phase + self.step).simd_ge(T::one()) + } + /// Sets the frequency of this phasor. Phase is not reset, which means the phase remains /// continuous. /// # Arguments From e0b611b50b8727de4bf5f84909438b18ad37d568 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 19:03:24 +0200 Subject: [PATCH 11/67] feat(examples): polysynth: drift parameter --- examples/polysynth/src/dsp.rs | 59 +++++++++++++++++++++++++++----- examples/polysynth/src/editor.rs | 4 +-- examples/polysynth/src/lib.rs | 6 ++-- examples/polysynth/src/params.rs | 9 ++++- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index d5ad4a6..8bbfb0e 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -10,15 +10,55 @@ use std::sync::Arc; use valib::dsp::parameter::SmoothedParam; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; use valib::filters::ladder::{Ladder, OTA}; +use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; use valib::saturators::{bjt, Tanh}; +use valib::simd::SimdBool; use valib::util::semitone_to_ratio; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; use valib::Scalar; +struct Drift { + rng: Rng, + phasor: Phasor, + last_value: T, + next_value: T, + interp: Sine, +} + +impl Drift { + pub fn new(mut rng: Rng, samplerate: T, frequency: T) -> Self { + let phasor = Phasor::new(samplerate, frequency); + let last_value = T::from_f64(rng.f64_range(-1.0..1.0)); + let next_value = T::from_f64(rng.f64_range(-1.0..1.0)); + Self { + rng, + phasor, + last_value, + next_value, + interp: sine_interpolation(), + } + } + + pub fn next_sample(&mut self) -> T { + let reset_mask = self.phasor.next_sample_resets(); + if reset_mask.any() { + self.last_value = reset_mask.if_else(|| self.next_value, || self.last_value); + self.next_value = reset_mask.if_else( + || T::from_f64(self.rng.f64_range(-1.0..1.0)), + || self.next_value, + ); + } + + let [t] = self.phasor.process([]); + self.interp + .interpolate(t, [self.last_value, self.next_value]) + } +} + pub enum PolyOsc { Sine(Phasor), Triangle(Triangle), @@ -121,19 +161,22 @@ impl DSPProcess<1, 1> for PolyOsc { } } +pub(crate) const NUM_OSCILLATORS: usize = 2; + pub struct RawVoice { - osc: [PolyOsc; 2], + osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, filter: Ladder>, params: Arc, gate: SmoothedParam, note_data: NoteData, + drift: [Drift; NUM_OSCILLATORS], samplerate: T, rng: Rng, } impl RawVoice { - fn create_voice( + fn new( target_samplerate_f64: f64, params: Arc, note_data: NoteData, @@ -171,6 +214,7 @@ impl RawVoice { params: params.clone(), gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), note_data, + drift: std::array::from_fn(|_| Drift::new(rng.fork(), target_samplerate_f64 as _, 0.2)), samplerate: target_samplerate, rng, } @@ -238,6 +282,7 @@ impl DSPMeta for RawVoice { impl DSPProcess<0, 1> for RawVoice { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + const DRIFT_MAX_ST: f32 = 0.1; // Process oscillators let frequency = self.note_data.frequency; let osc_params = self.params.osc_params.clone(); @@ -246,9 +291,11 @@ impl DSPProcess<0, 1> for RawVoice { let [osc1, osc2] = std::array::from_fn(|i| { let osc = &mut self.osc[i]; let params = &self.params.osc_params[i]; + let drift = &mut self.drift[i]; + let drift = drift.next_sample() * DRIFT_MAX_ST * params.drift.smoothed.next(); let osc_freq = frequency * T::from_f64(semitone_to_ratio( - params.pitch_coarse.value() + params.pitch_fine.value(), + params.pitch_coarse.value() + params.pitch_fine.value() + drift, ) as _); osc.set_pulse_width(T::from_f64(params.pulse_width.smoothed.next() as _)); let [osc] = osc.process([osc_freq]); @@ -293,11 +340,7 @@ pub fn create_voice_manager( SampleAdapter::new(UpsampledVoice::new( OVERSAMPLE, MAX_BUFFER_SIZE, - BlockAdapter(RawVoice::create_voice( - target_samplerate, - params.clone(), - note_data, - )), + BlockAdapter(RawVoice::new(target_samplerate, params.clone(), note_data)), )) }) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index c950de8..84770f8 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -14,7 +14,7 @@ impl Model for Data {} // Makes sense to also define this here, makes it a bit easier to keep track of pub(crate) fn default_state() -> Arc { - ViziaState::new(|| (750, 550)) + ViziaState::new(|| (750, 650)) } pub(crate) fn create( @@ -39,7 +39,7 @@ pub(crate) fn create( .child_top(Stretch(1.0)) .child_bottom(Pixels(0.0)); HStack::new(cx, |cx| { - for ix in 0..2 { + for ix in 0..crate::dsp::NUM_OSCILLATORS { let p = Data::params.map(move |p| p.osc_params[ix].clone()); VStack::new(cx, |cx| { Label::new(cx, &format!("Oscillator {}", ix + 1)) diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 0c86d28..29f3884 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -19,9 +19,9 @@ const NUM_VOICES: usize = 16; const OVERSAMPLE: usize = 8; const MAX_BUFFER_SIZE: usize = 64; -const POLYMOD_OSC_AMP: [u32; 2] = [0, 1]; -const POLYMOD_OSC_PITCH_COARSE: [u32; 2] = [2, 3]; -const POLYMOD_OSC_PITCH_FINE: [u32; 2] = [4, 5]; +const POLYMOD_OSC_AMP: [u32; dsp::NUM_OSCILLATORS] = [0, 1]; +const POLYMOD_OSC_PITCH_COARSE: [u32; dsp::NUM_OSCILLATORS] = [2, 3]; +const POLYMOD_OSC_PITCH_FINE: [u32; dsp::NUM_OSCILLATORS] = [4, 5]; const POLYMOD_FILTER_CUTOFF: u32 = 6; #[derive(Debug, Copy, Clone)] diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index f08dfaf..3fb7f53 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -28,6 +28,8 @@ pub struct OscParams { pub pitch_fine: FloatParam, #[id = "pw"] pub pulse_width: FloatParam, + #[id = "drift"] + pub drift: FloatParam, #[id = "rtrg"] pub retrigger: BoolParam, } @@ -94,6 +96,11 @@ impl OscParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + drift: FloatParam::new("Drift", 0.1, FloatRange::Linear { min: 0.0, max: 1.0 }) + .with_unit(" %") + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_value_to_string(formatters::v2s_f32_percentage(1)) + .with_smoother(SmoothingStyle::Exponential(100.)), retrigger: BoolParam::new("Retrigger", false), } } @@ -162,7 +169,7 @@ impl FilterParams { #[derive(Debug, Params)] pub struct PolysynthParams { #[nested(array)] - pub osc_params: [Arc; 2], + pub osc_params: [Arc; crate::dsp::NUM_OSCILLATORS], #[nested] pub filter_params: Arc, #[id = "out"] From dc90390962acf8549d3b5899c3b72fff6bd1453b Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 23:24:06 +0200 Subject: [PATCH 12/67] fix(voice): crash on polyphonic voice manager --- crates/valib-voice/src/polyphonic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index afccb4b..9ae218d 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -127,7 +127,7 @@ impl VoiceManager for Polyphonic { } else { // Find first available slot while self.voice_pool[self.next_voice].is_some() { - self.next_voice += 1; + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); } let id = self.next_voice; From e124466aa69d7647bf77131838c42bbc25f9a6c3 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 23:24:29 +0200 Subject: [PATCH 13/67] feat(examples): polyphonic: VCA/VCF envelopes --- examples/polysynth/src/dsp.rs | 255 +++++++++++++++++++++++++++++-- examples/polysynth/src/editor.rs | 75 +++++---- examples/polysynth/src/lib.rs | 1 + examples/polysynth/src/params.rs | 109 ++++++++++++- 4 files changed, 397 insertions(+), 43 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 8bbfb0e..cc9bda6 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,4 +1,4 @@ -use crate::params::{OscShape, PolysynthParams}; +use crate::params::{FilterParams, OscShape, PolysynthParams}; use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; @@ -7,7 +7,6 @@ use nih_plug::util::db_to_gain_fast; use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -use valib::dsp::parameter::SmoothedParam; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; use valib::filters::ladder::{Ladder, OTA}; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; @@ -21,6 +20,196 @@ use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; use valib::Scalar; +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +enum AdsrState { + Idle, + Attack, + Decay, + Sustain, + Release, +} + +impl AdsrState { + pub fn next_state(self, gate: bool) -> Self { + if gate { + Self::Attack + } else if !matches!(self, Self::Idle) { + Self::Release + } else { + self + } + } +} + +struct Adsr { + attack: f32, + decay: f32, + sustain: f32, + release: f32, + samplerate: f32, + attack_base: f32, + decay_base: f32, + release_base: f32, + attack_rate: f32, + decay_rate: f32, + release_rate: f32, + cur_state: AdsrState, + cur_value: f32, + release_coeff: f32, + decay_coeff: f32, + attack_coeff: f32, +} + +impl Default for Adsr { + fn default() -> Self { + Self { + samplerate: 0., + attack: 0., + decay: 0., + sustain: 0., + release: 0., + attack_base: 1. + Self::TARGET_RATIO_A, + decay_base: -Self::TARGET_RATIO_DR, + release_base: -Self::TARGET_RATIO_DR, + attack_coeff: 0., + decay_coeff: 0., + release_coeff: 0., + attack_rate: 0., + decay_rate: 0., + release_rate: 0., + cur_state: AdsrState::Idle, + cur_value: 0., + } + } +} + +impl Adsr { + const TARGET_RATIO_A: f32 = 0.3; + const TARGET_RATIO_DR: f32 = 1e-4; + pub fn new( + samplerate: f32, + attack: f32, + decay: f32, + sustain: f32, + release: f32, + gate: bool, + ) -> Self { + let mut this = Self { + samplerate, + cur_state: AdsrState::Idle.next_state(gate), + ..Self::default() + }; + this.set_attack(attack); + this.set_decay(decay); + this.set_sustain(sustain); + this.set_release(release); + this.cur_state = this.cur_state.next_state(gate); + this + } + + pub fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = samplerate; + self.set_attack(self.attack); + self.set_decay(self.decay); + self.set_release(self.release); + } + + pub fn set_attack(&mut self, attack: f32) { + if (self.attack - attack).abs() < 1e-6 { + return; + } + self.attack = attack; + self.attack_rate = self.samplerate * attack; + self.attack_coeff = Self::calc_coeff(self.attack_rate, Self::TARGET_RATIO_A); + self.attack_base = (1. + Self::TARGET_RATIO_A) * (1.0 - self.attack_coeff); + } + + pub fn set_decay(&mut self, decay: f32) { + if (self.decay - decay).abs() < 1e-6 { + return; + } + self.decay = decay; + self.decay_rate = self.samplerate * decay; + self.decay_coeff = Self::calc_coeff(self.decay_rate, Self::TARGET_RATIO_DR); + self.decay_base = (self.sustain - Self::TARGET_RATIO_DR) * (1. - self.decay_coeff); + } + + pub fn set_sustain(&mut self, sustain: f32) { + self.sustain = sustain; + } + + pub fn set_release(&mut self, release: f32) { + if (self.release - release).abs() < 1e-6 { + return; + } + self.release = release; + self.release_rate = self.samplerate * release; + self.release_coeff = Self::calc_coeff(self.release_rate, Self::TARGET_RATIO_DR); + self.release_base = -Self::TARGET_RATIO_DR * (1. - self.release_coeff); + } + + pub fn gate(&mut self, gate: bool) { + self.cur_state = self.cur_state.next_state(gate); + } + + pub fn next_sample(&mut self) -> f32 { + match self.cur_state { + AdsrState::Attack => { + self.cur_value = self.attack_base + self.cur_value * self.attack_coeff; + if self.cur_value >= 1. { + self.cur_value = 1.; + self.cur_state = AdsrState::Decay; + } + } + AdsrState::Decay => { + self.cur_value = self.decay_base + self.cur_value * self.decay_coeff; + if self.cur_value <= self.sustain { + self.cur_value = self.sustain; + self.cur_state = AdsrState::Sustain; + } + } + AdsrState::Release => { + self.cur_value = self.release_base + self.cur_value * self.release_coeff; + if self.cur_value <= 0. { + self.cur_value = 0.; + self.cur_state = AdsrState::Idle; + } + } + AdsrState::Sustain | AdsrState::Idle => {} + } + self.cur_value + } + + pub fn state(&self) -> AdsrState { + self.cur_state + } + + pub fn current_value(&self) -> f32 { + self.cur_value + } + + pub fn current_value_as(&self) -> T { + T::from_f64(self.current_value() as _) + } + + pub fn is_idle(&self) -> bool { + matches!(self.cur_state, AdsrState::Idle) + } + + pub fn reset(&mut self) { + self.cur_state = AdsrState::Idle; + self.cur_value = 0.; + } + + fn calc_coeff(rate: f32, ratio: f32) -> f32 { + if rate <= 0. { + 0. + } else { + (-((1.0 + ratio) / ratio).ln() / rate).exp() + } + } +} + struct Drift { rng: Rng, phasor: Phasor, @@ -168,7 +357,8 @@ pub struct RawVoice { osc_out_sat: bjt::CommonCollector, filter: Ladder>, params: Arc, - gate: SmoothedParam, + vca_env: Adsr, + vcf_env: Adsr, note_data: NoteData, drift: [Drift; NUM_OSCILLATORS], samplerate: T, @@ -212,7 +402,22 @@ impl RawVoice { ybias: T::from_f64(-0.1), }, params: params.clone(), - gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + vca_env: Adsr::new( + target_samplerate_f64 as _, + params.vca_env.attack.value(), + params.vca_env.decay.value(), + params.vca_env.sustain.value(), + params.vca_env.release.value(), + true, + ), + vcf_env: Adsr::new( + target_samplerate_f64 as _, + params.vcf_env.attack.value(), + params.vcf_env.decay.value(), + params.vcf_env.sustain.value(), + params.vcf_env.release.value(), + true, + ), note_data, drift: std::array::from_fn(|_| Drift::new(rng.fork(), target_samplerate_f64 as _, 0.2)), samplerate: target_samplerate, @@ -236,11 +441,30 @@ impl RawVoice { } } } + + fn update_envelopes(&mut self) { + self.vca_env + .set_attack(self.params.vca_env.attack.smoothed.next()); + self.vca_env + .set_decay(self.params.vca_env.decay.smoothed.next()); + self.vca_env + .set_sustain(self.params.vca_env.sustain.smoothed.next()); + self.vca_env + .set_release(self.params.vca_env.release.smoothed.next()); + self.vcf_env + .set_attack(self.params.vcf_env.attack.smoothed.next()); + self.vcf_env + .set_decay(self.params.vcf_env.decay.smoothed.next()); + self.vcf_env + .set_sustain(self.params.vcf_env.sustain.smoothed.next()); + self.vcf_env + .set_release(self.params.vcf_env.release.smoothed.next()); + } } impl Voice for RawVoice { fn active(&self) -> bool { - self.gate.current_value() > 1e-4 + !self.vca_env.is_idle() } fn note_data(&self) -> &NoteData { @@ -253,11 +477,13 @@ impl Voice for RawVoice { fn release(&mut self, _: f32) { nih_log!("RawVoice: release(_)"); - self.gate.param = 0.; + self.vca_env.gate(false); + self.vcf_env.gate(false); } fn reuse(&mut self) { - self.gate.param = 1.; + self.vca_env.gate(true); + self.vcf_env.gate(true); } } @@ -270,6 +496,8 @@ impl DSPMeta for RawVoice { osc.set_samplerate(samplerate); } self.filter.set_samplerate(samplerate); + self.vca_env.set_samplerate(samplerate); + self.vcf_env.set_samplerate(samplerate); } fn reset(&mut self) { @@ -277,17 +505,21 @@ impl DSPMeta for RawVoice { osc.reset(); } self.filter.reset(); + self.vca_env.reset(); + self.vcf_env.reset(); } } impl DSPProcess<0, 1> for RawVoice { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { const DRIFT_MAX_ST: f32 = 0.1; + self.update_osc_types(); + self.update_envelopes(); + // Process oscillators let frequency = self.note_data.frequency; let osc_params = self.params.osc_params.clone(); let filter_params = self.params.filter_params.clone(); - self.update_osc_types(); let [osc1, osc2] = std::array::from_fn(|i| { let osc = &mut self.osc[i]; let params = &self.params.osc_params[i]; @@ -312,7 +544,10 @@ impl DSPProcess<0, 1> for RawVoice { let freq_ratio = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) * frequency - / T::from_f64(440.); + / T::from_f64(440.) + + T::from_f64(semitone_to_ratio( + filter_params.env_amt.smoothed.next() * self.vcf_env.next_sample(), + ) as _); let filter_freq = (T::one() + freq_ratio) * T::from_f64(filter_params.cutoff.smoothed.next() as _); @@ -321,7 +556,7 @@ impl DSPProcess<0, 1> for RawVoice { self.filter.set_resonance(T::from_f64( 4f64 * filter_params.resonance.smoothed.next() as f64, )); - let vca = self.gate.next_sample_as::(); + let vca = T::from_f64(self.vca_env.next_sample() as _); let static_amp = T::from_f64(self.params.output_level.smoothed.next() as _); self.filter.process(filter_in).map(|x| static_amp * vca * x) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index 84770f8..f1c37b3 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -1,5 +1,5 @@ use crate::params::PolysynthParams; -use nih_plug::prelude::Editor; +use nih_plug::prelude::{Editor, Param}; use nih_plug_vizia::vizia::prelude::*; use nih_plug_vizia::widgets::*; use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; @@ -14,7 +14,7 @@ impl Model for Data {} // Makes sense to also define this here, makes it a bit easier to keep track of pub(crate) fn default_state() -> Arc { - ViziaState::new(|| (750, 650)) + ViziaState::new(|| (1000, 600)) } pub(crate) fn create( @@ -31,49 +31,66 @@ pub(crate) fn create( .build(cx); VStack::new(cx, |cx| { - VStack::new(cx, |cx| { + HStack::new(cx, move |cx| { Label::new(cx, "Polysynth") .font_weight(FontWeightKeyword::Thin) .font_size(30.0) .height(Pixels(50.0)) - .child_top(Stretch(1.0)) - .child_bottom(Pixels(0.0)); + .child_left(Stretch(1.0)) + .child_right(Stretch(1.0)); + HStack::new(cx, |cx| { + Label::new(cx, "Output Level") + .child_top(Pixels(5.)) + .width(Auto) + .height(Pixels(30.0)); + ParamSlider::new(cx, Data::params, |p| &p.output_level).width(Pixels(200.)); + }) + .col_between(Pixels(8.0)); + }) + .col_between(Stretch(1.0)) + .width(Percentage(100.)) + .height(Pixels(30.)); + VStack::new(cx, |cx| { HStack::new(cx, |cx| { for ix in 0..crate::dsp::NUM_OSCILLATORS { let p = Data::params.map(move |p| p.osc_params[ix].clone()); VStack::new(cx, |cx| { Label::new(cx, &format!("Oscillator {}", ix + 1)) .font_size(22.) - .height(Pixels(30.)) .child_bottom(Pixels(8.)); - GenericUi::new(cx, p).width(Percentage(100.)); - }) - .width(Stretch(1.0)); + GenericUi::new(cx, p); + }); } + VStack::new(cx, |cx| { + Label::new(cx, "Filter") + .font_size(22.) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())); + }); }) - .row_between(Pixels(0.0)); - }); - - VStack::new(cx, |cx| { - Label::new(cx, "Filter") - .font_size(22.) - .height(Pixels(30.)) - .child_bottom(Pixels(8.)); - GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())) - .width(Percentage(100.)); - }); - - VStack::new(cx, |cx| { + .row_between(Stretch(1.0)); HStack::new(cx, |cx| { - Label::new(cx, "Output Level").width(Stretch(1.)); - ParamSlider::new(cx, Data::params, |p| &p.output_level).width(Stretch(1.)); - }); - }); + VStack::new(cx, |cx| { + Label::new(cx, "Amp Env").font_size(22.); + GenericUi::new(cx, Data::params.map(|p| p.vca_env.clone())); + }); + VStack::new(cx, |cx| { + Label::new(cx, "Filter Env").font_size(22.); + GenericUi::new(cx, Data::params.map(|p| p.vcf_env.clone())); + }); + }) + .left(Stretch(1.0)) + .right(Stretch(1.0)) + .width(Percentage(50.)); + }) + .top(Pixels(16.)) + .width(Percentage(100.)) + .height(Percentage(100.)) + .row_between(Pixels(0.0)); }) + .row_between(Pixels(0.0)) .width(Percentage(100.)) - .height(Percentage(100.)) - .row_between(Pixels(0.0)); - + .height(Percentage(100.)); ResizeHandle::new(cx); }) } diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 29f3884..c051303 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(generic_const_exprs)] use crate::params::PolysynthParams; use nih_plug::audio_setup::{AudioIOLayout, AuxiliaryBuffers}; use nih_plug::buffer::Buffer; diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index 3fb7f53..d0a174e 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -8,6 +8,83 @@ use nih_plug_vizia::ViziaState; use std::sync::Arc; use valib::dsp::parameter::{ParamId, ParamName}; +#[derive(Debug, Params)] +pub struct AdsrParams { + #[id = "atk"] + pub attack: FloatParam, + #[id = "dec"] + pub decay: FloatParam, + #[id = "sus"] + pub sustain: FloatParam, + #[id = "rel"] + pub release: FloatParam, +} + +fn v2s_f32_ms_then_s(digits: usize) -> Arc String> { + Arc::new(move |v| { + if v < 0.9 { + format!("{:1$} ms", v * 1e3, digits) + } else { + format!("{v:0$} s", digits) + } + }) +} + +fn s2v_f32_ms_then_s() -> Arc Option> { + Arc::new(move |input: &str| { + let s = input.trim(); + if s.ends_with("ms") { + s[..(s.len() - 2)].parse::().map(|v| 1e-3 * v).ok() + } else { + s.parse::().ok() + } + }) +} + +impl Default for AdsrParams { + fn default() -> Self { + Self { + attack: FloatParam::new( + "Attack", + 0.1, + FloatRange::Skewed { + min: 1e-3, + max: 10., + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(v2s_f32_ms_then_s(2)) + .with_string_to_value(s2v_f32_ms_then_s()), + decay: FloatParam::new( + "Decay", + 0.5, + FloatRange::Skewed { + min: 1e-3, + max: 10., + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(v2s_f32_ms_then_s(2)) + .with_string_to_value(s2v_f32_ms_then_s()), + sustain: FloatParam::new("Sustain", 0.8, FloatRange::Linear { min: 0., max: 1. }) + .with_unit(" %") + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_string_to_value(formatters::s2v_f32_percentage()), + release: FloatParam::new( + "Decay", + 1., + FloatRange::Skewed { + min: 1e-2, + max: 15., + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(v2s_f32_ms_then_s(2)) + .with_string_to_value(s2v_f32_ms_then_s()), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq, ParamName, Enum)] pub enum OscShape { Sine, @@ -40,7 +117,7 @@ impl OscParams { shape: EnumParam::new("Shape", OscShape::Saw), amplitude: FloatParam::new( "Amplitude", - 0.8, + 0.25, FloatRange::Skewed { min: db_to_gain(MINUS_INFINITY_DB), max: 1.0, @@ -49,6 +126,7 @@ impl OscParams { ) .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") .with_smoother(SmoothingStyle::OversamplingAware( oversample.clone(), &SmoothingStyle::Exponential(10.), @@ -114,6 +192,8 @@ pub struct FilterParams { pub resonance: FloatParam, #[id = "kt"] pub keyboard_tracking: FloatParam, + #[id = "env"] + pub env_amt: FloatParam, } impl FilterParams { @@ -162,15 +242,33 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + env_amt: FloatParam::new( + "Env Amt", + 0., + FloatRange::Linear { + min: -96., + max: 96., + }, + ) + .with_unit(" st") + .with_value_to_string(Arc::new(|x| format!("{:.2}", x))) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(50.), + )), } } } #[derive(Debug, Params)] pub struct PolysynthParams { - #[nested(array)] + #[nested(array, group = "Osc")] pub osc_params: [Arc; crate::dsp::NUM_OSCILLATORS], - #[nested] + #[nested(id_prefix = "vca_", group = "Amp Env")] + pub vca_env: Arc, + #[nested(id_prefix = "vcf_", group = "Filter Env")] + pub vcf_env: Arc, + #[nested(group = "Filter")] pub filter_params: Arc, #[id = "out"] pub output_level: FloatParam, @@ -185,9 +283,11 @@ impl Default for PolysynthParams { Self { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), + vca_env: Arc::default(), + vcf_env: Arc::default(), output_level: FloatParam::new( "Output Level", - 0.25, + 0.5, FloatRange::Skewed { min: 0.0, max: 1.0, @@ -196,6 +296,7 @@ impl Default for PolysynthParams { ) .with_string_to_value(formatters::s2v_f32_gain_to_db()) .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_unit(" dB") .with_smoother(SmoothingStyle::OversamplingAware( oversample.clone(), &SmoothingStyle::Exponential(50.), From 2cdadaf8679df34cb16f1d1b55d0c7341d3b2105 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 17 Sep 2024 08:48:06 +0200 Subject: [PATCH 14/67] fix(test): add snapshot for sine interpolation --- ...terpolation_MappedLinear_fn(f64) -_ f64_.snap | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap diff --git a/crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap b/crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap new file mode 100644 index 0000000..ad17605 --- /dev/null +++ b/crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap @@ -0,0 +1,16 @@ +--- +source: crates/valib-core/src/math/interpolation.rs +expression: "&actual as &[_]" +--- +0.0 +0.146447 +0.5 +0.853553 +1.0 +1.0 +1.0 +1.0 +1.0 +1.0 +1.0 +1.0 From 15b9a5cec081bd5d3de9fc4a703565dd565c0196 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 12:29:11 +0200 Subject: [PATCH 15/67] feat(examples): polyphonic: tweak ADSR decay curves --- examples/polysynth/src/dsp.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index cc9bda6..43c42ed 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -68,9 +68,9 @@ impl Default for Adsr { decay: 0., sustain: 0., release: 0., - attack_base: 1. + Self::TARGET_RATIO_A, - decay_base: -Self::TARGET_RATIO_DR, - release_base: -Self::TARGET_RATIO_DR, + attack_base: 1. + Self::TARGET_RATIO_ATTACK, + decay_base: -Self::TARGET_RATIO_RELEASE, + release_base: -Self::TARGET_RATIO_RELEASE, attack_coeff: 0., decay_coeff: 0., release_coeff: 0., @@ -84,8 +84,9 @@ impl Default for Adsr { } impl Adsr { - const TARGET_RATIO_A: f32 = 0.3; - const TARGET_RATIO_DR: f32 = 1e-4; + const TARGET_RATIO_ATTACK: f32 = 0.3; + const TARGET_RATIO_DECAY: f32 = 0.1; + const TARGET_RATIO_RELEASE: f32 = 1e-3; pub fn new( samplerate: f32, attack: f32, @@ -120,8 +121,8 @@ impl Adsr { } self.attack = attack; self.attack_rate = self.samplerate * attack; - self.attack_coeff = Self::calc_coeff(self.attack_rate, Self::TARGET_RATIO_A); - self.attack_base = (1. + Self::TARGET_RATIO_A) * (1.0 - self.attack_coeff); + self.attack_coeff = Self::calc_coeff(self.attack_rate, Self::TARGET_RATIO_ATTACK); + self.attack_base = (1. + Self::TARGET_RATIO_ATTACK) * (1.0 - self.attack_coeff); } pub fn set_decay(&mut self, decay: f32) { @@ -130,8 +131,8 @@ impl Adsr { } self.decay = decay; self.decay_rate = self.samplerate * decay; - self.decay_coeff = Self::calc_coeff(self.decay_rate, Self::TARGET_RATIO_DR); - self.decay_base = (self.sustain - Self::TARGET_RATIO_DR) * (1. - self.decay_coeff); + self.decay_coeff = Self::calc_coeff(self.decay_rate, Self::TARGET_RATIO_DECAY); + self.decay_base = (self.sustain - Self::TARGET_RATIO_DECAY) * (1. - self.decay_coeff); } pub fn set_sustain(&mut self, sustain: f32) { @@ -144,8 +145,8 @@ impl Adsr { } self.release = release; self.release_rate = self.samplerate * release; - self.release_coeff = Self::calc_coeff(self.release_rate, Self::TARGET_RATIO_DR); - self.release_base = -Self::TARGET_RATIO_DR * (1. - self.release_coeff); + self.release_coeff = Self::calc_coeff(self.release_rate, Self::TARGET_RATIO_RELEASE); + self.release_base = -Self::TARGET_RATIO_RELEASE * (1. - self.release_coeff); } pub fn gate(&mut self, gate: bool) { From 7a0a69d0ba9994644b7623e4ce6f3b6df2ad9e06 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 12:58:42 +0200 Subject: [PATCH 16/67] feat(examples): polyphonic: integrate DC blocker after the voices --- crates/valib-filters/src/specialized.rs | 1 + examples/polysynth/src/dsp.rs | 48 ++++++++++++++++++++++--- examples/polysynth/src/lib.rs | 37 +++++++++++-------- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/crates/valib-filters/src/specialized.rs b/crates/valib-filters/src/specialized.rs index 2930083..4d54004 100644 --- a/crates/valib-filters/src/specialized.rs +++ b/crates/valib-filters/src/specialized.rs @@ -7,6 +7,7 @@ use valib_core::Scalar; use valib_saturators::Linear; /// Specialized filter that removes DC offsets by applying a 5 Hz biquad highpass filter +#[derive(Debug, Copy, Clone)] pub struct DcBlocker(Biquad); impl DcBlocker { diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 43c42ed..cbb381c 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,5 +1,5 @@ use crate::params::{FilterParams, OscShape, PolysynthParams}; -use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; use nih_plug::nih_log; @@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; use valib::filters::ladder::{Ladder, OTA}; +use valib::filters::specialized::DcBlocker; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; @@ -581,11 +582,50 @@ pub fn create_voice_manager( }) } -pub type Dsp = VoiceManager; +pub type Voices = VoiceManager; -pub fn create( +pub fn create_voices( samplerate: f32, params: Arc, -) -> Dsp { +) -> Voices { create_voice_manager(samplerate, params) } + +#[derive(Debug, Copy, Clone)] +pub struct Effects { + dc_blocker: DcBlocker, +} + +impl DSPMeta for Effects { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.dc_blocker.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.dc_blocker.latency() + } + + fn reset(&mut self) { + self.dc_blocker.reset(); + } +} + +impl DSPProcess<1, 1> for Effects { + fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { + self.dc_blocker.process(x) + } +} + +impl Effects { + pub fn new(samplerate: f32) -> Self { + Self { + dc_blocker: DcBlocker::new(samplerate), + } + } +} + +pub fn create_effects(samplerate: f32) -> Effects { + Effects::new(samplerate) +} diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index c051303..cff0b35 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -8,7 +8,7 @@ use nih_plug::prelude::*; use std::cmp::Ordering; use std::sync::{atomic, Arc}; use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; -use valib::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock}; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, DSPProcessBlock}; use valib::util::Rms; use valib::voice::{NoteData, VoiceId, VoiceManager}; @@ -73,7 +73,7 @@ impl VoiceKey { #[derive(Debug)] struct VoiceIdMap { - data: [Option<(VoiceKey, VoiceId>)>; NUM_VOICES], + data: [Option<(VoiceKey, VoiceId>)>; NUM_VOICES], } impl Default for VoiceIdMap { @@ -85,7 +85,7 @@ impl Default for VoiceIdMap { } impl VoiceIdMap { - fn add_voice(&mut self, key: VoiceKey, v: VoiceId>) -> bool { + fn add_voice(&mut self, key: VoiceKey, v: VoiceId>) -> bool { let Some(position) = self.data.iter().position(|x| x.is_none()) else { return false; }; @@ -93,21 +93,21 @@ impl VoiceIdMap { true } - fn get_voice(&self, key: VoiceKey) -> Option>> { + fn get_voice(&self, key: VoiceKey) -> Option>> { self.data.iter().find_map(|x| { x.as_ref() .and_then(|(vkey, id)| (*vkey == key).then_some(*id)) }) } - fn get_voice_by_poly_id(&self, voice_id: i32) -> Option>> { + fn get_voice_by_poly_id(&self, voice_id: i32) -> Option>> { self.data .iter() .flatten() .find_map(|(vkey, id)| (vkey.voice_id == Some(voice_id)).then_some(*id)) } - fn remove_voice(&mut self, key: VoiceKey) -> Option<(VoiceKey, VoiceId>)> { + fn remove_voice(&mut self, key: VoiceKey) -> Option<(VoiceKey, VoiceId>)> { let position = self .data .iter() @@ -120,7 +120,8 @@ type SynthSample = f32; #[derive(Debug)] pub struct PolysynthPlugin { - dsp: BlockAdapter>, + voices: BlockAdapter>, + effects: dsp::Effects, params: Arc, voice_id_map: VoiceIdMap, } @@ -130,7 +131,8 @@ impl Default for PolysynthPlugin { const DEFAULT_SAMPLERATE: f32 = 44100.; let params = Arc::new(PolysynthParams::default()); Self { - dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, params.clone())), + voices: BlockAdapter(dsp::create_voices(DEFAULT_SAMPLERATE, params.clone())), + effects: dsp::create_effects(DEFAULT_SAMPLERATE), params, voice_id_map: VoiceIdMap::default(), } @@ -168,12 +170,12 @@ impl Plugin for PolysynthPlugin { _: &mut impl InitContext, ) -> bool { let sample_rate = buffer_config.sample_rate; - self.dsp.set_samplerate(sample_rate); + self.voices.set_samplerate(sample_rate); true } fn reset(&mut self) { - self.dsp.reset(); + self.voices.reset(); } fn process( @@ -202,7 +204,7 @@ impl Plugin for PolysynthPlugin { } => { let key = VoiceKey::new(voice_id, channel, note); let note_data = NoteData::from_midi(note, velocity); - let id = self.dsp.note_on(note_data); + let id = self.voices.note_on(note_data); nih_log!("Note on {id} <- {key:?}"); self.voice_id_map.add_voice(key, id); } @@ -216,7 +218,7 @@ impl Plugin for PolysynthPlugin { let key = VoiceKey::new(voice_id, channel, note); if let Some((_, id)) = self.voice_id_map.remove_voice(key) { nih_log!("Note off {id} <- {key:?}"); - self.dsp.note_off(id, velocity); + self.voices.note_off(id, velocity); } else { nih_log!("Note off {key:?}: ID not found"); } @@ -229,7 +231,7 @@ impl Plugin for PolysynthPlugin { } => { let key = VoiceKey::new(voice_id, channel, note); if let Some((_, id)) = self.voice_id_map.remove_voice(key) { - self.dsp.choke(id); + self.voices.choke(id); } } NoteEvent::PolyModulation { voice_id, .. } => { @@ -301,13 +303,18 @@ impl Plugin for PolysynthPlugin { } let dsp_block = AudioBufferMut::from(&mut output[0][block_start..block_end]); let input = AudioBufferRef::::empty(dsp_block.samples()); - self.dsp.process_block(input, dsp_block); + self.voices.process_block(input, dsp_block); block_start = block_end; block_end = (block_start + MAX_BUFFER_SIZE).min(num_samples); } - self.dsp.0.clean_inactive_voices(); + self.voices.0.clean_inactive_voices(); + + // Effects processing + for s in &mut output[0][..] { + *s = self.effects.process([*s])[0]; + } ProcessStatus::Normal } } From a42afeed081a652270631b38b11ad1c5258addb9 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 16:52:43 +0200 Subject: [PATCH 17/67] fix(core): BlockAdapter doesn't call set_samplerate on inner processor --- crates/valib-core/src/dsp/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/valib-core/src/dsp/mod.rs b/crates/valib-core/src/dsp/mod.rs index 477cf9e..72b012f 100644 --- a/crates/valib-core/src/dsp/mod.rs +++ b/crates/valib-core/src/dsp/mod.rs @@ -87,6 +87,18 @@ impl HasParameters for BlockAdapter

{ impl DSPMeta for BlockAdapter

{ type Sample = P::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + self.0.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.0.latency() + } + + fn reset(&mut self) { + self.0.reset(); + } } impl, const I: usize, const O: usize> DSPProcess for BlockAdapter

{ From 076fc7dcc06e2b1eb193b94f92cd96f207019df8 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 16:53:01 +0200 Subject: [PATCH 18/67] fix(oscillators): Phasor does not update its step after a samplerate change --- crates/valib-oscillators/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 20b8847..91f1305 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -26,6 +26,7 @@ impl DSPMeta for Phasor { fn set_samplerate(&mut self, samplerate: f32) { self.samplerate = T::from_f64(samplerate as _); + self.set_frequency(self.frequency); } fn reset(&mut self) { From 45d76bbf61e8dd1bb5c2a445d4d5116259dff1d6 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 16:53:55 +0200 Subject: [PATCH 19/67] feat(examples): polysynth: switch to transistor ladder topology --- examples/polysynth/src/dsp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index cbb381c..a720baf 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -8,7 +8,7 @@ use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; -use valib::filters::ladder::{Ladder, OTA}; +use valib::filters::ladder::{Ladder, Transistor}; use valib::filters::specialized::DcBlocker; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; @@ -357,7 +357,7 @@ pub(crate) const NUM_OSCILLATORS: usize = 2; pub struct RawVoice { osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, - filter: Ladder>, + filter: Ladder>, params: Arc, vca_env: Adsr, vcf_env: Adsr, From 57a741a2b969f561c5179ae5deaace88776eaf50 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 21:04:59 +0200 Subject: [PATCH 20/67] fix(examples): polysynth: fix formatting of s/ms --- examples/polysynth/src/params.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index d0a174e..f611d27 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -23,9 +23,9 @@ pub struct AdsrParams { fn v2s_f32_ms_then_s(digits: usize) -> Arc String> { Arc::new(move |v| { if v < 0.9 { - format!("{:1$} ms", v * 1e3, digits) + format!("{:.1$} ms", v * 1e3, digits) } else { - format!("{v:0$} s", digits) + format!("{v:.0$} s", digits) } }) } From 255680f404a932b9eeb893a1f827efb02e04b88a Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 21:05:21 +0200 Subject: [PATCH 21/67] fix(examples): polysynth: fix samplerate not being updated in effects --- examples/polysynth/src/dsp.rs | 4 ++-- examples/polysynth/src/lib.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index a720baf..0ba5f2a 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -572,8 +572,8 @@ pub fn create_voice_manager( samplerate: f32, params: Arc, ) -> VoiceManager { - let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; - Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { + Polyphonic::new(samplerate, NUM_VOICES, move |samplerate, note_data| { + let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; SampleAdapter::new(UpsampledVoice::new( OVERSAMPLE, MAX_BUFFER_SIZE, diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index cff0b35..2ef4518 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -171,6 +171,7 @@ impl Plugin for PolysynthPlugin { ) -> bool { let sample_rate = buffer_config.sample_rate; self.voices.set_samplerate(sample_rate); + self.effects.set_samplerate(sample_rate); true } From b4b9a89aa5ce5c5378f60c306c833187e0af43f5 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 22:28:10 +0200 Subject: [PATCH 22/67] feat(examples): polysynth: add noise + ring mod mixer output --- examples/polysynth/src/dsp.rs | 56 +++++++++++++--- examples/polysynth/src/editor.rs | 6 +- examples/polysynth/src/lib.rs | 33 +++++++--- examples/polysynth/src/params.rs | 107 +++++++++++++++++++++++++------ 4 files changed, 165 insertions(+), 37 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 0ba5f2a..090087b 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -14,7 +14,7 @@ use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; use valib::saturators::{bjt, Tanh}; -use valib::simd::SimdBool; +use valib::simd::{SimdBool, SimdValue}; use valib::util::semitone_to_ratio; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; @@ -352,11 +352,37 @@ impl DSPProcess<1, 1> for PolyOsc { } } +#[derive(Debug, Clone)] +struct Noise { + rng: Rng, +} + +impl Noise { + pub fn from_rng(rng: Rng) -> Self { + Self { rng } + } + + pub fn next_value_f32>(&mut self) -> T + where + [(); ::LANES]:, + { + T::from_values(std::array::from_fn(|_| self.rng.f32_range(-1.0..1.0))) + } + + pub fn next_value_f64>(&mut self) -> T + where + [(); ::LANES]:, + { + T::from_values(std::array::from_fn(|_| self.rng.f64_range(-1.0..1.0))) + } +} + pub(crate) const NUM_OSCILLATORS: usize = 2; pub struct RawVoice { osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, + noise: Noise, filter: Ladder>, params: Arc, vca_env: Adsr, @@ -397,6 +423,7 @@ impl RawVoice { T::from_f64(params.filter_params.cutoff.value() as _), T::from_f64(params.filter_params.resonance.value() as _), ), + noise: Noise::from_rng(rng.fork()), osc_out_sat: bjt::CommonCollector { vee: -T::ONE, vcc: T::ONE, @@ -512,7 +539,10 @@ impl DSPMeta for RawVoice { } } -impl DSPProcess<0, 1> for RawVoice { +impl> DSPProcess<0, 1> for RawVoice +where + [(); ::LANES]:, +{ fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { const DRIFT_MAX_ST: f32 = 0.1; self.update_osc_types(); @@ -535,10 +565,14 @@ impl DSPProcess<0, 1> for RawVoice { let [osc] = osc.process([osc_freq]); osc }); + let noise = self.noise.next_value_f32::(); // Process filter input - let osc_mixer = osc1 * T::from_f64(osc_params[0].amplitude.smoothed.next() as _) - + osc2 * T::from_f64(osc_params[1].amplitude.smoothed.next() as _); + let mixer_params = &self.params.mixer_params; + let osc_mixer = osc1 * T::from_f64(mixer_params.osc1_amplitude.smoothed.next() as _) + + osc2 * T::from_f64(mixer_params.osc2_amplitude.smoothed.next() as _) + + noise * T::from_f64(mixer_params.noise_amplitude.smoothed.next() as _) + + osc1 * osc2 * T::from_f64(mixer_params.rm_amplitude.smoothed.next() as _); let filter_in = self .osc_out_sat .process([osc_mixer]) @@ -568,10 +602,13 @@ type SynthVoice = SampleAdapter>>, 0, pub type VoiceManager = Polyphonic>; -pub fn create_voice_manager( +pub fn create_voice_manager>( samplerate: f32, params: Arc, -) -> VoiceManager { +) -> VoiceManager +where + [(); ::LANES]:, +{ Polyphonic::new(samplerate, NUM_VOICES, move |samplerate, note_data| { let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; SampleAdapter::new(UpsampledVoice::new( @@ -584,10 +621,13 @@ pub fn create_voice_manager( pub type Voices = VoiceManager; -pub fn create_voices( +pub fn create_voices>( samplerate: f32, params: Arc, -) -> Voices { +) -> Voices +where + [(); ::LANES]:, +{ create_voice_manager(samplerate, params) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index f1c37b3..29a93c9 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -70,6 +70,10 @@ pub(crate) fn create( }) .row_between(Stretch(1.0)); HStack::new(cx, |cx| { + VStack::new(cx, |cx| { + Label::new(cx, "Mixer").font_size(22.); + GenericUi::new(cx, Data::params.map(|p| p.mixer_params.clone())); + }); VStack::new(cx, |cx| { Label::new(cx, "Amp Env").font_size(22.); GenericUi::new(cx, Data::params.map(|p| p.vca_env.clone())); @@ -81,7 +85,7 @@ pub(crate) fn create( }) .left(Stretch(1.0)) .right(Stretch(1.0)) - .width(Percentage(50.)); + .width(Pixels(750.)); }) .top(Pixels(16.)) .width(Percentage(100.)) diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 2ef4518..8b2a4cb 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -257,6 +257,30 @@ impl Plugin for PolysynthPlugin { .smoothed .set_target(sample_rate, target_plain_value); } + id if id == POLYMOD_OSC_AMP[0] => { + let target_plain_value = self + .params + .mixer_params + .osc1_amplitude + .preview_plain(normalized_value); + self.params + .mixer_params + .osc1_amplitude + .smoothed + .set_target(sample_rate, target_plain_value); + } + id if id == POLYMOD_OSC_AMP[1] => { + let target_plain_value = self + .params + .mixer_params + .osc2_amplitude + .preview_plain(normalized_value); + self.params + .mixer_params + .osc2_amplitude + .smoothed + .set_target(sample_rate, target_plain_value); + } _ => { for i in 0..2 { match poly_modulation_id { @@ -278,15 +302,6 @@ impl Plugin for PolysynthPlugin { .smoothed .set_target(sample_rate, target_plain_value); } - id if id == POLYMOD_OSC_AMP[i] => { - let target_plain_value = self.params.osc_params[i] - .amplitude - .preview_plain(normalized_value); - self.params.osc_params[i] - .amplitude - .smoothed - .set_target(sample_rate, target_plain_value); - } _ => {} } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index f611d27..cbceca0 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -97,8 +97,6 @@ pub enum OscShape { pub struct OscParams { #[id = "shp"] pub shape: EnumParam, - #[id = "amp"] - pub amplitude: FloatParam, #[id = "pco"] pub pitch_coarse: FloatParam, #[id = "pfi"] @@ -115,23 +113,6 @@ impl OscParams { fn new(osc_index: usize, oversample: Arc) -> Self { Self { shape: EnumParam::new("Shape", OscShape::Saw), - amplitude: FloatParam::new( - "Amplitude", - 0.25, - FloatRange::Skewed { - min: db_to_gain(MINUS_INFINITY_DB), - max: 1.0, - factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), - }, - ) - .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) - .with_string_to_value(formatters::s2v_f32_gain_to_db()) - .with_unit(" dB") - .with_smoother(SmoothingStyle::OversamplingAware( - oversample.clone(), - &SmoothingStyle::Exponential(10.), - )) - .with_poly_modulation_id(POLYMOD_OSC_AMP[osc_index]), pitch_coarse: FloatParam::new( "Pitch (Coarse)", 0.0, @@ -260,10 +241,97 @@ impl FilterParams { } } +#[derive(Debug, Params)] +pub struct MixerParams { + #[id = "osc1_amp"] + pub osc1_amplitude: FloatParam, + #[id = "osc2_amp"] + pub osc2_amplitude: FloatParam, + #[id = "rm_amp"] + pub rm_amplitude: FloatParam, + #[id = "noise_amp"] + pub noise_amplitude: FloatParam, +} + +impl MixerParams { + fn new(oversample: Arc) -> Self { + Self { + osc1_amplitude: FloatParam::new( + "OSC1 Amplitude", + 0.25, + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_AMP[0]), + osc2_amplitude: FloatParam::new( + "OSC2 Amplitude", + 0.25, + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_AMP[1]), + rm_amplitude: FloatParam::new( + "RM Amplitude", + 0., + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )), + noise_amplitude: FloatParam::new( + "Noise Amplitude", + 0., + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )), + } + } +} + #[derive(Debug, Params)] pub struct PolysynthParams { #[nested(array, group = "Osc")] pub osc_params: [Arc; crate::dsp::NUM_OSCILLATORS], + #[nested] + pub mixer_params: Arc, #[nested(id_prefix = "vca_", group = "Amp Env")] pub vca_env: Arc, #[nested(id_prefix = "vcf_", group = "Filter Env")] @@ -283,6 +351,7 @@ impl Default for PolysynthParams { Self { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), + mixer_params: Arc::new(MixerParams::new(oversample.clone())), vca_env: Arc::default(), vcf_env: Arc::default(), output_level: FloatParam::new( From 9932fa654fd1ce58a72553c050457a643c129f27 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 23:18:37 +0200 Subject: [PATCH 23/67] fix(filters): make ladder filter take a samplerate of type T --- crates/valib-filters/src/ladder.rs | 3 +-- crates/valib-filters/src/svf.rs | 2 +- examples/ladder/src/dsp.rs | 14 ++++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/valib-filters/src/ladder.rs b/crates/valib-filters/src/ladder.rs index b2b985a..312fc26 100644 --- a/crates/valib-filters/src/ladder.rs +++ b/crates/valib-filters/src/ladder.rs @@ -153,8 +153,7 @@ impl> Ladder { /// let transistor_ladder = Ladder::<_, Transistor>>::new(48000.0, 440.0, 1.0); /// ``` #[replace_float_literals(T::from_f64(literal))] - pub fn new(samplerate: impl Into, cutoff: T, resonance: T) -> Self { - let samplerate = T::from_f64(samplerate.into()); + pub fn new(samplerate: T, cutoff: T, resonance: T) -> Self { let mut this = Self { inv_2fs: T::simd_recip(2.0 * samplerate), samplerate, diff --git a/crates/valib-filters/src/svf.rs b/crates/valib-filters/src/svf.rs index 5d7133a..a313c6c 100644 --- a/crates/valib-filters/src/svf.rs +++ b/crates/valib-filters/src/svf.rs @@ -112,7 +112,7 @@ impl Svf { pub fn new(samplerate: T, fc: T, r: T) -> Self { let mut this = Self { s: [T::zero(); 2], - r, + r: r + r, fc, g: T::zero(), g1: T::zero(), diff --git a/examples/ladder/src/dsp.rs b/examples/ladder/src/dsp.rs index 580bce5..abce13a 100644 --- a/examples/ladder/src/dsp.rs +++ b/examples/ladder/src/dsp.rs @@ -9,6 +9,7 @@ use valib::oversample::{Oversample, Oversampled}; use valib::saturators::bjt::CommonCollector; use valib::saturators::Tanh; use valib::simd::{AutoF32x2, SimdValue}; +use valib::Scalar; use crate::{MAX_BUFFER_SIZE, OVERSAMPLE}; @@ -100,7 +101,7 @@ impl fmt::Display for LadderType { } impl LadderType { - fn as_ladder(&self, samplerate: f32, fc: Sample, res: Sample) -> DspLadder { + fn as_ladder(&self, samplerate: Sample, fc: Sample, res: Sample) -> DspLadder { match self { Self::Ideal => DspLadder::Ideal(Ladder::new(samplerate, fc, res)), Self::Transistor => DspLadder::Transistor(Ladder::new(samplerate, fc, res)), @@ -126,7 +127,7 @@ pub struct DspInner { resonance: SmoothedParam, compensated: bool, ladder: DspLadder, - samplerate: f32, + samplerate: Sample, } impl DspInner { @@ -210,13 +211,14 @@ impl HasParameters for DspInner { pub type Dsp = Oversampled>; pub fn create(orig_samplerate: f32) -> RemoteControlled { - let samplerate = orig_samplerate * OVERSAMPLE as f32; + let sr_f32 = orig_samplerate * OVERSAMPLE as f32; + let samplerate = Sample::from_f64(sr_f32 as _); let dsp = DspInner { ladder_type: LadderType::Ideal, ladder_type_changed: false, - drive: SmoothedParam::exponential(1.0, samplerate, 50.0), - cutoff: SmoothedParam::exponential(300.0, samplerate, 10.0), - resonance: SmoothedParam::linear(0.5, samplerate, 10.0), + drive: SmoothedParam::exponential(1.0, sr_f32, 50.0), + cutoff: SmoothedParam::exponential(300.0, sr_f32, 10.0), + resonance: SmoothedParam::linear(0.5, sr_f32, 10.0), ladder: LadderType::Ideal.as_ladder(samplerate, Sample::splat(300.0), Sample::splat(0.5)), compensated: false, samplerate, From 1f13f1671c1414ba4b5c83ec304f080c3ca12fc7 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 23:19:11 +0200 Subject: [PATCH 24/67] feat(examples): polysynth: new filter processor with multiple options and accurate pitch tracking --- crates/valib-core/src/util.rs | 21 +++ examples/polysynth/src/dsp.rs | 223 +++++++++++++++++++++++++++---- examples/polysynth/src/params.rs | 14 ++ 3 files changed, 232 insertions(+), 26 deletions(-) diff --git a/crates/valib-core/src/util.rs b/crates/valib-core/src/util.rs index 596b5e2..5adce3f 100644 --- a/crates/valib-core/src/util.rs +++ b/crates/valib-core/src/util.rs @@ -199,6 +199,27 @@ pub fn semitone_to_ratio(semi: T) -> T { 2.0.simd_powf(semi / 12.0) } +/// Compute the semitone equivalent change in pitch that would have resulted by multiplying the +/// input ratio to a frequency value. +/// +/// # Arguments +/// +/// * `ratio`: Frequency ratio (unitless) +/// +/// returns: T +/// +/// # Examples +/// +/// ``` +/// use valib_core::util::ratio_to_semitone; +/// assert_eq!(0., ratio_to_semitone(1.)); +/// assert_eq!(12., ratio_to_semitone(2.)); +/// assert_eq!(-12., ratio_to_semitone(0.5)); +/// ``` +pub fn ratio_to_semitone(ratio: T) -> T { + T::from_f64(12.) * ratio.simd_log2() +} + #[cfg(feature = "test-utils")] pub mod tests; diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 090087b..9ee4ab7 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,4 +1,4 @@ -use crate::params::{FilterParams, OscShape, PolysynthParams}; +use crate::params::{FilterParams, FilterType, OscShape, PolysynthParams}; use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; @@ -8,14 +8,16 @@ use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; -use valib::filters::ladder::{Ladder, Transistor}; +use valib::filters::biquad::Biquad; +use valib::filters::ladder::{Ladder, Transistor, OTA}; use valib::filters::specialized::DcBlocker; +use valib::filters::svf::Svf; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; -use valib::saturators::{bjt, Tanh}; +use valib::saturators::{bjt, Asinh, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; -use valib::util::semitone_to_ratio; +use valib::util::{ratio_to_semitone, semitone_to_ratio}; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; @@ -377,13 +379,193 @@ impl Noise { } } +#[derive(Debug, Default, Copy, Clone)] +struct Sinh; + +impl Saturator for Sinh { + fn saturate(&self, x: T) -> T { + x.simd_sinh() + } +} + +#[derive(Debug, Copy, Clone)] +enum FilterImpl { + Transistor(Ladder>), + Ota(Ladder>), + Svf(Svf), + Biquad(Biquad), +} + +impl FilterImpl { + fn from_type(samplerate: T, ftype: FilterType, cutoff: T, resonance: T) -> FilterImpl { + match ftype { + FilterType::TransistorLadder => { + Self::Transistor(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + } + FilterType::OTALadder => { + Self::Ota(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + } + FilterType::Svf => Self::Svf(Svf::new(samplerate, cutoff, T::one() - resonance)), + FilterType::Digital => Self::Biquad( + Biquad::lowpass( + cutoff / samplerate, + (T::from_f64(3.) * resonance).simd_exp(), + ) + .with_saturators(Asinh, Asinh), + ), + } + } +} + +impl FilterImpl { + fn set_params(&mut self, samplerate: T, cutoff: T, resonance: T) { + match self { + Self::Transistor(p) => { + p.set_cutoff(cutoff); + p.set_resonance(T::from_f64(4.) * resonance); + } + Self::Ota(p) => { + p.set_cutoff(cutoff); + p.set_resonance(T::from_f64(4.) * resonance); + } + Self::Svf(p) => { + p.set_cutoff(cutoff); + p.set_r(T::one() - resonance); + } + Self::Biquad(p) => { + p.update_coefficients(&Biquad::lowpass( + cutoff / samplerate, + (T::from_f64(3.) * resonance).simd_exp(), + )); + } + } + } +} + +impl DSPMeta for FilterImpl { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + match self { + FilterImpl::Transistor(p) => p.set_samplerate(samplerate), + FilterImpl::Ota(p) => p.set_samplerate(samplerate), + FilterImpl::Svf(p) => p.set_samplerate(samplerate), + FilterImpl::Biquad(p) => p.set_samplerate(samplerate), + } + } + + fn latency(&self) -> usize { + match self { + FilterImpl::Transistor(p) => p.latency(), + FilterImpl::Ota(p) => p.latency(), + FilterImpl::Svf(p) => p.latency(), + FilterImpl::Biquad(p) => p.latency(), + } + } + + fn reset(&mut self) { + match self { + FilterImpl::Transistor(p) => p.reset(), + FilterImpl::Ota(p) => p.reset(), + FilterImpl::Svf(p) => p.reset(), + FilterImpl::Biquad(p) => p.reset(), + } + } +} + +impl DSPProcess<1, 1> for FilterImpl { + fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { + match self { + FilterImpl::Transistor(p) => p.process(x), + FilterImpl::Ota(p) => p.process(x), + FilterImpl::Svf(p) => [p.process(x)[0]], + FilterImpl::Biquad(p) => p.process(x), + } + } +} + +#[derive(Debug, Clone)] +struct Filter { + fimpl: FilterImpl, + params: Arc, + samplerate: T, +} + +impl Filter { + fn new(samplerate: T, params: Arc) -> Filter { + let cutoff = T::from_f64(params.cutoff.value() as _); + let resonance = T::from_f64(params.resonance.value() as _); + Self { + fimpl: FilterImpl::from_type(samplerate, params.filter_type.value(), cutoff, resonance), + params, + samplerate, + } + } +} + +impl Filter { + fn update_filter(&mut self, modulation_st: T) { + let cutoff = + semitone_to_ratio(modulation_st) * T::from_f64(self.params.cutoff.smoothed.next() as _); + let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); + self.fimpl = match self.params.filter_type.value() { + FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { + FilterImpl::Transistor(Ladder::new( + self.samplerate, + cutoff, + T::from_f64(4.) * resonance, + )) + } + FilterType::OTALadder if !matches!(self.fimpl, FilterImpl::Ota(..)) => FilterImpl::Ota( + Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance), + ), + FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => { + FilterImpl::Svf(Svf::new(self.samplerate, cutoff, T::one() - resonance)) + } + FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { + FilterImpl::Biquad( + Biquad::lowpass(cutoff / self.samplerate, resonance) + .with_saturators(Asinh, Asinh), + ) + } + _ => { + self.fimpl.set_params(self.samplerate, cutoff, resonance); + return; + } + }; + } +} + +impl DSPMeta for Filter { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = T::from_f64(samplerate as _); + } + + fn latency(&self) -> usize { + self.fimpl.latency() + } + + fn reset(&mut self) { + self.fimpl.reset(); + } +} + +impl DSPProcess<2, 1> for Filter { + fn process(&mut self, [x, mod_st]: [Self::Sample; 2]) -> [Self::Sample; 1] { + self.update_filter(mod_st); + self.fimpl.process([x]) + } +} + pub(crate) const NUM_OSCILLATORS: usize = 2; pub struct RawVoice { osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, noise: Noise, - filter: Ladder>, + filter: Filter, params: Arc, vca_env: Adsr, vcf_env: Adsr, @@ -418,11 +600,7 @@ impl RawVoice { }, ) }), - filter: Ladder::new( - target_samplerate_f64, - T::from_f64(params.filter_params.cutoff.value() as _), - T::from_f64(params.filter_params.resonance.value() as _), - ), + filter: Filter::new(target_samplerate, params.filter_params.clone()), noise: Noise::from_rng(rng.fork()), osc_out_sat: bjt::CommonCollector { vee: -T::ONE, @@ -550,7 +728,6 @@ where // Process oscillators let frequency = self.note_data.frequency; - let osc_params = self.params.osc_params.clone(); let filter_params = self.params.filter_params.clone(); let [osc1, osc2] = std::array::from_fn(|i| { let osc = &mut self.osc[i]; @@ -573,28 +750,22 @@ where + osc2 * T::from_f64(mixer_params.osc2_amplitude.smoothed.next() as _) + noise * T::from_f64(mixer_params.noise_amplitude.smoothed.next() as _) + osc1 * osc2 * T::from_f64(mixer_params.rm_amplitude.smoothed.next() as _); - let filter_in = self + let [filter_in] = self .osc_out_sat .process([osc_mixer]) .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); - let freq_ratio = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) - * frequency - / T::from_f64(440.) - + T::from_f64(semitone_to_ratio( - filter_params.env_amt.smoothed.next() * self.vcf_env.next_sample(), - ) as _); - let filter_freq = - (T::one() + freq_ratio) * T::from_f64(filter_params.cutoff.smoothed.next() as _); - // Process filter - self.filter.set_cutoff(filter_freq); - self.filter.set_resonance(T::from_f64( - 4f64 * filter_params.resonance.smoothed.next() as f64, - )); + let freq_mod = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) + * ratio_to_semitone(frequency / T::from_f64(440.)) + + T::from_f64( + (filter_params.env_amt.smoothed.next() * self.vcf_env.next_sample()) as f64, + ); let vca = T::from_f64(self.vca_env.next_sample() as _); let static_amp = T::from_f64(self.params.output_level.smoothed.next() as _); - self.filter.process(filter_in).map(|x| static_amp * vca * x) + self.filter + .process([filter_in, freq_mod]) + .map(|x| static_amp * vca * x) } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index cbceca0..a76bf52 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -165,6 +165,17 @@ impl OscParams { } } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Enum)] +pub enum FilterType { + #[name = "Transistor Ladder"] + TransistorLadder, + #[name = "OTA Ladder"] + OTALadder, + #[name = "SVF"] + Svf, + Digital, +} + #[derive(Debug, Params)] pub struct FilterParams { #[id = "fc"] @@ -175,6 +186,8 @@ pub struct FilterParams { pub keyboard_tracking: FloatParam, #[id = "env"] pub env_amt: FloatParam, + #[id = "fty"] + pub filter_type: EnumParam, } impl FilterParams { @@ -237,6 +250,7 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Exponential(50.), )), + filter_type: EnumParam::new("Filter Type", FilterType::TransistorLadder), } } } From 689a7e82fc87fce3271151acdc17fb4d49d7af4f Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 16:05:59 +0200 Subject: [PATCH 25/67] feat(examples): polysynth: make ladder filters compensated + digital filter more digital --- examples/polysynth/src/dsp.rs | 46 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 9ee4ab7..919a745 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -15,7 +15,7 @@ use valib::filters::svf::Svf; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; -use valib::saturators::{bjt, Asinh, Saturator, Tanh}; +use valib::saturators::{bjt, Asinh, Clipper, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; use valib::util::{ratio_to_semitone, semitone_to_ratio}; use valib::voice::polyphonic::Polyphonic; @@ -393,17 +393,21 @@ enum FilterImpl { Transistor(Ladder>), Ota(Ladder>), Svf(Svf), - Biquad(Biquad), + Biquad(Biquad>), } impl FilterImpl { fn from_type(samplerate: T, ftype: FilterType, cutoff: T, resonance: T) -> FilterImpl { match ftype { FilterType::TransistorLadder => { - Self::Transistor(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + Self::Transistor(ladder) } FilterType::OTALadder => { - Self::Ota(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + Self::Ota(ladder) } FilterType::Svf => Self::Svf(Svf::new(samplerate, cutoff, T::one() - resonance)), FilterType::Digital => Self::Biquad( @@ -411,7 +415,7 @@ impl FilterImpl { cutoff / samplerate, (T::from_f64(3.) * resonance).simd_exp(), ) - .with_saturators(Asinh, Asinh), + .with_saturators(Default::default(), Default::default()), ), } } @@ -510,22 +514,22 @@ impl Filter { let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); self.fimpl = match self.params.filter_type.value() { FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { - FilterImpl::Transistor(Ladder::new( - self.samplerate, - cutoff, - T::from_f64(4.) * resonance, - )) + let mut ladder = Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + FilterImpl::Transistor(ladder) + } + FilterType::OTALadder if !matches!(self.fimpl, FilterImpl::Ota(..)) => { + let mut ladder = Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + FilterImpl::Ota(ladder) } - FilterType::OTALadder if !matches!(self.fimpl, FilterImpl::Ota(..)) => FilterImpl::Ota( - Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance), - ), FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => { FilterImpl::Svf(Svf::new(self.samplerate, cutoff, T::one() - resonance)) } FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { FilterImpl::Biquad( Biquad::lowpass(cutoff / self.samplerate, resonance) - .with_saturators(Asinh, Asinh), + .with_saturators(Default::default(), Default::default()), ) } _ => { @@ -750,10 +754,7 @@ where + osc2 * T::from_f64(mixer_params.osc2_amplitude.smoothed.next() as _) + noise * T::from_f64(mixer_params.noise_amplitude.smoothed.next() as _) + osc1 * osc2 * T::from_f64(mixer_params.rm_amplitude.smoothed.next() as _); - let [filter_in] = self - .osc_out_sat - .process([osc_mixer]) - .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); + let [filter_in] = self.osc_out_sat.process([osc_mixer]); // Process filter let freq_mod = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) @@ -805,6 +806,7 @@ where #[derive(Debug, Copy, Clone)] pub struct Effects { dc_blocker: DcBlocker, + bjt: bjt::CommonCollector, } impl DSPMeta for Effects { @@ -825,7 +827,7 @@ impl DSPMeta for Effects { impl DSPProcess<1, 1> for Effects { fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { - self.dc_blocker.process(x) + self.bjt.process(self.dc_blocker.process(x)) } } @@ -833,6 +835,12 @@ impl Effects { pub fn new(samplerate: f32) -> Self { Self { dc_blocker: DcBlocker::new(samplerate), + bjt: bjt::CommonCollector { + vee: T::from_f64(-2.5), + vcc: T::from_f64(2.5), + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, } } } From 38437779ab9d60dda8f4e20b146f90d0a9017717 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 21:08:34 +0200 Subject: [PATCH 26/67] chore(examples): polysynth: tweak filter internal drive levels --- examples/polysynth/src/dsp.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 919a745..8011189 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -3,7 +3,7 @@ use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; use nih_plug::nih_log; -use nih_plug::util::db_to_gain_fast; +use nih_plug::util::{db_to_gain, db_to_gain_fast}; use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -419,9 +419,7 @@ impl FilterImpl { ), } } -} -impl FilterImpl { fn set_params(&mut self, samplerate: T, cutoff: T, resonance: T) { match self { Self::Transistor(p) => { @@ -444,6 +442,15 @@ impl FilterImpl { } } } + + fn filter_drive(&self) -> T { + match self { + Self::Transistor(..) => T::from_f64(0.5), + Self::Ota(..) => T::from_f64(db_to_gain(9.) as _), + Self::Svf(..) => T::from_f64(db_to_gain(9.) as _), + Self::Biquad(..) => T::from_f64(db_to_gain(6.) as _), + } + } } impl DSPMeta for FilterImpl { @@ -479,12 +486,16 @@ impl DSPMeta for FilterImpl { impl DSPProcess<1, 1> for FilterImpl { fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { - match self { + let drive_in = self.filter_drive(); + let drive_out = drive_in.simd_asinh().simd_recip(); + let x = x.map(|x| drive_in * x); + let y = match self { FilterImpl::Transistor(p) => p.process(x), FilterImpl::Ota(p) => p.process(x), FilterImpl::Svf(p) => [p.process(x)[0]], FilterImpl::Biquad(p) => p.process(x), - } + }; + y.map(|x| drive_out * x) } } @@ -726,7 +737,7 @@ where [(); ::LANES]:, { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { - const DRIFT_MAX_ST: f32 = 0.1; + const DRIFT_MAX_ST: f32 = 0.15; self.update_osc_types(); self.update_envelopes(); From 0cb68015ef8d843472ec904295d6b4cc7614c395 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 21:46:08 +0200 Subject: [PATCH 27/67] fix(examples): polysynth: NaNs with biquad and svf filters --- examples/polysynth/src/dsp.rs | 57 ++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 8011189..0e2a6ca 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -388,16 +388,26 @@ impl Saturator for Sinh { } } +fn svf_clipper() -> bjt::CommonCollector { + bjt::CommonCollector { + vee: T::from_f64(-1.), + vcc: T::from_f64(1.), + xbias: T::from_f64(-0.1), + ybias: T::from_f64(0.1), + } +} + #[derive(Debug, Copy, Clone)] enum FilterImpl { Transistor(Ladder>), Ota(Ladder>), - Svf(Svf), + Svf(bjt::CommonCollector, Svf), Biquad(Biquad>), } impl FilterImpl { fn from_type(samplerate: T, ftype: FilterType, cutoff: T, resonance: T) -> FilterImpl { + let cutoff = cutoff.simd_clamp(T::zero(), samplerate / T::from_f64(12.)); match ftype { FilterType::TransistorLadder => { let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); @@ -409,13 +419,13 @@ impl FilterImpl { ladder.compensated = true; Self::Ota(ladder) } - FilterType::Svf => Self::Svf(Svf::new(samplerate, cutoff, T::one() - resonance)), + FilterType::Svf => Self::Svf( + svf_clipper(), + Svf::new(samplerate, cutoff, T::one() - resonance), + ), FilterType::Digital => Self::Biquad( - Biquad::lowpass( - cutoff / samplerate, - (T::from_f64(3.) * resonance).simd_exp(), - ) - .with_saturators(Default::default(), Default::default()), + Biquad::lowpass(cutoff / samplerate, T::one()) + .with_saturators(Default::default(), Default::default()), ), } } @@ -430,14 +440,14 @@ impl FilterImpl { p.set_cutoff(cutoff); p.set_resonance(T::from_f64(4.) * resonance); } - Self::Svf(p) => { + Self::Svf(_, p) => { p.set_cutoff(cutoff); - p.set_r(T::one() - resonance); + p.set_r(T::one() - resonance.simd_sqrt()); } Self::Biquad(p) => { p.update_coefficients(&Biquad::lowpass( cutoff / samplerate, - (T::from_f64(3.) * resonance).simd_exp(), + T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(), )); } } @@ -448,7 +458,7 @@ impl FilterImpl { Self::Transistor(..) => T::from_f64(0.5), Self::Ota(..) => T::from_f64(db_to_gain(9.) as _), Self::Svf(..) => T::from_f64(db_to_gain(9.) as _), - Self::Biquad(..) => T::from_f64(db_to_gain(6.) as _), + Self::Biquad(..) => T::from_f64(db_to_gain(12.) as _), } } } @@ -460,7 +470,7 @@ impl DSPMeta for FilterImpl { match self { FilterImpl::Transistor(p) => p.set_samplerate(samplerate), FilterImpl::Ota(p) => p.set_samplerate(samplerate), - FilterImpl::Svf(p) => p.set_samplerate(samplerate), + FilterImpl::Svf(_, p) => p.set_samplerate(samplerate), FilterImpl::Biquad(p) => p.set_samplerate(samplerate), } } @@ -469,7 +479,7 @@ impl DSPMeta for FilterImpl { match self { FilterImpl::Transistor(p) => p.latency(), FilterImpl::Ota(p) => p.latency(), - FilterImpl::Svf(p) => p.latency(), + FilterImpl::Svf(_, p) => p.latency(), FilterImpl::Biquad(p) => p.latency(), } } @@ -478,7 +488,7 @@ impl DSPMeta for FilterImpl { match self { FilterImpl::Transistor(p) => p.reset(), FilterImpl::Ota(p) => p.reset(), - FilterImpl::Svf(p) => p.reset(), + FilterImpl::Svf(_, p) => p.reset(), FilterImpl::Biquad(p) => p.reset(), } } @@ -492,7 +502,7 @@ impl DSPProcess<1, 1> for FilterImpl { let y = match self { FilterImpl::Transistor(p) => p.process(x), FilterImpl::Ota(p) => p.process(x), - FilterImpl::Svf(p) => [p.process(x)[0]], + FilterImpl::Svf(bjt, p) => [p.process(bjt.process(x))[0]], FilterImpl::Biquad(p) => p.process(x), }; y.map(|x| drive_out * x) @@ -522,6 +532,7 @@ impl Filter { fn update_filter(&mut self, modulation_st: T) { let cutoff = semitone_to_ratio(modulation_st) * T::from_f64(self.params.cutoff.smoothed.next() as _); + let cutoff = cutoff.simd_clamp(T::zero(), self.samplerate / T::from_f64(12.)); let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); self.fimpl = match self.params.filter_type.value() { FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { @@ -534,10 +545,13 @@ impl Filter { ladder.compensated = true; FilterImpl::Ota(ladder) } - FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => { - FilterImpl::Svf(Svf::new(self.samplerate, cutoff, T::one() - resonance)) - } + FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => FilterImpl::Svf( + svf_clipper(), + Svf::new(self.samplerate, cutoff, T::one() - resonance), + ), FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { + let resonance = + T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(); FilterImpl::Biquad( Biquad::lowpass(cutoff / self.samplerate, resonance) .with_saturators(Default::default(), Default::default()), @@ -838,7 +852,8 @@ impl DSPMeta for Effects { impl DSPProcess<1, 1> for Effects { fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { - self.bjt.process(self.dc_blocker.process(x)) + let y = self.bjt.process(self.dc_blocker.process(x)); + y.map(|x| T::from_f64(0.5) * x) } } @@ -847,8 +862,8 @@ impl Effects { Self { dc_blocker: DcBlocker::new(samplerate), bjt: bjt::CommonCollector { - vee: T::from_f64(-2.5), - vcc: T::from_f64(2.5), + vee: T::from_f64(-2.), + vcc: T::from_f64(2.), xbias: T::from_f64(0.1), ybias: T::from_f64(-0.1), }, From 340347369eebe3ec2a9830e83ed80dfb5da531f5 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 22:17:35 +0200 Subject: [PATCH 28/67] feat(core): fast math module --- crates/valib-core/src/math/fast.rs | 99 ++++++++++++++++++++++++++++++ crates/valib-core/src/math/mod.rs | 2 + 2 files changed, 101 insertions(+) create mode 100644 crates/valib-core/src/math/fast.rs diff --git a/crates/valib-core/src/math/fast.rs b/crates/valib-core/src/math/fast.rs new file mode 100644 index 0000000..0f0f62f --- /dev/null +++ b/crates/valib-core/src/math/fast.rs @@ -0,0 +1,99 @@ +use crate::Scalar; +use numeric_literals::replace_float_literals; +use simba::simd::SimdBool; + +/// Rational approximation of tanh(x) which is valid in the range -3..3 +/// +/// This approximation only includes the rational approximation part, and will diverge outside the +/// bounds. In order to apply the tanh function over a bigger interval, consider clamping either the +/// input or the output. +/// +/// You should consider using [`tanh`] for a general-purpose faster tanh function, which uses +/// branching. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: Input value (low-error range: -3..3) +/// +/// returns: T +#[replace_float_literals(T::from_f64(literal))] +pub fn rational_tanh(x: T) -> T { + x * (27. + x * x) / (27. + 9. * x * x) +} + +/// Fast approximation of tanh(x). +/// +/// This approximation uses branching to clamp the output to -1..1 in order to be useful as a +/// general-purpose approximation of tanh. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: Input value +/// +/// returns: T +pub fn tanh(x: T) -> T { + rational_tanh(x).simd_clamp(-T::one(), T::one()) +} + +/// Fast approximation of exp, with maximum error in -1..1 of 0.59%, and in -3.14..3.14 of 9.8%. +/// +/// You should consider using [`exp`] for a better approximation which uses this function, but +/// allows a greater range at the cost of branching. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: Input value +/// +/// returns: T +#[replace_float_literals(T::from_f64(literal))] +pub fn fast_exp5(x: T) -> T { + (120. + x * (120. + x * (60. + x * (20. + x * (5. + x))))) * 0.0083333333 +} + +/// Fast approximation of exp, using [`fast_exp5`]. Uses branching to get a bigger range. +/// +/// Maximum error in the 0..10.58 range is 0.45%. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: +/// +/// returns: T +#[replace_float_literals(T::from_f64(literal))] +pub fn exp(x: T) -> T { + x.simd_lt(2.5).if_else2( + || T::simd_e() * fast_exp5(x - 1.), + (|| x.simd_lt(5.), || 33.115452 * fast_exp5(x - 3.5)), + || 403.42879 * fast_exp5(x - 6.), + ) +} + +/// Fast 2^x approximation, using [`exp`]. +/// +/// Maximum error in the 0..15.26 range is 0.45%. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: +/// +/// returns: T +/// +/// # Examples +/// +/// ``` +/// +/// ``` +pub fn pow2(x: T) -> T { + let log_two = T::simd_ln_2(); + exp(log_two * x) +} diff --git a/crates/valib-core/src/math/mod.rs b/crates/valib-core/src/math/mod.rs index 82af633..55cd576 100644 --- a/crates/valib-core/src/math/mod.rs +++ b/crates/valib-core/src/math/mod.rs @@ -8,6 +8,7 @@ use simba::simd::{SimdBool, SimdComplexField}; use crate::Scalar; +pub mod fast; pub mod interpolation; pub mod lut; pub mod nr; @@ -86,6 +87,7 @@ pub fn bilinear_prewarming_bounded(samplerate: T, wc: T) -> T { #[inline] pub fn smooth_min(t: T, a: T, b: T) -> T { let r = (-a / t).simd_exp2() + (-b / t).simd_exp2(); + // let r = fast::pow2(-a / t) + fast::pow2(-b / t); -t * r.simd_log2() } From 8dacae28a1da45ec935fbf91e2a73be744322554 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 22:18:00 +0200 Subject: [PATCH 29/67] perf(saturators): use fast tanh --- crates/valib-saturators/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/valib-saturators/src/lib.rs b/crates/valib-saturators/src/lib.rs index eb6ce53..6c394f5 100644 --- a/crates/valib-saturators/src/lib.rs +++ b/crates/valib-saturators/src/lib.rs @@ -14,6 +14,7 @@ use std::ops; use clippers::DiodeClipperModel; use valib_core::dsp::{DSPMeta, DSPProcess}; +use valib_core::math::fast; use valib_core::Scalar; pub mod adaa; @@ -146,13 +147,14 @@ pub struct Tanh; impl Saturator for Tanh { #[inline(always)] fn saturate(&self, x: S) -> S { - x.simd_tanh() + fast::tanh(x) } #[inline(always)] #[replace_float_literals(S::from_f64(literal))] fn sat_diff(&self, x: S) -> S { - 1. - x.simd_tanh().simd_powi(2) + let tanh = fast::tanh(x); + 1. - tanh * tanh } } From 7b0db47d0b0abbf968222cc7c9b56a5adb57bd54 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 21 Sep 2024 22:16:37 +0200 Subject: [PATCH 30/67] feat(examples): polysynth: filter fm --- examples/polysynth/src/dsp.rs | 9 +++++---- examples/polysynth/src/params.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 0e2a6ca..9b556d7 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -529,9 +529,10 @@ impl Filter { } impl Filter { - fn update_filter(&mut self, modulation_st: T) { - let cutoff = - semitone_to_ratio(modulation_st) * T::from_f64(self.params.cutoff.smoothed.next() as _); + fn update_filter(&mut self, modulation_st: T, input: T) { + let fm = semitone_to_ratio(T::from_f64(self.params.freq_mod.smoothed.next() as _) * input); + let modulation = semitone_to_ratio(modulation_st); + let cutoff = modulation * fm * T::from_f64(self.params.cutoff.smoothed.next() as _); let cutoff = cutoff.simd_clamp(T::zero(), self.samplerate / T::from_f64(12.)); let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); self.fimpl = match self.params.filter_type.value() { @@ -583,7 +584,7 @@ impl DSPMeta for Filter { impl DSPProcess<2, 1> for Filter { fn process(&mut self, [x, mod_st]: [Self::Sample; 2]) -> [Self::Sample; 1] { - self.update_filter(mod_st); + self.update_filter(mod_st, x); self.fimpl.process([x]) } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index a76bf52..8b96e3e 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -186,6 +186,8 @@ pub struct FilterParams { pub keyboard_tracking: FloatParam, #[id = "env"] pub env_amt: FloatParam, + #[id = "fm"] + pub freq_mod: FloatParam, #[id = "fty"] pub filter_type: EnumParam, } @@ -250,6 +252,20 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Exponential(50.), )), + freq_mod: FloatParam::new( + "Freq. Modulation", + 0.0, + FloatRange::Linear { + min: -24., + max: 24., + }, + ) + .with_unit(" st") + .with_value_to_string(Arc::new(|x| format!("{:.2}", x))) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), filter_type: EnumParam::new("Filter Type", FilterType::TransistorLadder), } } From a567026f8ea41b009da6e11a15c8e1d5f05cbdd8 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 22 Sep 2024 00:06:24 +0200 Subject: [PATCH 31/67] wip(voice): dynamic mono/poly voice manager --- crates/valib-voice/src/dynamic.rs | 282 +++++++++++++++++++++++++++ crates/valib-voice/src/lib.rs | 10 +- crates/valib-voice/src/monophonic.rs | 63 ++++-- crates/valib-voice/src/polyphonic.rs | 46 +++++ examples/polysynth/src/dsp.rs | 24 ++- examples/polysynth/src/lib.rs | 1 - 6 files changed, 403 insertions(+), 23 deletions(-) create mode 100644 crates/valib-voice/src/dynamic.rs diff --git a/crates/valib-voice/src/dynamic.rs b/crates/valib-voice/src/dynamic.rs new file mode 100644 index 0000000..4fe8494 --- /dev/null +++ b/crates/valib-voice/src/dynamic.rs @@ -0,0 +1,282 @@ +use crate::monophonic::Monophonic; +use crate::polyphonic::Polyphonic; +use crate::{NoteData, Voice, VoiceManager}; +use std::fmt; +use std::fmt::Formatter; +use std::ops::Range; +use std::sync::Arc; +use valib_core::dsp::{DSPMeta, DSPProcess}; + +#[derive(Debug)] +enum Impl { + Monophonic(Monophonic), + Polyphonic(Polyphonic), +} + +impl DSPMeta for Impl { + type Sample = V::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + match self { + Impl::Monophonic(mono) => mono.set_samplerate(samplerate), + Impl::Polyphonic(poly) => poly.set_samplerate(samplerate), + } + } + + fn latency(&self) -> usize { + match self { + Impl::Monophonic(mono) => mono.latency(), + Impl::Polyphonic(poly) => poly.latency(), + } + } + + fn reset(&mut self) { + match self { + Impl::Monophonic(mono) => mono.reset(), + Impl::Polyphonic(poly) => poly.reset(), + } + } +} + +pub struct DynamicVoice { + pitch_bend_st: Range, + poly_voice_capacity: usize, + create_voice: Arc) -> V>, + current_manager: Impl, + legato: bool, + samplerate: f32, +} + +impl fmt::Debug for DynamicVoice { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("DynamicVoice") + .field("pitch_bend_st", &self.pitch_bend_st) + .field("poly_voice_capacity", &self.poly_voice_capacity) + .field("create_voice", &"Arc) -> V") + .field("current_manager", &self.current_manager) + .field("legato", &self.legato) + .field("samplerate", &self.samplerate) + .finish() + } +} + +impl DynamicVoice { + pub fn new_mono( + samplerate: f32, + poly_voice_capacity: usize, + legato: bool, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V, + ) -> Self { + let create_voice = Arc::new(create_voice); + let mono = Monophonic::new( + samplerate, + { + let create_voice = create_voice.clone(); + move |sr, nd| create_voice.clone()(sr, nd) + }, + legato, + ); + let pitch_bend_st = mono.pitch_bend_min_st..mono.pitch_bend_max_st; + Self { + pitch_bend_st, + poly_voice_capacity, + create_voice, + current_manager: Impl::Monophonic(mono), + legato, + samplerate, + } + } + + pub fn new_poly( + samplerate: f32, + capacity: usize, + legato: bool, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V, + ) -> Self { + let create_voice = Arc::new(create_voice); + let poly = Polyphonic::new(samplerate, capacity, { + let create_voice = create_voice.clone(); + move |sr, nd| create_voice.clone()(sr, nd) + }); + let pitch_bend_st = poly.pitch_bend_st.clone(); + Self { + pitch_bend_st, + poly_voice_capacity: capacity, + create_voice, + current_manager: Impl::Polyphonic(poly), + legato, + samplerate, + } + } + + pub fn switch(&mut self, polyphonic: bool) { + let new = match self.current_manager { + Impl::Monophonic(..) if polyphonic => { + let create_voice = self.create_voice.clone(); + let mut poly = + Polyphonic::new(self.samplerate, self.poly_voice_capacity, move |sr, nd| { + create_voice.clone()(sr, nd) + }); + poly.pitch_bend_st = self.pitch_bend_st.clone(); + Impl::Polyphonic(poly) + } + Impl::Polyphonic(..) if !polyphonic => { + let create_voice = self.create_voice.clone(); + let mut mono = Monophonic::new( + self.samplerate, + move |sr, nd| create_voice.clone()(sr, nd), + self.legato, + ); + mono.pitch_bend_min_st = self.pitch_bend_st.start; + mono.pitch_bend_max_st = self.pitch_bend_st.end; + Impl::Monophonic(mono) + } + _ => { + return; + } + }; + self.current_manager = new; + } + + pub fn is_monophonic(&self) -> bool { + matches!(self.current_manager, Impl::Monophonic(..)) + } + + pub fn is_polyphonic(&self) -> bool { + matches!(self.current_manager, Impl::Polyphonic(..)) + } + + pub fn legato(&self) -> bool { + self.legato + } + + pub fn set_legato(&mut self, legato: bool) { + self.legato = legato; + if let Impl::Monophonic(ref mut mono) = self.current_manager { + mono.set_legato(legato); + } + } + + pub fn clean_inactive_voices(&mut self) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.clean_voice_if_inactive(), + Impl::Polyphonic(ref mut poly) => poly.clean_inactive_voices(), + } + } +} + +impl DSPMeta for DynamicVoice { + type Sample = V::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = samplerate; + self.current_manager.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.current_manager.latency() + } + + fn reset(&mut self) { + self.current_manager.reset(); + } +} + +impl VoiceManager for DynamicVoice { + type Voice = V; + type ID = as VoiceManager>::ID; + + fn capacity(&self) -> usize { + self.poly_voice_capacity + } + + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice> { + match self.current_manager { + Impl::Monophonic(ref mono) => mono.get_voice(()), + Impl::Polyphonic(ref poly) => poly.get_voice(id), + } + } + + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice> { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.get_voice_mut(()), + Impl::Polyphonic(ref mut poly) => poly.get_voice_mut(id), + } + } + + fn all_voices(&self) -> impl Iterator { + 0..self.poly_voice_capacity + } + + fn note_on(&mut self, note_data: NoteData) -> Self::ID { + match self.current_manager { + Impl::Monophonic(ref mut mono) => { + mono.note_on(note_data); + 0 + } + Impl::Polyphonic(ref mut poly) => poly.note_on(note_data), + } + } + + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => { + mono.note_off((), release_velocity); + } + Impl::Polyphonic(ref mut poly) => { + poly.note_off(id, release_velocity); + } + } + } + + fn choke(&mut self, id: Self::ID) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.choke(()), + Impl::Polyphonic(ref mut poly) => poly.choke(id), + } + } + + fn panic(&mut self) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.panic(), + Impl::Polyphonic(ref mut poly) => poly.panic(), + } + } + + fn pitch_bend(&mut self, amount: f64) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.pitch_bend(amount), + Impl::Polyphonic(ref mut poly) => poly.pitch_bend(amount), + } + } + + fn aftertouch(&mut self, amount: f64) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.aftertouch(amount), + Impl::Polyphonic(ref mut poly) => poly.aftertouch(amount), + } + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.glide((), pressure), + Impl::Polyphonic(ref mut poly) => poly.glide(id, pressure), + } + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.glide((), semitones), + Impl::Polyphonic(ref mut poly) => poly.glide(id, semitones), + } + } +} + +impl> DSPProcess<0, 1> for DynamicVoice { + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.process([]), + Impl::Polyphonic(ref mut poly) => poly.process([]), + } + } +} diff --git a/crates/valib-voice/src/lib.rs b/crates/valib-voice/src/lib.rs index 62fbbf6..718f842 100644 --- a/crates/valib-voice/src/lib.rs +++ b/crates/valib-voice/src/lib.rs @@ -4,9 +4,10 @@ //! This crate provides abstractions around voice processing and voice management. use valib_core::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock, SampleAdapter}; use valib_core::simd::SimdRealField; -use valib_core::util::midi_to_freq; +use valib_core::util::{midi_to_freq, semitone_to_ratio}; use valib_core::Scalar; +pub mod dynamic; pub mod monophonic; pub mod polyphonic; #[cfg(feature = "resampled")] @@ -161,6 +162,8 @@ impl Gain { pub struct NoteData { /// Note frequency pub frequency: T, + /// Frequency modulation (pitch bend, glide) + pub modulation_st: T, /// Note velocity pub velocity: Velocity, /// Note gain @@ -180,12 +183,17 @@ impl NoteData { let pressure = T::zero(); Self { frequency, + modulation_st: T::zero(), velocity, gain, pan, pressure, } } + + pub fn resolve_frequency(&self) -> T { + semitone_to_ratio(self.modulation_st) * self.frequency + } } /// Trait for types which manage voices. diff --git a/crates/valib-voice/src/monophonic.rs b/crates/valib-voice/src/monophonic.rs index 0f16ad0..b48e9a9 100644 --- a/crates/valib-voice/src/monophonic.rs +++ b/crates/valib-voice/src/monophonic.rs @@ -4,6 +4,9 @@ use crate::{NoteData, Voice, VoiceManager}; use num_traits::zero; +use numeric_literals::replace_float_literals; +use std::fmt; +use std::fmt::Formatter; use valib_core::dsp::buffer::{AudioBufferMut, AudioBufferRef}; use valib_core::dsp::{DSPMeta, DSPProcess, DSPProcessBlock}; use valib_core::util::lerp; @@ -15,15 +18,33 @@ pub struct Monophonic { pub pitch_bend_min_st: V::Sample, /// Maximum pitch bend amount (semitones) pub pitch_bend_max_st: V::Sample, - create_voice: Box) -> V>, + create_voice: Box) -> V>, voice: Option, base_frequency: V::Sample, - pitch_bend_st: V::Sample, + modulation_st: V::Sample, released: bool, legato: bool, samplerate: f32, } +impl fmt::Debug for Monophonic { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Monophonic") + .field( + "pitch_bend_st", + &(self.pitch_bend_min_st..self.pitch_bend_max_st), + ) + .field("create_voice", &"Box) -> V") + .field("voice", &self.voice) + .field("base_frequency", &self.base_frequency) + .field("pitch_bend", &self.modulation_st) + .field("released", &self.released) + .field("legato", &self.legato) + .field("samplerate", &self.samplerate) + .finish() + } +} + impl DSPMeta for Monophonic { type Sample = V::Sample; @@ -55,7 +76,7 @@ impl Monophonic { /// returns: Monophonic pub fn new( samplerate: f32, - create_voice: impl Fn(f32, NoteData) -> V + 'static, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V, legato: bool, ) -> Self { Self { @@ -65,7 +86,7 @@ impl Monophonic { voice: None, released: false, base_frequency: V::Sample::from_f64(440.), - pitch_bend_st: zero(), + modulation_st: zero(), legato, samplerate, } @@ -80,6 +101,16 @@ impl Monophonic { pub fn set_legato(&mut self, legato: bool) { self.legato = legato; } + + pub fn clean_voice_if_inactive(&mut self) { + self.voice.take_if(|v| !v.active()); + } + + #[replace_float_literals(V::Sample::from_f64(literal))] + fn pitch_bend_st(&self, amt: V::Sample) -> V::Sample { + let t = 0.5 * amt + 0.5; + lerp(t, self.pitch_bend_min_st, self.pitch_bend_max_st) + } } impl VoiceManager for Monophonic { @@ -110,9 +141,9 @@ impl VoiceManager for Monophonic { } } - fn note_on(&mut self, note_data: NoteData) -> Self::ID { + fn note_on(&mut self, mut note_data: NoteData) -> Self::ID { self.base_frequency = note_data.frequency; - self.pitch_bend_st = zero(); + note_data.modulation_st = self.modulation_st; if let Some(voice) = &mut self.voice { *voice.note_data_mut() = note_data; if self.released || !self.legato { @@ -138,11 +169,9 @@ impl VoiceManager for Monophonic { } fn pitch_bend(&mut self, amount: f64) { - self.pitch_bend_st = lerp( - V::Sample::from_f64(0.5 + amount / 2.), - self.pitch_bend_min_st, - self.pitch_bend_max_st, - ); + let mod_st = self.pitch_bend_st(V::Sample::from_f64(amount)); + self.modulation_st = mod_st; + self.update_voice_pitchmod(); } fn aftertouch(&mut self, amount: f64) { @@ -156,8 +185,18 @@ impl VoiceManager for Monophonic { voice.note_data_mut().pressure = V::Sample::from_f64(pressure as _); } } + fn glide(&mut self, _: Self::ID, semitones: f32) { - self.pitch_bend_st = V::Sample::from_f64(semitones as _); + self.modulation_st = V::Sample::from_f64(semitones as _); + self.update_voice_pitchmod(); + } +} + +impl Monophonic { + fn update_voice_pitchmod(&mut self) { + if let Some(voice) = &mut self.voice { + voice.note_data_mut().modulation_st = self.modulation_st; + } } } diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index 9ae218d..d8e309c 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -4,17 +4,23 @@ use crate::{NoteData, Voice, VoiceManager}; use num_traits::zero; +use numeric_literals::replace_float_literals; use std::fmt; use std::fmt::Formatter; +use std::ops::Range; use valib_core::dsp::{DSPMeta, DSPProcess}; +use valib_core::util::lerp; +use valib_core::Scalar; /// Polyphonic voice manager with rotating voice allocation pub struct Polyphonic { + pub pitch_bend_st: Range, create_voice: Box) -> V>, voice_pool: Box<[Option]>, active_voices: usize, next_voice: usize, samplerate: f32, + pitch_bend: V::Sample, } impl fmt::Debug for Polyphonic { @@ -47,11 +53,13 @@ impl Polyphonic { create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V + 'static, ) -> Self { Self { + pitch_bend_st: V::Sample::from_f64(-2.)..V::Sample::from_f64(2.), create_voice: Box::new(create_voice), next_voice: 0, voice_pool: (0..voice_capacity).map(|_| None).collect(), active_voices: 0, samplerate, + pitch_bend: zero(), } } @@ -64,6 +72,19 @@ impl Polyphonic { } } } + + fn update_voices_pitchmod(&mut self) { + let mod_st = self.get_pitch_bend(); + for voice in self.voice_pool.iter_mut().filter_map(|opt| opt.as_mut()) { + voice.note_data_mut().modulation_st = mod_st; + } + } + + #[replace_float_literals(V::Sample::from_f64(literal))] + fn get_pitch_bend(&self) -> V::Sample { + let t = 0.5 * self.pitch_bend + 0.5; + lerp(t, self.pitch_bend_st.start, self.pitch_bend_st.end) + } } impl DSPMeta for Polyphonic { @@ -153,6 +174,31 @@ impl VoiceManager for Polyphonic { self.voice_pool.fill_with(|| None); self.active_voices = 0; } + + fn pitch_bend(&mut self, amount: f64) { + self.pitch_bend = V::Sample::from_f64(amount); + self.update_voices_pitchmod(); + } + + fn aftertouch(&mut self, amount: f64) { + let pressure = V::Sample::from_f64(amount); + for voice in self.voice_pool.iter_mut().filter_map(|x| x.as_mut()) { + voice.note_data_mut().pressure = pressure; + } + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + if let Some(voice) = &mut self.voice_pool[id] { + voice.note_data_mut().pressure = V::Sample::from_f64(pressure as _); + } + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + let mod_st = V::Sample::from_f64(semitones as _); + if let Some(voice) = &mut self.voice_pool[id] { + voice.note_data_mut().modulation_st = mod_st; + } + } } impl> DSPProcess<0, 1> for Polyphonic { diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 9b556d7..1542912 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -18,6 +18,7 @@ use valib::oscillators::Phasor; use valib::saturators::{bjt, Asinh, Clipper, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; use valib::util::{ratio_to_semitone, semitone_to_ratio}; +use valib::voice::dynamic::DynamicVoice; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; @@ -798,7 +799,7 @@ where type SynthVoice = SampleAdapter>>, 0, 1>; -pub type VoiceManager = Polyphonic>; +pub type VoiceManager = DynamicVoice>; pub fn create_voice_manager>( samplerate: f32, @@ -807,14 +808,19 @@ pub fn create_voice_manager>( where [(); ::LANES]:, { - Polyphonic::new(samplerate, NUM_VOICES, move |samplerate, note_data| { - let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; - SampleAdapter::new(UpsampledVoice::new( - OVERSAMPLE, - MAX_BUFFER_SIZE, - BlockAdapter(RawVoice::new(target_samplerate, params.clone(), note_data)), - )) - }) + DynamicVoice::new_poly( + samplerate, + NUM_VOICES, + true, + move |samplerate, note_data| { + let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; + SampleAdapter::new(UpsampledVoice::new( + OVERSAMPLE, + MAX_BUFFER_SIZE, + BlockAdapter(RawVoice::new(target_samplerate, params.clone(), note_data)), + )) + }, + ) } pub type Voices = VoiceManager; diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 8b2a4cb..b3b1b99 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -118,7 +118,6 @@ impl VoiceIdMap { type SynthSample = f32; -#[derive(Debug)] pub struct PolysynthPlugin { voices: BlockAdapter>, effects: dsp::Effects, From ed6f5bc09990081029bbd5e478a061af8dacd805 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 11:40:46 +0200 Subject: [PATCH 32/67] wip: working polysynth example --- .idea/valib.iml | 1 - Cargo.lock | 9 + Cargo.toml | 1 + Makefile.plugins.toml | 6 +- crates/valib-core/src/dsp/mod.rs | 3 +- crates/valib-oscillators/src/lib.rs | 21 +- crates/valib-oscillators/src/polyblep.rs | 196 +++++++++++++++++ crates/valib-oversample/src/lib.rs | 29 ++- crates/valib-voice/src/lib.rs | 227 +++++++++++++++++++- crates/valib-voice/src/monophonic.rs | 7 +- crates/valib-voice/src/polyphonic.rs | 39 +++- crates/valib-voice/src/upsample.rs | 25 ++- examples/polysynth/Makefile.toml | 1 + examples/polysynth/src/dsp.rs | 255 +++++++++++++++++++++++ examples/polysynth/src/main.rs | 6 + examples/polysynth/src/params.rs | 162 ++++++++++++++ 16 files changed, 962 insertions(+), 26 deletions(-) create mode 100644 crates/valib-oscillators/src/polyblep.rs create mode 100644 examples/polysynth/Makefile.toml create mode 100644 examples/polysynth/src/dsp.rs create mode 100644 examples/polysynth/src/main.rs create mode 100644 examples/polysynth/src/params.rs diff --git a/.idea/valib.iml b/.idea/valib.iml index 8f2a232..7c1daf9 100644 --- a/.idea/valib.iml +++ b/.idea/valib.iml @@ -31,7 +31,6 @@ - diff --git a/Cargo.lock b/Cargo.lock index fddfe0e..fe7e84d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3600,6 +3600,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polysynth" +version = "0.1.0" +dependencies = [ + "nih_plug", + "num-traits", + "valib", +] + [[package]] name = "portable-atomic" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 30fc86b..1e3adc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ filters = ["saturators", "dep:valib-filters"] oscillators = ["dep:valib-oscillators"] oversample = ["filters", "dep:valib-oversample"] voice = ["dep:valib-voice"] +voice-upsampled = ["voice", "valib-voice/resampled"] wdf = ["filters", "dep:valib-wdf"] fundsp = ["dep:valib-fundsp"] nih-plug = ["dep:valib-nih-plug"] diff --git a/Makefile.plugins.toml b/Makefile.plugins.toml index 0ae6898..bb248d8 100644 --- a/Makefile.plugins.toml +++ b/Makefile.plugins.toml @@ -9,8 +9,12 @@ dependencies = ["xtask-build"] command = "cargo" args = ["xtask", "bundle", "${CARGO_MAKE_CRATE_NAME}", "${@}"] +[tasks.install-target-x86_64-darwin] +command = "rustup" +args = ["target", "add", "x86_64-apple-darwin"] + [tasks.bundle-universal] -dependencies = ["xtask-build"] +dependencies = ["xtask-build", "install-target-x86_64-darwin"] command = "cargo" args = ["xtask", "bundle-universal", "${CARGO_MAKE_CRATE_NAME}", "${@}"] diff --git a/crates/valib-core/src/dsp/mod.rs b/crates/valib-core/src/dsp/mod.rs index f504245..477cf9e 100644 --- a/crates/valib-core/src/dsp/mod.rs +++ b/crates/valib-core/src/dsp/mod.rs @@ -123,13 +123,14 @@ pub struct SampleAdapter where P: DSPProcessBlock, { + /// Inner block processor + pub inner: P, /// Size of the buffers passed into the inner block processor. pub buffer_size: usize, input_buffer: AudioBufferBox, input_filled: usize, output_buffer: AudioBufferBox, output_filled: usize, - inner: P, } impl std::ops::Deref for SampleAdapter diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 76c665b..9aa04b2 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -8,18 +8,29 @@ use valib_core::dsp::DSPProcess; use valib_core::Scalar; pub mod blit; +pub mod polyblep; pub mod wavetable; /// Tracks normalized phase for a given frequency. Phase is smooth even when frequency changes, so /// it is suitable for driving oscillators. #[derive(Debug, Clone, Copy)] pub struct Phasor { + samplerate: T, + frequency: T, phase: T, step: T, } impl DSPMeta for Phasor { type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = T::from_f64(samplerate as _); + } + + fn reset(&mut self) { + self.phase = T::zero(); + } } #[profiling::all_functions] @@ -43,10 +54,12 @@ impl Phasor { /// /// returns: Phasor #[replace_float_literals(T::from_f64(literal))] - pub fn new(samplerate: T, freq: T) -> Self { + pub fn new(samplerate: T, frequency: T) -> Self { Self { + samplerate, + frequency, phase: 0.0, - step: freq / samplerate, + step: frequency / samplerate, } } @@ -58,7 +71,7 @@ impl Phasor { /// * `freq`: New frequency /// /// returns: () - pub fn set_frequency(&mut self, samplerate: T, freq: T) { - self.step = freq / samplerate; + pub fn set_frequency(&mut self, freq: T) { + self.step = freq / self.samplerate; } } diff --git a/crates/valib-oscillators/src/polyblep.rs b/crates/valib-oscillators/src/polyblep.rs new file mode 100644 index 0000000..2495c6e --- /dev/null +++ b/crates/valib-oscillators/src/polyblep.rs @@ -0,0 +1,196 @@ +use crate::Phasor; +use num_traits::{one, zero, ConstOne, ConstZero}; +use std::marker::PhantomData; +use valib_core::dsp::blocks::P1; +use valib_core::dsp::{DSPMeta, DSPProcess}; +use valib_core::simd::SimdBool; +use valib_core::Scalar; + +pub struct PolyBLEP { + pub amplitude: T, + pub phase: T, +} + +impl PolyBLEP { + pub fn eval(&self, dt: T, phase: T) -> T { + let t = T::simd_fract(phase + self.phase); + let ret = t.simd_lt(dt).if_else( + || { + let t = t / dt; + t + t - t * t - one() + }, + || { + t.simd_gt(one::() - dt).if_else( + || { + let t = (t - one()) / dt; + t * t + t + t + one() + }, + || zero(), + ) + }, + ); + self.amplitude * ret + } +} + +pub trait PolyBLEPOscillator: DSPMeta { + fn bleps() -> impl IntoIterator>; + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample; +} + +pub struct PolyBLEPDriver { + pub phasor: Phasor, + pub blep: Osc, + samplerate: Osc::Sample, +} + +impl PolyBLEPDriver { + pub fn new(samplerate: Osc::Sample, frequency: Osc::Sample, blep: Osc) -> Self { + Self { + phasor: Phasor::new(samplerate, frequency), + blep, + samplerate, + } + } + + pub fn set_frequency(&mut self, frequency: Osc::Sample) { + self.phasor.set_frequency(frequency); + } +} + +impl DSPProcess<0, 1> for PolyBLEPDriver { + fn process(&mut self, _: [Self::Sample; 0]) -> [Self::Sample; 1] { + let [phase] = self.phasor.process([]); + let mut y = self.blep.naive_eval(phase); + for blep in Osc::bleps() { + y += blep.eval(self.phasor.step, phase); + } + [y] + } +} + +impl DSPMeta for PolyBLEPDriver { + type Sample = Osc::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + self.phasor.set_samplerate(samplerate); + self.blep.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.blep.latency() + } + + fn reset(&mut self) { + self.phasor.reset(); + self.blep.reset(); + } +} + +#[derive(Debug, Copy, Clone)] +pub struct SawBLEP(PhantomData); + +impl Default for SawBLEP { + fn default() -> Self { + Self(PhantomData) + } +} + +impl DSPMeta for SawBLEP { + type Sample = T; +} + +impl PolyBLEPOscillator for SawBLEP { + fn bleps() -> impl IntoIterator> { + [PolyBLEP { + amplitude: -T::ONE, + phase: T::ZERO, + }] + } + + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { + T::from_f64(2.0) * phase - T::one() + } +} + +pub type Sawtooth = PolyBLEPDriver>; + +#[derive(Debug, Copy, Clone)] +pub struct SquareBLEP { + pw: T, +} + +impl SquareBLEP { + pub fn new(pulse_width: T) -> Self { + Self { + pw: pulse_width.simd_clamp(zero(), one()), + } + } +} + +impl SquareBLEP { + pub fn set_pulse_width(&mut self, pw: T) { + self.pw = pw.simd_clamp(zero(), one()); + } +} + +impl DSPMeta for SquareBLEP { + type Sample = T; +} + +impl PolyBLEPOscillator for SquareBLEP { + fn bleps() -> impl IntoIterator> { + [ + PolyBLEP { + amplitude: T::ONE, + phase: T::ZERO, + }, + PolyBLEP { + amplitude: -T::ONE, + phase: T::from_f64(0.5), + }, + ] + } + + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { + T::from_f64(2.0) * phase - T::one() + } +} + +pub type Square = PolyBLEPDriver>; + +pub struct Triangle { + square: Square, + integrator: P1, +} + +impl DSPMeta for Triangle { + type Sample = T; + fn set_samplerate(&mut self, samplerate: f32) { + self.square.set_samplerate(samplerate); + self.integrator.set_samplerate(samplerate); + } + fn reset(&mut self) { + self.square.reset(); + self.integrator.reset(); + } +} + +impl DSPProcess<0, 1> for Triangle { + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + self.integrator.process(self.square.process([])) + } +} + +impl Triangle { + pub fn new(samplerate: T, frequency: T) -> Self { + let square = PolyBLEPDriver::new(samplerate, frequency, SquareBLEP::new(T::from_f64(0.5))); + let integrator = P1::new(samplerate, frequency); + Self { square, integrator } + } + + pub fn set_frequency(&mut self, frequency: T) { + self.square.set_frequency(frequency); + self.integrator.set_fc(frequency); + } +} diff --git a/crates/valib-oversample/src/lib.rs b/crates/valib-oversample/src/lib.rs index b2562e4..59b93be 100644 --- a/crates/valib-oversample/src/lib.rs +++ b/crates/valib-oversample/src/lib.rs @@ -169,6 +169,19 @@ impl ResampleStage { } impl ResampleStage { + /// Upsample a single sample of audio + /// + /// # Arguments + /// + /// * `s`: Input sample + /// + /// returns: [T; 2] + pub fn process(&mut self, s: T) -> [T; 2] { + let [x0] = self.filter.process([s + s]); + let [x1] = self.filter.process([T::zero()]); + [x0, x1] + } + /// Upsample the input buffer by a factor of 2. /// /// The output slice should be twice the length of the input slice. @@ -176,8 +189,7 @@ impl ResampleStage { pub fn process_block(&mut self, input: &[T], output: &mut [T]) { assert_eq!(input.len() * 2, output.len()); for (i, s) in input.iter().copied().enumerate() { - let [x0] = self.filter.process([s + s]); - let [x1] = self.filter.process([T::zero()]); + let [x0, x1] = self.process(s); output[2 * i + 0] = x0; output[2 * i + 1] = x1; } @@ -185,6 +197,19 @@ impl ResampleStage { } impl ResampleStage { + /// Downsample 2 samples of input audio, and output a single sample of audio. + /// + /// # Arguments + /// + /// * `[x0, x1]`: Inputs samples + /// + /// returns: T + pub fn process(&mut self, [x0, x1]: [T; 2]) -> T { + let [y] = self.filter.process([x0]); + let _ = self.filter.process([x1]); + y + } + /// Downsample the input buffer by a factor of 2. /// /// The output slice should be twice the length of the input slice. diff --git a/crates/valib-voice/src/lib.rs b/crates/valib-voice/src/lib.rs index 475af22..62fbbf6 100644 --- a/crates/valib-voice/src/lib.rs +++ b/crates/valib-voice/src/lib.rs @@ -2,8 +2,9 @@ //! # Voice abstractions //! //! This crate provides abstractions around voice processing and voice management. -use valib_core::dsp::DSPMeta; +use valib_core::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock, SampleAdapter}; use valib_core::simd::SimdRealField; +use valib_core::util::midi_to_freq; use valib_core::Scalar; pub mod monophonic; @@ -20,11 +21,57 @@ pub trait Voice: DSPMeta { /// Return a mutable reference to the voice's note data fn note_data_mut(&mut self) -> &mut NoteData; /// Release the note (corresponding to a note off) - fn release(&mut self); + fn release(&mut self, release_velocity: f32); /// Reuse the note (corresponding to a soft reset) fn reuse(&mut self); } +impl + Voice, const I: usize, const O: usize> Voice + for SampleAdapter +{ + fn active(&self) -> bool { + self.inner.active() + } + + fn note_data(&self) -> &NoteData { + self.inner.note_data() + } + + fn note_data_mut(&mut self) -> &mut NoteData { + self.inner.note_data_mut() + } + + fn release(&mut self, release_velocity: f32) { + self.inner.release(release_velocity); + } + + fn reuse(&mut self) { + self.inner.reuse(); + } +} + +impl Voice for BlockAdapter { + fn active(&self) -> bool { + self.0.active() + } + + fn note_data(&self) -> &NoteData { + self.0.note_data() + } + + fn note_data_mut(&mut self) -> &mut NoteData { + self.0.note_data_mut() + } + + fn release(&mut self, release_velocity: f32) { + self.0.release(release_velocity); + } + + fn reuse(&mut self) { + self.0.reuse(); + } +} + /// Value representing velocity. The square root is precomputed to be used in voices directly. #[derive(Debug, Copy, Clone)] pub struct Velocity { @@ -124,9 +171,30 @@ pub struct NoteData { pub pressure: T, } +impl NoteData { + pub fn from_midi(midi_note: u8, velocity: f32) -> Self { + let frequency = midi_to_freq(midi_note); + let velocity = Velocity::new(T::from_f64(velocity as _)); + let gain = Gain::from_linear(T::one()); + let pan = T::zero(); + let pressure = T::zero(); + Self { + frequency, + velocity, + gain, + pan, + pressure, + } + } +} + /// Trait for types which manage voices. #[allow(unused_variables)] -pub trait VoiceManager: DSPMeta { +pub trait VoiceManager: + DSPMeta::Voice as DSPMeta>::Sample> +{ + /// Type of the inner voice. + type Voice: Voice; /// Type for the voice ID. type ID: Copy; @@ -134,9 +202,9 @@ pub trait VoiceManager: DSPMeta { fn capacity(&self) -> usize; /// Get the voice by its ID - fn get_voice(&self, id: Self::ID) -> Option<&V>; + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice>; /// Get the voice mutably by its ID - fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut V>; + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice>; /// Return true if the voice referred by the given ID is currently active fn is_voice_active(&self, id: Self::ID) -> bool { @@ -153,9 +221,9 @@ pub trait VoiceManager: DSPMeta { } /// Indicate a note on event, with the given note data to instanciate the voice. - fn note_on(&mut self, note_data: NoteData) -> Self::ID; + fn note_on(&mut self, note_data: NoteData) -> Self::ID; /// Indicate a note off event on the given voice ID. - fn note_off(&mut self, id: Self::ID); + fn note_off(&mut self, id: Self::ID, release_velocity: f32); /// Choke the voice, causing all processing on that voice to stop. fn choke(&mut self, id: Self::ID); /// Choke all the notes. @@ -177,3 +245,148 @@ pub trait VoiceManager: DSPMeta { /// Note gain fn gain(&mut self, id: Self::ID, gain: f32) {} } + +impl VoiceManager for BlockAdapter { + type Voice = V::Voice; + type ID = V::ID; + + fn capacity(&self) -> usize { + self.0.capacity() + } + + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice> { + self.0.get_voice(id) + } + + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice> { + self.0.get_voice_mut(id) + } + + fn is_voice_active(&self, id: Self::ID) -> bool { + self.0.is_voice_active(id) + } + + fn all_voices(&self) -> impl Iterator { + self.0.all_voices() + } + + fn active(&self) -> usize { + self.0.active() + } + + fn note_on(&mut self, note_data: NoteData) -> Self::ID { + self.0.note_on(note_data) + } + + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { + self.0.note_off(id, release_velocity) + } + + fn choke(&mut self, id: Self::ID) { + self.0.choke(id) + } + + fn panic(&mut self) { + self.0.panic() + } + + fn pitch_bend(&mut self, amount: f64) { + self.0.pitch_bend(amount) + } + + fn aftertouch(&mut self, amount: f64) { + self.0.aftertouch(amount) + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + self.0.pressure(id, pressure) + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + self.0.glide(id, semitones) + } + + fn pan(&mut self, id: Self::ID, pan: f32) { + self.0.pan(id, pan) + } + + fn gain(&mut self, id: Self::ID, gain: f32) { + self.0.gain(id, gain) + } +} + +impl + VoiceManager, const I: usize, const O: usize> VoiceManager + for SampleAdapter +{ + type Voice = V::Voice; + type ID = V::ID; + + fn capacity(&self) -> usize { + self.inner.capacity() + } + + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice> { + self.inner.get_voice(id) + } + + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice> { + self.inner.get_voice_mut(id) + } + + fn is_voice_active(&self, id: Self::ID) -> bool { + self.inner.is_voice_active(id) + } + + fn all_voices(&self) -> impl Iterator { + self.inner.all_voices() + } + + fn active(&self) -> usize { + self.inner.active() + } + + fn note_on(&mut self, note_data: NoteData) -> Self::ID { + self.inner.note_on(note_data) + } + + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { + self.inner.note_off(id, release_velocity) + } + + fn choke(&mut self, id: Self::ID) { + self.inner.choke(id) + } + + fn panic(&mut self) { + self.inner.panic() + } + + fn pitch_bend(&mut self, amount: f64) { + self.inner.pitch_bend(amount) + } + + fn aftertouch(&mut self, amount: f64) { + self.inner.aftertouch(amount) + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + self.inner.pressure(id, pressure) + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + self.inner.glide(id, semitones) + } + + fn pan(&mut self, id: Self::ID, pan: f32) { + self.inner.pan(id, pan) + } + + fn gain(&mut self, id: Self::ID, gain: f32) { + self.inner.gain(id, gain) + } +} + +/// Inner voice of the voice manager. +pub type InnerVoice = ::Voice; +/// Inner voice ID of the voice manager. +pub type VoiceId = ::ID; diff --git a/crates/valib-voice/src/monophonic.rs b/crates/valib-voice/src/monophonic.rs index 86f43f5..0f16ad0 100644 --- a/crates/valib-voice/src/monophonic.rs +++ b/crates/valib-voice/src/monophonic.rs @@ -82,7 +82,8 @@ impl Monophonic { } } -impl VoiceManager for Monophonic { +impl VoiceManager for Monophonic { + type Voice = V; type ID = (); fn capacity(&self) -> usize { @@ -122,9 +123,9 @@ impl VoiceManager for Monophonic { } } - fn note_off(&mut self, _id: Self::ID) { + fn note_off(&mut self, _: Self::ID, release_velocity: f32) { if let Some(voice) = &mut self.voice { - voice.release(); + voice.release(release_velocity); } } diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index b9b1f89..61d4f95 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -1,18 +1,35 @@ //! # Polyphonic voice manager //! //! Provides a polyphonic voice manager with rotating voice allocation. + use crate::{NoteData, Voice, VoiceManager}; use num_traits::zero; +use std::fmt; +use std::fmt::Formatter; use valib_core::dsp::{DSPMeta, DSPProcess}; /// Polyphonic voice manager with rotating voice allocation pub struct Polyphonic { - create_voice: Box) -> V>, + create_voice: Box) -> V>, voice_pool: Box<[Option]>, next_voice: usize, samplerate: f32, } +impl fmt::Debug for Polyphonic { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Polyphonic") + .field( + "create_voice", + &"Box) -> V>", + ) + .field("voice_pool", &"Box<[Option]>") + .field("next_voice", &self.next_voice) + .field("samplerate", &self.samplerate) + .finish() + } +} + impl Polyphonic { /// Create a new polyphonice voice manager. /// @@ -26,7 +43,7 @@ impl Polyphonic { pub fn new( samplerate: f32, voice_capacity: usize, - create_voice: impl Fn(f32, NoteData) -> V + 'static, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V + 'static, ) -> Self { Self { create_voice: Box::new(create_voice), @@ -35,6 +52,15 @@ impl Polyphonic { samplerate, } } + + /// Clean inactive voices to prevent them being processed for nothing. + pub fn clean_inactive_voices(&mut self) { + for slot in &mut self.voice_pool { + if slot.as_ref().is_some_and(|v| !v.active()) { + slot.take(); + } + } + } } impl DSPMeta for Polyphonic { @@ -61,7 +87,8 @@ impl DSPMeta for Polyphonic { } } -impl VoiceManager for Polyphonic { +impl VoiceManager for Polyphonic { + type Voice = V; type ID = usize; fn capacity(&self) -> usize { @@ -82,7 +109,7 @@ impl VoiceManager for Polyphonic { fn note_on(&mut self, note_data: NoteData) -> Self::ID { let id = self.next_voice; - self.next_voice += 1; + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); if let Some(voice) = &mut self.voice_pool[id] { *voice.note_data_mut() = note_data; @@ -94,9 +121,9 @@ impl VoiceManager for Polyphonic { id } - fn note_off(&mut self, id: Self::ID) { + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { if let Some(voice) = &mut self.voice_pool[id] { - voice.release(); + voice.release(release_velocity); } } diff --git a/crates/valib-voice/src/upsample.rs b/crates/valib-voice/src/upsample.rs index 71384b4..e3d26b8 100644 --- a/crates/valib-voice/src/upsample.rs +++ b/crates/valib-voice/src/upsample.rs @@ -1,6 +1,7 @@ //! # Upsampled voices //! //! Provides upsampling for DSP process which are generators (0 input channels). +use crate::{NoteData, Voice}; use num_traits::zero; use valib_core::dsp::buffer::{AudioBufferBox, AudioBufferMut, AudioBufferRef}; use valib_core::dsp::{DSPMeta, DSPProcessBlock}; @@ -15,6 +16,28 @@ pub struct UpsampledVoice { num_active_stages: usize, } +impl Voice for UpsampledVoice

{ + fn active(&self) -> bool { + self.inner.active() + } + + fn note_data(&self) -> &NoteData { + self.inner.note_data() + } + + fn note_data_mut(&mut self) -> &mut NoteData { + self.inner.note_data_mut() + } + + fn release(&mut self, release_velocity: f32) { + self.inner.release(release_velocity); + } + + fn reuse(&mut self) { + self.inner.reuse() + } +} + impl> DSPProcessBlock<0, 1> for UpsampledVoice

{ fn process_block( &mut self, @@ -32,7 +55,7 @@ impl> DSPProcessBlock<0, 1> for UpsampledVoice

{ let mut length = inner_len; for stage in &mut self.downsample_stages[..self.num_active_stages] { let (input, output) = self.ping_pong_buffer.get_io_buffers(..length); - stage.process_block(input, output); + stage.process_block(input, &mut output[..length / 2]); self.ping_pong_buffer.switch(); length /= 2; } diff --git a/examples/polysynth/Makefile.toml b/examples/polysynth/Makefile.toml new file mode 100644 index 0000000..1243632 --- /dev/null +++ b/examples/polysynth/Makefile.toml @@ -0,0 +1 @@ +extend = "../../Makefile.plugins.toml" \ No newline at end of file diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs new file mode 100644 index 0000000..2240c5d --- /dev/null +++ b/examples/polysynth/src/dsp.rs @@ -0,0 +1,255 @@ +use crate::params::{FilterParams, OscParams, OscShape, PolysynthParams}; +use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use nih_plug::nih_log; +use nih_plug::util::db_to_gain_fast; +use num_traits::{ConstOne, ConstZero}; +use std::sync::Arc; +use valib::dsp::parameter::SmoothedParam; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; +use valib::filters::ladder::{Ladder, OTA}; +use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; +use valib::oscillators::Phasor; +use valib::saturators::{bjt, Tanh}; +use valib::util::semitone_to_ratio; +use valib::voice::polyphonic::Polyphonic; +use valib::voice::upsample::UpsampledVoice; +use valib::voice::{NoteData, Voice}; +use valib::Scalar; + +pub enum PolyOsc { + Sine(Phasor), + Triangle(Triangle), + Square(Square), + Sawtooth(Sawtooth), +} + +impl PolyOsc { + fn new(samplerate: T, shape: OscShape, note_data: NoteData, pulse_width: T) -> Self { + match shape { + OscShape::Sine => Self::Sine(Phasor::new(samplerate, note_data.frequency)), + OscShape::Triangle => Self::Triangle(Triangle::new(samplerate, note_data.frequency)), + OscShape::Square => Self::Square(Square::new( + samplerate, + note_data.frequency, + SquareBLEP::new(pulse_width), + )), + OscShape::Saw => Self::Sawtooth(Sawtooth::new( + samplerate, + note_data.frequency, + SawBLEP::default(), + )), + } + } + + fn is_osc_shape(&self, osc_shape: OscShape) -> bool { + match self { + Self::Sine(..) if matches!(osc_shape, OscShape::Sine) => true, + Self::Triangle(..) if matches!(osc_shape, OscShape::Triangle) => true, + Self::Square(..) if matches!(osc_shape, OscShape::Square) => true, + Self::Sawtooth(..) if matches!(osc_shape, OscShape::Saw) => true, + _ => false, + } + } +} + +impl DSPMeta for PolyOsc { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + match self { + Self::Sine(p) => p.set_samplerate(samplerate), + Self::Triangle(tri) => tri.set_samplerate(samplerate), + Self::Square(sq) => sq.set_samplerate(samplerate), + Self::Sawtooth(sw) => sw.set_samplerate(samplerate), + } + } + + fn reset(&mut self) { + match self { + PolyOsc::Sine(p) => p.reset(), + PolyOsc::Triangle(tri) => tri.reset(), + PolyOsc::Square(sqr) => sqr.reset(), + PolyOsc::Sawtooth(saw) => saw.reset(), + } + } +} + +impl DSPProcess<1, 1> for PolyOsc { + fn process(&mut self, [freq]: [Self::Sample; 1]) -> [Self::Sample; 1] { + match self { + Self::Sine(p) => { + p.set_frequency(freq); + p.process([]).map(|x| (T::simd_two_pi() * x).simd_sin()) + } + Self::Triangle(tri) => { + tri.set_frequency(freq); + tri.process([]) + } + Self::Square(sq) => { + sq.set_frequency(freq); + sq.process([]) + } + Self::Sawtooth(sw) => { + sw.set_frequency(freq); + sw.process([]) + } + } + } +} + +pub struct RawVoice { + osc: [PolyOsc; 2], + osc_out_sat: bjt::CommonCollector, + filter: Ladder>, + osc_params: [Arc; 2], + filter_params: Arc, + gate: SmoothedParam, + note_data: NoteData, + samplerate: T, +} + +impl RawVoice { + pub(crate) fn update_osc_types(&mut self) { + for i in 0..2 { + let params = &self.osc_params[i]; + let shape = params.shape.value(); + let osc = &mut self.osc[i]; + if !osc.is_osc_shape(shape) { + let pulse_width = T::from_f64(params.pulse_width.value() as _); + *osc = PolyOsc::new(self.samplerate, shape, self.note_data, pulse_width); + } + } + } +} + +impl Voice for RawVoice { + fn active(&self) -> bool { + self.gate.current_value() > 0.5 + } + + fn note_data(&self) -> &NoteData { + &self.note_data + } + + fn note_data_mut(&mut self) -> &mut NoteData { + &mut self.note_data + } + + fn release(&mut self, _: f32) { + nih_log!("RawVoice: release(_)"); + self.gate.param = 0.; + } + + fn reuse(&mut self) { + self.gate.param = 1.; + } +} + +impl DSPMeta for RawVoice { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = T::from_f64(samplerate as _); + for osc in &mut self.osc { + osc.set_samplerate(samplerate); + } + self.filter.set_samplerate(samplerate); + } + + fn reset(&mut self) { + for osc in &mut self.osc { + osc.reset(); + } + self.filter.reset(); + } +} + +impl DSPProcess<0, 1> for RawVoice { + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + let frequency = self.note_data.frequency; + self.update_osc_types(); + let osc1_freq = frequency + * T::from_f64(semitone_to_ratio( + self.osc_params[0].pitch_coarse.value() + self.osc_params[0].pitch_fine.value(), + ) as _); + let osc2_freq = frequency + * T::from_f64(semitone_to_ratio( + self.osc_params[1].pitch_coarse.value() + self.osc_params[1].pitch_fine.value(), + ) as _); + let [osc1] = self.osc[0].process([osc1_freq]); + let [osc2] = self.osc[1].process([osc2_freq]); + let osc_mixer = osc1 * T::from_f64(self.osc_params[0].amplitude.smoothed.next() as _) + + osc2 * T::from_f64(self.osc_params[1].amplitude.smoothed.next() as _); + let filter_in = self + .osc_out_sat + .process([osc_mixer]) + .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); + self.filter + .set_cutoff(T::from_f64(self.filter_params.cutoff.smoothed.next() as _)); + self.filter.set_resonance(T::from_f64( + 4f64 * self.filter_params.resonance.smoothed.next() as f64, + )); + let vca = self.gate.next_sample_as::(); + self.filter.process(filter_in).map(|x| vca * x) + } +} + +type SynthVoice = SampleAdapter>>, 0, 1>; + +pub type VoiceManager = Polyphonic>; + +pub fn create_voice_manager( + samplerate: f32, + osc_params: [Arc; 2], + filter_params: Arc, +) -> VoiceManager { + let target_samplerate_f64 = OVERSAMPLE as f64 * samplerate as f64; + let target_samplerate = T::from_f64(target_samplerate_f64); + Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { + SampleAdapter::new(UpsampledVoice::new( + 2, + MAX_BUFFER_SIZE, + BlockAdapter(RawVoice { + osc: std::array::from_fn(|i| { + let osc_param = &osc_params[i]; + let pulse_width = T::from_f64(osc_param.pulse_width.value() as _); + PolyOsc::new( + target_samplerate, + osc_param.shape.value(), + note_data, + pulse_width, + ) + }), + osc_params: osc_params.clone(), + filter: Ladder::new( + target_samplerate_f64, + T::from_f64(filter_params.cutoff.value() as _), + T::from_f64(filter_params.resonance.value() as _), + ), + filter_params: filter_params.clone(), + osc_out_sat: bjt::CommonCollector { + vee: -T::ONE, + vcc: T::ONE, + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, + gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + note_data, + samplerate: target_samplerate, + }), + )) + }) +} + +pub type Dsp = VoiceManager; + +pub fn create( + samplerate: f32, + params: &PolysynthParams, +) -> Dsp { + create_voice_manager( + samplerate, + params.osc_params.clone(), + params.filter_params.clone(), + ) +} diff --git a/examples/polysynth/src/main.rs b/examples/polysynth/src/main.rs new file mode 100644 index 0000000..b95b123 --- /dev/null +++ b/examples/polysynth/src/main.rs @@ -0,0 +1,6 @@ +use nih_plug::nih_export_standalone; +use polysynth::PolysynthPlugin; + +fn main() { + nih_export_standalone::(); +} diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs new file mode 100644 index 0000000..e2fad43 --- /dev/null +++ b/examples/polysynth/src/params.rs @@ -0,0 +1,162 @@ +use crate::{ + OVERSAMPLE, POLYMOD_FILTER_CUTOFF, POLYMOD_OSC_AMP, POLYMOD_OSC_PITCH_COARSE, + POLYMOD_OSC_PITCH_FINE, +}; +use nih_plug::prelude::*; +use nih_plug::util::{db_to_gain, MINUS_INFINITY_DB}; +use std::sync::Arc; +use valib::dsp::parameter::{ParamId, ParamName}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, ParamName, Enum)] +pub enum OscShape { + Sine, + Triangle, + Square, + Saw, +} + +#[derive(Debug, Params)] +pub struct OscParams { + #[id = "shp"] + pub shape: EnumParam, + #[id = "amp"] + pub amplitude: FloatParam, + #[id = "pco"] + pub pitch_coarse: FloatParam, + #[id = "pfi"] + pub pitch_fine: FloatParam, + #[id = "pw"] + pub pulse_width: FloatParam, +} + +impl OscParams { + fn new(osc_index: usize, oversample: Arc) -> Self { + Self { + shape: EnumParam::new("Shape", OscShape::Saw), + amplitude: FloatParam::new( + "Amplitude", + 0.8, + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_AMP[osc_index]), + pitch_coarse: FloatParam::new( + "Pitch (Coarse)", + 0.0, + FloatRange::Linear { + min: -24., + max: 24., + }, + ) + .with_step_size(1.) + .with_unit(" st") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_PITCH_COARSE[osc_index]), + pitch_fine: FloatParam::new( + "Pitch (Fine)", + 0.0, + FloatRange::Linear { + min: -0.5, + max: 0.5, + }, + ) + .with_value_to_string(formatters::v2s_f32_rounded(3)) + .with_unit(" st") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_PITCH_FINE[osc_index]), + pulse_width: FloatParam::new( + "Pulse Width", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ) + .with_unit(" %") + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), + } + } +} + +#[derive(Debug, Params)] +pub struct FilterParams { + #[id = "fc"] + pub cutoff: FloatParam, + #[id = "res"] + pub resonance: FloatParam, +} + +impl FilterParams { + fn new(oversample: Arc) -> Self { + Self { + cutoff: FloatParam::new( + "Cutoff", + 3000., + FloatRange::Skewed { + min: 20., + max: 20e3, + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_hz_then_khz_with_note_name(2, true)) + .with_string_to_value(formatters::s2v_f32_hz_then_khz()) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_FILTER_CUTOFF), + resonance: FloatParam::new( + "Resonance", + 0.1, + FloatRange::Linear { + min: 0.0, + max: 1.25, + }, + ) + .with_value_to_string(formatters::v2s_f32_percentage(1)) + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_unit(" %") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), + } + } +} + +#[derive(Debug, Params)] +pub struct PolysynthParams { + #[nested(array)] + pub osc_params: [Arc; 2], + #[nested] + pub filter_params: Arc, + pub oversample: Arc, +} + +impl Default for PolysynthParams { + fn default() -> Self { + let oversample = Arc::new(AtomicF32::new(OVERSAMPLE as _)); + Self { + osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), + filter_params: Arc::new(FilterParams::new(oversample.clone())), + oversample, + } + } +} From 86c36ef68316677d8212d58bfce70f4aa538e00b Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 13:14:40 +0200 Subject: [PATCH 33/67] fix: add missing files --- examples/polysynth/Cargo.toml | 18 ++ examples/polysynth/src/lib.rs | 328 ++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 examples/polysynth/Cargo.toml create mode 100644 examples/polysynth/src/lib.rs diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml new file mode 100644 index 0000000..17c16da --- /dev/null +++ b/examples/polysynth/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "polysynth" +version.workspace = true +rust-version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] + +[dependencies] +valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} +nih_plug = { workspace = true, features = ["standalone"] } +num-traits.workspace = true \ No newline at end of file diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs new file mode 100644 index 0000000..0d5eb58 --- /dev/null +++ b/examples/polysynth/src/lib.rs @@ -0,0 +1,328 @@ +use crate::params::PolysynthParams; +use nih_plug::audio_setup::{AudioIOLayout, AuxiliaryBuffers}; +use nih_plug::buffer::Buffer; +use nih_plug::params::Params; +use nih_plug::plugin::ProcessStatus; +use nih_plug::prelude::*; +use std::cmp::Ordering; +use std::sync::Arc; +use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock}; +use valib::voice::{NoteData, VoiceId, VoiceManager}; + +mod dsp; +mod params; + +const NUM_VOICES: usize = 16; +const OVERSAMPLE: usize = 4; +const MAX_BUFFER_SIZE: usize = 64; + +const POLYMOD_OSC_AMP: [u32; 2] = [0, 1]; +const POLYMOD_OSC_PITCH_COARSE: [u32; 2] = [2, 3]; +const POLYMOD_OSC_PITCH_FINE: [u32; 2] = [4, 5]; +const POLYMOD_FILTER_CUTOFF: u32 = 6; + +#[derive(Debug, Copy, Clone)] +struct VoiceKey { + voice_id: Option, + channel: u8, + note: u8, +} + +impl PartialEq for VoiceKey { + fn eq(&self, other: &Self) -> bool { + match (self.voice_id, other.voice_id) { + (Some(a), Some(b)) => a == b, + _ => self.channel == other.channel && self.note == other.note, + } + } +} + +impl Eq for VoiceKey {} + +impl Ord for VoiceKey { + fn cmp(&self, other: &Self) -> Ordering { + match (self.voice_id, other.voice_id) { + (Some(a), Some(b)) => a.cmp(&b), + _ => self + .channel + .cmp(&other.channel) + .then(self.note.cmp(&other.note)), + } + } +} + +impl PartialOrd for VoiceKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl VoiceKey { + fn new(voice_id: Option, channel: u8, note: u8) -> Self { + Self { + voice_id, + channel, + note, + } + } +} + +#[derive(Debug)] +struct VoiceIdMap { + data: [Option<(VoiceKey, VoiceId>)>; NUM_VOICES], +} + +impl Default for VoiceIdMap { + fn default() -> Self { + Self { + data: [None; NUM_VOICES], + } + } +} + +impl VoiceIdMap { + fn add_voice(&mut self, key: VoiceKey, v: VoiceId>) -> bool { + let Some(position) = self.data.iter().position(|x| x.is_none()) else { + return false; + }; + self.data[position] = Some((key, v)); + true + } + + fn get_voice(&self, key: VoiceKey) -> Option>> { + self.data.iter().find_map(|x| { + x.as_ref() + .and_then(|(vkey, id)| (*vkey == key).then_some(*id)) + }) + } + + fn get_voice_by_poly_id(&self, voice_id: i32) -> Option>> { + self.data + .iter() + .flatten() + .find_map(|(vkey, id)| (vkey.voice_id == Some(voice_id)).then_some(*id)) + } + + fn remove_voice(&mut self, key: VoiceKey) -> Option<(VoiceKey, VoiceId>)> { + let position = self + .data + .iter() + .position(|x| x.as_ref().is_some_and(|(vkey, _)| *vkey == key))?; + self.data[position].take() + } +} + +type SynthSample = f32; + +#[derive(Debug)] +pub struct PolysynthPlugin { + dsp: BlockAdapter>, + params: Arc, + voice_id_map: VoiceIdMap, +} + +impl Default for PolysynthPlugin { + fn default() -> Self { + const DEFAULT_SAMPLERATE: f32 = 44100.; + let params = Arc::new(PolysynthParams::default()); + Self { + dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, ¶ms)), + params, + voice_id_map: VoiceIdMap::default(), + } + } +} + +impl Plugin for PolysynthPlugin { + const NAME: &'static str = "Polysynth"; + const VENDOR: &'static str = "SolarLiner"; + const URL: &'static str = "https://github.com/SolarLiner/valib"; + const EMAIL: &'static str = "me@solarliner.dev"; + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { + main_input_channels: NonZeroU32::new(0), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }]; + const MIDI_INPUT: MidiConfig = MidiConfig::Basic; + const SAMPLE_ACCURATE_AUTOMATION: bool = true; + type SysExMessage = (); + type BackgroundTask = (); + + fn params(&self) -> Arc { + self.params.clone() + } + + fn reset(&mut self) { + self.dsp.reset(); + } + + fn process( + &mut self, + buffer: &mut Buffer, + _: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext, + ) -> ProcessStatus { + let num_samples = buffer.samples(); + let sample_rate = context.transport().sample_rate; + let output = buffer.as_slice(); + + let mut next_event = context.next_event(); + let mut block_start: usize = 0; + let mut block_end: usize = MAX_BUFFER_SIZE.min(num_samples); + while block_start < num_samples { + 'events: loop { + match next_event { + Some(event) if (event.timing() as usize) <= block_start => match event { + NoteEvent::NoteOn { + voice_id, + channel, + note, + velocity, + .. + } => { + let key = VoiceKey::new(voice_id, channel, note); + let note_data = NoteData::from_midi(note, velocity); + let id = self.dsp.note_on(note_data); + nih_log!("Note on {id} <- {key:?}"); + self.voice_id_map.add_voice(key, id); + } + NoteEvent::NoteOff { + voice_id, + channel, + note, + velocity, + .. + } => { + let key = VoiceKey::new(voice_id, channel, note); + if let Some((_, id)) = self.voice_id_map.remove_voice(key) { + nih_log!("Note off {id} <- {key:?}"); + self.dsp.note_off(id, velocity); + } else { + nih_log!("Note off {key:?}: ID not found"); + } + } + NoteEvent::Choke { + voice_id, + channel, + note, + .. + } => { + let key = VoiceKey::new(voice_id, channel, note); + if let Some((_, id)) = self.voice_id_map.remove_voice(key) { + self.dsp.choke(id); + } + } + NoteEvent::PolyModulation { voice_id, .. } => { + if let Some(id) = self.voice_id_map.get_voice_by_poly_id(voice_id) { + nih_log!("TODO: Poly modulation ({id})"); + } + } + NoteEvent::MonoAutomation { + poly_modulation_id, + normalized_value, + .. + } => match poly_modulation_id { + POLYMOD_FILTER_CUTOFF => { + let target_plain_value = self + .params + .filter_params + .cutoff + .preview_plain(normalized_value); + self.params + .filter_params + .cutoff + .smoothed + .set_target(sample_rate, target_plain_value); + } + _ => { + for i in 0..2 { + match poly_modulation_id { + id if id == POLYMOD_OSC_PITCH_COARSE[i] => { + let target_plain_value = self.params.osc_params[i] + .pitch_coarse + .preview_plain(normalized_value); + self.params.osc_params[i] + .pitch_coarse + .smoothed + .set_target(sample_rate, target_plain_value); + } + id if id == POLYMOD_OSC_PITCH_FINE[i] => { + let target_plain_value = self.params.osc_params[i] + .pitch_fine + .preview_plain(normalized_value); + self.params.osc_params[i] + .pitch_fine + .smoothed + .set_target(sample_rate, target_plain_value); + } + id if id == POLYMOD_OSC_AMP[i] => { + let target_plain_value = self.params.osc_params[i] + .amplitude + .preview_plain(normalized_value); + self.params.osc_params[i] + .amplitude + .smoothed + .set_target(sample_rate, target_plain_value); + } + id => nih_error!("Unknown poly ID {id}"), + } + } + } + }, + _ => {} + }, + Some(event) if (event.timing() as usize) < block_end => { + block_end = event.timing() as usize; + break 'events; + } + _ => break 'events, + } + next_event = context.next_event(); + } + let dsp_block = AudioBufferMut::from(&mut output[0][block_start..block_end]); + let input = AudioBufferRef::::empty(dsp_block.samples()); + self.dsp.process_block(input, dsp_block); + + block_start = block_end; + block_end = (block_start + MAX_BUFFER_SIZE).min(num_samples); + } + + self.dsp.0.clean_inactive_voices(); + ProcessStatus::Normal + } +} + +impl Vst3Plugin for PolysynthPlugin { + const VST3_CLASS_ID: [u8; 16] = *b"VaLibPlySynTHSLN"; + const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[ + Vst3SubCategory::Synth, + Vst3SubCategory::Instrument, + Vst3SubCategory::Mono, + ]; +} + +impl ClapPlugin for PolysynthPlugin { + const CLAP_ID: &'static str = "dev.solarliner.valib.polysynth"; + const CLAP_DESCRIPTION: Option<&'static str> = option_env!("CARGO_PKG_DESCRIPTION"); + const CLAP_MANUAL_URL: Option<&'static str> = option_env!("CARGO_PKG_MANIFEST_URL"); + const CLAP_SUPPORT_URL: Option<&'static str> = None; + const CLAP_FEATURES: &'static [ClapFeature] = &[ + ClapFeature::Synthesizer, + ClapFeature::Instrument, + ClapFeature::Mono, + ]; + const CLAP_POLY_MODULATION_CONFIG: Option = Some(PolyModulationConfig { + // If the plugin's voice capacity changes at runtime (for instance, when switching to a + // monophonic mode), then the plugin should inform the host in the `initialize()` function + // as well as in the `process()` function if it changes at runtime using + // `context.set_current_voice_capacity()` + max_voice_capacity: NUM_VOICES as _, + // This enables voice stacking in Bitwig. + supports_overlapping_voices: true, + }); +} + +nih_export_clap!(PolysynthPlugin); +nih_export_vst3!(PolysynthPlugin); From 2b747e751d0323d4ff194c8a112c7f9cf951fac4 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 14:10:08 +0200 Subject: [PATCH 34/67] refactor: move RMS type to valib-core --- crates/valib-core/src/util.rs | 32 ++++++++++++++++++++++++++++++- plugins/ts404/src/dsp/clipping.rs | 3 ++- plugins/ts404/src/util.rs | 32 ------------------------------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/crates/valib-core/src/util.rs b/crates/valib-core/src/util.rs index f8827db..65daecb 100644 --- a/crates/valib-core/src/util.rs +++ b/crates/valib-core/src/util.rs @@ -7,7 +7,8 @@ use nalgebra::{ }; use num_traits::{AsPrimitive, Float, Zero}; use numeric_literals::replace_float_literals; -use simba::simd::SimdValue; +use simba::simd::{SimdComplexField, SimdValue}; +use std::collections::VecDeque; /// Transmutes a slice into a slice of static arrays, putting the remainder of the slice not fitting /// as a separate slice. @@ -264,3 +265,32 @@ pub fn vector_view_mut>( #[cfg(feature = "test-utils")] pub mod tests; + +#[derive(Debug, Clone)] +pub struct Rms { + data: VecDeque, + summed_squared: T, +} + +impl Rms { + pub fn new(size: usize) -> Self { + Self { + data: (0..size).map(|_| T::zero()).collect(), + summed_squared: T::zero(), + } + } +} + +impl Rms { + pub fn add_element(&mut self, value: T) -> T { + let v2 = value.simd_powi(2); + self.summed_squared -= self.data.pop_front().unwrap(); + self.summed_squared += v2; + self.data.push_back(v2); + self.get_rms() + } + + pub fn get_rms(&self) -> T { + self.summed_squared.simd_sqrt() + } +} diff --git a/plugins/ts404/src/dsp/clipping.rs b/plugins/ts404/src/dsp/clipping.rs index 3605662..a0fbb47 100644 --- a/plugins/ts404/src/dsp/clipping.rs +++ b/plugins/ts404/src/dsp/clipping.rs @@ -1,4 +1,4 @@ -use crate::{util::Rms, TARGET_SAMPLERATE}; +use crate::TARGET_SAMPLERATE; use nih_plug::prelude::AtomicF32; use nih_plug::util::db_to_gain_fast; use num_traits::{Float, ToPrimitive}; @@ -8,6 +8,7 @@ use valib::math::smooth_clamp; use valib::saturators::clippers::DiodeClipper; use valib::saturators::{Saturator, Slew}; use valib::simd::SimdValue; +use valib::util::Rms; use valib::wdf::dsl::*; use valib::{ dsp::{DSPMeta, DSPProcess}, diff --git a/plugins/ts404/src/util.rs b/plugins/ts404/src/util.rs index 0e10379..8b13789 100644 --- a/plugins/ts404/src/util.rs +++ b/plugins/ts404/src/util.rs @@ -1,33 +1 @@ -use std::collections::VecDeque; -use num_traits::Zero; -use valib::Scalar; - -#[derive(Debug, Clone)] -pub struct Rms { - data: VecDeque, - summed_squared: T, -} - -impl Rms { - pub fn new(size: usize) -> Self { - Self { - data: (0..size).map(|_| T::zero()).collect(), - summed_squared: T::zero(), - } - } -} - -impl Rms { - pub fn add_element(&mut self, value: T) -> T { - let v2 = value.simd_powi(2); - self.summed_squared -= self.data.pop_front().unwrap(); - self.summed_squared += v2; - self.data.push_back(v2); - self.get_rms() - } - - pub fn get_rms(&self) -> T { - self.summed_squared.simd_sqrt() - } -} From f086e9e9b097e832365684310a68cc76a41c5e5d Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 14:10:52 +0200 Subject: [PATCH 35/67] feat(examples): polysynth prototype gui --- Cargo.lock | 1 + examples/polysynth/Cargo.toml | 1 + examples/polysynth/src/dsp.rs | 3 +- examples/polysynth/src/editor.rs | 72 ++++++++++++++++++++++++++++++++ examples/polysynth/src/lib.rs | 21 +++++++++- examples/polysynth/src/params.rs | 4 ++ 6 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 examples/polysynth/src/editor.rs diff --git a/Cargo.lock b/Cargo.lock index fe7e84d..daa8d26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3605,6 +3605,7 @@ name = "polysynth" version = "0.1.0" dependencies = [ "nih_plug", + "nih_plug_vizia", "num-traits", "valib", ] diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml index 17c16da..473096e 100644 --- a/examples/polysynth/Cargo.toml +++ b/examples/polysynth/Cargo.toml @@ -15,4 +15,5 @@ crate-type = ["lib", "cdylib"] [dependencies] valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} nih_plug = { workspace = true, features = ["standalone"] } +nih_plug_vizia.workspace = true num-traits.workspace = true \ No newline at end of file diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 2240c5d..fef3d53 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -190,7 +190,8 @@ impl DSPProcess<0, 1> for RawVoice { 4f64 * self.filter_params.resonance.smoothed.next() as f64, )); let vca = self.gate.next_sample_as::(); - self.filter.process(filter_in).map(|x| vca * x) + let static_amp = T::from_f64(0.25); + self.filter.process(filter_in).map(|x| static_amp * vca * x) } } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs new file mode 100644 index 0000000..5462092 --- /dev/null +++ b/examples/polysynth/src/editor.rs @@ -0,0 +1,72 @@ +use crate::params::PolysynthParams; +use nih_plug::prelude::{util, AtomicF32, Editor}; +use nih_plug_vizia::vizia::prelude::*; +use nih_plug_vizia::widgets::*; +use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Lens)] +struct Data { + params: Arc, +} + +impl Model for Data {} + +// Makes sense to also define this here, makes it a bit easier to keep track of +pub(crate) fn default_state() -> Arc { + ViziaState::new(|| (750, 550)) +} + +pub(crate) fn create( + params: Arc, + editor_state: Arc, +) -> Option> { + create_vizia_editor(editor_state, ViziaTheming::Custom, move |cx, _| { + assets::register_noto_sans_light(cx); + assets::register_noto_sans_thin(cx); + + Data { + params: params.clone(), + } + .build(cx); + + VStack::new(cx, |cx| { + Label::new(cx, "Polysynth") + .font_weight(FontWeightKeyword::Thin) + .font_size(30.0) + .height(Pixels(50.0)) + .child_top(Stretch(1.0)) + .child_bottom(Pixels(0.0)); + HStack::new(cx, |cx| { + for ix in 0..2 { + let p = Data::params.map(move |p| p.osc_params[ix].clone()); + VStack::new(cx, |cx| { + Label::new(cx, &format!("Oscillator {}", ix + 1)) + .font_size(22.) + .height(Pixels(30.)) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, p).width(Percentage(100.)); + }) + .width(Stretch(1.0)); + } + }) + .row_between(Pixels(0.0)); + + VStack::new(cx, |cx| { + Label::new(cx, "Filter") + .font_size(22.) + .height(Pixels(30.)) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())) + .width(Percentage(100.)); + }); + }) + .width(Percentage(100.)) + .height(Percentage(100.)) + .row_between(Pixels(0.0)); + + ResizeHandle::new(cx); + }) +} diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 0d5eb58..3775170 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -5,12 +5,14 @@ use nih_plug::params::Params; use nih_plug::plugin::ProcessStatus; use nih_plug::prelude::*; use std::cmp::Ordering; -use std::sync::Arc; +use std::sync::{atomic, Arc}; use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock}; +use valib::util::Rms; use valib::voice::{NoteData, VoiceId, VoiceManager}; mod dsp; +mod editor; mod params; const NUM_VOICES: usize = 16; @@ -158,6 +160,21 @@ impl Plugin for PolysynthPlugin { self.dsp.reset(); } + fn initialize( + &mut self, + _: &AudioIOLayout, + buffer_config: &BufferConfig, + _: &mut impl InitContext, + ) -> bool { + let sample_rate = buffer_config.sample_rate; + self.dsp.set_samplerate(sample_rate); + true + } + + fn editor(&mut self, _: AsyncExecutor) -> Option> { + editor::create(self.params.clone(), self.params.editor_state.clone()) + } + fn process( &mut self, buffer: &mut Buffer, @@ -266,7 +283,7 @@ impl Plugin for PolysynthPlugin { .smoothed .set_target(sample_rate, target_plain_value); } - id => nih_error!("Unknown poly ID {id}"), + _ => {} } } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index e2fad43..bb8b017 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -4,6 +4,7 @@ use crate::{ }; use nih_plug::prelude::*; use nih_plug::util::{db_to_gain, MINUS_INFINITY_DB}; +use nih_plug_vizia::ViziaState; use std::sync::Arc; use valib::dsp::parameter::{ParamId, ParamName}; @@ -148,6 +149,8 @@ pub struct PolysynthParams { #[nested] pub filter_params: Arc, pub oversample: Arc, + #[persist = "editor"] + pub editor_state: Arc, } impl Default for PolysynthParams { @@ -157,6 +160,7 @@ impl Default for PolysynthParams { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), oversample, + editor_state: crate::editor::default_state(), } } } From 08ef2171a925635805ea0bae4f20420d5f4979fc Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 15:26:42 +0200 Subject: [PATCH 36/67] fix(oscillators): square polyblep correct implementation --- crates/valib-oscillators/src/polyblep.rs | 14 +++++----- crates/valib-voice/src/polyphonic.rs | 33 +++++++++++++++++++----- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/valib-oscillators/src/polyblep.rs b/crates/valib-oscillators/src/polyblep.rs index 2495c6e..d99dd5b 100644 --- a/crates/valib-oscillators/src/polyblep.rs +++ b/crates/valib-oscillators/src/polyblep.rs @@ -4,6 +4,7 @@ use std::marker::PhantomData; use valib_core::dsp::blocks::P1; use valib_core::dsp::{DSPMeta, DSPProcess}; use valib_core::simd::SimdBool; +use valib_core::util::lerp; use valib_core::Scalar; pub struct PolyBLEP { @@ -34,7 +35,7 @@ impl PolyBLEP { } pub trait PolyBLEPOscillator: DSPMeta { - fn bleps() -> impl IntoIterator>; + fn bleps(&self) -> impl IntoIterator>; fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample; } @@ -62,7 +63,7 @@ impl DSPProcess<0, 1> for PolyBLEPDriver { fn process(&mut self, _: [Self::Sample; 0]) -> [Self::Sample; 1] { let [phase] = self.phasor.process([]); let mut y = self.blep.naive_eval(phase); - for blep in Osc::bleps() { + for blep in self.blep.bleps() { y += blep.eval(self.phasor.step, phase); } [y] @@ -101,7 +102,7 @@ impl DSPMeta for SawBLEP { } impl PolyBLEPOscillator for SawBLEP { - fn bleps() -> impl IntoIterator> { + fn bleps(&self) -> impl IntoIterator> { [PolyBLEP { amplitude: -T::ONE, phase: T::ZERO, @@ -139,7 +140,7 @@ impl DSPMeta for SquareBLEP { } impl PolyBLEPOscillator for SquareBLEP { - fn bleps() -> impl IntoIterator> { + fn bleps(&self) -> impl IntoIterator> { [ PolyBLEP { amplitude: T::ONE, @@ -147,13 +148,14 @@ impl PolyBLEPOscillator for SquareBLEP { }, PolyBLEP { amplitude: -T::ONE, - phase: T::from_f64(0.5), + phase: T::one() - self.pw, }, ] } fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { - T::from_f64(2.0) * phase - T::one() + let dc_offset = lerp(self.pw, -T::ONE, T::ONE); + phase.simd_gt(self.pw).if_else(T::one, || -T::one()) + dc_offset } } diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index 61d4f95..afccb4b 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -12,6 +12,7 @@ use valib_core::dsp::{DSPMeta, DSPProcess}; pub struct Polyphonic { create_voice: Box) -> V>, voice_pool: Box<[Option]>, + active_voices: usize, next_voice: usize, samplerate: f32, } @@ -49,6 +50,7 @@ impl Polyphonic { create_voice: Box::new(create_voice), next_voice: 0, voice_pool: (0..voice_capacity).map(|_| None).collect(), + active_voices: 0, samplerate, } } @@ -58,6 +60,7 @@ impl Polyphonic { for slot in &mut self.voice_pool { if slot.as_ref().is_some_and(|v| !v.active()) { slot.take(); + self.active_voices -= 1; } } } @@ -108,17 +111,31 @@ impl VoiceManager for Polyphonic { } fn note_on(&mut self, note_data: NoteData) -> Self::ID { - let id = self.next_voice; - self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); + if self.active_voices == self.capacity() { + // At capacity, we must steal a voice + let id = self.next_voice; + + if let Some(voice) = &mut self.voice_pool[id] { + *voice.note_data_mut() = note_data; + voice.reuse(); + } else { + self.voice_pool[id] = Some((self.create_voice)(self.samplerate, note_data)); + } - if let Some(voice) = &mut self.voice_pool[id] { - *voice.note_data_mut() = note_data; - voice.reuse(); + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); + id } else { + // Find first available slot + while self.voice_pool[self.next_voice].is_some() { + self.next_voice += 1; + } + + let id = self.next_voice; self.voice_pool[id] = Some((self.create_voice)(self.samplerate, note_data)); + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); + self.active_voices += 1; + id } - - id } fn note_off(&mut self, id: Self::ID, release_velocity: f32) { @@ -129,10 +146,12 @@ impl VoiceManager for Polyphonic { fn choke(&mut self, id: Self::ID) { self.voice_pool[id] = None; + self.active_voices -= 1; } fn panic(&mut self) { self.voice_pool.fill_with(|| None); + self.active_voices = 0; } } From e9d4f87215a1883ec17e9b347bcc2fa1ded28f0b Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 15 Sep 2024 15:27:48 +0200 Subject: [PATCH 37/67] feat(examples): polysynth: output level and filter keyboard tracking --- examples/polysynth/src/dsp.rs | 74 +++++++++++++++++++------------- examples/polysynth/src/editor.rs | 53 +++++++++++++---------- examples/polysynth/src/lib.rs | 10 ++--- examples/polysynth/src/params.rs | 31 +++++++++++++ 4 files changed, 110 insertions(+), 58 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index fef3d53..c6bf7bd 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -50,6 +50,12 @@ impl PolyOsc { _ => false, } } + + pub fn set_pulse_width(&mut self, pw: T) { + if let Self::Square(sq) = self { + sq.blep.set_pulse_width(pw) + } + } } impl DSPMeta for PolyOsc { @@ -101,8 +107,7 @@ pub struct RawVoice { osc: [PolyOsc; 2], osc_out_sat: bjt::CommonCollector, filter: Ladder>, - osc_params: [Arc; 2], - filter_params: Arc, + params: Arc, gate: SmoothedParam, note_data: NoteData, samplerate: T, @@ -111,7 +116,7 @@ pub struct RawVoice { impl RawVoice { pub(crate) fn update_osc_types(&mut self) { for i in 0..2 { - let params = &self.osc_params[i]; + let params = &self.params.osc_params[i]; let shape = params.shape.value(); let osc = &mut self.osc[i]; if !osc.is_osc_shape(shape) { @@ -124,7 +129,7 @@ impl RawVoice { impl Voice for RawVoice { fn active(&self) -> bool { - self.gate.current_value() > 0.5 + self.gate.current_value() > 1e-4 } fn note_data(&self) -> &NoteData { @@ -166,31 +171,44 @@ impl DSPMeta for RawVoice { impl DSPProcess<0, 1> for RawVoice { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + // Process oscillators let frequency = self.note_data.frequency; + let osc_params = self.params.osc_params.clone(); + let filter_params = self.params.filter_params.clone(); self.update_osc_types(); - let osc1_freq = frequency - * T::from_f64(semitone_to_ratio( - self.osc_params[0].pitch_coarse.value() + self.osc_params[0].pitch_fine.value(), - ) as _); - let osc2_freq = frequency - * T::from_f64(semitone_to_ratio( - self.osc_params[1].pitch_coarse.value() + self.osc_params[1].pitch_fine.value(), - ) as _); - let [osc1] = self.osc[0].process([osc1_freq]); - let [osc2] = self.osc[1].process([osc2_freq]); - let osc_mixer = osc1 * T::from_f64(self.osc_params[0].amplitude.smoothed.next() as _) - + osc2 * T::from_f64(self.osc_params[1].amplitude.smoothed.next() as _); + let [osc1, osc2] = std::array::from_fn(|i| { + let osc = &mut self.osc[i]; + let params = &self.params.osc_params[i]; + let osc_freq = frequency + * T::from_f64(semitone_to_ratio( + params.pitch_coarse.value() + params.pitch_fine.value(), + ) as _); + osc.set_pulse_width(T::from_f64(params.pulse_width.smoothed.next() as _)); + let [osc] = osc.process([osc_freq]); + osc + }); + + // Process filter input + let osc_mixer = osc1 * T::from_f64(osc_params[0].amplitude.smoothed.next() as _) + + osc2 * T::from_f64(osc_params[1].amplitude.smoothed.next() as _); let filter_in = self .osc_out_sat .process([osc_mixer]) .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); - self.filter - .set_cutoff(T::from_f64(self.filter_params.cutoff.smoothed.next() as _)); + + let freq_ratio = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) + * frequency + / T::from_f64(440.); + let filter_freq = + (T::one() + freq_ratio) * T::from_f64(filter_params.cutoff.smoothed.next() as _); + + // Process filter + self.filter.set_cutoff(filter_freq); self.filter.set_resonance(T::from_f64( - 4f64 * self.filter_params.resonance.smoothed.next() as f64, + 4f64 * filter_params.resonance.smoothed.next() as f64, )); let vca = self.gate.next_sample_as::(); - let static_amp = T::from_f64(0.25); + let static_amp = T::from_f64(self.params.output_level.smoothed.next() as _); self.filter.process(filter_in).map(|x| static_amp * vca * x) } } @@ -201,11 +219,12 @@ pub type VoiceManager = Polyphonic>; pub fn create_voice_manager( samplerate: f32, - osc_params: [Arc; 2], - filter_params: Arc, + params: Arc, ) -> VoiceManager { let target_samplerate_f64 = OVERSAMPLE as f64 * samplerate as f64; let target_samplerate = T::from_f64(target_samplerate_f64); + let osc_params = params.osc_params.clone(); + let filter_params = params.filter_params.clone(); Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { SampleAdapter::new(UpsampledVoice::new( 2, @@ -221,19 +240,18 @@ pub fn create_voice_manager( pulse_width, ) }), - osc_params: osc_params.clone(), filter: Ladder::new( target_samplerate_f64, T::from_f64(filter_params.cutoff.value() as _), T::from_f64(filter_params.resonance.value() as _), ), - filter_params: filter_params.clone(), osc_out_sat: bjt::CommonCollector { vee: -T::ONE, vcc: T::ONE, xbias: T::from_f64(0.1), ybias: T::from_f64(-0.1), }, + params: params.clone(), gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), note_data, samplerate: target_samplerate, @@ -246,11 +264,7 @@ pub type Dsp = VoiceManager; pub fn create( samplerate: f32, - params: &PolysynthParams, + params: Arc, ) -> Dsp { - create_voice_manager( - samplerate, - params.osc_params.clone(), - params.filter_params.clone(), - ) + create_voice_manager(samplerate, params) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index 5462092..c950de8 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -1,11 +1,9 @@ use crate::params::PolysynthParams; -use nih_plug::prelude::{util, AtomicF32, Editor}; +use nih_plug::prelude::Editor; use nih_plug_vizia::vizia::prelude::*; use nih_plug_vizia::widgets::*; use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; -use std::sync::atomic::Ordering; use std::sync::Arc; -use std::time::Duration; #[derive(Lens)] struct Data { @@ -33,26 +31,28 @@ pub(crate) fn create( .build(cx); VStack::new(cx, |cx| { - Label::new(cx, "Polysynth") - .font_weight(FontWeightKeyword::Thin) - .font_size(30.0) - .height(Pixels(50.0)) - .child_top(Stretch(1.0)) - .child_bottom(Pixels(0.0)); - HStack::new(cx, |cx| { - for ix in 0..2 { - let p = Data::params.map(move |p| p.osc_params[ix].clone()); - VStack::new(cx, |cx| { - Label::new(cx, &format!("Oscillator {}", ix + 1)) - .font_size(22.) - .height(Pixels(30.)) - .child_bottom(Pixels(8.)); - GenericUi::new(cx, p).width(Percentage(100.)); - }) - .width(Stretch(1.0)); - } - }) - .row_between(Pixels(0.0)); + VStack::new(cx, |cx| { + Label::new(cx, "Polysynth") + .font_weight(FontWeightKeyword::Thin) + .font_size(30.0) + .height(Pixels(50.0)) + .child_top(Stretch(1.0)) + .child_bottom(Pixels(0.0)); + HStack::new(cx, |cx| { + for ix in 0..2 { + let p = Data::params.map(move |p| p.osc_params[ix].clone()); + VStack::new(cx, |cx| { + Label::new(cx, &format!("Oscillator {}", ix + 1)) + .font_size(22.) + .height(Pixels(30.)) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, p).width(Percentage(100.)); + }) + .width(Stretch(1.0)); + } + }) + .row_between(Pixels(0.0)); + }); VStack::new(cx, |cx| { Label::new(cx, "Filter") @@ -62,6 +62,13 @@ pub(crate) fn create( GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())) .width(Percentage(100.)); }); + + VStack::new(cx, |cx| { + HStack::new(cx, |cx| { + Label::new(cx, "Output Level").width(Stretch(1.)); + ParamSlider::new(cx, Data::params, |p| &p.output_level).width(Stretch(1.)); + }); + }); }) .width(Percentage(100.)) .height(Percentage(100.)) diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 3775170..9fe44c9 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -129,7 +129,7 @@ impl Default for PolysynthPlugin { const DEFAULT_SAMPLERATE: f32 = 44100.; let params = Arc::new(PolysynthParams::default()); Self { - dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, ¶ms)), + dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, params.clone())), params, voice_id_map: VoiceIdMap::default(), } @@ -156,8 +156,8 @@ impl Plugin for PolysynthPlugin { self.params.clone() } - fn reset(&mut self) { - self.dsp.reset(); + fn editor(&mut self, _: AsyncExecutor) -> Option> { + editor::create(self.params.clone(), self.params.editor_state.clone()) } fn initialize( @@ -171,8 +171,8 @@ impl Plugin for PolysynthPlugin { true } - fn editor(&mut self, _: AsyncExecutor) -> Option> { - editor::create(self.params.clone(), self.params.editor_state.clone()) + fn reset(&mut self) { + self.dsp.reset(); } fn process( diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index bb8b017..eff006f 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -102,6 +102,8 @@ pub struct FilterParams { pub cutoff: FloatParam, #[id = "res"] pub resonance: FloatParam, + #[id = "kt"] + pub keyboard_tracking: FloatParam, } impl FilterParams { @@ -138,6 +140,18 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + keyboard_tracking: FloatParam::new( + "Keyboard Tracking", + 0.5, + FloatRange::Linear { min: 0., max: 2. }, + ) + .with_unit(" %") + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), } } } @@ -148,6 +162,8 @@ pub struct PolysynthParams { pub osc_params: [Arc; 2], #[nested] pub filter_params: Arc, + #[id = "out"] + pub output_level: FloatParam, pub oversample: Arc, #[persist = "editor"] pub editor_state: Arc, @@ -159,6 +175,21 @@ impl Default for PolysynthParams { Self { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), + output_level: FloatParam::new( + "Output Level", + 0.25, + FloatRange::Skewed { + min: 0.0, + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.), + }, + ) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(50.), + )), oversample, editor_state: crate::editor::default_state(), } From 0ca9780113adf5cd0a60fc1e57ce16acf1f50b2f Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 13:23:28 +0200 Subject: [PATCH 38/67] feat(examples): polysynth: phase retriggering option --- Cargo.lock | 11 ++ crates/valib-oscillators/src/lib.rs | 13 +++ crates/valib-oscillators/src/polyblep.rs | 6 +- examples/polysynth/Cargo.toml | 2 + examples/polysynth/src/dsp.rs | 134 +++++++++++++++-------- examples/polysynth/src/params.rs | 3 + 6 files changed, 121 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index daa8d26..efac4e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1611,6 +1611,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fastrand-contrib" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb6c045880cda8f657f4859baf534963ff0595e2dcce0de5f52dcdf3076c290b" +dependencies = [ + "fastrand 2.1.1", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -3604,6 +3613,8 @@ dependencies = [ name = "polysynth" version = "0.1.0" dependencies = [ + "fastrand 2.1.1", + "fastrand-contrib", "nih_plug", "nih_plug_vizia", "num-traits", diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 9aa04b2..4427dc1 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -63,6 +63,19 @@ impl Phasor { } } + pub fn phase(&self) -> T { + self.phase + } + + pub fn set_phase(&mut self, phase: T) { + self.phase = phase.simd_fract(); + } + + pub fn with_phase(mut self, phase: T) -> Self { + self.set_phase(phase); + self + } + /// Sets the frequency of this phasor. Phase is not reset, which means the phase remains /// continuous. /// # Arguments diff --git a/crates/valib-oscillators/src/polyblep.rs b/crates/valib-oscillators/src/polyblep.rs index d99dd5b..eb132e9 100644 --- a/crates/valib-oscillators/src/polyblep.rs +++ b/crates/valib-oscillators/src/polyblep.rs @@ -185,8 +185,10 @@ impl DSPProcess<0, 1> for Triangle { } impl Triangle { - pub fn new(samplerate: T, frequency: T) -> Self { - let square = PolyBLEPDriver::new(samplerate, frequency, SquareBLEP::new(T::from_f64(0.5))); + pub fn new(samplerate: T, frequency: T, phase: T) -> Self { + let mut square = + PolyBLEPDriver::new(samplerate, frequency, SquareBLEP::new(T::from_f64(0.5))); + square.phasor.phase = phase; let integrator = P1::new(samplerate, frequency); Self { square, integrator } } diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml index 473096e..994d380 100644 --- a/examples/polysynth/Cargo.toml +++ b/examples/polysynth/Cargo.toml @@ -13,6 +13,8 @@ keywords.workspace = true crate-type = ["lib", "cdylib"] [dependencies] +fastrand = { version = "2.1.1", default-features = false } +fastrand-contrib = { version = "0.1.0", default-features = false } valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} nih_plug = { workspace = true, features = ["standalone"] } nih_plug_vizia.workspace = true diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index c6bf7bd..3998a2e 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,8 +1,11 @@ -use crate::params::{FilterParams, OscParams, OscShape, PolysynthParams}; +use crate::params::{OscShape, PolysynthParams}; use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use fastrand::Rng; +use fastrand_contrib::RngExt; use nih_plug::nih_log; use nih_plug::util::db_to_gain_fast; use num_traits::{ConstOne, ConstZero}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::parameter::SmoothedParam; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; @@ -24,20 +27,35 @@ pub enum PolyOsc { } impl PolyOsc { - fn new(samplerate: T, shape: OscShape, note_data: NoteData, pulse_width: T) -> Self { + fn new( + samplerate: T, + shape: OscShape, + note_data: NoteData, + pulse_width: T, + phase: T, + ) -> Self { match shape { - OscShape::Sine => Self::Sine(Phasor::new(samplerate, note_data.frequency)), - OscShape::Triangle => Self::Triangle(Triangle::new(samplerate, note_data.frequency)), - OscShape::Square => Self::Square(Square::new( - samplerate, - note_data.frequency, - SquareBLEP::new(pulse_width), - )), - OscShape::Saw => Self::Sawtooth(Sawtooth::new( - samplerate, - note_data.frequency, - SawBLEP::default(), - )), + OscShape::Sine => { + Self::Sine(Phasor::new(samplerate, note_data.frequency).with_phase(phase)) + } + OscShape::Triangle => { + Self::Triangle(Triangle::new(samplerate, note_data.frequency, phase)) + } + OscShape::Square => { + let mut square = Square::new( + samplerate, + note_data.frequency, + SquareBLEP::new(pulse_width), + ); + square.phasor.set_phase(phase); + Self::Square(square) + } + OscShape::Saw => { + let mut sawtooth = + Sawtooth::new(samplerate, note_data.frequency, SawBLEP::default()); + sawtooth.phasor.set_phase(phase); + Self::Sawtooth(sawtooth) + } } } @@ -111,17 +129,66 @@ pub struct RawVoice { gate: SmoothedParam, note_data: NoteData, samplerate: T, + rng: Rng, } impl RawVoice { - pub(crate) fn update_osc_types(&mut self) { + fn create_voice( + target_samplerate_f64: f64, + params: Arc, + note_data: NoteData, + ) -> Self { + static VOICE_SEED: AtomicU64 = AtomicU64::new(0); + let target_samplerate = T::from_f64(target_samplerate_f64); + let mut rng = Rng::with_seed(VOICE_SEED.fetch_add(1, Ordering::SeqCst)); + RawVoice { + osc: std::array::from_fn(|i| { + let osc_param = ¶ms.osc_params[i]; + let pulse_width = T::from_f64(osc_param.pulse_width.value() as _); + PolyOsc::new( + target_samplerate, + osc_param.shape.value(), + note_data, + pulse_width, + if osc_param.retrigger.value() { + T::zero() + } else { + T::from_f64(rng.f64_range(0.0..1.0)) + }, + ) + }), + filter: Ladder::new( + target_samplerate_f64, + T::from_f64(params.filter_params.cutoff.value() as _), + T::from_f64(params.filter_params.resonance.value() as _), + ), + osc_out_sat: bjt::CommonCollector { + vee: -T::ONE, + vcc: T::ONE, + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, + params: params.clone(), + gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + note_data, + samplerate: target_samplerate, + rng, + } + } + + fn update_osc_types(&mut self) { for i in 0..2 { let params = &self.params.osc_params[i]; let shape = params.shape.value(); let osc = &mut self.osc[i]; if !osc.is_osc_shape(shape) { let pulse_width = T::from_f64(params.pulse_width.value() as _); - *osc = PolyOsc::new(self.samplerate, shape, self.note_data, pulse_width); + let phase = if params.retrigger.value() { + T::zero() + } else { + T::from_f64(self.rng.f64_range(0.0..1.0)) + }; + *osc = PolyOsc::new(self.samplerate, shape, self.note_data, pulse_width, phase); } } } @@ -221,41 +288,16 @@ pub fn create_voice_manager( samplerate: f32, params: Arc, ) -> VoiceManager { - let target_samplerate_f64 = OVERSAMPLE as f64 * samplerate as f64; - let target_samplerate = T::from_f64(target_samplerate_f64); - let osc_params = params.osc_params.clone(); - let filter_params = params.filter_params.clone(); + let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { SampleAdapter::new(UpsampledVoice::new( 2, MAX_BUFFER_SIZE, - BlockAdapter(RawVoice { - osc: std::array::from_fn(|i| { - let osc_param = &osc_params[i]; - let pulse_width = T::from_f64(osc_param.pulse_width.value() as _); - PolyOsc::new( - target_samplerate, - osc_param.shape.value(), - note_data, - pulse_width, - ) - }), - filter: Ladder::new( - target_samplerate_f64, - T::from_f64(filter_params.cutoff.value() as _), - T::from_f64(filter_params.resonance.value() as _), - ), - osc_out_sat: bjt::CommonCollector { - vee: -T::ONE, - vcc: T::ONE, - xbias: T::from_f64(0.1), - ybias: T::from_f64(-0.1), - }, - params: params.clone(), - gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + BlockAdapter(RawVoice::create_voice( + target_samplerate, + params.clone(), note_data, - samplerate: target_samplerate, - }), + )), )) }) } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index eff006f..f08dfaf 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -28,6 +28,8 @@ pub struct OscParams { pub pitch_fine: FloatParam, #[id = "pw"] pub pulse_width: FloatParam, + #[id = "rtrg"] + pub retrigger: BoolParam, } impl OscParams { @@ -92,6 +94,7 @@ impl OscParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + retrigger: BoolParam::new("Retrigger", false), } } } From a027b3c4ea368f606c6ee32cac4339510a4d4b4b Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 15:23:49 +0200 Subject: [PATCH 39/67] feat(examples): polysynth: use oversample const everywhere and increase to 8x for voices --- examples/polysynth/src/dsp.rs | 2 +- examples/polysynth/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 3998a2e..d5ad4a6 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -291,7 +291,7 @@ pub fn create_voice_manager( let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { SampleAdapter::new(UpsampledVoice::new( - 2, + OVERSAMPLE, MAX_BUFFER_SIZE, BlockAdapter(RawVoice::create_voice( target_samplerate, diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 9fe44c9..0c86d28 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -16,7 +16,7 @@ mod editor; mod params; const NUM_VOICES: usize = 16; -const OVERSAMPLE: usize = 4; +const OVERSAMPLE: usize = 8; const MAX_BUFFER_SIZE: usize = 64; const POLYMOD_OSC_AMP: [u32; 2] = [0, 1]; From fc2b5488403c793f2a96184ddc06da8de3fdea5c Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 19:02:50 +0200 Subject: [PATCH 40/67] chore(core): provide type alias for sine interpolation --- crates/valib-core/src/math/interpolation.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/valib-core/src/math/interpolation.rs b/crates/valib-core/src/math/interpolation.rs index 14b6107..2cf2a0c 100644 --- a/crates/valib-core/src/math/interpolation.rs +++ b/crates/valib-core/src/math/interpolation.rs @@ -133,9 +133,12 @@ impl Interpolate for Linear { #[derive(Debug, Copy, Clone)] pub struct MappedLinear(pub F); +pub type Sine = MappedLinear T>; + /// Returns an interpolator that performs sine interpolation. -pub fn sine_interpolation() -> MappedLinear T> { - MappedLinear(|t| T::simd_cos(t * T::simd_pi())) +#[replace_float_literals(T::from_f64(literal))] +pub fn sine_interpolation() -> Sine { + MappedLinear(|t| 0.5 - 0.5 * T::simd_cos(t * T::simd_pi())) } impl T> Interpolate for MappedLinear From cc3c05e344afbe55f96f0417477fdd18d9cde370 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 19:03:11 +0200 Subject: [PATCH 41/67] fix(oscillators): make phasor phase reset consistent --- crates/valib-oscillators/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 4427dc1..20b8847 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -38,7 +38,7 @@ impl DSPProcess<0, 1> for Phasor { fn process(&mut self, _: [Self::Sample; 0]) -> [Self::Sample; 1] { let p = self.phase; let new_phase = self.phase + self.step; - let gt = new_phase.simd_gt(T::one()); + let gt = new_phase.simd_ge(T::one()); self.phase = (new_phase - T::one()).select(gt, new_phase); [p] } @@ -76,6 +76,10 @@ impl Phasor { self } + pub fn next_sample_resets(&self) -> T::SimdBool { + (self.phase + self.step).simd_ge(T::one()) + } + /// Sets the frequency of this phasor. Phase is not reset, which means the phase remains /// continuous. /// # Arguments From 7e9ab822243683b5896fa22f96b983795ae8393a Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 19:03:24 +0200 Subject: [PATCH 42/67] feat(examples): polysynth: drift parameter --- examples/polysynth/src/dsp.rs | 59 +++++++++++++++++++++++++++----- examples/polysynth/src/editor.rs | 4 +-- examples/polysynth/src/lib.rs | 6 ++-- examples/polysynth/src/params.rs | 9 ++++- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index d5ad4a6..8bbfb0e 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -10,15 +10,55 @@ use std::sync::Arc; use valib::dsp::parameter::SmoothedParam; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; use valib::filters::ladder::{Ladder, OTA}; +use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; use valib::saturators::{bjt, Tanh}; +use valib::simd::SimdBool; use valib::util::semitone_to_ratio; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; use valib::Scalar; +struct Drift { + rng: Rng, + phasor: Phasor, + last_value: T, + next_value: T, + interp: Sine, +} + +impl Drift { + pub fn new(mut rng: Rng, samplerate: T, frequency: T) -> Self { + let phasor = Phasor::new(samplerate, frequency); + let last_value = T::from_f64(rng.f64_range(-1.0..1.0)); + let next_value = T::from_f64(rng.f64_range(-1.0..1.0)); + Self { + rng, + phasor, + last_value, + next_value, + interp: sine_interpolation(), + } + } + + pub fn next_sample(&mut self) -> T { + let reset_mask = self.phasor.next_sample_resets(); + if reset_mask.any() { + self.last_value = reset_mask.if_else(|| self.next_value, || self.last_value); + self.next_value = reset_mask.if_else( + || T::from_f64(self.rng.f64_range(-1.0..1.0)), + || self.next_value, + ); + } + + let [t] = self.phasor.process([]); + self.interp + .interpolate(t, [self.last_value, self.next_value]) + } +} + pub enum PolyOsc { Sine(Phasor), Triangle(Triangle), @@ -121,19 +161,22 @@ impl DSPProcess<1, 1> for PolyOsc { } } +pub(crate) const NUM_OSCILLATORS: usize = 2; + pub struct RawVoice { - osc: [PolyOsc; 2], + osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, filter: Ladder>, params: Arc, gate: SmoothedParam, note_data: NoteData, + drift: [Drift; NUM_OSCILLATORS], samplerate: T, rng: Rng, } impl RawVoice { - fn create_voice( + fn new( target_samplerate_f64: f64, params: Arc, note_data: NoteData, @@ -171,6 +214,7 @@ impl RawVoice { params: params.clone(), gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), note_data, + drift: std::array::from_fn(|_| Drift::new(rng.fork(), target_samplerate_f64 as _, 0.2)), samplerate: target_samplerate, rng, } @@ -238,6 +282,7 @@ impl DSPMeta for RawVoice { impl DSPProcess<0, 1> for RawVoice { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + const DRIFT_MAX_ST: f32 = 0.1; // Process oscillators let frequency = self.note_data.frequency; let osc_params = self.params.osc_params.clone(); @@ -246,9 +291,11 @@ impl DSPProcess<0, 1> for RawVoice { let [osc1, osc2] = std::array::from_fn(|i| { let osc = &mut self.osc[i]; let params = &self.params.osc_params[i]; + let drift = &mut self.drift[i]; + let drift = drift.next_sample() * DRIFT_MAX_ST * params.drift.smoothed.next(); let osc_freq = frequency * T::from_f64(semitone_to_ratio( - params.pitch_coarse.value() + params.pitch_fine.value(), + params.pitch_coarse.value() + params.pitch_fine.value() + drift, ) as _); osc.set_pulse_width(T::from_f64(params.pulse_width.smoothed.next() as _)); let [osc] = osc.process([osc_freq]); @@ -293,11 +340,7 @@ pub fn create_voice_manager( SampleAdapter::new(UpsampledVoice::new( OVERSAMPLE, MAX_BUFFER_SIZE, - BlockAdapter(RawVoice::create_voice( - target_samplerate, - params.clone(), - note_data, - )), + BlockAdapter(RawVoice::new(target_samplerate, params.clone(), note_data)), )) }) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index c950de8..84770f8 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -14,7 +14,7 @@ impl Model for Data {} // Makes sense to also define this here, makes it a bit easier to keep track of pub(crate) fn default_state() -> Arc { - ViziaState::new(|| (750, 550)) + ViziaState::new(|| (750, 650)) } pub(crate) fn create( @@ -39,7 +39,7 @@ pub(crate) fn create( .child_top(Stretch(1.0)) .child_bottom(Pixels(0.0)); HStack::new(cx, |cx| { - for ix in 0..2 { + for ix in 0..crate::dsp::NUM_OSCILLATORS { let p = Data::params.map(move |p| p.osc_params[ix].clone()); VStack::new(cx, |cx| { Label::new(cx, &format!("Oscillator {}", ix + 1)) diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 0c86d28..29f3884 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -19,9 +19,9 @@ const NUM_VOICES: usize = 16; const OVERSAMPLE: usize = 8; const MAX_BUFFER_SIZE: usize = 64; -const POLYMOD_OSC_AMP: [u32; 2] = [0, 1]; -const POLYMOD_OSC_PITCH_COARSE: [u32; 2] = [2, 3]; -const POLYMOD_OSC_PITCH_FINE: [u32; 2] = [4, 5]; +const POLYMOD_OSC_AMP: [u32; dsp::NUM_OSCILLATORS] = [0, 1]; +const POLYMOD_OSC_PITCH_COARSE: [u32; dsp::NUM_OSCILLATORS] = [2, 3]; +const POLYMOD_OSC_PITCH_FINE: [u32; dsp::NUM_OSCILLATORS] = [4, 5]; const POLYMOD_FILTER_CUTOFF: u32 = 6; #[derive(Debug, Copy, Clone)] diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index f08dfaf..3fb7f53 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -28,6 +28,8 @@ pub struct OscParams { pub pitch_fine: FloatParam, #[id = "pw"] pub pulse_width: FloatParam, + #[id = "drift"] + pub drift: FloatParam, #[id = "rtrg"] pub retrigger: BoolParam, } @@ -94,6 +96,11 @@ impl OscParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + drift: FloatParam::new("Drift", 0.1, FloatRange::Linear { min: 0.0, max: 1.0 }) + .with_unit(" %") + .with_string_to_value(formatters::s2v_f32_percentage()) + .with_value_to_string(formatters::v2s_f32_percentage(1)) + .with_smoother(SmoothingStyle::Exponential(100.)), retrigger: BoolParam::new("Retrigger", false), } } @@ -162,7 +169,7 @@ impl FilterParams { #[derive(Debug, Params)] pub struct PolysynthParams { #[nested(array)] - pub osc_params: [Arc; 2], + pub osc_params: [Arc; crate::dsp::NUM_OSCILLATORS], #[nested] pub filter_params: Arc, #[id = "out"] From d6e9d4f0f66d26dd2a093d3e39f517a9288d7a0e Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 23:24:06 +0200 Subject: [PATCH 43/67] fix(voice): crash on polyphonic voice manager --- crates/valib-voice/src/polyphonic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index afccb4b..9ae218d 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -127,7 +127,7 @@ impl VoiceManager for Polyphonic { } else { // Find first available slot while self.voice_pool[self.next_voice].is_some() { - self.next_voice += 1; + self.next_voice = (self.next_voice + 1) % self.voice_pool.len(); } let id = self.next_voice; From e85eb9bab33e96a7a090c8e16ca9cd3934d14d59 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 16 Sep 2024 23:24:29 +0200 Subject: [PATCH 44/67] feat(examples): polyphonic: VCA/VCF envelopes --- examples/polysynth/src/dsp.rs | 255 +++++++++++++++++++++++++++++-- examples/polysynth/src/editor.rs | 75 +++++---- examples/polysynth/src/lib.rs | 1 + examples/polysynth/src/params.rs | 109 ++++++++++++- 4 files changed, 397 insertions(+), 43 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 8bbfb0e..cc9bda6 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,4 +1,4 @@ -use crate::params::{OscShape, PolysynthParams}; +use crate::params::{FilterParams, OscShape, PolysynthParams}; use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; @@ -7,7 +7,6 @@ use nih_plug::util::db_to_gain_fast; use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -use valib::dsp::parameter::SmoothedParam; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; use valib::filters::ladder::{Ladder, OTA}; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; @@ -21,6 +20,196 @@ use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; use valib::Scalar; +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +enum AdsrState { + Idle, + Attack, + Decay, + Sustain, + Release, +} + +impl AdsrState { + pub fn next_state(self, gate: bool) -> Self { + if gate { + Self::Attack + } else if !matches!(self, Self::Idle) { + Self::Release + } else { + self + } + } +} + +struct Adsr { + attack: f32, + decay: f32, + sustain: f32, + release: f32, + samplerate: f32, + attack_base: f32, + decay_base: f32, + release_base: f32, + attack_rate: f32, + decay_rate: f32, + release_rate: f32, + cur_state: AdsrState, + cur_value: f32, + release_coeff: f32, + decay_coeff: f32, + attack_coeff: f32, +} + +impl Default for Adsr { + fn default() -> Self { + Self { + samplerate: 0., + attack: 0., + decay: 0., + sustain: 0., + release: 0., + attack_base: 1. + Self::TARGET_RATIO_A, + decay_base: -Self::TARGET_RATIO_DR, + release_base: -Self::TARGET_RATIO_DR, + attack_coeff: 0., + decay_coeff: 0., + release_coeff: 0., + attack_rate: 0., + decay_rate: 0., + release_rate: 0., + cur_state: AdsrState::Idle, + cur_value: 0., + } + } +} + +impl Adsr { + const TARGET_RATIO_A: f32 = 0.3; + const TARGET_RATIO_DR: f32 = 1e-4; + pub fn new( + samplerate: f32, + attack: f32, + decay: f32, + sustain: f32, + release: f32, + gate: bool, + ) -> Self { + let mut this = Self { + samplerate, + cur_state: AdsrState::Idle.next_state(gate), + ..Self::default() + }; + this.set_attack(attack); + this.set_decay(decay); + this.set_sustain(sustain); + this.set_release(release); + this.cur_state = this.cur_state.next_state(gate); + this + } + + pub fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = samplerate; + self.set_attack(self.attack); + self.set_decay(self.decay); + self.set_release(self.release); + } + + pub fn set_attack(&mut self, attack: f32) { + if (self.attack - attack).abs() < 1e-6 { + return; + } + self.attack = attack; + self.attack_rate = self.samplerate * attack; + self.attack_coeff = Self::calc_coeff(self.attack_rate, Self::TARGET_RATIO_A); + self.attack_base = (1. + Self::TARGET_RATIO_A) * (1.0 - self.attack_coeff); + } + + pub fn set_decay(&mut self, decay: f32) { + if (self.decay - decay).abs() < 1e-6 { + return; + } + self.decay = decay; + self.decay_rate = self.samplerate * decay; + self.decay_coeff = Self::calc_coeff(self.decay_rate, Self::TARGET_RATIO_DR); + self.decay_base = (self.sustain - Self::TARGET_RATIO_DR) * (1. - self.decay_coeff); + } + + pub fn set_sustain(&mut self, sustain: f32) { + self.sustain = sustain; + } + + pub fn set_release(&mut self, release: f32) { + if (self.release - release).abs() < 1e-6 { + return; + } + self.release = release; + self.release_rate = self.samplerate * release; + self.release_coeff = Self::calc_coeff(self.release_rate, Self::TARGET_RATIO_DR); + self.release_base = -Self::TARGET_RATIO_DR * (1. - self.release_coeff); + } + + pub fn gate(&mut self, gate: bool) { + self.cur_state = self.cur_state.next_state(gate); + } + + pub fn next_sample(&mut self) -> f32 { + match self.cur_state { + AdsrState::Attack => { + self.cur_value = self.attack_base + self.cur_value * self.attack_coeff; + if self.cur_value >= 1. { + self.cur_value = 1.; + self.cur_state = AdsrState::Decay; + } + } + AdsrState::Decay => { + self.cur_value = self.decay_base + self.cur_value * self.decay_coeff; + if self.cur_value <= self.sustain { + self.cur_value = self.sustain; + self.cur_state = AdsrState::Sustain; + } + } + AdsrState::Release => { + self.cur_value = self.release_base + self.cur_value * self.release_coeff; + if self.cur_value <= 0. { + self.cur_value = 0.; + self.cur_state = AdsrState::Idle; + } + } + AdsrState::Sustain | AdsrState::Idle => {} + } + self.cur_value + } + + pub fn state(&self) -> AdsrState { + self.cur_state + } + + pub fn current_value(&self) -> f32 { + self.cur_value + } + + pub fn current_value_as(&self) -> T { + T::from_f64(self.current_value() as _) + } + + pub fn is_idle(&self) -> bool { + matches!(self.cur_state, AdsrState::Idle) + } + + pub fn reset(&mut self) { + self.cur_state = AdsrState::Idle; + self.cur_value = 0.; + } + + fn calc_coeff(rate: f32, ratio: f32) -> f32 { + if rate <= 0. { + 0. + } else { + (-((1.0 + ratio) / ratio).ln() / rate).exp() + } + } +} + struct Drift { rng: Rng, phasor: Phasor, @@ -168,7 +357,8 @@ pub struct RawVoice { osc_out_sat: bjt::CommonCollector, filter: Ladder>, params: Arc, - gate: SmoothedParam, + vca_env: Adsr, + vcf_env: Adsr, note_data: NoteData, drift: [Drift; NUM_OSCILLATORS], samplerate: T, @@ -212,7 +402,22 @@ impl RawVoice { ybias: T::from_f64(-0.1), }, params: params.clone(), - gate: SmoothedParam::exponential(1., target_samplerate_f64 as _, 1.0), + vca_env: Adsr::new( + target_samplerate_f64 as _, + params.vca_env.attack.value(), + params.vca_env.decay.value(), + params.vca_env.sustain.value(), + params.vca_env.release.value(), + true, + ), + vcf_env: Adsr::new( + target_samplerate_f64 as _, + params.vcf_env.attack.value(), + params.vcf_env.decay.value(), + params.vcf_env.sustain.value(), + params.vcf_env.release.value(), + true, + ), note_data, drift: std::array::from_fn(|_| Drift::new(rng.fork(), target_samplerate_f64 as _, 0.2)), samplerate: target_samplerate, @@ -236,11 +441,30 @@ impl RawVoice { } } } + + fn update_envelopes(&mut self) { + self.vca_env + .set_attack(self.params.vca_env.attack.smoothed.next()); + self.vca_env + .set_decay(self.params.vca_env.decay.smoothed.next()); + self.vca_env + .set_sustain(self.params.vca_env.sustain.smoothed.next()); + self.vca_env + .set_release(self.params.vca_env.release.smoothed.next()); + self.vcf_env + .set_attack(self.params.vcf_env.attack.smoothed.next()); + self.vcf_env + .set_decay(self.params.vcf_env.decay.smoothed.next()); + self.vcf_env + .set_sustain(self.params.vcf_env.sustain.smoothed.next()); + self.vcf_env + .set_release(self.params.vcf_env.release.smoothed.next()); + } } impl Voice for RawVoice { fn active(&self) -> bool { - self.gate.current_value() > 1e-4 + !self.vca_env.is_idle() } fn note_data(&self) -> &NoteData { @@ -253,11 +477,13 @@ impl Voice for RawVoice { fn release(&mut self, _: f32) { nih_log!("RawVoice: release(_)"); - self.gate.param = 0.; + self.vca_env.gate(false); + self.vcf_env.gate(false); } fn reuse(&mut self) { - self.gate.param = 1.; + self.vca_env.gate(true); + self.vcf_env.gate(true); } } @@ -270,6 +496,8 @@ impl DSPMeta for RawVoice { osc.set_samplerate(samplerate); } self.filter.set_samplerate(samplerate); + self.vca_env.set_samplerate(samplerate); + self.vcf_env.set_samplerate(samplerate); } fn reset(&mut self) { @@ -277,17 +505,21 @@ impl DSPMeta for RawVoice { osc.reset(); } self.filter.reset(); + self.vca_env.reset(); + self.vcf_env.reset(); } } impl DSPProcess<0, 1> for RawVoice { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { const DRIFT_MAX_ST: f32 = 0.1; + self.update_osc_types(); + self.update_envelopes(); + // Process oscillators let frequency = self.note_data.frequency; let osc_params = self.params.osc_params.clone(); let filter_params = self.params.filter_params.clone(); - self.update_osc_types(); let [osc1, osc2] = std::array::from_fn(|i| { let osc = &mut self.osc[i]; let params = &self.params.osc_params[i]; @@ -312,7 +544,10 @@ impl DSPProcess<0, 1> for RawVoice { let freq_ratio = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) * frequency - / T::from_f64(440.); + / T::from_f64(440.) + + T::from_f64(semitone_to_ratio( + filter_params.env_amt.smoothed.next() * self.vcf_env.next_sample(), + ) as _); let filter_freq = (T::one() + freq_ratio) * T::from_f64(filter_params.cutoff.smoothed.next() as _); @@ -321,7 +556,7 @@ impl DSPProcess<0, 1> for RawVoice { self.filter.set_resonance(T::from_f64( 4f64 * filter_params.resonance.smoothed.next() as f64, )); - let vca = self.gate.next_sample_as::(); + let vca = T::from_f64(self.vca_env.next_sample() as _); let static_amp = T::from_f64(self.params.output_level.smoothed.next() as _); self.filter.process(filter_in).map(|x| static_amp * vca * x) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index 84770f8..f1c37b3 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -1,5 +1,5 @@ use crate::params::PolysynthParams; -use nih_plug::prelude::Editor; +use nih_plug::prelude::{Editor, Param}; use nih_plug_vizia::vizia::prelude::*; use nih_plug_vizia::widgets::*; use nih_plug_vizia::{assets, create_vizia_editor, ViziaState, ViziaTheming}; @@ -14,7 +14,7 @@ impl Model for Data {} // Makes sense to also define this here, makes it a bit easier to keep track of pub(crate) fn default_state() -> Arc { - ViziaState::new(|| (750, 650)) + ViziaState::new(|| (1000, 600)) } pub(crate) fn create( @@ -31,49 +31,66 @@ pub(crate) fn create( .build(cx); VStack::new(cx, |cx| { - VStack::new(cx, |cx| { + HStack::new(cx, move |cx| { Label::new(cx, "Polysynth") .font_weight(FontWeightKeyword::Thin) .font_size(30.0) .height(Pixels(50.0)) - .child_top(Stretch(1.0)) - .child_bottom(Pixels(0.0)); + .child_left(Stretch(1.0)) + .child_right(Stretch(1.0)); + HStack::new(cx, |cx| { + Label::new(cx, "Output Level") + .child_top(Pixels(5.)) + .width(Auto) + .height(Pixels(30.0)); + ParamSlider::new(cx, Data::params, |p| &p.output_level).width(Pixels(200.)); + }) + .col_between(Pixels(8.0)); + }) + .col_between(Stretch(1.0)) + .width(Percentage(100.)) + .height(Pixels(30.)); + VStack::new(cx, |cx| { HStack::new(cx, |cx| { for ix in 0..crate::dsp::NUM_OSCILLATORS { let p = Data::params.map(move |p| p.osc_params[ix].clone()); VStack::new(cx, |cx| { Label::new(cx, &format!("Oscillator {}", ix + 1)) .font_size(22.) - .height(Pixels(30.)) .child_bottom(Pixels(8.)); - GenericUi::new(cx, p).width(Percentage(100.)); - }) - .width(Stretch(1.0)); + GenericUi::new(cx, p); + }); } + VStack::new(cx, |cx| { + Label::new(cx, "Filter") + .font_size(22.) + .child_bottom(Pixels(8.)); + GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())); + }); }) - .row_between(Pixels(0.0)); - }); - - VStack::new(cx, |cx| { - Label::new(cx, "Filter") - .font_size(22.) - .height(Pixels(30.)) - .child_bottom(Pixels(8.)); - GenericUi::new(cx, Data::params.map(|p| p.filter_params.clone())) - .width(Percentage(100.)); - }); - - VStack::new(cx, |cx| { + .row_between(Stretch(1.0)); HStack::new(cx, |cx| { - Label::new(cx, "Output Level").width(Stretch(1.)); - ParamSlider::new(cx, Data::params, |p| &p.output_level).width(Stretch(1.)); - }); - }); + VStack::new(cx, |cx| { + Label::new(cx, "Amp Env").font_size(22.); + GenericUi::new(cx, Data::params.map(|p| p.vca_env.clone())); + }); + VStack::new(cx, |cx| { + Label::new(cx, "Filter Env").font_size(22.); + GenericUi::new(cx, Data::params.map(|p| p.vcf_env.clone())); + }); + }) + .left(Stretch(1.0)) + .right(Stretch(1.0)) + .width(Percentage(50.)); + }) + .top(Pixels(16.)) + .width(Percentage(100.)) + .height(Percentage(100.)) + .row_between(Pixels(0.0)); }) + .row_between(Pixels(0.0)) .width(Percentage(100.)) - .height(Percentage(100.)) - .row_between(Pixels(0.0)); - + .height(Percentage(100.)); ResizeHandle::new(cx); }) } diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 29f3884..c051303 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(generic_const_exprs)] use crate::params::PolysynthParams; use nih_plug::audio_setup::{AudioIOLayout, AuxiliaryBuffers}; use nih_plug::buffer::Buffer; diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index 3fb7f53..d0a174e 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -8,6 +8,83 @@ use nih_plug_vizia::ViziaState; use std::sync::Arc; use valib::dsp::parameter::{ParamId, ParamName}; +#[derive(Debug, Params)] +pub struct AdsrParams { + #[id = "atk"] + pub attack: FloatParam, + #[id = "dec"] + pub decay: FloatParam, + #[id = "sus"] + pub sustain: FloatParam, + #[id = "rel"] + pub release: FloatParam, +} + +fn v2s_f32_ms_then_s(digits: usize) -> Arc String> { + Arc::new(move |v| { + if v < 0.9 { + format!("{:1$} ms", v * 1e3, digits) + } else { + format!("{v:0$} s", digits) + } + }) +} + +fn s2v_f32_ms_then_s() -> Arc Option> { + Arc::new(move |input: &str| { + let s = input.trim(); + if s.ends_with("ms") { + s[..(s.len() - 2)].parse::().map(|v| 1e-3 * v).ok() + } else { + s.parse::().ok() + } + }) +} + +impl Default for AdsrParams { + fn default() -> Self { + Self { + attack: FloatParam::new( + "Attack", + 0.1, + FloatRange::Skewed { + min: 1e-3, + max: 10., + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(v2s_f32_ms_then_s(2)) + .with_string_to_value(s2v_f32_ms_then_s()), + decay: FloatParam::new( + "Decay", + 0.5, + FloatRange::Skewed { + min: 1e-3, + max: 10., + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(v2s_f32_ms_then_s(2)) + .with_string_to_value(s2v_f32_ms_then_s()), + sustain: FloatParam::new("Sustain", 0.8, FloatRange::Linear { min: 0., max: 1. }) + .with_unit(" %") + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_string_to_value(formatters::s2v_f32_percentage()), + release: FloatParam::new( + "Decay", + 1., + FloatRange::Skewed { + min: 1e-2, + max: 15., + factor: FloatRange::skew_factor(-2.0), + }, + ) + .with_value_to_string(v2s_f32_ms_then_s(2)) + .with_string_to_value(s2v_f32_ms_then_s()), + } + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq, ParamName, Enum)] pub enum OscShape { Sine, @@ -40,7 +117,7 @@ impl OscParams { shape: EnumParam::new("Shape", OscShape::Saw), amplitude: FloatParam::new( "Amplitude", - 0.8, + 0.25, FloatRange::Skewed { min: db_to_gain(MINUS_INFINITY_DB), max: 1.0, @@ -49,6 +126,7 @@ impl OscParams { ) .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") .with_smoother(SmoothingStyle::OversamplingAware( oversample.clone(), &SmoothingStyle::Exponential(10.), @@ -114,6 +192,8 @@ pub struct FilterParams { pub resonance: FloatParam, #[id = "kt"] pub keyboard_tracking: FloatParam, + #[id = "env"] + pub env_amt: FloatParam, } impl FilterParams { @@ -162,15 +242,33 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Linear(10.), )), + env_amt: FloatParam::new( + "Env Amt", + 0., + FloatRange::Linear { + min: -96., + max: 96., + }, + ) + .with_unit(" st") + .with_value_to_string(Arc::new(|x| format!("{:.2}", x))) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(50.), + )), } } } #[derive(Debug, Params)] pub struct PolysynthParams { - #[nested(array)] + #[nested(array, group = "Osc")] pub osc_params: [Arc; crate::dsp::NUM_OSCILLATORS], - #[nested] + #[nested(id_prefix = "vca_", group = "Amp Env")] + pub vca_env: Arc, + #[nested(id_prefix = "vcf_", group = "Filter Env")] + pub vcf_env: Arc, + #[nested(group = "Filter")] pub filter_params: Arc, #[id = "out"] pub output_level: FloatParam, @@ -185,9 +283,11 @@ impl Default for PolysynthParams { Self { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), + vca_env: Arc::default(), + vcf_env: Arc::default(), output_level: FloatParam::new( "Output Level", - 0.25, + 0.5, FloatRange::Skewed { min: 0.0, max: 1.0, @@ -196,6 +296,7 @@ impl Default for PolysynthParams { ) .with_string_to_value(formatters::s2v_f32_gain_to_db()) .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_unit(" dB") .with_smoother(SmoothingStyle::OversamplingAware( oversample.clone(), &SmoothingStyle::Exponential(50.), From 898154ebe94d8ae427a75586c695dbad0201d557 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Tue, 17 Sep 2024 08:48:06 +0200 Subject: [PATCH 45/67] fix(test): add snapshot for sine interpolation --- ...terpolation_MappedLinear_fn(f64) -_ f64_.snap | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap diff --git a/crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap b/crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap new file mode 100644 index 0000000..ad17605 --- /dev/null +++ b/crates/valib-core/src/math/snapshots/valib_core__math__interpolation__tests__valib_core_math_interpolation_MappedLinear_fn(f64) -_ f64_.snap @@ -0,0 +1,16 @@ +--- +source: crates/valib-core/src/math/interpolation.rs +expression: "&actual as &[_]" +--- +0.0 +0.146447 +0.5 +0.853553 +1.0 +1.0 +1.0 +1.0 +1.0 +1.0 +1.0 +1.0 From 2c65131d726767ac800b3195a2ca6e68c5f93549 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 12:29:11 +0200 Subject: [PATCH 46/67] feat(examples): polyphonic: tweak ADSR decay curves --- examples/polysynth/src/dsp.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index cc9bda6..43c42ed 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -68,9 +68,9 @@ impl Default for Adsr { decay: 0., sustain: 0., release: 0., - attack_base: 1. + Self::TARGET_RATIO_A, - decay_base: -Self::TARGET_RATIO_DR, - release_base: -Self::TARGET_RATIO_DR, + attack_base: 1. + Self::TARGET_RATIO_ATTACK, + decay_base: -Self::TARGET_RATIO_RELEASE, + release_base: -Self::TARGET_RATIO_RELEASE, attack_coeff: 0., decay_coeff: 0., release_coeff: 0., @@ -84,8 +84,9 @@ impl Default for Adsr { } impl Adsr { - const TARGET_RATIO_A: f32 = 0.3; - const TARGET_RATIO_DR: f32 = 1e-4; + const TARGET_RATIO_ATTACK: f32 = 0.3; + const TARGET_RATIO_DECAY: f32 = 0.1; + const TARGET_RATIO_RELEASE: f32 = 1e-3; pub fn new( samplerate: f32, attack: f32, @@ -120,8 +121,8 @@ impl Adsr { } self.attack = attack; self.attack_rate = self.samplerate * attack; - self.attack_coeff = Self::calc_coeff(self.attack_rate, Self::TARGET_RATIO_A); - self.attack_base = (1. + Self::TARGET_RATIO_A) * (1.0 - self.attack_coeff); + self.attack_coeff = Self::calc_coeff(self.attack_rate, Self::TARGET_RATIO_ATTACK); + self.attack_base = (1. + Self::TARGET_RATIO_ATTACK) * (1.0 - self.attack_coeff); } pub fn set_decay(&mut self, decay: f32) { @@ -130,8 +131,8 @@ impl Adsr { } self.decay = decay; self.decay_rate = self.samplerate * decay; - self.decay_coeff = Self::calc_coeff(self.decay_rate, Self::TARGET_RATIO_DR); - self.decay_base = (self.sustain - Self::TARGET_RATIO_DR) * (1. - self.decay_coeff); + self.decay_coeff = Self::calc_coeff(self.decay_rate, Self::TARGET_RATIO_DECAY); + self.decay_base = (self.sustain - Self::TARGET_RATIO_DECAY) * (1. - self.decay_coeff); } pub fn set_sustain(&mut self, sustain: f32) { @@ -144,8 +145,8 @@ impl Adsr { } self.release = release; self.release_rate = self.samplerate * release; - self.release_coeff = Self::calc_coeff(self.release_rate, Self::TARGET_RATIO_DR); - self.release_base = -Self::TARGET_RATIO_DR * (1. - self.release_coeff); + self.release_coeff = Self::calc_coeff(self.release_rate, Self::TARGET_RATIO_RELEASE); + self.release_base = -Self::TARGET_RATIO_RELEASE * (1. - self.release_coeff); } pub fn gate(&mut self, gate: bool) { From 3b12dab3854a27da672076e166f85d169ee5e7f1 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 12:58:42 +0200 Subject: [PATCH 47/67] feat(examples): polyphonic: integrate DC blocker after the voices --- crates/valib-filters/src/specialized.rs | 1 + examples/polysynth/src/dsp.rs | 48 ++++++++++++++++++++++--- examples/polysynth/src/lib.rs | 37 +++++++++++-------- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/crates/valib-filters/src/specialized.rs b/crates/valib-filters/src/specialized.rs index 2930083..4d54004 100644 --- a/crates/valib-filters/src/specialized.rs +++ b/crates/valib-filters/src/specialized.rs @@ -7,6 +7,7 @@ use valib_core::Scalar; use valib_saturators::Linear; /// Specialized filter that removes DC offsets by applying a 5 Hz biquad highpass filter +#[derive(Debug, Copy, Clone)] pub struct DcBlocker(Biquad); impl DcBlocker { diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 43c42ed..cbb381c 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,5 +1,5 @@ use crate::params::{FilterParams, OscShape, PolysynthParams}; -use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; use nih_plug::nih_log; @@ -9,6 +9,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; use valib::filters::ladder::{Ladder, OTA}; +use valib::filters::specialized::DcBlocker; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; @@ -581,11 +582,50 @@ pub fn create_voice_manager( }) } -pub type Dsp = VoiceManager; +pub type Voices = VoiceManager; -pub fn create( +pub fn create_voices( samplerate: f32, params: Arc, -) -> Dsp { +) -> Voices { create_voice_manager(samplerate, params) } + +#[derive(Debug, Copy, Clone)] +pub struct Effects { + dc_blocker: DcBlocker, +} + +impl DSPMeta for Effects { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.dc_blocker.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.dc_blocker.latency() + } + + fn reset(&mut self) { + self.dc_blocker.reset(); + } +} + +impl DSPProcess<1, 1> for Effects { + fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { + self.dc_blocker.process(x) + } +} + +impl Effects { + pub fn new(samplerate: f32) -> Self { + Self { + dc_blocker: DcBlocker::new(samplerate), + } + } +} + +pub fn create_effects(samplerate: f32) -> Effects { + Effects::new(samplerate) +} diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index c051303..cff0b35 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -8,7 +8,7 @@ use nih_plug::prelude::*; use std::cmp::Ordering; use std::sync::{atomic, Arc}; use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; -use valib::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock}; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, DSPProcessBlock}; use valib::util::Rms; use valib::voice::{NoteData, VoiceId, VoiceManager}; @@ -73,7 +73,7 @@ impl VoiceKey { #[derive(Debug)] struct VoiceIdMap { - data: [Option<(VoiceKey, VoiceId>)>; NUM_VOICES], + data: [Option<(VoiceKey, VoiceId>)>; NUM_VOICES], } impl Default for VoiceIdMap { @@ -85,7 +85,7 @@ impl Default for VoiceIdMap { } impl VoiceIdMap { - fn add_voice(&mut self, key: VoiceKey, v: VoiceId>) -> bool { + fn add_voice(&mut self, key: VoiceKey, v: VoiceId>) -> bool { let Some(position) = self.data.iter().position(|x| x.is_none()) else { return false; }; @@ -93,21 +93,21 @@ impl VoiceIdMap { true } - fn get_voice(&self, key: VoiceKey) -> Option>> { + fn get_voice(&self, key: VoiceKey) -> Option>> { self.data.iter().find_map(|x| { x.as_ref() .and_then(|(vkey, id)| (*vkey == key).then_some(*id)) }) } - fn get_voice_by_poly_id(&self, voice_id: i32) -> Option>> { + fn get_voice_by_poly_id(&self, voice_id: i32) -> Option>> { self.data .iter() .flatten() .find_map(|(vkey, id)| (vkey.voice_id == Some(voice_id)).then_some(*id)) } - fn remove_voice(&mut self, key: VoiceKey) -> Option<(VoiceKey, VoiceId>)> { + fn remove_voice(&mut self, key: VoiceKey) -> Option<(VoiceKey, VoiceId>)> { let position = self .data .iter() @@ -120,7 +120,8 @@ type SynthSample = f32; #[derive(Debug)] pub struct PolysynthPlugin { - dsp: BlockAdapter>, + voices: BlockAdapter>, + effects: dsp::Effects, params: Arc, voice_id_map: VoiceIdMap, } @@ -130,7 +131,8 @@ impl Default for PolysynthPlugin { const DEFAULT_SAMPLERATE: f32 = 44100.; let params = Arc::new(PolysynthParams::default()); Self { - dsp: BlockAdapter(dsp::create(DEFAULT_SAMPLERATE, params.clone())), + voices: BlockAdapter(dsp::create_voices(DEFAULT_SAMPLERATE, params.clone())), + effects: dsp::create_effects(DEFAULT_SAMPLERATE), params, voice_id_map: VoiceIdMap::default(), } @@ -168,12 +170,12 @@ impl Plugin for PolysynthPlugin { _: &mut impl InitContext, ) -> bool { let sample_rate = buffer_config.sample_rate; - self.dsp.set_samplerate(sample_rate); + self.voices.set_samplerate(sample_rate); true } fn reset(&mut self) { - self.dsp.reset(); + self.voices.reset(); } fn process( @@ -202,7 +204,7 @@ impl Plugin for PolysynthPlugin { } => { let key = VoiceKey::new(voice_id, channel, note); let note_data = NoteData::from_midi(note, velocity); - let id = self.dsp.note_on(note_data); + let id = self.voices.note_on(note_data); nih_log!("Note on {id} <- {key:?}"); self.voice_id_map.add_voice(key, id); } @@ -216,7 +218,7 @@ impl Plugin for PolysynthPlugin { let key = VoiceKey::new(voice_id, channel, note); if let Some((_, id)) = self.voice_id_map.remove_voice(key) { nih_log!("Note off {id} <- {key:?}"); - self.dsp.note_off(id, velocity); + self.voices.note_off(id, velocity); } else { nih_log!("Note off {key:?}: ID not found"); } @@ -229,7 +231,7 @@ impl Plugin for PolysynthPlugin { } => { let key = VoiceKey::new(voice_id, channel, note); if let Some((_, id)) = self.voice_id_map.remove_voice(key) { - self.dsp.choke(id); + self.voices.choke(id); } } NoteEvent::PolyModulation { voice_id, .. } => { @@ -301,13 +303,18 @@ impl Plugin for PolysynthPlugin { } let dsp_block = AudioBufferMut::from(&mut output[0][block_start..block_end]); let input = AudioBufferRef::::empty(dsp_block.samples()); - self.dsp.process_block(input, dsp_block); + self.voices.process_block(input, dsp_block); block_start = block_end; block_end = (block_start + MAX_BUFFER_SIZE).min(num_samples); } - self.dsp.0.clean_inactive_voices(); + self.voices.0.clean_inactive_voices(); + + // Effects processing + for s in &mut output[0][..] { + *s = self.effects.process([*s])[0]; + } ProcessStatus::Normal } } From 1b437799d03b89f5f4a31e7e061971acff367dff Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 16:52:43 +0200 Subject: [PATCH 48/67] fix(core): BlockAdapter doesn't call set_samplerate on inner processor --- crates/valib-core/src/dsp/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/valib-core/src/dsp/mod.rs b/crates/valib-core/src/dsp/mod.rs index 477cf9e..72b012f 100644 --- a/crates/valib-core/src/dsp/mod.rs +++ b/crates/valib-core/src/dsp/mod.rs @@ -87,6 +87,18 @@ impl HasParameters for BlockAdapter

{ impl DSPMeta for BlockAdapter

{ type Sample = P::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + self.0.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.0.latency() + } + + fn reset(&mut self) { + self.0.reset(); + } } impl, const I: usize, const O: usize> DSPProcess for BlockAdapter

{ From df859abb933adbfe1a4349401fce130a9e5d6766 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 16:53:01 +0200 Subject: [PATCH 49/67] fix(oscillators): Phasor does not update its step after a samplerate change --- crates/valib-oscillators/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 20b8847..91f1305 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -26,6 +26,7 @@ impl DSPMeta for Phasor { fn set_samplerate(&mut self, samplerate: f32) { self.samplerate = T::from_f64(samplerate as _); + self.set_frequency(self.frequency); } fn reset(&mut self) { From 92755a39c0b3ca8efe64b6d43f64a63d6e2936f2 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 16:53:55 +0200 Subject: [PATCH 50/67] feat(examples): polysynth: switch to transistor ladder topology --- examples/polysynth/src/dsp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index cbb381c..a720baf 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -8,7 +8,7 @@ use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; -use valib::filters::ladder::{Ladder, OTA}; +use valib::filters::ladder::{Ladder, Transistor}; use valib::filters::specialized::DcBlocker; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; @@ -357,7 +357,7 @@ pub(crate) const NUM_OSCILLATORS: usize = 2; pub struct RawVoice { osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, - filter: Ladder>, + filter: Ladder>, params: Arc, vca_env: Adsr, vcf_env: Adsr, From 3e061dc0060a6f88a0036290fbc63c6fe8a6966c Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 21:04:59 +0200 Subject: [PATCH 51/67] fix(examples): polysynth: fix formatting of s/ms --- examples/polysynth/src/params.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index d0a174e..f611d27 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -23,9 +23,9 @@ pub struct AdsrParams { fn v2s_f32_ms_then_s(digits: usize) -> Arc String> { Arc::new(move |v| { if v < 0.9 { - format!("{:1$} ms", v * 1e3, digits) + format!("{:.1$} ms", v * 1e3, digits) } else { - format!("{v:0$} s", digits) + format!("{v:.0$} s", digits) } }) } From 702dadc5734ae190749ff864d7f6d16f6c140f86 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 21:05:21 +0200 Subject: [PATCH 52/67] fix(examples): polysynth: fix samplerate not being updated in effects --- examples/polysynth/src/dsp.rs | 4 ++-- examples/polysynth/src/lib.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index a720baf..0ba5f2a 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -572,8 +572,8 @@ pub fn create_voice_manager( samplerate: f32, params: Arc, ) -> VoiceManager { - let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; - Polyphonic::new(samplerate, NUM_VOICES, move |_, note_data| { + Polyphonic::new(samplerate, NUM_VOICES, move |samplerate, note_data| { + let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; SampleAdapter::new(UpsampledVoice::new( OVERSAMPLE, MAX_BUFFER_SIZE, diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index cff0b35..2ef4518 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -171,6 +171,7 @@ impl Plugin for PolysynthPlugin { ) -> bool { let sample_rate = buffer_config.sample_rate; self.voices.set_samplerate(sample_rate); + self.effects.set_samplerate(sample_rate); true } From 54c642b3492749abf5994a8a6ba823f1af796621 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 22:28:10 +0200 Subject: [PATCH 53/67] feat(examples): polysynth: add noise + ring mod mixer output --- examples/polysynth/src/dsp.rs | 56 +++++++++++++--- examples/polysynth/src/editor.rs | 6 +- examples/polysynth/src/lib.rs | 33 +++++++--- examples/polysynth/src/params.rs | 107 +++++++++++++++++++++++++------ 4 files changed, 165 insertions(+), 37 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 0ba5f2a..090087b 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -14,7 +14,7 @@ use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; use valib::saturators::{bjt, Tanh}; -use valib::simd::SimdBool; +use valib::simd::{SimdBool, SimdValue}; use valib::util::semitone_to_ratio; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; @@ -352,11 +352,37 @@ impl DSPProcess<1, 1> for PolyOsc { } } +#[derive(Debug, Clone)] +struct Noise { + rng: Rng, +} + +impl Noise { + pub fn from_rng(rng: Rng) -> Self { + Self { rng } + } + + pub fn next_value_f32>(&mut self) -> T + where + [(); ::LANES]:, + { + T::from_values(std::array::from_fn(|_| self.rng.f32_range(-1.0..1.0))) + } + + pub fn next_value_f64>(&mut self) -> T + where + [(); ::LANES]:, + { + T::from_values(std::array::from_fn(|_| self.rng.f64_range(-1.0..1.0))) + } +} + pub(crate) const NUM_OSCILLATORS: usize = 2; pub struct RawVoice { osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, + noise: Noise, filter: Ladder>, params: Arc, vca_env: Adsr, @@ -397,6 +423,7 @@ impl RawVoice { T::from_f64(params.filter_params.cutoff.value() as _), T::from_f64(params.filter_params.resonance.value() as _), ), + noise: Noise::from_rng(rng.fork()), osc_out_sat: bjt::CommonCollector { vee: -T::ONE, vcc: T::ONE, @@ -512,7 +539,10 @@ impl DSPMeta for RawVoice { } } -impl DSPProcess<0, 1> for RawVoice { +impl> DSPProcess<0, 1> for RawVoice +where + [(); ::LANES]:, +{ fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { const DRIFT_MAX_ST: f32 = 0.1; self.update_osc_types(); @@ -535,10 +565,14 @@ impl DSPProcess<0, 1> for RawVoice { let [osc] = osc.process([osc_freq]); osc }); + let noise = self.noise.next_value_f32::(); // Process filter input - let osc_mixer = osc1 * T::from_f64(osc_params[0].amplitude.smoothed.next() as _) - + osc2 * T::from_f64(osc_params[1].amplitude.smoothed.next() as _); + let mixer_params = &self.params.mixer_params; + let osc_mixer = osc1 * T::from_f64(mixer_params.osc1_amplitude.smoothed.next() as _) + + osc2 * T::from_f64(mixer_params.osc2_amplitude.smoothed.next() as _) + + noise * T::from_f64(mixer_params.noise_amplitude.smoothed.next() as _) + + osc1 * osc2 * T::from_f64(mixer_params.rm_amplitude.smoothed.next() as _); let filter_in = self .osc_out_sat .process([osc_mixer]) @@ -568,10 +602,13 @@ type SynthVoice = SampleAdapter>>, 0, pub type VoiceManager = Polyphonic>; -pub fn create_voice_manager( +pub fn create_voice_manager>( samplerate: f32, params: Arc, -) -> VoiceManager { +) -> VoiceManager +where + [(); ::LANES]:, +{ Polyphonic::new(samplerate, NUM_VOICES, move |samplerate, note_data| { let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; SampleAdapter::new(UpsampledVoice::new( @@ -584,10 +621,13 @@ pub fn create_voice_manager( pub type Voices = VoiceManager; -pub fn create_voices( +pub fn create_voices>( samplerate: f32, params: Arc, -) -> Voices { +) -> Voices +where + [(); ::LANES]:, +{ create_voice_manager(samplerate, params) } diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs index f1c37b3..29a93c9 100644 --- a/examples/polysynth/src/editor.rs +++ b/examples/polysynth/src/editor.rs @@ -70,6 +70,10 @@ pub(crate) fn create( }) .row_between(Stretch(1.0)); HStack::new(cx, |cx| { + VStack::new(cx, |cx| { + Label::new(cx, "Mixer").font_size(22.); + GenericUi::new(cx, Data::params.map(|p| p.mixer_params.clone())); + }); VStack::new(cx, |cx| { Label::new(cx, "Amp Env").font_size(22.); GenericUi::new(cx, Data::params.map(|p| p.vca_env.clone())); @@ -81,7 +85,7 @@ pub(crate) fn create( }) .left(Stretch(1.0)) .right(Stretch(1.0)) - .width(Percentage(50.)); + .width(Pixels(750.)); }) .top(Pixels(16.)) .width(Percentage(100.)) diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 2ef4518..8b2a4cb 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -257,6 +257,30 @@ impl Plugin for PolysynthPlugin { .smoothed .set_target(sample_rate, target_plain_value); } + id if id == POLYMOD_OSC_AMP[0] => { + let target_plain_value = self + .params + .mixer_params + .osc1_amplitude + .preview_plain(normalized_value); + self.params + .mixer_params + .osc1_amplitude + .smoothed + .set_target(sample_rate, target_plain_value); + } + id if id == POLYMOD_OSC_AMP[1] => { + let target_plain_value = self + .params + .mixer_params + .osc2_amplitude + .preview_plain(normalized_value); + self.params + .mixer_params + .osc2_amplitude + .smoothed + .set_target(sample_rate, target_plain_value); + } _ => { for i in 0..2 { match poly_modulation_id { @@ -278,15 +302,6 @@ impl Plugin for PolysynthPlugin { .smoothed .set_target(sample_rate, target_plain_value); } - id if id == POLYMOD_OSC_AMP[i] => { - let target_plain_value = self.params.osc_params[i] - .amplitude - .preview_plain(normalized_value); - self.params.osc_params[i] - .amplitude - .smoothed - .set_target(sample_rate, target_plain_value); - } _ => {} } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index f611d27..cbceca0 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -97,8 +97,6 @@ pub enum OscShape { pub struct OscParams { #[id = "shp"] pub shape: EnumParam, - #[id = "amp"] - pub amplitude: FloatParam, #[id = "pco"] pub pitch_coarse: FloatParam, #[id = "pfi"] @@ -115,23 +113,6 @@ impl OscParams { fn new(osc_index: usize, oversample: Arc) -> Self { Self { shape: EnumParam::new("Shape", OscShape::Saw), - amplitude: FloatParam::new( - "Amplitude", - 0.25, - FloatRange::Skewed { - min: db_to_gain(MINUS_INFINITY_DB), - max: 1.0, - factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), - }, - ) - .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) - .with_string_to_value(formatters::s2v_f32_gain_to_db()) - .with_unit(" dB") - .with_smoother(SmoothingStyle::OversamplingAware( - oversample.clone(), - &SmoothingStyle::Exponential(10.), - )) - .with_poly_modulation_id(POLYMOD_OSC_AMP[osc_index]), pitch_coarse: FloatParam::new( "Pitch (Coarse)", 0.0, @@ -260,10 +241,97 @@ impl FilterParams { } } +#[derive(Debug, Params)] +pub struct MixerParams { + #[id = "osc1_amp"] + pub osc1_amplitude: FloatParam, + #[id = "osc2_amp"] + pub osc2_amplitude: FloatParam, + #[id = "rm_amp"] + pub rm_amplitude: FloatParam, + #[id = "noise_amp"] + pub noise_amplitude: FloatParam, +} + +impl MixerParams { + fn new(oversample: Arc) -> Self { + Self { + osc1_amplitude: FloatParam::new( + "OSC1 Amplitude", + 0.25, + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_AMP[0]), + osc2_amplitude: FloatParam::new( + "OSC2 Amplitude", + 0.25, + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )) + .with_poly_modulation_id(POLYMOD_OSC_AMP[1]), + rm_amplitude: FloatParam::new( + "RM Amplitude", + 0., + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )), + noise_amplitude: FloatParam::new( + "Noise Amplitude", + 0., + FloatRange::Skewed { + min: db_to_gain(MINUS_INFINITY_DB), + max: 1.0, + factor: FloatRange::gain_skew_factor(MINUS_INFINITY_DB, 0.0), + }, + ) + .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) + .with_string_to_value(formatters::s2v_f32_gain_to_db()) + .with_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(10.), + )), + } + } +} + #[derive(Debug, Params)] pub struct PolysynthParams { #[nested(array, group = "Osc")] pub osc_params: [Arc; crate::dsp::NUM_OSCILLATORS], + #[nested] + pub mixer_params: Arc, #[nested(id_prefix = "vca_", group = "Amp Env")] pub vca_env: Arc, #[nested(id_prefix = "vcf_", group = "Filter Env")] @@ -283,6 +351,7 @@ impl Default for PolysynthParams { Self { osc_params: std::array::from_fn(|i| Arc::new(OscParams::new(i, oversample.clone()))), filter_params: Arc::new(FilterParams::new(oversample.clone())), + mixer_params: Arc::new(MixerParams::new(oversample.clone())), vca_env: Arc::default(), vcf_env: Arc::default(), output_level: FloatParam::new( From a2fd7d6daf276a04bffad0ce98c9e2dcfcc98903 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 23:18:37 +0200 Subject: [PATCH 54/67] fix(filters): make ladder filter take a samplerate of type T --- crates/valib-filters/src/ladder.rs | 3 +-- crates/valib-filters/src/svf.rs | 2 +- examples/ladder/src/dsp.rs | 14 ++++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/valib-filters/src/ladder.rs b/crates/valib-filters/src/ladder.rs index b2b985a..312fc26 100644 --- a/crates/valib-filters/src/ladder.rs +++ b/crates/valib-filters/src/ladder.rs @@ -153,8 +153,7 @@ impl> Ladder { /// let transistor_ladder = Ladder::<_, Transistor>>::new(48000.0, 440.0, 1.0); /// ``` #[replace_float_literals(T::from_f64(literal))] - pub fn new(samplerate: impl Into, cutoff: T, resonance: T) -> Self { - let samplerate = T::from_f64(samplerate.into()); + pub fn new(samplerate: T, cutoff: T, resonance: T) -> Self { let mut this = Self { inv_2fs: T::simd_recip(2.0 * samplerate), samplerate, diff --git a/crates/valib-filters/src/svf.rs b/crates/valib-filters/src/svf.rs index 745e6fe..70c6a12 100644 --- a/crates/valib-filters/src/svf.rs +++ b/crates/valib-filters/src/svf.rs @@ -112,7 +112,7 @@ impl Svf { pub fn new(samplerate: T, fc: T, r: T) -> Self { let mut this = Self { s: [T::zero(); 2], - r, + r: r + r, fc, g: T::zero(), g1: T::zero(), diff --git a/examples/ladder/src/dsp.rs b/examples/ladder/src/dsp.rs index 580bce5..abce13a 100644 --- a/examples/ladder/src/dsp.rs +++ b/examples/ladder/src/dsp.rs @@ -9,6 +9,7 @@ use valib::oversample::{Oversample, Oversampled}; use valib::saturators::bjt::CommonCollector; use valib::saturators::Tanh; use valib::simd::{AutoF32x2, SimdValue}; +use valib::Scalar; use crate::{MAX_BUFFER_SIZE, OVERSAMPLE}; @@ -100,7 +101,7 @@ impl fmt::Display for LadderType { } impl LadderType { - fn as_ladder(&self, samplerate: f32, fc: Sample, res: Sample) -> DspLadder { + fn as_ladder(&self, samplerate: Sample, fc: Sample, res: Sample) -> DspLadder { match self { Self::Ideal => DspLadder::Ideal(Ladder::new(samplerate, fc, res)), Self::Transistor => DspLadder::Transistor(Ladder::new(samplerate, fc, res)), @@ -126,7 +127,7 @@ pub struct DspInner { resonance: SmoothedParam, compensated: bool, ladder: DspLadder, - samplerate: f32, + samplerate: Sample, } impl DspInner { @@ -210,13 +211,14 @@ impl HasParameters for DspInner { pub type Dsp = Oversampled>; pub fn create(orig_samplerate: f32) -> RemoteControlled { - let samplerate = orig_samplerate * OVERSAMPLE as f32; + let sr_f32 = orig_samplerate * OVERSAMPLE as f32; + let samplerate = Sample::from_f64(sr_f32 as _); let dsp = DspInner { ladder_type: LadderType::Ideal, ladder_type_changed: false, - drive: SmoothedParam::exponential(1.0, samplerate, 50.0), - cutoff: SmoothedParam::exponential(300.0, samplerate, 10.0), - resonance: SmoothedParam::linear(0.5, samplerate, 10.0), + drive: SmoothedParam::exponential(1.0, sr_f32, 50.0), + cutoff: SmoothedParam::exponential(300.0, sr_f32, 10.0), + resonance: SmoothedParam::linear(0.5, sr_f32, 10.0), ladder: LadderType::Ideal.as_ladder(samplerate, Sample::splat(300.0), Sample::splat(0.5)), compensated: false, samplerate, From 8e34b5603725aae366a0b1a5338efb8054620cba Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Wed, 18 Sep 2024 23:19:11 +0200 Subject: [PATCH 55/67] feat(examples): polysynth: new filter processor with multiple options and accurate pitch tracking --- crates/valib-core/src/util.rs | 21 +++ examples/polysynth/src/dsp.rs | 223 +++++++++++++++++++++++++++---- examples/polysynth/src/params.rs | 14 ++ 3 files changed, 232 insertions(+), 26 deletions(-) diff --git a/crates/valib-core/src/util.rs b/crates/valib-core/src/util.rs index 65daecb..56150c7 100644 --- a/crates/valib-core/src/util.rs +++ b/crates/valib-core/src/util.rs @@ -203,6 +203,27 @@ pub fn semitone_to_ratio(semi: T) -> T { 2.0.simd_powf(semi / 12.0) } +/// Compute the semitone equivalent change in pitch that would have resulted by multiplying the +/// input ratio to a frequency value. +/// +/// # Arguments +/// +/// * `ratio`: Frequency ratio (unitless) +/// +/// returns: T +/// +/// # Examples +/// +/// ``` +/// use valib_core::util::ratio_to_semitone; +/// assert_eq!(0., ratio_to_semitone(1.)); +/// assert_eq!(12., ratio_to_semitone(2.)); +/// assert_eq!(-12., ratio_to_semitone(0.5)); +/// ``` +pub fn ratio_to_semitone(ratio: T) -> T { + T::from_f64(12.) * ratio.simd_log2() +} + /// Create a new matrix referencing this one as storage. The resulting matrix will have the same /// shape and same strides as the input one. /// diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 090087b..9ee4ab7 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,4 +1,4 @@ -use crate::params::{FilterParams, OscShape, PolysynthParams}; +use crate::params::{FilterParams, FilterType, OscShape, PolysynthParams}; use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; @@ -8,14 +8,16 @@ use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; -use valib::filters::ladder::{Ladder, Transistor}; +use valib::filters::biquad::Biquad; +use valib::filters::ladder::{Ladder, Transistor, OTA}; use valib::filters::specialized::DcBlocker; +use valib::filters::svf::Svf; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; -use valib::saturators::{bjt, Tanh}; +use valib::saturators::{bjt, Asinh, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; -use valib::util::semitone_to_ratio; +use valib::util::{ratio_to_semitone, semitone_to_ratio}; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; @@ -377,13 +379,193 @@ impl Noise { } } +#[derive(Debug, Default, Copy, Clone)] +struct Sinh; + +impl Saturator for Sinh { + fn saturate(&self, x: T) -> T { + x.simd_sinh() + } +} + +#[derive(Debug, Copy, Clone)] +enum FilterImpl { + Transistor(Ladder>), + Ota(Ladder>), + Svf(Svf), + Biquad(Biquad), +} + +impl FilterImpl { + fn from_type(samplerate: T, ftype: FilterType, cutoff: T, resonance: T) -> FilterImpl { + match ftype { + FilterType::TransistorLadder => { + Self::Transistor(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + } + FilterType::OTALadder => { + Self::Ota(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + } + FilterType::Svf => Self::Svf(Svf::new(samplerate, cutoff, T::one() - resonance)), + FilterType::Digital => Self::Biquad( + Biquad::lowpass( + cutoff / samplerate, + (T::from_f64(3.) * resonance).simd_exp(), + ) + .with_saturators(Asinh, Asinh), + ), + } + } +} + +impl FilterImpl { + fn set_params(&mut self, samplerate: T, cutoff: T, resonance: T) { + match self { + Self::Transistor(p) => { + p.set_cutoff(cutoff); + p.set_resonance(T::from_f64(4.) * resonance); + } + Self::Ota(p) => { + p.set_cutoff(cutoff); + p.set_resonance(T::from_f64(4.) * resonance); + } + Self::Svf(p) => { + p.set_cutoff(cutoff); + p.set_r(T::one() - resonance); + } + Self::Biquad(p) => { + p.update_coefficients(&Biquad::lowpass( + cutoff / samplerate, + (T::from_f64(3.) * resonance).simd_exp(), + )); + } + } + } +} + +impl DSPMeta for FilterImpl { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + match self { + FilterImpl::Transistor(p) => p.set_samplerate(samplerate), + FilterImpl::Ota(p) => p.set_samplerate(samplerate), + FilterImpl::Svf(p) => p.set_samplerate(samplerate), + FilterImpl::Biquad(p) => p.set_samplerate(samplerate), + } + } + + fn latency(&self) -> usize { + match self { + FilterImpl::Transistor(p) => p.latency(), + FilterImpl::Ota(p) => p.latency(), + FilterImpl::Svf(p) => p.latency(), + FilterImpl::Biquad(p) => p.latency(), + } + } + + fn reset(&mut self) { + match self { + FilterImpl::Transistor(p) => p.reset(), + FilterImpl::Ota(p) => p.reset(), + FilterImpl::Svf(p) => p.reset(), + FilterImpl::Biquad(p) => p.reset(), + } + } +} + +impl DSPProcess<1, 1> for FilterImpl { + fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { + match self { + FilterImpl::Transistor(p) => p.process(x), + FilterImpl::Ota(p) => p.process(x), + FilterImpl::Svf(p) => [p.process(x)[0]], + FilterImpl::Biquad(p) => p.process(x), + } + } +} + +#[derive(Debug, Clone)] +struct Filter { + fimpl: FilterImpl, + params: Arc, + samplerate: T, +} + +impl Filter { + fn new(samplerate: T, params: Arc) -> Filter { + let cutoff = T::from_f64(params.cutoff.value() as _); + let resonance = T::from_f64(params.resonance.value() as _); + Self { + fimpl: FilterImpl::from_type(samplerate, params.filter_type.value(), cutoff, resonance), + params, + samplerate, + } + } +} + +impl Filter { + fn update_filter(&mut self, modulation_st: T) { + let cutoff = + semitone_to_ratio(modulation_st) * T::from_f64(self.params.cutoff.smoothed.next() as _); + let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); + self.fimpl = match self.params.filter_type.value() { + FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { + FilterImpl::Transistor(Ladder::new( + self.samplerate, + cutoff, + T::from_f64(4.) * resonance, + )) + } + FilterType::OTALadder if !matches!(self.fimpl, FilterImpl::Ota(..)) => FilterImpl::Ota( + Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance), + ), + FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => { + FilterImpl::Svf(Svf::new(self.samplerate, cutoff, T::one() - resonance)) + } + FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { + FilterImpl::Biquad( + Biquad::lowpass(cutoff / self.samplerate, resonance) + .with_saturators(Asinh, Asinh), + ) + } + _ => { + self.fimpl.set_params(self.samplerate, cutoff, resonance); + return; + } + }; + } +} + +impl DSPMeta for Filter { + type Sample = T; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = T::from_f64(samplerate as _); + } + + fn latency(&self) -> usize { + self.fimpl.latency() + } + + fn reset(&mut self) { + self.fimpl.reset(); + } +} + +impl DSPProcess<2, 1> for Filter { + fn process(&mut self, [x, mod_st]: [Self::Sample; 2]) -> [Self::Sample; 1] { + self.update_filter(mod_st); + self.fimpl.process([x]) + } +} + pub(crate) const NUM_OSCILLATORS: usize = 2; pub struct RawVoice { osc: [PolyOsc; NUM_OSCILLATORS], osc_out_sat: bjt::CommonCollector, noise: Noise, - filter: Ladder>, + filter: Filter, params: Arc, vca_env: Adsr, vcf_env: Adsr, @@ -418,11 +600,7 @@ impl RawVoice { }, ) }), - filter: Ladder::new( - target_samplerate_f64, - T::from_f64(params.filter_params.cutoff.value() as _), - T::from_f64(params.filter_params.resonance.value() as _), - ), + filter: Filter::new(target_samplerate, params.filter_params.clone()), noise: Noise::from_rng(rng.fork()), osc_out_sat: bjt::CommonCollector { vee: -T::ONE, @@ -550,7 +728,6 @@ where // Process oscillators let frequency = self.note_data.frequency; - let osc_params = self.params.osc_params.clone(); let filter_params = self.params.filter_params.clone(); let [osc1, osc2] = std::array::from_fn(|i| { let osc = &mut self.osc[i]; @@ -573,28 +750,22 @@ where + osc2 * T::from_f64(mixer_params.osc2_amplitude.smoothed.next() as _) + noise * T::from_f64(mixer_params.noise_amplitude.smoothed.next() as _) + osc1 * osc2 * T::from_f64(mixer_params.rm_amplitude.smoothed.next() as _); - let filter_in = self + let [filter_in] = self .osc_out_sat .process([osc_mixer]) .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); - let freq_ratio = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) - * frequency - / T::from_f64(440.) - + T::from_f64(semitone_to_ratio( - filter_params.env_amt.smoothed.next() * self.vcf_env.next_sample(), - ) as _); - let filter_freq = - (T::one() + freq_ratio) * T::from_f64(filter_params.cutoff.smoothed.next() as _); - // Process filter - self.filter.set_cutoff(filter_freq); - self.filter.set_resonance(T::from_f64( - 4f64 * filter_params.resonance.smoothed.next() as f64, - )); + let freq_mod = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) + * ratio_to_semitone(frequency / T::from_f64(440.)) + + T::from_f64( + (filter_params.env_amt.smoothed.next() * self.vcf_env.next_sample()) as f64, + ); let vca = T::from_f64(self.vca_env.next_sample() as _); let static_amp = T::from_f64(self.params.output_level.smoothed.next() as _); - self.filter.process(filter_in).map(|x| static_amp * vca * x) + self.filter + .process([filter_in, freq_mod]) + .map(|x| static_amp * vca * x) } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index cbceca0..a76bf52 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -165,6 +165,17 @@ impl OscParams { } } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Enum)] +pub enum FilterType { + #[name = "Transistor Ladder"] + TransistorLadder, + #[name = "OTA Ladder"] + OTALadder, + #[name = "SVF"] + Svf, + Digital, +} + #[derive(Debug, Params)] pub struct FilterParams { #[id = "fc"] @@ -175,6 +186,8 @@ pub struct FilterParams { pub keyboard_tracking: FloatParam, #[id = "env"] pub env_amt: FloatParam, + #[id = "fty"] + pub filter_type: EnumParam, } impl FilterParams { @@ -237,6 +250,7 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Exponential(50.), )), + filter_type: EnumParam::new("Filter Type", FilterType::TransistorLadder), } } } From 4faa2d5b71d9f63ef85157988fb79fb34a12fd3f Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 16:05:59 +0200 Subject: [PATCH 56/67] feat(examples): polysynth: make ladder filters compensated + digital filter more digital --- examples/polysynth/src/dsp.rs | 46 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 9ee4ab7..919a745 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -15,7 +15,7 @@ use valib::filters::svf::Svf; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; -use valib::saturators::{bjt, Asinh, Saturator, Tanh}; +use valib::saturators::{bjt, Asinh, Clipper, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; use valib::util::{ratio_to_semitone, semitone_to_ratio}; use valib::voice::polyphonic::Polyphonic; @@ -393,17 +393,21 @@ enum FilterImpl { Transistor(Ladder>), Ota(Ladder>), Svf(Svf), - Biquad(Biquad), + Biquad(Biquad>), } impl FilterImpl { fn from_type(samplerate: T, ftype: FilterType, cutoff: T, resonance: T) -> FilterImpl { match ftype { FilterType::TransistorLadder => { - Self::Transistor(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + Self::Transistor(ladder) } FilterType::OTALadder => { - Self::Ota(Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance)) + let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + Self::Ota(ladder) } FilterType::Svf => Self::Svf(Svf::new(samplerate, cutoff, T::one() - resonance)), FilterType::Digital => Self::Biquad( @@ -411,7 +415,7 @@ impl FilterImpl { cutoff / samplerate, (T::from_f64(3.) * resonance).simd_exp(), ) - .with_saturators(Asinh, Asinh), + .with_saturators(Default::default(), Default::default()), ), } } @@ -510,22 +514,22 @@ impl Filter { let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); self.fimpl = match self.params.filter_type.value() { FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { - FilterImpl::Transistor(Ladder::new( - self.samplerate, - cutoff, - T::from_f64(4.) * resonance, - )) + let mut ladder = Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + FilterImpl::Transistor(ladder) + } + FilterType::OTALadder if !matches!(self.fimpl, FilterImpl::Ota(..)) => { + let mut ladder = Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + FilterImpl::Ota(ladder) } - FilterType::OTALadder if !matches!(self.fimpl, FilterImpl::Ota(..)) => FilterImpl::Ota( - Ladder::new(self.samplerate, cutoff, T::from_f64(4.) * resonance), - ), FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => { FilterImpl::Svf(Svf::new(self.samplerate, cutoff, T::one() - resonance)) } FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { FilterImpl::Biquad( Biquad::lowpass(cutoff / self.samplerate, resonance) - .with_saturators(Asinh, Asinh), + .with_saturators(Default::default(), Default::default()), ) } _ => { @@ -750,10 +754,7 @@ where + osc2 * T::from_f64(mixer_params.osc2_amplitude.smoothed.next() as _) + noise * T::from_f64(mixer_params.noise_amplitude.smoothed.next() as _) + osc1 * osc2 * T::from_f64(mixer_params.rm_amplitude.smoothed.next() as _); - let [filter_in] = self - .osc_out_sat - .process([osc_mixer]) - .map(|x| T::from_f64(db_to_gain_fast(9.0) as _) * x); + let [filter_in] = self.osc_out_sat.process([osc_mixer]); // Process filter let freq_mod = T::from_f64(filter_params.keyboard_tracking.smoothed.next() as _) @@ -805,6 +806,7 @@ where #[derive(Debug, Copy, Clone)] pub struct Effects { dc_blocker: DcBlocker, + bjt: bjt::CommonCollector, } impl DSPMeta for Effects { @@ -825,7 +827,7 @@ impl DSPMeta for Effects { impl DSPProcess<1, 1> for Effects { fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { - self.dc_blocker.process(x) + self.bjt.process(self.dc_blocker.process(x)) } } @@ -833,6 +835,12 @@ impl Effects { pub fn new(samplerate: f32) -> Self { Self { dc_blocker: DcBlocker::new(samplerate), + bjt: bjt::CommonCollector { + vee: T::from_f64(-2.5), + vcc: T::from_f64(2.5), + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, } } } From f884db2dd622395d93b6c979e1775e3a2e16d927 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 21:08:34 +0200 Subject: [PATCH 57/67] chore(examples): polysynth: tweak filter internal drive levels --- examples/polysynth/src/dsp.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 919a745..8011189 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -3,7 +3,7 @@ use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; use nih_plug::nih_log; -use nih_plug::util::db_to_gain_fast; +use nih_plug::util::{db_to_gain, db_to_gain_fast}; use num_traits::{ConstOne, ConstZero}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -419,9 +419,7 @@ impl FilterImpl { ), } } -} -impl FilterImpl { fn set_params(&mut self, samplerate: T, cutoff: T, resonance: T) { match self { Self::Transistor(p) => { @@ -444,6 +442,15 @@ impl FilterImpl { } } } + + fn filter_drive(&self) -> T { + match self { + Self::Transistor(..) => T::from_f64(0.5), + Self::Ota(..) => T::from_f64(db_to_gain(9.) as _), + Self::Svf(..) => T::from_f64(db_to_gain(9.) as _), + Self::Biquad(..) => T::from_f64(db_to_gain(6.) as _), + } + } } impl DSPMeta for FilterImpl { @@ -479,12 +486,16 @@ impl DSPMeta for FilterImpl { impl DSPProcess<1, 1> for FilterImpl { fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { - match self { + let drive_in = self.filter_drive(); + let drive_out = drive_in.simd_asinh().simd_recip(); + let x = x.map(|x| drive_in * x); + let y = match self { FilterImpl::Transistor(p) => p.process(x), FilterImpl::Ota(p) => p.process(x), FilterImpl::Svf(p) => [p.process(x)[0]], FilterImpl::Biquad(p) => p.process(x), - } + }; + y.map(|x| drive_out * x) } } @@ -726,7 +737,7 @@ where [(); ::LANES]:, { fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { - const DRIFT_MAX_ST: f32 = 0.1; + const DRIFT_MAX_ST: f32 = 0.15; self.update_osc_types(); self.update_envelopes(); From 0a38b5cd3bc6be13dfdc53699eafa0dd01ee1f7e Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 21:46:08 +0200 Subject: [PATCH 58/67] fix(examples): polysynth: NaNs with biquad and svf filters --- examples/polysynth/src/dsp.rs | 57 ++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 8011189..0e2a6ca 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -388,16 +388,26 @@ impl Saturator for Sinh { } } +fn svf_clipper() -> bjt::CommonCollector { + bjt::CommonCollector { + vee: T::from_f64(-1.), + vcc: T::from_f64(1.), + xbias: T::from_f64(-0.1), + ybias: T::from_f64(0.1), + } +} + #[derive(Debug, Copy, Clone)] enum FilterImpl { Transistor(Ladder>), Ota(Ladder>), - Svf(Svf), + Svf(bjt::CommonCollector, Svf), Biquad(Biquad>), } impl FilterImpl { fn from_type(samplerate: T, ftype: FilterType, cutoff: T, resonance: T) -> FilterImpl { + let cutoff = cutoff.simd_clamp(T::zero(), samplerate / T::from_f64(12.)); match ftype { FilterType::TransistorLadder => { let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); @@ -409,13 +419,13 @@ impl FilterImpl { ladder.compensated = true; Self::Ota(ladder) } - FilterType::Svf => Self::Svf(Svf::new(samplerate, cutoff, T::one() - resonance)), + FilterType::Svf => Self::Svf( + svf_clipper(), + Svf::new(samplerate, cutoff, T::one() - resonance), + ), FilterType::Digital => Self::Biquad( - Biquad::lowpass( - cutoff / samplerate, - (T::from_f64(3.) * resonance).simd_exp(), - ) - .with_saturators(Default::default(), Default::default()), + Biquad::lowpass(cutoff / samplerate, T::one()) + .with_saturators(Default::default(), Default::default()), ), } } @@ -430,14 +440,14 @@ impl FilterImpl { p.set_cutoff(cutoff); p.set_resonance(T::from_f64(4.) * resonance); } - Self::Svf(p) => { + Self::Svf(_, p) => { p.set_cutoff(cutoff); - p.set_r(T::one() - resonance); + p.set_r(T::one() - resonance.simd_sqrt()); } Self::Biquad(p) => { p.update_coefficients(&Biquad::lowpass( cutoff / samplerate, - (T::from_f64(3.) * resonance).simd_exp(), + T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(), )); } } @@ -448,7 +458,7 @@ impl FilterImpl { Self::Transistor(..) => T::from_f64(0.5), Self::Ota(..) => T::from_f64(db_to_gain(9.) as _), Self::Svf(..) => T::from_f64(db_to_gain(9.) as _), - Self::Biquad(..) => T::from_f64(db_to_gain(6.) as _), + Self::Biquad(..) => T::from_f64(db_to_gain(12.) as _), } } } @@ -460,7 +470,7 @@ impl DSPMeta for FilterImpl { match self { FilterImpl::Transistor(p) => p.set_samplerate(samplerate), FilterImpl::Ota(p) => p.set_samplerate(samplerate), - FilterImpl::Svf(p) => p.set_samplerate(samplerate), + FilterImpl::Svf(_, p) => p.set_samplerate(samplerate), FilterImpl::Biquad(p) => p.set_samplerate(samplerate), } } @@ -469,7 +479,7 @@ impl DSPMeta for FilterImpl { match self { FilterImpl::Transistor(p) => p.latency(), FilterImpl::Ota(p) => p.latency(), - FilterImpl::Svf(p) => p.latency(), + FilterImpl::Svf(_, p) => p.latency(), FilterImpl::Biquad(p) => p.latency(), } } @@ -478,7 +488,7 @@ impl DSPMeta for FilterImpl { match self { FilterImpl::Transistor(p) => p.reset(), FilterImpl::Ota(p) => p.reset(), - FilterImpl::Svf(p) => p.reset(), + FilterImpl::Svf(_, p) => p.reset(), FilterImpl::Biquad(p) => p.reset(), } } @@ -492,7 +502,7 @@ impl DSPProcess<1, 1> for FilterImpl { let y = match self { FilterImpl::Transistor(p) => p.process(x), FilterImpl::Ota(p) => p.process(x), - FilterImpl::Svf(p) => [p.process(x)[0]], + FilterImpl::Svf(bjt, p) => [p.process(bjt.process(x))[0]], FilterImpl::Biquad(p) => p.process(x), }; y.map(|x| drive_out * x) @@ -522,6 +532,7 @@ impl Filter { fn update_filter(&mut self, modulation_st: T) { let cutoff = semitone_to_ratio(modulation_st) * T::from_f64(self.params.cutoff.smoothed.next() as _); + let cutoff = cutoff.simd_clamp(T::zero(), self.samplerate / T::from_f64(12.)); let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); self.fimpl = match self.params.filter_type.value() { FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { @@ -534,10 +545,13 @@ impl Filter { ladder.compensated = true; FilterImpl::Ota(ladder) } - FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => { - FilterImpl::Svf(Svf::new(self.samplerate, cutoff, T::one() - resonance)) - } + FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => FilterImpl::Svf( + svf_clipper(), + Svf::new(self.samplerate, cutoff, T::one() - resonance), + ), FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { + let resonance = + T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(); FilterImpl::Biquad( Biquad::lowpass(cutoff / self.samplerate, resonance) .with_saturators(Default::default(), Default::default()), @@ -838,7 +852,8 @@ impl DSPMeta for Effects { impl DSPProcess<1, 1> for Effects { fn process(&mut self, x: [Self::Sample; 1]) -> [Self::Sample; 1] { - self.bjt.process(self.dc_blocker.process(x)) + let y = self.bjt.process(self.dc_blocker.process(x)); + y.map(|x| T::from_f64(0.5) * x) } } @@ -847,8 +862,8 @@ impl Effects { Self { dc_blocker: DcBlocker::new(samplerate), bjt: bjt::CommonCollector { - vee: T::from_f64(-2.5), - vcc: T::from_f64(2.5), + vee: T::from_f64(-2.), + vcc: T::from_f64(2.), xbias: T::from_f64(0.1), ybias: T::from_f64(-0.1), }, From 4c25cc5f4396f60b531e71033a2cfe1518c64bc6 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 22:17:35 +0200 Subject: [PATCH 59/67] feat(core): fast math module --- crates/valib-core/src/math/fast.rs | 99 ++++++++++++++++++++++++++++++ crates/valib-core/src/math/mod.rs | 2 + 2 files changed, 101 insertions(+) create mode 100644 crates/valib-core/src/math/fast.rs diff --git a/crates/valib-core/src/math/fast.rs b/crates/valib-core/src/math/fast.rs new file mode 100644 index 0000000..0f0f62f --- /dev/null +++ b/crates/valib-core/src/math/fast.rs @@ -0,0 +1,99 @@ +use crate::Scalar; +use numeric_literals::replace_float_literals; +use simba::simd::SimdBool; + +/// Rational approximation of tanh(x) which is valid in the range -3..3 +/// +/// This approximation only includes the rational approximation part, and will diverge outside the +/// bounds. In order to apply the tanh function over a bigger interval, consider clamping either the +/// input or the output. +/// +/// You should consider using [`tanh`] for a general-purpose faster tanh function, which uses +/// branching. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: Input value (low-error range: -3..3) +/// +/// returns: T +#[replace_float_literals(T::from_f64(literal))] +pub fn rational_tanh(x: T) -> T { + x * (27. + x * x) / (27. + 9. * x * x) +} + +/// Fast approximation of tanh(x). +/// +/// This approximation uses branching to clamp the output to -1..1 in order to be useful as a +/// general-purpose approximation of tanh. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: Input value +/// +/// returns: T +pub fn tanh(x: T) -> T { + rational_tanh(x).simd_clamp(-T::one(), T::one()) +} + +/// Fast approximation of exp, with maximum error in -1..1 of 0.59%, and in -3.14..3.14 of 9.8%. +/// +/// You should consider using [`exp`] for a better approximation which uses this function, but +/// allows a greater range at the cost of branching. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: Input value +/// +/// returns: T +#[replace_float_literals(T::from_f64(literal))] +pub fn fast_exp5(x: T) -> T { + (120. + x * (120. + x * (60. + x * (20. + x * (5. + x))))) * 0.0083333333 +} + +/// Fast approximation of exp, using [`fast_exp5`]. Uses branching to get a bigger range. +/// +/// Maximum error in the 0..10.58 range is 0.45%. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: +/// +/// returns: T +#[replace_float_literals(T::from_f64(literal))] +pub fn exp(x: T) -> T { + x.simd_lt(2.5).if_else2( + || T::simd_e() * fast_exp5(x - 1.), + (|| x.simd_lt(5.), || 33.115452 * fast_exp5(x - 3.5)), + || 403.42879 * fast_exp5(x - 6.), + ) +} + +/// Fast 2^x approximation, using [`exp`]. +/// +/// Maximum error in the 0..15.26 range is 0.45%. +/// +/// Source: +/// +/// # Arguments +/// +/// * `x`: +/// +/// returns: T +/// +/// # Examples +/// +/// ``` +/// +/// ``` +pub fn pow2(x: T) -> T { + let log_two = T::simd_ln_2(); + exp(log_two * x) +} diff --git a/crates/valib-core/src/math/mod.rs b/crates/valib-core/src/math/mod.rs index df8a757..139b036 100644 --- a/crates/valib-core/src/math/mod.rs +++ b/crates/valib-core/src/math/mod.rs @@ -8,6 +8,7 @@ use simba::simd::{SimdBool, SimdComplexField}; use crate::Scalar; +pub mod fast; pub mod interpolation; pub mod lut; pub mod nr; @@ -86,6 +87,7 @@ pub fn bilinear_prewarming_bounded(samplerate: T, wc: T) -> T { #[inline] pub fn smooth_min(t: T, a: T, b: T) -> T { let r = (-a / t).simd_exp2() + (-b / t).simd_exp2(); + // let r = fast::pow2(-a / t) + fast::pow2(-b / t); -t * r.simd_log2() } From ab9ef4f976c85e1b7095e9958b7933ae0cbfa27a Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Thu, 19 Sep 2024 22:18:00 +0200 Subject: [PATCH 60/67] perf(saturators): use fast tanh --- crates/valib-saturators/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/valib-saturators/src/lib.rs b/crates/valib-saturators/src/lib.rs index 387db9b..e99a9bf 100644 --- a/crates/valib-saturators/src/lib.rs +++ b/crates/valib-saturators/src/lib.rs @@ -14,6 +14,7 @@ use std::ops; use clippers::DiodeClipperModel; use valib_core::dsp::{DSPMeta, DSPProcess}; +use valib_core::math::fast; use valib_core::Scalar; pub mod adaa; @@ -152,13 +153,14 @@ pub struct Tanh; impl Saturator for Tanh { #[inline(always)] fn saturate(&self, x: S) -> S { - x.simd_tanh() + fast::tanh(x) } #[inline(always)] #[replace_float_literals(S::from_f64(literal))] fn sat_diff(&self, x: S) -> S { - 1. - x.simd_tanh().simd_powi(2) + let tanh = fast::tanh(x); + 1. - tanh * tanh } } From 63e67278621a9b2f048a2018a4eb42c034797c35 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sat, 21 Sep 2024 22:16:37 +0200 Subject: [PATCH 61/67] feat(examples): polysynth: filter fm --- examples/polysynth/src/dsp.rs | 9 +++++---- examples/polysynth/src/params.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 0e2a6ca..9b556d7 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -529,9 +529,10 @@ impl Filter { } impl Filter { - fn update_filter(&mut self, modulation_st: T) { - let cutoff = - semitone_to_ratio(modulation_st) * T::from_f64(self.params.cutoff.smoothed.next() as _); + fn update_filter(&mut self, modulation_st: T, input: T) { + let fm = semitone_to_ratio(T::from_f64(self.params.freq_mod.smoothed.next() as _) * input); + let modulation = semitone_to_ratio(modulation_st); + let cutoff = modulation * fm * T::from_f64(self.params.cutoff.smoothed.next() as _); let cutoff = cutoff.simd_clamp(T::zero(), self.samplerate / T::from_f64(12.)); let resonance = T::from_f64(self.params.resonance.smoothed.next() as _); self.fimpl = match self.params.filter_type.value() { @@ -583,7 +584,7 @@ impl DSPMeta for Filter { impl DSPProcess<2, 1> for Filter { fn process(&mut self, [x, mod_st]: [Self::Sample; 2]) -> [Self::Sample; 1] { - self.update_filter(mod_st); + self.update_filter(mod_st, x); self.fimpl.process([x]) } } diff --git a/examples/polysynth/src/params.rs b/examples/polysynth/src/params.rs index a76bf52..8b96e3e 100644 --- a/examples/polysynth/src/params.rs +++ b/examples/polysynth/src/params.rs @@ -186,6 +186,8 @@ pub struct FilterParams { pub keyboard_tracking: FloatParam, #[id = "env"] pub env_amt: FloatParam, + #[id = "fm"] + pub freq_mod: FloatParam, #[id = "fty"] pub filter_type: EnumParam, } @@ -250,6 +252,20 @@ impl FilterParams { oversample.clone(), &SmoothingStyle::Exponential(50.), )), + freq_mod: FloatParam::new( + "Freq. Modulation", + 0.0, + FloatRange::Linear { + min: -24., + max: 24., + }, + ) + .with_unit(" st") + .with_value_to_string(Arc::new(|x| format!("{:.2}", x))) + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Linear(10.), + )), filter_type: EnumParam::new("Filter Type", FilterType::TransistorLadder), } } From 14cc2066b02ec5753e4fa0e79cc89c29a5fd85c0 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 22 Sep 2024 00:06:24 +0200 Subject: [PATCH 62/67] wip(voice): dynamic mono/poly voice manager --- crates/valib-voice/src/dynamic.rs | 282 +++++++++++++++++++++++++++ crates/valib-voice/src/lib.rs | 10 +- crates/valib-voice/src/monophonic.rs | 63 ++++-- crates/valib-voice/src/polyphonic.rs | 46 +++++ examples/polysynth/src/dsp.rs | 24 ++- examples/polysynth/src/lib.rs | 1 - 6 files changed, 403 insertions(+), 23 deletions(-) create mode 100644 crates/valib-voice/src/dynamic.rs diff --git a/crates/valib-voice/src/dynamic.rs b/crates/valib-voice/src/dynamic.rs new file mode 100644 index 0000000..4fe8494 --- /dev/null +++ b/crates/valib-voice/src/dynamic.rs @@ -0,0 +1,282 @@ +use crate::monophonic::Monophonic; +use crate::polyphonic::Polyphonic; +use crate::{NoteData, Voice, VoiceManager}; +use std::fmt; +use std::fmt::Formatter; +use std::ops::Range; +use std::sync::Arc; +use valib_core::dsp::{DSPMeta, DSPProcess}; + +#[derive(Debug)] +enum Impl { + Monophonic(Monophonic), + Polyphonic(Polyphonic), +} + +impl DSPMeta for Impl { + type Sample = V::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + match self { + Impl::Monophonic(mono) => mono.set_samplerate(samplerate), + Impl::Polyphonic(poly) => poly.set_samplerate(samplerate), + } + } + + fn latency(&self) -> usize { + match self { + Impl::Monophonic(mono) => mono.latency(), + Impl::Polyphonic(poly) => poly.latency(), + } + } + + fn reset(&mut self) { + match self { + Impl::Monophonic(mono) => mono.reset(), + Impl::Polyphonic(poly) => poly.reset(), + } + } +} + +pub struct DynamicVoice { + pitch_bend_st: Range, + poly_voice_capacity: usize, + create_voice: Arc) -> V>, + current_manager: Impl, + legato: bool, + samplerate: f32, +} + +impl fmt::Debug for DynamicVoice { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("DynamicVoice") + .field("pitch_bend_st", &self.pitch_bend_st) + .field("poly_voice_capacity", &self.poly_voice_capacity) + .field("create_voice", &"Arc) -> V") + .field("current_manager", &self.current_manager) + .field("legato", &self.legato) + .field("samplerate", &self.samplerate) + .finish() + } +} + +impl DynamicVoice { + pub fn new_mono( + samplerate: f32, + poly_voice_capacity: usize, + legato: bool, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V, + ) -> Self { + let create_voice = Arc::new(create_voice); + let mono = Monophonic::new( + samplerate, + { + let create_voice = create_voice.clone(); + move |sr, nd| create_voice.clone()(sr, nd) + }, + legato, + ); + let pitch_bend_st = mono.pitch_bend_min_st..mono.pitch_bend_max_st; + Self { + pitch_bend_st, + poly_voice_capacity, + create_voice, + current_manager: Impl::Monophonic(mono), + legato, + samplerate, + } + } + + pub fn new_poly( + samplerate: f32, + capacity: usize, + legato: bool, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V, + ) -> Self { + let create_voice = Arc::new(create_voice); + let poly = Polyphonic::new(samplerate, capacity, { + let create_voice = create_voice.clone(); + move |sr, nd| create_voice.clone()(sr, nd) + }); + let pitch_bend_st = poly.pitch_bend_st.clone(); + Self { + pitch_bend_st, + poly_voice_capacity: capacity, + create_voice, + current_manager: Impl::Polyphonic(poly), + legato, + samplerate, + } + } + + pub fn switch(&mut self, polyphonic: bool) { + let new = match self.current_manager { + Impl::Monophonic(..) if polyphonic => { + let create_voice = self.create_voice.clone(); + let mut poly = + Polyphonic::new(self.samplerate, self.poly_voice_capacity, move |sr, nd| { + create_voice.clone()(sr, nd) + }); + poly.pitch_bend_st = self.pitch_bend_st.clone(); + Impl::Polyphonic(poly) + } + Impl::Polyphonic(..) if !polyphonic => { + let create_voice = self.create_voice.clone(); + let mut mono = Monophonic::new( + self.samplerate, + move |sr, nd| create_voice.clone()(sr, nd), + self.legato, + ); + mono.pitch_bend_min_st = self.pitch_bend_st.start; + mono.pitch_bend_max_st = self.pitch_bend_st.end; + Impl::Monophonic(mono) + } + _ => { + return; + } + }; + self.current_manager = new; + } + + pub fn is_monophonic(&self) -> bool { + matches!(self.current_manager, Impl::Monophonic(..)) + } + + pub fn is_polyphonic(&self) -> bool { + matches!(self.current_manager, Impl::Polyphonic(..)) + } + + pub fn legato(&self) -> bool { + self.legato + } + + pub fn set_legato(&mut self, legato: bool) { + self.legato = legato; + if let Impl::Monophonic(ref mut mono) = self.current_manager { + mono.set_legato(legato); + } + } + + pub fn clean_inactive_voices(&mut self) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.clean_voice_if_inactive(), + Impl::Polyphonic(ref mut poly) => poly.clean_inactive_voices(), + } + } +} + +impl DSPMeta for DynamicVoice { + type Sample = V::Sample; + + fn set_samplerate(&mut self, samplerate: f32) { + self.samplerate = samplerate; + self.current_manager.set_samplerate(samplerate); + } + + fn latency(&self) -> usize { + self.current_manager.latency() + } + + fn reset(&mut self) { + self.current_manager.reset(); + } +} + +impl VoiceManager for DynamicVoice { + type Voice = V; + type ID = as VoiceManager>::ID; + + fn capacity(&self) -> usize { + self.poly_voice_capacity + } + + fn get_voice(&self, id: Self::ID) -> Option<&Self::Voice> { + match self.current_manager { + Impl::Monophonic(ref mono) => mono.get_voice(()), + Impl::Polyphonic(ref poly) => poly.get_voice(id), + } + } + + fn get_voice_mut(&mut self, id: Self::ID) -> Option<&mut Self::Voice> { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.get_voice_mut(()), + Impl::Polyphonic(ref mut poly) => poly.get_voice_mut(id), + } + } + + fn all_voices(&self) -> impl Iterator { + 0..self.poly_voice_capacity + } + + fn note_on(&mut self, note_data: NoteData) -> Self::ID { + match self.current_manager { + Impl::Monophonic(ref mut mono) => { + mono.note_on(note_data); + 0 + } + Impl::Polyphonic(ref mut poly) => poly.note_on(note_data), + } + } + + fn note_off(&mut self, id: Self::ID, release_velocity: f32) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => { + mono.note_off((), release_velocity); + } + Impl::Polyphonic(ref mut poly) => { + poly.note_off(id, release_velocity); + } + } + } + + fn choke(&mut self, id: Self::ID) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.choke(()), + Impl::Polyphonic(ref mut poly) => poly.choke(id), + } + } + + fn panic(&mut self) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.panic(), + Impl::Polyphonic(ref mut poly) => poly.panic(), + } + } + + fn pitch_bend(&mut self, amount: f64) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.pitch_bend(amount), + Impl::Polyphonic(ref mut poly) => poly.pitch_bend(amount), + } + } + + fn aftertouch(&mut self, amount: f64) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.aftertouch(amount), + Impl::Polyphonic(ref mut poly) => poly.aftertouch(amount), + } + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.glide((), pressure), + Impl::Polyphonic(ref mut poly) => poly.glide(id, pressure), + } + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.glide((), semitones), + Impl::Polyphonic(ref mut poly) => poly.glide(id, semitones), + } + } +} + +impl> DSPProcess<0, 1> for DynamicVoice { + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + match self.current_manager { + Impl::Monophonic(ref mut mono) => mono.process([]), + Impl::Polyphonic(ref mut poly) => poly.process([]), + } + } +} diff --git a/crates/valib-voice/src/lib.rs b/crates/valib-voice/src/lib.rs index 62fbbf6..718f842 100644 --- a/crates/valib-voice/src/lib.rs +++ b/crates/valib-voice/src/lib.rs @@ -4,9 +4,10 @@ //! This crate provides abstractions around voice processing and voice management. use valib_core::dsp::{BlockAdapter, DSPMeta, DSPProcessBlock, SampleAdapter}; use valib_core::simd::SimdRealField; -use valib_core::util::midi_to_freq; +use valib_core::util::{midi_to_freq, semitone_to_ratio}; use valib_core::Scalar; +pub mod dynamic; pub mod monophonic; pub mod polyphonic; #[cfg(feature = "resampled")] @@ -161,6 +162,8 @@ impl Gain { pub struct NoteData { /// Note frequency pub frequency: T, + /// Frequency modulation (pitch bend, glide) + pub modulation_st: T, /// Note velocity pub velocity: Velocity, /// Note gain @@ -180,12 +183,17 @@ impl NoteData { let pressure = T::zero(); Self { frequency, + modulation_st: T::zero(), velocity, gain, pan, pressure, } } + + pub fn resolve_frequency(&self) -> T { + semitone_to_ratio(self.modulation_st) * self.frequency + } } /// Trait for types which manage voices. diff --git a/crates/valib-voice/src/monophonic.rs b/crates/valib-voice/src/monophonic.rs index 0f16ad0..b48e9a9 100644 --- a/crates/valib-voice/src/monophonic.rs +++ b/crates/valib-voice/src/monophonic.rs @@ -4,6 +4,9 @@ use crate::{NoteData, Voice, VoiceManager}; use num_traits::zero; +use numeric_literals::replace_float_literals; +use std::fmt; +use std::fmt::Formatter; use valib_core::dsp::buffer::{AudioBufferMut, AudioBufferRef}; use valib_core::dsp::{DSPMeta, DSPProcess, DSPProcessBlock}; use valib_core::util::lerp; @@ -15,15 +18,33 @@ pub struct Monophonic { pub pitch_bend_min_st: V::Sample, /// Maximum pitch bend amount (semitones) pub pitch_bend_max_st: V::Sample, - create_voice: Box) -> V>, + create_voice: Box) -> V>, voice: Option, base_frequency: V::Sample, - pitch_bend_st: V::Sample, + modulation_st: V::Sample, released: bool, legato: bool, samplerate: f32, } +impl fmt::Debug for Monophonic { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Monophonic") + .field( + "pitch_bend_st", + &(self.pitch_bend_min_st..self.pitch_bend_max_st), + ) + .field("create_voice", &"Box) -> V") + .field("voice", &self.voice) + .field("base_frequency", &self.base_frequency) + .field("pitch_bend", &self.modulation_st) + .field("released", &self.released) + .field("legato", &self.legato) + .field("samplerate", &self.samplerate) + .finish() + } +} + impl DSPMeta for Monophonic { type Sample = V::Sample; @@ -55,7 +76,7 @@ impl Monophonic { /// returns: Monophonic pub fn new( samplerate: f32, - create_voice: impl Fn(f32, NoteData) -> V + 'static, + create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V, legato: bool, ) -> Self { Self { @@ -65,7 +86,7 @@ impl Monophonic { voice: None, released: false, base_frequency: V::Sample::from_f64(440.), - pitch_bend_st: zero(), + modulation_st: zero(), legato, samplerate, } @@ -80,6 +101,16 @@ impl Monophonic { pub fn set_legato(&mut self, legato: bool) { self.legato = legato; } + + pub fn clean_voice_if_inactive(&mut self) { + self.voice.take_if(|v| !v.active()); + } + + #[replace_float_literals(V::Sample::from_f64(literal))] + fn pitch_bend_st(&self, amt: V::Sample) -> V::Sample { + let t = 0.5 * amt + 0.5; + lerp(t, self.pitch_bend_min_st, self.pitch_bend_max_st) + } } impl VoiceManager for Monophonic { @@ -110,9 +141,9 @@ impl VoiceManager for Monophonic { } } - fn note_on(&mut self, note_data: NoteData) -> Self::ID { + fn note_on(&mut self, mut note_data: NoteData) -> Self::ID { self.base_frequency = note_data.frequency; - self.pitch_bend_st = zero(); + note_data.modulation_st = self.modulation_st; if let Some(voice) = &mut self.voice { *voice.note_data_mut() = note_data; if self.released || !self.legato { @@ -138,11 +169,9 @@ impl VoiceManager for Monophonic { } fn pitch_bend(&mut self, amount: f64) { - self.pitch_bend_st = lerp( - V::Sample::from_f64(0.5 + amount / 2.), - self.pitch_bend_min_st, - self.pitch_bend_max_st, - ); + let mod_st = self.pitch_bend_st(V::Sample::from_f64(amount)); + self.modulation_st = mod_st; + self.update_voice_pitchmod(); } fn aftertouch(&mut self, amount: f64) { @@ -156,8 +185,18 @@ impl VoiceManager for Monophonic { voice.note_data_mut().pressure = V::Sample::from_f64(pressure as _); } } + fn glide(&mut self, _: Self::ID, semitones: f32) { - self.pitch_bend_st = V::Sample::from_f64(semitones as _); + self.modulation_st = V::Sample::from_f64(semitones as _); + self.update_voice_pitchmod(); + } +} + +impl Monophonic { + fn update_voice_pitchmod(&mut self) { + if let Some(voice) = &mut self.voice { + voice.note_data_mut().modulation_st = self.modulation_st; + } } } diff --git a/crates/valib-voice/src/polyphonic.rs b/crates/valib-voice/src/polyphonic.rs index 9ae218d..d8e309c 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -4,17 +4,23 @@ use crate::{NoteData, Voice, VoiceManager}; use num_traits::zero; +use numeric_literals::replace_float_literals; use std::fmt; use std::fmt::Formatter; +use std::ops::Range; use valib_core::dsp::{DSPMeta, DSPProcess}; +use valib_core::util::lerp; +use valib_core::Scalar; /// Polyphonic voice manager with rotating voice allocation pub struct Polyphonic { + pub pitch_bend_st: Range, create_voice: Box) -> V>, voice_pool: Box<[Option]>, active_voices: usize, next_voice: usize, samplerate: f32, + pitch_bend: V::Sample, } impl fmt::Debug for Polyphonic { @@ -47,11 +53,13 @@ impl Polyphonic { create_voice: impl 'static + Send + Sync + Fn(f32, NoteData) -> V + 'static, ) -> Self { Self { + pitch_bend_st: V::Sample::from_f64(-2.)..V::Sample::from_f64(2.), create_voice: Box::new(create_voice), next_voice: 0, voice_pool: (0..voice_capacity).map(|_| None).collect(), active_voices: 0, samplerate, + pitch_bend: zero(), } } @@ -64,6 +72,19 @@ impl Polyphonic { } } } + + fn update_voices_pitchmod(&mut self) { + let mod_st = self.get_pitch_bend(); + for voice in self.voice_pool.iter_mut().filter_map(|opt| opt.as_mut()) { + voice.note_data_mut().modulation_st = mod_st; + } + } + + #[replace_float_literals(V::Sample::from_f64(literal))] + fn get_pitch_bend(&self) -> V::Sample { + let t = 0.5 * self.pitch_bend + 0.5; + lerp(t, self.pitch_bend_st.start, self.pitch_bend_st.end) + } } impl DSPMeta for Polyphonic { @@ -153,6 +174,31 @@ impl VoiceManager for Polyphonic { self.voice_pool.fill_with(|| None); self.active_voices = 0; } + + fn pitch_bend(&mut self, amount: f64) { + self.pitch_bend = V::Sample::from_f64(amount); + self.update_voices_pitchmod(); + } + + fn aftertouch(&mut self, amount: f64) { + let pressure = V::Sample::from_f64(amount); + for voice in self.voice_pool.iter_mut().filter_map(|x| x.as_mut()) { + voice.note_data_mut().pressure = pressure; + } + } + + fn pressure(&mut self, id: Self::ID, pressure: f32) { + if let Some(voice) = &mut self.voice_pool[id] { + voice.note_data_mut().pressure = V::Sample::from_f64(pressure as _); + } + } + + fn glide(&mut self, id: Self::ID, semitones: f32) { + let mod_st = V::Sample::from_f64(semitones as _); + if let Some(voice) = &mut self.voice_pool[id] { + voice.note_data_mut().modulation_st = mod_st; + } + } } impl> DSPProcess<0, 1> for Polyphonic { diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 9b556d7..1542912 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -18,6 +18,7 @@ use valib::oscillators::Phasor; use valib::saturators::{bjt, Asinh, Clipper, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; use valib::util::{ratio_to_semitone, semitone_to_ratio}; +use valib::voice::dynamic::DynamicVoice; use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; @@ -798,7 +799,7 @@ where type SynthVoice = SampleAdapter>>, 0, 1>; -pub type VoiceManager = Polyphonic>; +pub type VoiceManager = DynamicVoice>; pub fn create_voice_manager>( samplerate: f32, @@ -807,14 +808,19 @@ pub fn create_voice_manager>( where [(); ::LANES]:, { - Polyphonic::new(samplerate, NUM_VOICES, move |samplerate, note_data| { - let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; - SampleAdapter::new(UpsampledVoice::new( - OVERSAMPLE, - MAX_BUFFER_SIZE, - BlockAdapter(RawVoice::new(target_samplerate, params.clone(), note_data)), - )) - }) + DynamicVoice::new_poly( + samplerate, + NUM_VOICES, + true, + move |samplerate, note_data| { + let target_samplerate = OVERSAMPLE as f64 * samplerate as f64; + SampleAdapter::new(UpsampledVoice::new( + OVERSAMPLE, + MAX_BUFFER_SIZE, + BlockAdapter(RawVoice::new(target_samplerate, params.clone(), note_data)), + )) + }, + ) } pub type Voices = VoiceManager; diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index 8b2a4cb..b3b1b99 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -118,7 +118,6 @@ impl VoiceIdMap { type SynthSample = f32; -#[derive(Debug)] pub struct PolysynthPlugin { voices: BlockAdapter>, effects: dsp::Effects, From ba562c3c0c99d76a63f416e13b26ec28616060a8 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 30 Sep 2024 17:35:10 +0200 Subject: [PATCH 63/67] fix(polysynth): compile errors post rebase --- examples/polysynth/src/dsp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 1542912..62f7050 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -422,7 +422,7 @@ impl FilterImpl { } FilterType::Svf => Self::Svf( svf_clipper(), - Svf::new(samplerate, cutoff, T::one() - resonance), + Svf::new(samplerate, cutoff, T::one() - resonance).with_saturator(Sinh), ), FilterType::Digital => Self::Biquad( Biquad::lowpass(cutoff / samplerate, T::one()) @@ -549,7 +549,7 @@ impl Filter { } FilterType::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => FilterImpl::Svf( svf_clipper(), - Svf::new(self.samplerate, cutoff, T::one() - resonance), + Svf::new(self.samplerate, cutoff, T::one() - resonance).with_saturator(Sinh), ), FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { let resonance = From 6cbac83b77ad65cddf981798138c766956ce9d70 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 30 Sep 2024 18:42:16 +0200 Subject: [PATCH 64/67] fix(polysynth): change mapping of resonance into biquad --- Cargo.lock | 1 + examples/polysynth/Cargo.toml | 10 ++++++---- examples/polysynth/src/dsp.rs | 12 +++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efac4e7..3b90270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3618,6 +3618,7 @@ dependencies = [ "nih_plug", "nih_plug_vizia", "num-traits", + "numeric_literals", "valib", ] diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml index 994d380..05e48d3 100644 --- a/examples/polysynth/Cargo.toml +++ b/examples/polysynth/Cargo.toml @@ -13,9 +13,11 @@ keywords.workspace = true crate-type = ["lib", "cdylib"] [dependencies] -fastrand = { version = "2.1.1", default-features = false } -fastrand-contrib = { version = "0.1.0", default-features = false } -valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} nih_plug = { workspace = true, features = ["standalone"] } nih_plug_vizia.workspace = true -num-traits.workspace = true \ No newline at end of file +num-traits.workspace = true +numeric_literals.workspace = true +valib = { path = "../..", features = ["filters", "oversample", "oscillators", "voice", "voice-upsampled", "nih-plug"]} + +fastrand = { version = "2.1.1", default-features = false } +fastrand-contrib = { version = "0.1.0", default-features = false } diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 62f7050..5ad7219 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -5,6 +5,7 @@ use fastrand_contrib::RngExt; use nih_plug::nih_log; use nih_plug::util::{db_to_gain, db_to_gain_fast}; use num_traits::{ConstOne, ConstZero}; +use numeric_literals::replace_float_literals; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, SampleAdapter}; @@ -431,15 +432,16 @@ impl FilterImpl { } } + #[replace_float_literals(T::from_f64(literal))] fn set_params(&mut self, samplerate: T, cutoff: T, resonance: T) { match self { Self::Transistor(p) => { p.set_cutoff(cutoff); - p.set_resonance(T::from_f64(4.) * resonance); + p.set_resonance(4. * resonance); } Self::Ota(p) => { p.set_cutoff(cutoff); - p.set_resonance(T::from_f64(4.) * resonance); + p.set_resonance(4. * resonance); } Self::Svf(_, p) => { p.set_cutoff(cutoff); @@ -448,7 +450,7 @@ impl FilterImpl { Self::Biquad(p) => { p.update_coefficients(&Biquad::lowpass( cutoff / samplerate, - T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(), + 0.1 + 4.7 * (2. * resonance - 1.).simd_exp() * resonance, )); } } @@ -552,8 +554,8 @@ impl Filter { Svf::new(self.samplerate, cutoff, T::one() - resonance).with_saturator(Sinh), ), FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { - let resonance = - T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(); + let resonance = T::from_f64(0.1) + + T::from_f64(4.7) * (T::from_f64(2.) * resonance - T::one()).simd_exp(); FilterImpl::Biquad( Biquad::lowpass(cutoff / self.samplerate, resonance) .with_saturators(Default::default(), Default::default()), From a45b0d7f93a1ea133451b58b12b7679ec83b9af1 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 30 Sep 2024 18:43:22 +0200 Subject: [PATCH 65/67] chore(polysynth): make logging less verbose --- examples/polysynth/src/dsp.rs | 9 +++------ examples/polysynth/src/lib.rs | 4 ---- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/examples/polysynth/src/dsp.rs b/examples/polysynth/src/dsp.rs index 5ad7219..34432b0 100644 --- a/examples/polysynth/src/dsp.rs +++ b/examples/polysynth/src/dsp.rs @@ -1,9 +1,8 @@ use crate::params::{FilterParams, FilterType, OscShape, PolysynthParams}; -use crate::{SynthSample, MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; use fastrand::Rng; use fastrand_contrib::RngExt; -use nih_plug::nih_log; -use nih_plug::util::{db_to_gain, db_to_gain_fast}; +use nih_plug::util::db_to_gain; use num_traits::{ConstOne, ConstZero}; use numeric_literals::replace_float_literals; use std::sync::atomic::{AtomicU64, Ordering}; @@ -16,11 +15,10 @@ use valib::filters::svf::Svf; use valib::math::interpolation::{sine_interpolation, Interpolate, Sine}; use valib::oscillators::polyblep::{SawBLEP, Sawtooth, Square, SquareBLEP, Triangle}; use valib::oscillators::Phasor; -use valib::saturators::{bjt, Asinh, Clipper, Saturator, Tanh}; +use valib::saturators::{bjt, Clipper, Saturator, Tanh}; use valib::simd::{SimdBool, SimdValue}; use valib::util::{ratio_to_semitone, semitone_to_ratio}; use valib::voice::dynamic::DynamicVoice; -use valib::voice::polyphonic::Polyphonic; use valib::voice::upsample::UpsampledVoice; use valib::voice::{NoteData, Voice}; use valib::Scalar; @@ -716,7 +714,6 @@ impl Voice for RawVoice { } fn release(&mut self, _: f32) { - nih_log!("RawVoice: release(_)"); self.vca_env.gate(false); self.vcf_env.gate(false); } diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs index b3b1b99..db2ddf6 100644 --- a/examples/polysynth/src/lib.rs +++ b/examples/polysynth/src/lib.rs @@ -205,7 +205,6 @@ impl Plugin for PolysynthPlugin { let key = VoiceKey::new(voice_id, channel, note); let note_data = NoteData::from_midi(note, velocity); let id = self.voices.note_on(note_data); - nih_log!("Note on {id} <- {key:?}"); self.voice_id_map.add_voice(key, id); } NoteEvent::NoteOff { @@ -217,10 +216,7 @@ impl Plugin for PolysynthPlugin { } => { let key = VoiceKey::new(voice_id, channel, note); if let Some((_, id)) = self.voice_id_map.remove_voice(key) { - nih_log!("Note off {id} <- {key:?}"); self.voices.note_off(id, velocity); - } else { - nih_log!("Note off {key:?}: ID not found"); } } NoteEvent::Choke { From bf28832b2264e3f1d19df2c390e7ca727cc4b42c Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 30 Sep 2024 19:04:26 +0200 Subject: [PATCH 66/67] fix(filters,examples): compile errors --- crates/valib-filters/src/ladder.rs | 2 +- examples/ladder/src/dsp.rs | 2 +- plugins/abrasive/src/dsp/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/valib-filters/src/ladder.rs b/crates/valib-filters/src/ladder.rs index 312fc26..ff3c82a 100644 --- a/crates/valib-filters/src/ladder.rs +++ b/crates/valib-filters/src/ladder.rs @@ -368,7 +368,7 @@ mod tests { bode: true, series: &[Series { label: "Frequency response", - samplerate, + samplerate: samplerate as f32, series: &responsef32, color: &BLUE, }], diff --git a/examples/ladder/src/dsp.rs b/examples/ladder/src/dsp.rs index abce13a..7a83821 100644 --- a/examples/ladder/src/dsp.rs +++ b/examples/ladder/src/dsp.rs @@ -156,7 +156,7 @@ impl DSPMeta for DspInner { type Sample = Sample; fn set_samplerate(&mut self, samplerate: f32) { - self.samplerate = samplerate; + self.samplerate = Sample::from_f64(samplerate as f64); self.drive.set_samplerate(samplerate); self.cutoff.set_samplerate(samplerate); self.resonance.set_samplerate(samplerate); diff --git a/plugins/abrasive/src/dsp/mod.rs b/plugins/abrasive/src/dsp/mod.rs index 79e21e9..902fbf9 100644 --- a/plugins/abrasive/src/dsp/mod.rs +++ b/plugins/abrasive/src/dsp/mod.rs @@ -83,7 +83,7 @@ impl DSPProcess<1, 1> for Equalizer { filter.set_scale(scale); } let [y] = self.dsp.process([drive * x]); - [y / drive.simd_asinh()] + [y / drive] } } From 2f9fbaec91ac20f3fa15a82066574c8fb713ab72 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 30 Sep 2024 19:58:04 +0200 Subject: [PATCH 67/67] chore(filters): update snapshots --- ...A_valib_saturators__Tanh__cfalse_r0.1.snap | 86 ++--- ...A_valib_saturators__Tanh__cfalse_r0.5.snap | 92 ++--- ...OTA_valib_saturators__Tanh__cfalse_r0.snap | 100 ++--- ...OTA_valib_saturators__Tanh__cfalse_r1.snap | 108 +++--- ...TA_valib_saturators__Tanh__ctrue_r0.1.snap | 86 ++--- ...TA_valib_saturators__Tanh__ctrue_r0.5.snap | 128 +++---- ..._OTA_valib_saturators__Tanh__ctrue_r0.snap | 100 ++--- ..._OTA_valib_saturators__Tanh__ctrue_r1.snap | 190 +++++----- .../valib_filters__svf__tests__svf_hz.snap | 358 +++++++++--------- 9 files changed, 624 insertions(+), 624 deletions(-) diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.1.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.1.snap index d1128da..fc9ad91 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.1.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.1.snap @@ -11,53 +11,53 @@ expression: output.get_channel(0) 0.005 0.011 0.019 -0.03 -0.045 -0.063 -0.084 -0.108 -0.135 -0.164 -0.195 -0.228 -0.262 -0.297 -0.333 -0.369 -0.405 -0.441 -0.476 -0.51 -0.543 -0.574 -0.605 -0.634 -0.661 -0.686 -0.71 -0.733 -0.753 -0.772 -0.79 -0.806 -0.82 -0.834 -0.845 -0.856 -0.865 -0.874 -0.881 -0.888 -0.893 -0.898 +0.031 +0.046 +0.064 +0.085 +0.11 +0.137 +0.166 +0.198 +0.231 +0.266 +0.301 +0.337 +0.373 +0.409 +0.445 +0.48 +0.514 +0.547 +0.578 +0.609 +0.637 +0.664 +0.69 +0.713 +0.736 +0.756 +0.775 +0.792 +0.808 +0.822 +0.835 +0.847 +0.857 +0.867 +0.875 +0.882 +0.889 +0.894 +0.899 0.903 -0.906 -0.909 +0.907 +0.91 0.912 0.914 0.916 0.917 -0.918 +0.919 0.919 0.92 0.92 @@ -65,7 +65,7 @@ expression: output.get_channel(0) 0.921 0.921 0.921 -0.921 +0.92 0.92 0.92 0.92 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.5.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.5.snap index 5ed47df..273ce55 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.5.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.5.snap @@ -11,39 +11,39 @@ expression: output.get_channel(0) 0.005 0.011 0.019 -0.03 -0.045 -0.063 -0.084 -0.108 -0.135 -0.164 -0.195 -0.228 -0.261 -0.296 -0.331 -0.367 -0.402 -0.436 -0.469 -0.501 -0.532 -0.561 -0.588 -0.613 -0.636 -0.657 -0.676 -0.692 -0.707 -0.72 -0.73 -0.739 -0.746 -0.751 -0.755 -0.757 +0.031 +0.046 +0.064 +0.085 +0.11 +0.137 +0.166 +0.198 +0.231 +0.265 +0.3 +0.335 +0.37 +0.405 +0.44 +0.473 +0.505 +0.535 +0.564 +0.591 +0.616 +0.639 +0.66 +0.678 +0.695 +0.709 +0.721 +0.732 +0.74 +0.747 +0.752 +0.756 +0.758 0.759 0.759 0.758 @@ -52,23 +52,23 @@ expression: output.get_channel(0) 0.75 0.746 0.742 -0.738 -0.733 -0.728 +0.737 +0.732 +0.727 0.723 0.718 0.713 -0.709 +0.708 0.704 -0.7 -0.696 -0.692 +0.699 +0.695 +0.691 0.688 0.685 0.682 0.679 -0.677 -0.675 +0.676 +0.674 0.673 0.671 0.67 @@ -86,15 +86,15 @@ expression: output.get_channel(0) 0.669 0.669 0.67 -0.67 +0.671 0.671 0.672 -0.672 +0.673 0.673 0.674 0.674 0.675 -0.675 +0.676 0.676 0.676 0.677 @@ -136,7 +136,6 @@ expression: output.get_channel(0) 0.678 0.678 0.678 -0.678 0.677 0.677 0.677 @@ -1026,3 +1025,4 @@ expression: output.get_channel(0) 0.678 0.678 0.678 +0.678 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.snap index bcbb12a..3403312 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r0.snap @@ -11,67 +11,67 @@ expression: output.get_channel(0) 0.005 0.011 0.019 -0.03 -0.045 -0.063 -0.084 -0.108 -0.135 -0.164 -0.195 -0.228 -0.262 -0.298 -0.334 -0.37 -0.406 -0.442 -0.477 -0.512 -0.545 -0.578 -0.609 -0.639 -0.667 -0.694 -0.719 -0.743 -0.765 -0.786 -0.805 -0.823 -0.839 -0.854 -0.868 -0.881 -0.893 -0.903 -0.913 -0.922 -0.93 -0.937 -0.944 -0.95 -0.955 -0.96 -0.964 -0.968 +0.031 +0.046 +0.064 +0.085 +0.11 +0.137 +0.166 +0.198 +0.231 +0.266 +0.301 +0.337 +0.374 +0.41 +0.446 +0.481 +0.516 +0.549 +0.582 +0.613 +0.642 +0.671 +0.697 +0.722 +0.746 +0.768 +0.788 +0.807 +0.825 +0.841 +0.856 +0.87 +0.883 +0.894 +0.905 +0.914 +0.923 +0.931 +0.938 +0.945 +0.951 +0.956 +0.961 +0.965 +0.969 0.972 0.975 0.978 0.98 -0.982 -0.984 +0.983 +0.985 0.986 0.988 0.989 -0.99 +0.991 0.992 0.993 -0.993 +0.994 0.994 0.995 -0.995 +0.996 0.996 0.997 0.997 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r1.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r1.snap index 527fefc..5239649 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r1.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__cfalse_r1.snap @@ -11,85 +11,85 @@ expression: output.get_channel(0) 0.005 0.011 0.019 -0.03 -0.045 -0.063 -0.084 -0.108 -0.134 -0.163 -0.194 -0.227 -0.26 -0.295 -0.329 -0.363 -0.397 -0.43 -0.461 -0.49 -0.518 -0.543 -0.566 -0.587 -0.605 -0.62 -0.632 -0.642 -0.65 -0.654 +0.031 +0.046 +0.064 +0.085 +0.11 +0.137 +0.166 +0.197 +0.23 +0.264 +0.298 +0.333 +0.367 +0.401 +0.433 +0.464 +0.494 +0.521 +0.546 +0.569 +0.589 +0.607 +0.622 +0.634 +0.644 +0.651 +0.655 0.657 0.657 0.656 0.652 -0.647 +0.646 0.64 0.632 -0.624 -0.614 -0.604 -0.593 -0.582 -0.571 -0.56 -0.549 -0.538 -0.528 -0.519 -0.51 -0.502 -0.495 -0.489 -0.483 +0.623 +0.613 +0.602 +0.592 +0.58 +0.569 +0.558 +0.548 +0.537 +0.527 +0.518 +0.509 +0.501 +0.494 +0.488 +0.482 0.478 0.474 0.471 0.469 -0.468 +0.467 0.467 0.467 0.467 0.469 0.47 -0.472 +0.473 0.475 0.478 0.481 -0.484 +0.485 0.488 0.491 0.495 0.498 -0.501 +0.502 0.505 0.508 0.511 -0.513 +0.514 0.516 -0.518 -0.52 +0.519 +0.521 0.522 -0.523 +0.524 0.525 0.526 0.526 @@ -114,7 +114,7 @@ expression: output.get_channel(0) 0.513 0.512 0.511 -0.511 +0.51 0.51 0.509 0.509 @@ -157,7 +157,7 @@ expression: output.get_channel(0) 0.514 0.514 0.514 -0.514 +0.513 0.513 0.513 0.513 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.1.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.1.snap index 04eb70d..8a67d82 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.1.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.1.snap @@ -8,52 +8,52 @@ expression: output.get_channel(0) 0.0 0.0 0.002 -0.005 +0.006 0.011 0.02 -0.032 -0.048 -0.067 -0.089 -0.115 -0.144 -0.175 -0.209 -0.244 -0.281 -0.319 -0.358 -0.398 -0.437 -0.476 -0.514 -0.552 -0.588 -0.623 -0.657 -0.689 -0.719 -0.748 -0.775 -0.8 -0.823 -0.844 -0.864 -0.882 -0.898 -0.913 -0.926 -0.938 -0.949 -0.959 -0.967 -0.975 -0.981 -0.987 +0.033 +0.049 +0.068 +0.091 +0.117 +0.146 +0.178 +0.212 +0.248 +0.285 +0.324 +0.363 +0.402 +0.442 +0.481 +0.519 +0.557 +0.593 +0.628 +0.662 +0.694 +0.724 +0.752 +0.779 +0.803 +0.826 +0.847 +0.867 +0.885 +0.901 +0.915 +0.928 +0.94 +0.951 +0.96 +0.968 +0.976 +0.982 +0.988 0.992 0.996 -0.999 -1.002 +1.0 +1.003 1.005 1.007 1.009 @@ -79,7 +79,7 @@ expression: output.get_channel(0) 1.009 1.008 1.008 -1.008 +1.007 1.007 1.007 1.007 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.5.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.5.snap index 1fbd9ec..c922b1b 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.5.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.5.snap @@ -10,74 +10,74 @@ expression: output.get_channel(0) 0.002 0.006 0.013 -0.023 -0.037 -0.055 -0.078 -0.105 -0.136 -0.17 -0.209 -0.25 -0.294 -0.34 -0.388 -0.438 -0.488 -0.538 -0.588 -0.638 -0.687 -0.734 -0.779 -0.822 -0.863 -0.901 -0.937 -0.969 -0.998 -1.024 -1.047 -1.068 -1.085 -1.099 -1.111 -1.12 -1.127 -1.131 -1.134 -1.135 -1.134 +0.024 +0.038 +0.057 +0.08 +0.107 +0.139 +0.175 +0.214 +0.256 +0.301 +0.348 +0.397 +0.446 +0.497 +0.548 +0.599 +0.648 +0.697 +0.744 +0.789 +0.832 +0.872 +0.91 +0.945 +0.976 +1.005 +1.031 +1.053 +1.072 +1.089 +1.103 +1.114 +1.122 +1.129 1.133 +1.135 +1.136 +1.135 +1.132 1.129 -1.125 -1.12 -1.114 -1.108 -1.101 -1.094 -1.087 -1.08 -1.073 -1.066 -1.059 -1.053 -1.046 -1.04 -1.035 -1.03 -1.025 -1.021 -1.017 -1.014 -1.011 +1.124 +1.119 +1.113 +1.107 +1.1 +1.093 +1.086 +1.078 +1.071 +1.064 +1.058 +1.051 +1.045 +1.039 +1.034 +1.029 +1.024 +1.02 +1.016 +1.013 +1.01 1.008 1.006 1.004 1.003 1.002 1.001 -1.001 +1.0 1.0 1.0 1.0 @@ -86,11 +86,11 @@ expression: output.get_channel(0) 1.002 1.003 1.004 -1.004 1.005 1.006 1.007 1.008 +1.008 1.009 1.01 1.011 @@ -103,7 +103,7 @@ expression: output.get_channel(0) 1.016 1.017 1.017 -1.017 +1.018 1.018 1.018 1.018 @@ -123,7 +123,7 @@ expression: output.get_channel(0) 1.018 1.018 1.018 -1.018 +1.017 1.017 1.017 1.017 @@ -170,7 +170,7 @@ expression: output.get_channel(0) 1.016 1.016 1.016 -1.016 +1.017 1.017 1.017 1.017 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.snap index bcbb12a..3403312 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r0.snap @@ -11,67 +11,67 @@ expression: output.get_channel(0) 0.005 0.011 0.019 -0.03 -0.045 -0.063 -0.084 -0.108 -0.135 -0.164 -0.195 -0.228 -0.262 -0.298 -0.334 -0.37 -0.406 -0.442 -0.477 -0.512 -0.545 -0.578 -0.609 -0.639 -0.667 -0.694 -0.719 -0.743 -0.765 -0.786 -0.805 -0.823 -0.839 -0.854 -0.868 -0.881 -0.893 -0.903 -0.913 -0.922 -0.93 -0.937 -0.944 -0.95 -0.955 -0.96 -0.964 -0.968 +0.031 +0.046 +0.064 +0.085 +0.11 +0.137 +0.166 +0.198 +0.231 +0.266 +0.301 +0.337 +0.374 +0.41 +0.446 +0.481 +0.516 +0.549 +0.582 +0.613 +0.642 +0.671 +0.697 +0.722 +0.746 +0.768 +0.788 +0.807 +0.825 +0.841 +0.856 +0.87 +0.883 +0.894 +0.905 +0.914 +0.923 +0.931 +0.938 +0.945 +0.951 +0.956 +0.961 +0.965 +0.969 0.972 0.975 0.978 0.98 -0.982 -0.984 +0.983 +0.985 0.986 0.988 0.989 -0.99 +0.991 0.992 0.993 -0.993 +0.994 0.994 0.995 -0.995 +0.996 0.996 0.997 0.997 diff --git a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r1.snap b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r1.snap index f6d8e7f..bf8ef22 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r1.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__ladder__tests__test_ladder_ir_valib_filters__ladder__OTA_valib_saturators__Tanh__ctrue_r1.snap @@ -7,141 +7,141 @@ expression: output.get_channel(0) 0.0 0.0 0.001 -0.002 +0.003 0.007 0.014 0.025 -0.04 -0.06 -0.085 -0.115 -0.149 -0.188 -0.232 -0.279 -0.33 -0.384 -0.44 -0.499 -0.559 -0.62 -0.682 -0.743 -0.804 -0.863 -0.92 -0.974 -1.025 -1.073 -1.116 -1.155 -1.19 -1.219 -1.244 -1.263 -1.278 -1.288 -1.294 -1.296 +0.041 +0.062 +0.087 +0.118 +0.153 +0.194 +0.238 +0.287 +0.339 +0.394 +0.452 +0.512 +0.573 +0.635 +0.698 +0.759 +0.82 +0.879 +0.936 +0.99 +1.041 +1.088 +1.13 +1.168 +1.201 +1.229 +1.252 +1.27 +1.284 +1.293 +1.297 +1.297 1.294 -1.288 -1.279 -1.268 -1.254 -1.238 -1.221 -1.202 -1.182 -1.162 -1.141 -1.121 -1.101 -1.081 -1.062 -1.044 -1.027 -1.012 -0.998 -0.985 -0.974 -0.964 -0.957 -0.95 -0.945 -0.942 -0.94 -0.94 +1.287 +1.278 +1.265 +1.251 +1.234 +1.216 +1.197 +1.177 +1.156 +1.135 +1.115 +1.094 +1.075 +1.056 +1.038 +1.022 +1.007 +0.993 +0.981 +0.97 +0.961 +0.954 +0.948 +0.943 +0.941 +0.939 +0.939 0.94 0.942 0.945 -0.948 -0.953 -0.958 -0.963 -0.969 -0.976 -0.982 -0.989 -0.995 -1.002 -1.008 -1.014 -1.019 -1.025 -1.03 -1.034 -1.038 -1.041 -1.044 -1.047 +0.949 +0.954 +0.959 +0.965 +0.971 +0.978 +0.984 +0.991 +0.997 +1.004 +1.01 +1.016 +1.021 +1.027 +1.031 +1.036 +1.039 +1.043 +1.045 +1.048 1.049 -1.05 1.051 1.052 1.052 1.052 +1.052 1.051 1.05 1.049 1.047 -1.046 +1.045 1.044 1.042 1.04 -1.038 -1.036 -1.034 -1.032 -1.03 +1.037 +1.035 +1.033 +1.031 +1.029 1.028 1.026 -1.025 +1.024 1.023 -1.022 1.021 1.02 1.019 1.018 -1.017 +1.018 1.017 1.017 1.016 1.016 +1.016 1.017 1.017 1.017 1.017 1.018 -1.018 +1.019 1.019 1.02 1.02 1.021 -1.021 1.022 -1.023 +1.022 1.023 1.024 +1.024 1.025 1.025 1.026 @@ -151,7 +151,7 @@ expression: output.get_channel(0) 1.027 1.027 1.027 -1.027 +1.028 1.028 1.028 1.028 @@ -167,7 +167,7 @@ expression: output.get_channel(0) 1.026 1.026 1.026 -1.026 +1.025 1.025 1.025 1.025 diff --git a/crates/valib-filters/src/snapshots/valib_filters__svf__tests__svf_hz.snap b/crates/valib-filters/src/snapshots/valib_filters__svf__tests__svf_hz.snap index 073b805..d057a03 100644 --- a/crates/valib-filters/src/snapshots/valib_filters__svf__tests__svf_hz.snap +++ b/crates/valib-filters/src/snapshots/valib_filters__svf__tests__svf_hz.snap @@ -3,159 +3,159 @@ source: crates/valib-filters/src/svf.rs expression: "&hz as &[_]" --- 1.0,0.0,0.0 -1.01,0.101,0.01 -1.04,0.208,0.042 -1.094,0.328,0.098 -1.179,0.471,0.189 -1.308,0.654,0.327 -1.504,0.903,0.542 -1.814,1.27,0.889 -2.312,1.85,1.48 -3.031,2.728,2.456 -3.332,3.333,3.334 -2.553,2.809,3.091 -1.756,2.108,2.53 -1.259,1.638,2.13 -0.952,1.334,1.869 -0.751,1.127,1.692 -0.611,0.979,1.567 -0.509,0.867,1.475 -0.433,0.78,1.405 -0.373,0.71,1.35 -0.326,0.652,1.306 -0.287,0.604,1.271 -0.256,0.563,1.241 -0.229,0.528,1.217 -0.207,0.497,1.196 -0.188,0.47,1.178 -0.171,0.446,1.162 -0.157,0.424,1.149 -0.144,0.405,1.137 -0.133,0.387,1.126 -0.123,0.371,1.117 -0.115,0.357,1.109 -0.107,0.343,1.102 -0.1,0.331,1.095 -0.094,0.319,1.089 -0.088,0.308,1.083 -0.083,0.298,1.079 -0.078,0.289,1.074 -0.073,0.28,1.07 -0.069,0.272,1.066 -0.066,0.264,1.063 -0.062,0.257,1.059 -0.059,0.25,1.056 -0.056,0.244,1.054 -0.054,0.237,1.051 -0.051,0.232,1.049 -0.049,0.226,1.047 -0.047,0.221,1.044 -0.045,0.216,1.043 -0.043,0.211,1.041 -0.041,0.206,1.039 -0.039,0.202,1.037 -0.038,0.198,1.036 -0.036,0.193,1.034 -0.035,0.19,1.033 -0.033,0.186,1.032 -0.032,0.182,1.031 -0.031,0.179,1.03 -0.03,0.175,1.029 -0.029,0.172,1.028 -0.028,0.169,1.027 -0.027,0.166,1.026 -0.026,0.163,1.025 -0.025,0.161,1.024 -0.024,0.158,1.023 -0.024,0.155,1.022 -0.023,0.153,1.022 -0.022,0.15,1.021 -0.021,0.148,1.02 -0.021,0.146,1.02 -0.02,0.143,1.019 -0.02,0.141,1.019 -0.019,0.139,1.018 -0.018,0.137,1.018 -0.018,0.135,1.017 -0.017,0.133,1.017 -0.017,0.131,1.016 -0.016,0.129,1.016 -0.016,0.128,1.015 -0.016,0.126,1.015 -0.015,0.124,1.015 -0.015,0.123,1.014 -0.014,0.121,1.014 -0.014,0.119,1.013 -0.014,0.118,1.013 -0.013,0.116,1.013 -0.013,0.115,1.012 -0.013,0.114,1.012 -0.012,0.112,1.012 -0.012,0.111,1.012 -0.012,0.109,1.011 -0.012,0.108,1.011 -0.011,0.107,1.011 -0.011,0.106,1.011 -0.011,0.104,1.01 -0.011,0.103,1.01 -0.01,0.102,1.01 -0.01,0.101,1.01 -0.01,0.1,1.009 -0.01,0.099,1.009 -0.009,0.098,1.009 -0.009,0.097,1.009 -0.009,0.096,1.009 -0.009,0.095,1.008 -0.009,0.094,1.008 -0.009,0.093,1.008 -0.008,0.092,1.008 -0.008,0.091,1.008 -0.008,0.09,1.008 -0.008,0.089,1.007 -0.008,0.088,1.007 -0.008,0.087,1.007 -0.007,0.086,1.007 -0.007,0.086,1.007 -0.007,0.085,1.007 -0.007,0.084,1.007 -0.007,0.083,1.007 +1.008,0.101,0.01 +1.034,0.207,0.041 +1.078,0.323,0.097 +1.145,0.458,0.183 +1.238,0.619,0.31 +1.362,0.817,0.49 +1.514,1.06,0.742 +1.667,1.334,1.067 +1.747,1.573,1.416 +1.666,1.667,1.667 +1.443,1.588,1.747 +1.184,1.421,1.706 +0.959,1.247,1.622 +0.783,1.096,1.536 +0.648,0.973,1.46 +0.545,0.872,1.397 +0.465,0.79,1.345 +0.401,0.723,1.302 +0.35,0.666,1.267 +0.309,0.618,1.237 +0.274,0.577,1.212 +0.245,0.541,1.191 +0.221,0.509,1.173 +0.2,0.481,1.158 +0.182,0.457,1.144 +0.167,0.435,1.132 +0.153,0.415,1.122 +0.141,0.396,1.113 +0.131,0.38,1.104 +0.121,0.365,1.097 +0.113,0.351,1.09 +0.105,0.338,1.084 +0.098,0.326,1.079 +0.092,0.315,1.074 +0.087,0.304,1.07 +0.082,0.295,1.066 +0.077,0.286,1.062 +0.073,0.277,1.059 +0.069,0.269,1.056 +0.065,0.262,1.053 +0.062,0.255,1.05 +0.059,0.248,1.048 +0.056,0.242,1.045 +0.053,0.236,1.043 +0.051,0.23,1.041 +0.048,0.224,1.039 +0.046,0.219,1.038 +0.044,0.214,1.036 +0.042,0.21,1.035 +0.041,0.205,1.033 +0.039,0.201,1.032 +0.037,0.196,1.03 +0.036,0.192,1.029 +0.035,0.189,1.028 +0.033,0.185,1.027 +0.032,0.181,1.026 +0.031,0.178,1.025 +0.03,0.175,1.024 +0.029,0.172,1.023 +0.028,0.169,1.023 +0.027,0.166,1.022 +0.026,0.163,1.021 +0.025,0.16,1.02 +0.024,0.157,1.02 +0.023,0.155,1.019 +0.023,0.152,1.019 +0.022,0.15,1.018 +0.021,0.147,1.017 +0.021,0.145,1.017 +0.02,0.143,1.016 +0.02,0.141,1.016 +0.019,0.139,1.015 +0.018,0.137,1.015 +0.018,0.135,1.015 +0.017,0.133,1.014 +0.017,0.131,1.014 +0.016,0.129,1.013 +0.016,0.127,1.013 +0.016,0.126,1.013 +0.015,0.124,1.012 +0.015,0.122,1.012 +0.014,0.121,1.012 +0.014,0.119,1.011 +0.014,0.118,1.011 +0.013,0.116,1.011 +0.013,0.115,1.011 +0.013,0.113,1.01 +0.012,0.112,1.01 +0.012,0.111,1.01 +0.012,0.109,1.01 +0.012,0.108,1.009 +0.011,0.107,1.009 +0.011,0.106,1.009 +0.011,0.104,1.009 +0.011,0.103,1.009 +0.01,0.102,1.008 +0.01,0.101,1.008 +0.01,0.1,1.008 +0.01,0.099,1.008 +0.009,0.098,1.008 +0.009,0.097,1.008 +0.009,0.096,1.007 +0.009,0.095,1.007 +0.009,0.094,1.007 +0.009,0.093,1.007 +0.008,0.092,1.007 +0.008,0.091,1.007 +0.008,0.09,1.007 +0.008,0.089,1.006 +0.008,0.088,1.006 +0.008,0.087,1.006 +0.007,0.086,1.006 +0.007,0.085,1.006 +0.007,0.085,1.006 +0.007,0.084,1.006 +0.007,0.083,1.006 0.007,0.082,1.006 -0.007,0.082,1.006 -0.006,0.081,1.006 -0.006,0.08,1.006 -0.006,0.079,1.006 -0.006,0.079,1.006 -0.006,0.078,1.006 -0.006,0.077,1.006 -0.006,0.076,1.006 +0.007,0.081,1.005 +0.006,0.081,1.005 +0.006,0.08,1.005 +0.006,0.079,1.005 +0.006,0.078,1.005 +0.006,0.078,1.005 +0.006,0.077,1.005 +0.006,0.076,1.005 0.006,0.076,1.005 0.006,0.075,1.005 0.006,0.074,1.005 -0.005,0.074,1.005 -0.005,0.073,1.005 -0.005,0.073,1.005 -0.005,0.072,1.005 -0.005,0.071,1.005 -0.005,0.071,1.005 -0.005,0.07,1.005 -0.005,0.07,1.005 -0.005,0.069,1.005 +0.005,0.074,1.004 +0.005,0.073,1.004 +0.005,0.072,1.004 +0.005,0.072,1.004 +0.005,0.071,1.004 +0.005,0.071,1.004 +0.005,0.07,1.004 +0.005,0.069,1.004 +0.005,0.069,1.004 0.005,0.068,1.004 0.005,0.068,1.004 0.005,0.067,1.004 0.004,0.067,1.004 0.004,0.066,1.004 0.004,0.066,1.004 -0.004,0.065,1.004 -0.004,0.065,1.004 -0.004,0.064,1.004 -0.004,0.064,1.004 -0.004,0.063,1.004 -0.004,0.063,1.004 -0.004,0.062,1.004 -0.004,0.062,1.004 -0.004,0.061,1.004 -0.004,0.061,1.004 +0.004,0.065,1.003 +0.004,0.065,1.003 +0.004,0.064,1.003 +0.004,0.064,1.003 +0.004,0.063,1.003 +0.004,0.063,1.003 +0.004,0.062,1.003 +0.004,0.062,1.003 +0.004,0.061,1.003 +0.004,0.061,1.003 0.004,0.06,1.003 0.004,0.06,1.003 0.004,0.059,1.003 @@ -167,18 +167,18 @@ expression: "&hz as &[_]" 0.003,0.057,1.003 0.003,0.056,1.003 0.003,0.056,1.003 -0.003,0.056,1.003 0.003,0.055,1.003 -0.003,0.055,1.003 -0.003,0.054,1.003 -0.003,0.054,1.003 -0.003,0.054,1.003 -0.003,0.053,1.003 -0.003,0.053,1.003 -0.003,0.052,1.003 -0.003,0.052,1.003 -0.003,0.052,1.003 -0.003,0.051,1.003 +0.003,0.055,1.002 +0.003,0.055,1.002 +0.003,0.054,1.002 +0.003,0.054,1.002 +0.003,0.054,1.002 +0.003,0.053,1.002 +0.003,0.053,1.002 +0.003,0.052,1.002 +0.003,0.052,1.002 +0.003,0.052,1.002 +0.003,0.051,1.002 0.003,0.051,1.002 0.003,0.051,1.002 0.003,0.05,1.002 @@ -199,24 +199,24 @@ expression: "&hz as &[_]" 0.002,0.045,1.002 0.002,0.045,1.002 0.002,0.045,1.002 -0.002,0.045,1.002 +0.002,0.044,1.002 0.002,0.044,1.002 0.002,0.044,1.002 0.002,0.044,1.002 0.002,0.043,1.002 0.002,0.043,1.002 -0.002,0.043,1.002 -0.002,0.043,1.002 -0.002,0.042,1.002 -0.002,0.042,1.002 -0.002,0.042,1.002 -0.002,0.041,1.002 -0.002,0.041,1.002 -0.002,0.041,1.002 -0.002,0.041,1.002 -0.002,0.04,1.002 -0.002,0.04,1.002 -0.002,0.04,1.002 +0.002,0.043,1.001 +0.002,0.043,1.001 +0.002,0.042,1.001 +0.002,0.042,1.001 +0.002,0.042,1.001 +0.002,0.041,1.001 +0.002,0.041,1.001 +0.002,0.041,1.001 +0.002,0.041,1.001 +0.002,0.04,1.001 +0.002,0.04,1.001 +0.002,0.04,1.001 0.002,0.04,1.001 0.002,0.039,1.001 0.002,0.039,1.001 @@ -294,18 +294,18 @@ expression: "&hz as &[_]" 0.001,0.025,1.001 0.001,0.025,1.001 0.001,0.025,1.001 -0.001,0.025,1.001 -0.001,0.024,1.001 -0.001,0.024,1.001 -0.001,0.024,1.001 -0.001,0.024,1.001 -0.001,0.024,1.001 -0.001,0.024,1.001 -0.001,0.024,1.001 -0.001,0.023,1.001 -0.001,0.023,1.001 -0.001,0.023,1.001 -0.001,0.023,1.001 +0.001,0.025,1.0 +0.001,0.024,1.0 +0.001,0.024,1.0 +0.001,0.024,1.0 +0.001,0.024,1.0 +0.001,0.024,1.0 +0.001,0.024,1.0 +0.001,0.024,1.0 +0.001,0.023,1.0 +0.001,0.023,1.0 +0.001,0.023,1.0 +0.001,0.023,1.0 0.001,0.023,1.0 0.001,0.023,1.0 0.001,0.022,1.0