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..3b90270 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" @@ -3600,6 +3609,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polysynth" +version = "0.1.0" +dependencies = [ + "fastrand 2.1.1", + "fastrand-contrib", + "nih_plug", + "nih_plug_vizia", + "num-traits", + "numeric_literals", + "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..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

{ @@ -123,13 +135,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-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/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 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() } 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 diff --git a/crates/valib-core/src/util.rs b/crates/valib-core/src/util.rs index f8827db..56150c7 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. @@ -202,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. /// @@ -264,3 +286,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/crates/valib-filters/src/ladder.rs b/crates/valib-filters/src/ladder.rs index b2b985a..ff3c82a 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, @@ -369,7 +368,7 @@ mod tests { bode: true, series: &[Series { label: "Frequency response", - samplerate, + samplerate: samplerate as f32, series: &responsef32, color: &BLUE, }], 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 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/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/crates/valib-oscillators/src/lib.rs b/crates/valib-oscillators/src/lib.rs index 76c665b..91f1305 100644 --- a/crates/valib-oscillators/src/lib.rs +++ b/crates/valib-oscillators/src/lib.rs @@ -8,18 +8,30 @@ 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 _); + self.set_frequency(self.frequency); + } + + fn reset(&mut self) { + self.phase = T::zero(); + } } #[profiling::all_functions] @@ -27,7 +39,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] } @@ -43,13 +55,32 @@ 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, } } + 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 + } + + 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 @@ -58,7 +89,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..eb132e9 --- /dev/null +++ b/crates/valib-oscillators/src/polyblep.rs @@ -0,0 +1,200 @@ +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::util::lerp; +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(&self) -> 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 self.blep.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(&self) -> 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(&self) -> impl IntoIterator> { + [ + PolyBLEP { + amplitude: T::ONE, + phase: T::ZERO, + }, + PolyBLEP { + amplitude: -T::ONE, + phase: T::one() - self.pw, + }, + ] + } + + fn naive_eval(&mut self, phase: Self::Sample) -> Self::Sample { + let dc_offset = lerp(self.pw, -T::ONE, T::ONE); + phase.simd_gt(self.pw).if_else(T::one, || -T::one()) + dc_offset + } +} + +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, 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 } + } + + 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-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 } } 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 475af22..718f842 100644 --- a/crates/valib-voice/src/lib.rs +++ b/crates/valib-voice/src/lib.rs @@ -2,10 +2,12 @@ //! # 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, semitone_to_ratio}; use valib_core::Scalar; +pub mod dynamic; pub mod monophonic; pub mod polyphonic; #[cfg(feature = "resampled")] @@ -20,11 +22,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 { @@ -114,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 @@ -124,9 +174,35 @@ 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, + 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. #[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 +210,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 +229,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 +253,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..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,9 +101,20 @@ 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 { +impl VoiceManager for Monophonic { + type Voice = V; type ID = (); fn capacity(&self) -> usize { @@ -109,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 { @@ -122,9 +154,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); } } @@ -137,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) { @@ -155,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 b9b1f89..d8e309c 100644 --- a/crates/valib-voice/src/polyphonic.rs +++ b/crates/valib-voice/src/polyphonic.rs @@ -1,16 +1,40 @@ //! # Polyphonic voice manager //! //! Provides a polyphonic voice manager with rotating voice allocation. + 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 { - create_voice: Box) -> V>, + 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 { + 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 { @@ -26,15 +50,41 @@ 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 { + 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(), } } + + /// 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(); + self.active_voices -= 1; + } + } + } + + 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 { @@ -61,7 +111,8 @@ impl DSPMeta for Polyphonic { } } -impl VoiceManager for Polyphonic { +impl VoiceManager for Polyphonic { + type Voice = V; type ID = usize; fn capacity(&self) -> usize { @@ -81,31 +132,72 @@ impl VoiceManager for Polyphonic { } fn note_on(&mut self, note_data: NoteData) -> Self::ID { - let id = self.next_voice; - self.next_voice += 1; - - if let Some(voice) = &mut self.voice_pool[id] { - *voice.note_data_mut() = note_data; - voice.reuse(); + 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)); + } + + 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 = (self.next_voice + 1) % self.voice_pool.len(); + } + + 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) { + 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); } } 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; + } + + 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; + } } } 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/ladder/src/dsp.rs b/examples/ladder/src/dsp.rs index 580bce5..7a83821 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 { @@ -155,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); @@ -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, diff --git a/examples/polysynth/Cargo.toml b/examples/polysynth/Cargo.toml new file mode 100644 index 0000000..05e48d3 --- /dev/null +++ b/examples/polysynth/Cargo.toml @@ -0,0 +1,23 @@ +[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] +nih_plug = { workspace = true, features = ["standalone"] } +nih_plug_vizia.workspace = true +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/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..5eb96ad --- /dev/null +++ b/examples/polysynth/src/dsp.rs @@ -0,0 +1,884 @@ +use crate::params::{FilterParams, FilterType, OscShape, PolysynthParams}; +use crate::{MAX_BUFFER_SIZE, NUM_VOICES, OVERSAMPLE}; +use fastrand::Rng; +use fastrand_contrib::RngExt; +use nih_plug::util::db_to_gain; +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}; +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, 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; + +#[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_ATTACK, + decay_base: -Self::TARGET_RATIO_RELEASE, + release_base: -Self::TARGET_RATIO_RELEASE, + 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_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, + 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_ATTACK); + self.attack_base = (1. + Self::TARGET_RATIO_ATTACK) * (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_DECAY); + self.decay_base = (self.sustain - Self::TARGET_RATIO_DECAY) * (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_RELEASE); + self.release_base = -Self::TARGET_RATIO_RELEASE * (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, + 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), + Square(Square), + Sawtooth(Sawtooth), +} + +impl PolyOsc { + 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).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) + } + } + } + + 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, + } + } + + 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 { + 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([]) + } + } + } +} + +#[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))) + } +} + +#[derive(Debug, Default, Copy, Clone)] +struct Sinh; + +impl Saturator for Sinh { + fn saturate(&self, x: T) -> T { + x.simd_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(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); + ladder.compensated = true; + Self::Transistor(ladder) + } + FilterType::OTALadder => { + let mut ladder = Ladder::new(samplerate, cutoff, T::from_f64(4.) * resonance); + ladder.compensated = true; + Self::Ota(ladder) + } + FilterType::Svf => Self::Svf( + svf_clipper(), + Svf::new(samplerate, cutoff, T::one() - resonance).with_saturator(Sinh), + ), + FilterType::Digital => Self::Biquad( + Biquad::lowpass(cutoff / samplerate, T::one()) + .with_saturators(Default::default(), Default::default()), + ), + } + } + + #[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(4. * resonance); + } + Self::Ota(p) => { + p.set_cutoff(cutoff); + p.set_resonance(4. * resonance); + } + Self::Svf(_, p) => { + p.set_cutoff(cutoff); + p.set_r(T::one() - resonance.simd_sqrt()); + } + Self::Biquad(p) => { + p.update_coefficients(&Biquad::lowpass( + cutoff / samplerate, + 0.1 + 4.7 * (2. * resonance - 1.).simd_exp() * resonance, + )); + } + } + } + + 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(12.) as _), + } + } +} + +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] { + 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(bjt, p) => [p.process(bjt.process(x))[0]], + FilterImpl::Biquad(p) => p.process(x), + }; + y.map(|x| drive_out * 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, 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() { + FilterType::TransistorLadder if !matches!(self.fimpl, FilterImpl::Transistor(..)) => { + 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::Svf if !matches!(self.fimpl, FilterImpl::Svf(..)) => FilterImpl::Svf( + svf_clipper(), + Svf::new(self.samplerate, cutoff, T::one() - resonance).with_saturator(Sinh), + ), + FilterType::Digital if !matches!(self.fimpl, FilterImpl::Biquad(..)) => { + 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()), + ) + } + _ => { + 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, x); + 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: Filter, + params: Arc, + vca_env: Adsr, + vcf_env: Adsr, + note_data: NoteData, + drift: [Drift; NUM_OSCILLATORS], + samplerate: T, + rng: Rng, +} + +impl RawVoice { + fn new( + 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: Filter::new(target_samplerate, params.filter_params.clone()), + noise: Noise::from_rng(rng.fork()), + 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(), + 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, + 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 _); + 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); + } + } + } + + 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.vca_env.is_idle() + } + + 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.vca_env.gate(false); + self.vcf_env.gate(false); + } + + fn reuse(&mut self) { + self.vca_env.gate(true); + self.vcf_env.gate(true); + } +} + +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); + self.vca_env.set_samplerate(samplerate); + self.vcf_env.set_samplerate(samplerate); + } + + fn reset(&mut self) { + for osc in &mut self.osc { + osc.reset(); + } + self.filter.reset(); + self.vca_env.reset(); + self.vcf_env.reset(); + } +} + +impl> DSPProcess<0, 1> for RawVoice +where + [(); ::LANES]:, +{ + fn process(&mut self, []: [Self::Sample; 0]) -> [Self::Sample; 1] { + const DRIFT_MAX_ST: f32 = 0.15; + self.update_osc_types(); + self.update_envelopes(); + + // Process oscillators + let frequency = self.note_data.frequency; + let filter_params = self.params.filter_params.clone(); + 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() + drift, + ) as _); + osc.set_pulse_width(T::from_f64(params.pulse_width.smoothed.next() as _)); + let [osc] = osc.process([osc_freq]); + osc + }); + let noise = self.noise.next_value_f32::(); + + // Process filter input + 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]); + + // Process filter + 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, freq_mod]) + .map(|x| static_amp * vca * x) + } +} + +type SynthVoice = SampleAdapter>>, 0, 1>; + +pub type VoiceManager = DynamicVoice>; + +pub fn create_voice_manager>( + samplerate: f32, + params: Arc, +) -> VoiceManager +where + [(); ::LANES]:, +{ + 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; + +pub fn create_voices>( + samplerate: f32, + params: Arc, +) -> Voices +where + [(); ::LANES]:, +{ + create_voice_manager(samplerate, params) +} + +#[derive(Debug, Copy, Clone)] +pub struct Effects { + dc_blocker: DcBlocker, + bjt: bjt::CommonCollector, +} + +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] { + let y = self.bjt.process(self.dc_blocker.process(x)); + y.map(|x| T::from_f64(0.5) * x) + } +} + +impl Effects { + pub fn new(samplerate: f32) -> Self { + Self { + dc_blocker: DcBlocker::new(samplerate), + bjt: bjt::CommonCollector { + vee: T::from_f64(-2.), + vcc: T::from_f64(2.), + xbias: T::from_f64(0.1), + ybias: T::from_f64(-0.1), + }, + } + } +} + +pub fn create_effects(samplerate: f32) -> Effects { + Effects::new(samplerate) +} diff --git a/examples/polysynth/src/editor.rs b/examples/polysynth/src/editor.rs new file mode 100644 index 0000000..29a93c9 --- /dev/null +++ b/examples/polysynth/src/editor.rs @@ -0,0 +1,100 @@ +use crate::params::PolysynthParams; +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}; +use std::sync::Arc; + +#[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(|| (1000, 600)) +} + +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| { + HStack::new(cx, move |cx| { + Label::new(cx, "Polysynth") + .font_weight(FontWeightKeyword::Thin) + .font_size(30.0) + .height(Pixels(50.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.) + .child_bottom(Pixels(8.)); + 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(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())); + }); + 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(Pixels(750.)); + }) + .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.)); + ResizeHandle::new(cx); + }) +} diff --git a/examples/polysynth/src/lib.rs b/examples/polysynth/src/lib.rs new file mode 100644 index 0000000..db2ddf6 --- /dev/null +++ b/examples/polysynth/src/lib.rs @@ -0,0 +1,364 @@ +#![feature(generic_const_exprs)] +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::{atomic, Arc}; +use valib::dsp::buffer::{AudioBufferMut, AudioBufferRef}; +use valib::dsp::{BlockAdapter, DSPMeta, DSPProcess, DSPProcessBlock}; +use valib::util::Rms; +use valib::voice::{NoteData, VoiceId, VoiceManager}; + +mod dsp; +mod editor; +mod params; + +const NUM_VOICES: usize = 16; +const OVERSAMPLE: usize = 8; +const MAX_BUFFER_SIZE: usize = 64; + +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)] +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; + +pub struct PolysynthPlugin { + voices: BlockAdapter>, + effects: dsp::Effects, + 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 { + voices: BlockAdapter(dsp::create_voices(DEFAULT_SAMPLERATE, params.clone())), + effects: dsp::create_effects(DEFAULT_SAMPLERATE), + 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 editor(&mut self, _: AsyncExecutor) -> Option> { + editor::create(self.params.clone(), self.params.editor_state.clone()) + } + + fn initialize( + &mut self, + _: &AudioIOLayout, + buffer_config: &BufferConfig, + _: &mut impl InitContext, + ) -> bool { + let sample_rate = buffer_config.sample_rate; + self.voices.set_samplerate(sample_rate); + self.effects.set_samplerate(sample_rate); + true + } + + fn reset(&mut self) { + self.voices.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.voices.note_on(note_data); + 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) { + self.voices.note_off(id, velocity); + } + } + 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.voices.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); + } + 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 { + 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); + } + _ => {} + } + } + } + }, + _ => {} + }, + 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.voices.process_block(input, dsp_block); + + block_start = block_end; + block_end = (block_start + MAX_BUFFER_SIZE).min(num_samples); + } + + self.voices.0.clean_inactive_voices(); + + // Effects processing + for s in &mut output[0][..] { + *s = self.effects.process([*s])[0]; + } + 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); 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..8b96e3e --- /dev/null +++ b/examples/polysynth/src/params.rs @@ -0,0 +1,407 @@ +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 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, + Triangle, + Square, + Saw, +} + +#[derive(Debug, Params)] +pub struct OscParams { + #[id = "shp"] + pub shape: EnumParam, + #[id = "pco"] + pub pitch_coarse: FloatParam, + #[id = "pfi"] + pub pitch_fine: FloatParam, + #[id = "pw"] + pub pulse_width: FloatParam, + #[id = "drift"] + pub drift: FloatParam, + #[id = "rtrg"] + pub retrigger: BoolParam, +} + +impl OscParams { + fn new(osc_index: usize, oversample: Arc) -> Self { + Self { + shape: EnumParam::new("Shape", OscShape::Saw), + 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.), + )), + 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), + } + } +} + +#[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"] + pub cutoff: FloatParam, + #[id = "res"] + pub resonance: FloatParam, + #[id = "kt"] + pub keyboard_tracking: FloatParam, + #[id = "env"] + pub env_amt: FloatParam, + #[id = "fm"] + pub freq_mod: FloatParam, + #[id = "fty"] + pub filter_type: EnumParam, +} + +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.), + )), + 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.), + )), + 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.), + )), + 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), + } + } +} + +#[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")] + pub vcf_env: Arc, + #[nested(group = "Filter")] + pub filter_params: Arc, + #[id = "out"] + pub output_level: FloatParam, + pub oversample: Arc, + #[persist = "editor"] + pub editor_state: 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())), + mixer_params: Arc::new(MixerParams::new(oversample.clone())), + vca_env: Arc::default(), + vcf_env: Arc::default(), + output_level: FloatParam::new( + "Output Level", + 0.5, + 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_unit(" dB") + .with_smoother(SmoothingStyle::OversamplingAware( + oversample.clone(), + &SmoothingStyle::Exponential(50.), + )), + oversample, + editor_state: crate::editor::default_state(), + } + } +} 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] } } 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() - } -}