diff --git a/CHANGES.md b/CHANGES.md index 96c0e59..c7e0d51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,10 @@ To migrate, try `Adaptive::new(timescale, Tanh(hardness))`. - `Clip` shape now has a hardness parameter. `Clip(1.0)` to migrate. - `SvfCoeffs` is now `SvfCoefs`. -- Implemented denormal prevention for `x86`. +- Implemented denormal prevention for `x86` inside feedback loops. +- The resonator now accepts a Q parameter instead of bandwidth in Hz. + To migrate, Q = center / bandwidth. +- Feedback biquads and dirty biquads. ### Version 0.18.2 diff --git a/README.md b/README.md index 3563356..1f3090f 100644 --- a/README.md +++ b/README.md @@ -550,7 +550,7 @@ net.commit(); net = net >> peak_hz(1000.0, 1.0); net.commit(); /// Nodes can be replaced smoothly with a crossfade to avoid clicks. -not.crossfade(noise_id, Fade::Smooth, 1.0, Box::new(white())); +net.crossfade(noise_id, Fade::Smooth, 1.0, Box::new(white())); net.commit(); ``` @@ -714,7 +714,7 @@ Verified frequency responses are available for all linear filters. | `notch` | notch (2nd order) | frequency, Q | Simper SVF | | | `peak` | peaking (2nd order) | frequency, Q | Simper SVF | | | `pinkpass` | lowpass (3 dB/octave) | - | mixed FIR / 1st order | Turns white noise into pink noise. | -| `resonator` | bandpass (2nd order) | frequency, bandwidth | biquad | Gain stays constant as bandwidth is varied. | +| `resonator` | bandpass (2nd order) | frequency, Q | biquad | Gain stays constant as Q is varied. | ### Parameter Smoothing Filter @@ -726,16 +726,18 @@ This means the output value is always within input bounds. ### List of Nonlinear Filters -Unlike linear filters, nonlinear filters may be sensitive to incoming signal level. +Unlike linear filters, nonlinear filters are sensitive to incoming signal level. Due to nonlinearity, we do not attempt to calculate frequency responses for these filters. --- | Opcode | Type | Parameters | Family | Notes | | ------------ | ---------------------- | ------------ | ------------ | --------- | -| `bandrez` | bandpass (2nd order) | frequency, Q | nested 1st order | Sensitive to input level. | -| `lowrez` | lowpass (2nd order) | frequency, Q | nested 1st order | -..- | -| `moog` | lowpass (4th order) | frequency, Q | Moog ladder | -..- | +| `bandrez` | bandpass (2nd order) | frequency, Q | nested 1st order | | +| `dresonator` | bandpass (2nd order) | frequency, Q | dirty biquad | Stable when the feedback mapping is nonexpansive. | +| `fresonator` | bandpass (2nd order) | frequency, Q | feedback biquad | -..- | +| `lowrez` | lowpass (2nd order) | frequency, Q | nested 1st order | | +| `moog` | lowpass (4th order) | frequency, Q | Moog ladder | | --- @@ -899,9 +901,11 @@ The following table summarizes the available settings. | `constant` | `value` to set scalar value on all channels | | `dc` | `value` to set scalar value on all channels | | `dcblock_hz` | `center` | +| `dresonator_hz` | `center_q` | | `dsf_saw_r` | `roughness` in 0...1 | | `dsf_square_r` | `roughness` in 0...1 | | `follow` | `time` to set follow time in seconds | +| `fresonator_hz` | `center_q` | | `highpass_hz` | `center_q` | | `highpole_hz` | `center` | | `highshelf_hz` | `center_q_gain` | @@ -913,7 +917,7 @@ The following table summarizes the available settings. | `notch_hz` | `center_q` | | `pan` | `pan` to set pan value in -1...1 | | `peak_hz` | `center_q` | -| `resonator_hz` | `center_bandwidth` to set center and bandwidth in Hz | +| `resonator_hz` | `center_q` | If a node responds to `center_q_gain`, then it also responds to `center_q` and `center`. If a node responds to `center_q`, then it also responds to `center`. @@ -1075,6 +1079,8 @@ The type parameters in the table refer to the hacker preludes. | `declick()` | 1 | 1 | Apply 10 ms of fade-in to signal. | | `declick_s(t)` | 1 | 1 | Apply `t` seconds of fade-in to signal. | | `delay(t)` | 1 | 1 | Delay of `t` seconds. Delay time is rounded to the nearest sample. | +| `dresonator(shape)` | 3 (audio, frequency, bandwidth) | 1 | Dirty biquad resonator (2nd order) with feedback `shape`, for example, `Tanh(1.0)`. | +| `dresonator_hz(shape, f, q)` | 1 | 1 | Dirty biquad resonator (2nd order) with feedback `shape`, center `f` Hz and Q `q`. | | `dsf_saw()` | 2 (frequency, roughness) | 1 | Saw-like discrete summation formula oscillator. | | `dsf_saw_r(r)` | 1 (frequency) | 1 | Saw-like discrete summation formula oscillator with roughness `r` in 0...1. | | `dsf_square()` | 2 (frequency, roughness) | 1 | Square-like discrete summation formula oscillator. | @@ -1091,6 +1097,8 @@ The type parameters in the table refer to the hacker preludes. | `fir3(gain)` | 1 | 1 | Symmetric 3-point FIR calculated from desired `gain` at the Nyquist frequency. | | `flanger(fb, min_d, max_d, f)`| 1| 1 | Flanger effect with feedback amount `fb`, minimum delay `min_d` seconds, maximum delay `max_d` seconds and delay function `f`, e.g., `\|t\| lerp11(0.01, 0.02, sin_hz(0.1, t))`. | | `follow(t)` | 1 | 1 | Smoothing filter with halfway response time `t` seconds. | +| `fresonator(shape)` | 3 (audio, frequency, bandwidth) | 1 | Feedback biquad resonator (2nd order) with feedback `shape`, for example, `Softsign(1.0)`. | +| `fresonator_hz(shape, f, q)` | 1 | 1 | Feedback biquad resonator (2nd order) with feedback `shape`, center `f` Hz and Q `q`. | | `hammond()` | 1 (frequency) | 1 | Bandlimited Hammond oscillator. Emphasizes first three partials. | | `hammond_hz(f)` | - | 1 | Bandlimited Hammond oscillator at `f` Hz. Emphasizes first three partials. | | `highpass()` | 3 (audio, frequency, Q) | 1 | Highpass filter (2nd order). | @@ -1165,8 +1173,8 @@ The type parameters in the table refer to the hacker preludes. | `product(x, y)` | `x + y` | `x = y` | Multiply nodes `x` and `y`. Same as `x * y`. | | `pulse()` | 2 (frequency, duty cycle) | 1 | Bandlimited pulse wave with duty cycle in 0...1. | | `resample(node)` | 1 (speed) | `node` | Resample generator `node` using cubic interpolation at speed obtained from the input, where 1 is the original speed. | -| `resonator()` | 3 (audio, frequency, bandwidth) | 1 | Constant-gain bandpass resonator (2nd order). | -| `resonator_hz(f, bw)` | 1 | 1 | Constant-gain bandpass resonator (2nd order) with center frequency `f` Hz and bandwidth `bw` Hz. | +| `resonator()` | 3 (audio, frequency, Q) | 1 | Constant-gain bandpass resonator (2nd order). | +| `resonator_hz(f, q)` | 1 | 1 | Constant-gain bandpass resonator (2nd order) with center frequency `f` Hz and Q `q`. | | `resynth::(w, f)` | `I` | `O` | Frequency domain resynthesis with window length `w` and processing function `f`. | | `reverb_stereo(r, t, d)` | 2 | 2 | Stereo reverb (32-channel [FDN](https://ccrma.stanford.edu/~jos/pasp/Feedback_Delay_Networks_FDN.html)) with room size `r` meters (10 is average), reverberation time `t` seconds and high frequency damping `d` (in 0...1). | | `reverb2_stereo(r, t, d, m, f)` | 2 | 2 | Another stereo reverb (32-channel hybrid [FDN](https://ccrma.stanford.edu/~jos/pasp/Feedback_Delay_Networks_FDN.html)) with room size `r` meters (10-30 meters is supported), reverberation time `t` seconds, diffusion amount `d` (in 0...1), modulation speed `m` (nominal range 0...1, beyond starts being an effect), and loop filter `f`. | @@ -1257,7 +1265,8 @@ let partials = busf::(|f| noise() >> resonator_hz(xerp(1_000.0, 2_000 #### Waveshaping Modes These are arguments to the `shape` opcode. Shapes `Atan`, `Clip`, `Softsign` and `Tanh` -all have a slope of 1 at the origin when hardness is 1 and saturate in the range -1...1. +all have a slope of 1 at the origin when hardness is 1, saturate in the range -1...1, +and are nonexpansive up to a hardness of 1. - `Adaptive::new(timescale, inner)`: Apply adaptive normalizing distortion with smoothing `timescale` in seconds. Smoothing timescale is the time it takes for level estimation to move halfway to a new value. diff --git a/examples/keys.rs b/examples/keys.rs index 8b3308c..02dcdb0 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -29,6 +29,7 @@ enum Filter { Butterworth, Bandpass, Peak, + FeedbackBiquad, } #[allow(dead_code)] @@ -137,8 +138,6 @@ fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyh where T: SizedSample + FromSample, { - fundsp::denormal::prevent_denormals(); - let sample_rate = config.sample_rate.0 as f64; let channels = config.channels as usize; @@ -163,6 +162,7 @@ where net = net >> ((1.0 - var(&chorus_amount) >> follow(0.01) >> split()) * multipass() & (var(&chorus_amount) >> follow(0.01) >> split()) + * 2.0 * (chorus(0, 0.0, 0.02, 0.3) | chorus(1, 0.0, 0.02, 0.3))); net = net >> phaser >> flanger; net = net @@ -189,7 +189,7 @@ where )?; stream.play()?; - let viewport = ViewportBuilder::default().with_min_inner_size(vec2(360.0, 520.0)); + let viewport = ViewportBuilder::default().with_min_inner_size(vec2(360.0, 530.0)); let options = eframe::NativeOptions { viewport, @@ -197,7 +197,7 @@ where }; let state: State = State { - rnd: Rnd::from_u64(0), // Rnd::from_time(), + rnd: Rnd::from_u64(0), id: vec![None; KEYS.len()], sequencer, net, @@ -282,7 +282,10 @@ impl eframe::App for State { ui.selectable_value(&mut self.filter, Filter::Moog, "Moog"); ui.selectable_value(&mut self.filter, Filter::Butterworth, "Butterworth"); ui.selectable_value(&mut self.filter, Filter::Bandpass, "Bandpass"); + }); + ui.horizontal(|ui| { ui.selectable_value(&mut self.filter, Filter::Peak, "Peak"); + ui.selectable_value(&mut self.filter, Filter::FeedbackBiquad, "Feedback Biquad"); }); ui.separator(); @@ -469,13 +472,7 @@ impl eframe::App for State { Waveform::Noise => Net::wrap(Box::new( (noise() | pitch * 4.0 - | lfo(move |t| { - funutd::math::lerp( - 200.0, - 50.0 + 0.05 * pitch_hz, - clamp01(t * 3.0), - ) - })) + | lfo(move |t| funutd::math::lerp(2.0, 20.0, clamp01(t * 3.0)))) >> !resonator() >> resonator() >> shape(Adaptive::new(0.1, Atan(0.05))) * 0.5, @@ -499,6 +496,11 @@ impl eframe::App for State { (pass() | lfo(move |t| (xerp11(200.0, 10000.0, sin_hz(0.2, t)), 2.0))) >> peak(), )), + Filter::FeedbackBiquad => Net::wrap(Box::new( + (mul(5.0) + | lfo(move |t| (xerp11(200.0, 10000.0, sin_hz(0.2, t)), 5.0))) + >> fresonator(Softsign(1.0)), + )), }; let mut note = Box::new(waveform >> filter); // Give the note its own random seed. diff --git a/src/biquad.rs b/src/biquad.rs index f7e41d9..94acfcc 100644 --- a/src/biquad.rs +++ b/src/biquad.rs @@ -7,6 +7,7 @@ use super::shape::*; use super::signal::*; use super::*; use core::marker::PhantomData; +use numeric_array::typenum::*; #[derive(Copy, Clone, Debug, Default)] pub struct BiquadCoefs { @@ -18,7 +19,7 @@ pub struct BiquadCoefs { } impl BiquadCoefs { - /// Returns settings for a Butterworth lowpass filter. + /// Return settings for a Butterworth lowpass filter. /// Cutoff is the -3 dB point of the filter in Hz. pub fn butter_lowpass(sample_rate: F, cutoff: F) -> Self { let c = F::from_f64; @@ -32,13 +33,12 @@ impl BiquadCoefs { Self { a1, a2, b0, b1, b2 } } - /// Returns settings for a constant-gain bandpass resonator. + /// Return settings for a constant-gain bandpass resonator. /// The center frequency is given in Hz. - /// Bandwidth is the difference in Hz between -3 dB points of the filter response. /// The overall gain of the filter is independent of bandwidth. - pub fn resonator(sample_rate: F, center: F, bandwidth: F) -> Self { + pub fn resonator(sample_rate: F, center: F, q: F) -> Self { let c = F::from_f64; - let r: F = exp(-F::PI * bandwidth / sample_rate); + let r: F = exp(-F::PI * center / (q * sample_rate)); let a1: F = c(-2.0) * r * cos(F::TAU * center / sample_rate); let a2: F = r * r; let b0: F = sqrt(c(1.0) - r * r) * c(0.5); @@ -240,8 +240,8 @@ impl> AudioNode for ButterLowpass { /// Setting: (center, bandwidth). /// Number of inputs is `N`, either `U1` or `U3`. /// - Input 0: input signal -/// - Input 1 (optional): filter center frequency (peak) (Hz) -/// - Input 2 (optional): filter bandwidth (distance) between -3 dB points (Hz) +/// - Input 1 (optional): filter center (peak) frequency (Hz) +/// - Input 2 (optional): filter Q /// - Output 0: filtered signal #[derive(Clone)] pub struct Resonator> { @@ -249,28 +249,28 @@ pub struct Resonator> { biquad: Biquad, sample_rate: F, center: F, - bandwidth: F, + q: F, } impl> Resonator { - /// Create new resonator bandpass. Initial `center` frequency and `bandwidth` are specified in Hz. - pub fn new(center: F, bandwidth: F) -> Self { + /// Create new resonator bandpass. Initial `center` frequency is specified in Hz. + pub fn new(center: F, q: F) -> Self { let mut node = Resonator { _marker: PhantomData, biquad: Biquad::new(), sample_rate: F::from_f64(DEFAULT_SR), center, - bandwidth, + q, }; node.biquad.reset(); - node.set_center_bandwidth(center, bandwidth); + node.set_center_q(center, q); node } - pub fn set_center_bandwidth(&mut self, center: F, bandwidth: F) { + pub fn set_center_q(&mut self, center: F, q: F) { self.biquad - .set_coefs(BiquadCoefs::resonator(self.sample_rate, center, bandwidth)); + .set_coefs(BiquadCoefs::resonator(self.sample_rate, center, q)); self.center = center; - self.bandwidth = bandwidth; + self.q = q; } } @@ -285,36 +285,24 @@ impl> AudioNode for Resonator { fn set_sample_rate(&mut self, sample_rate: f64) { self.sample_rate = convert(sample_rate); - self.set_center_bandwidth(self.center, self.bandwidth); + self.set_center_q(self.center, self.q); } #[inline] fn tick(&mut self, input: &Frame) -> Frame { if N::USIZE >= 3 { let center: F = convert(input[1]); - let bandwidth: F = convert(input[2]); - if center != self.center || bandwidth != self.bandwidth { + let q: F = convert(input[2]); + if center != self.center || q != self.q { self.biquad - .set_coefs(BiquadCoefs::resonator(self.sample_rate, center, bandwidth)); + .set_coefs(BiquadCoefs::resonator(self.sample_rate, center, q)); self.center = center; - self.bandwidth = bandwidth; + self.q = q; } } self.biquad.tick(&[input[0]].into()) } - fn set(&mut self, setting: Setting) { - match setting.parameter() { - Parameter::Center(center) => { - self.set_center_bandwidth(F::from_f32(*center), self.bandwidth) - } - Parameter::CenterBandwidth(center, bandwidth) => { - self.set_center_bandwidth(F::from_f32(*center), F::from_f32(*bandwidth)) - } - _ => (), - } - } - fn route(&mut self, input: &SignalFrame, frequency: f64) -> SignalFrame { let mut output = SignalFrame::new(self.outputs()); output.set( @@ -330,58 +318,478 @@ impl> AudioNode for Resonator { } } +/// Biquad filter common mode parameters. Filter modes use a subset of these. +#[derive(Clone, Default)] +pub struct BiquadParams { + /// Sample rate in Hz. + pub sample_rate: F, + /// Center or cutoff in Hz. + pub center: F, + /// Q value, if applicable. + pub q: F, + /// Amplitude gain, if applicable. + pub gain: F, +} + +/// Operation of a filter mode. Retains any extra state needed +/// for efficient operation and can update filter coefficients. +/// The mode uses an optional set of inputs for continuously varying parameters. +/// - Input 0: audio +/// - Input 1: center or cutoff frequency in Hz +/// - Input 2: Q +/// - Input 3: amplitude gain +pub trait BiquadMode: Clone + Default + Sync + Send { + /// Number of inputs, which includes the audio input. + type Inputs: Size; + + /// Update coefficients and state from the full set of parameters. + fn update(&mut self, params: &BiquadParams, coefs: &mut BiquadCoefs); +} + +#[derive(Clone, Default)] +pub struct ResonatorBiquad { + _marker: PhantomData, +} + +impl ResonatorBiquad { + pub fn new() -> Self { + Self::default() + } +} + +impl BiquadMode for ResonatorBiquad { + type Inputs = U3; + fn update(&mut self, params: &BiquadParams, coefs: &mut BiquadCoefs) { + *coefs = BiquadCoefs::resonator(params.sample_rate, params.center, params.q); + } +} + #[derive(Clone)] /// Biquad in transposed direct form II with nonlinear feedback. -pub struct BiquadFb { - shape1: S, - shape2: S, +pub struct FbBiquad, S: Shape> { + mode: M, coefs: BiquadCoefs, + params: BiquadParams, + shape: S, s1: F, s2: F, } -// Transposed Direct Form II would be: -// y0 = b0 * x0 + s1 -// s1 = s2 + b1 * x0 - a1 * y0 -// s2 = b2 * x0 - a2 * y0 +impl, S: Shape> FbBiquad { + /// Create new feedback biquad filter. + pub fn new(mode: M, shape: S) -> Self { + let mut filter = Self { + mode, + coefs: BiquadCoefs::default(), + params: BiquadParams { + sample_rate: F::from_f64(DEFAULT_SR), + center: F::new(440), + q: F::one(), + gain: F::one(), + }, + shape, + s1: F::zero(), + s2: F::zero(), + }; + filter.set_sample_rate(DEFAULT_SR); + filter + } +} + +impl, S: Shape> AudioNode for FbBiquad { + const ID: u64 = 88; + type Inputs = M::Inputs; + type Outputs = U1; -impl BiquadFb { - /* - inline float process (float x) override - { - // process input sample, direct form II transposed - float y = z[1] + x*b[0]; - z[1] = z[2] + x*b[1] - saturator (y)*a[1]; - z[2] = x*b[2] - saturator (y)*a[2]; + fn reset(&mut self) { + self.s1 = F::zero(); + self.s2 = F::zero(); + self.shape.reset(); + } - return y; + fn set_sample_rate(&mut self, sample_rate: f64) { + self.params.sample_rate = F::from_f64(sample_rate); + self.mode.update(&self.params, &mut self.coefs); + } + + fn tick(&mut self, input: &Frame) -> Frame { + if M::Inputs::USIZE == 2 { + let center = F::from_f32(input[1]); + if center != self.params.center { + self.params.center = center; + self.mode.update(&self.params, &mut self.coefs); + } + } + if M::Inputs::USIZE == 3 { + let center = F::from_f32(input[1]); + let q = F::from_f32(input[2]); + if squared(center - self.params.center) + squared(q - self.params.q) != F::zero() { + self.params.center = center; + self.params.q = q; + self.mode.update(&self.params, &mut self.coefs); + } + } + if M::Inputs::USIZE == 4 { + let center = F::from_f32(input[1]); + let q = F::from_f32(input[2]); + let gain = F::from_f32(input[3]); + if squared(center - self.params.center) + + squared(q - self.params.q) + + squared(gain - self.params.gain) + != F::zero() + { + self.params.center = center; + self.params.q = q; + self.params.gain = gain; + self.mode.update(&self.params, &mut self.coefs); + } + } + // Transposed Direct Form II with nonlinear feedback is: + // y0 = b0 * x0 + s1 + // s1 = s2 + b1 * x0 - a1 * shape(y0) + // s2 = b2 * x0 - a2 * shape(y0) + let x0 = F::from_f32(input[0]); + let y0 = self.coefs.b0 * x0 + self.s1; + let fb = F::from_f32(self.shape.shape(y0.to_f32())); + self.s1 = self.s2 + self.coefs.b1 * x0 - fb * self.coefs.a1; + self.s2 = self.coefs.b2 * x0 - fb * self.coefs.a2; + [y0.to_f32()].into() + } + + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { + Routing::Arbitrary(0.0).route(input, self.outputs()) } - */ } -impl AudioNode for BiquadFb { - const ID: u64 = 88; - /// Input arity. - type Inputs = typenum::U1; - /// Output arity. +#[derive(Clone)] +/// Biquad in transposed direct form II with nonlinear feedback, fixed parameters. +pub struct FixedFbBiquad, S: Shape> { + mode: M, + coefs: BiquadCoefs, + params: BiquadParams, + shape: S, + s1: F, + s2: F, +} + +impl, S: Shape> FixedFbBiquad { + /// Create new feedback biquad filter. + pub fn new(mode: M, shape: S) -> Self { + let mut filter = Self { + mode, + coefs: BiquadCoefs::default(), + params: BiquadParams { + sample_rate: F::from_f64(DEFAULT_SR), + center: F::new(440), + q: F::one(), + gain: F::one(), + }, + shape, + s1: F::zero(), + s2: F::zero(), + }; + filter.set_sample_rate(DEFAULT_SR); + filter + } + + /// Set filter `center` or cutoff frequency in Hz. + pub fn set_center(&mut self, center: F) { + self.params.center = center; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter Q. + pub fn set_q(&mut self, q: F) { + self.params.q = q; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter amplitude `gain`. + pub fn set_gain(&mut self, gain: F) { + self.params.gain = gain; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter `center` or cutoff frequency in Hz and Q. + pub fn set_center_q(&mut self, center: F, q: F) { + self.params.center = center; + self.params.q = q; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter `center` or cutoff frequency in Hz, Q and amplitude `gain`. + pub fn set_center_q_gain(&mut self, center: F, q: F, gain: F) { + self.params.center = center; + self.params.q = q; + self.params.gain = gain; + self.mode.update(&self.params, &mut self.coefs); + } +} + +impl, S: Shape> AudioNode for FixedFbBiquad { + const ID: u64 = 90; + type Inputs = U1; + type Outputs = U1; + + fn reset(&mut self) { + self.s1 = F::zero(); + self.s2 = F::zero(); + self.shape.reset(); + } + + fn set(&mut self, setting: Setting) { + match setting.parameter() { + Parameter::Center(center) => self.set_center(F::from_f32(*center)), + Parameter::CenterQ(center, q) => { + self.set_center_q(F::from_f32(*center), F::from_f32(*q)) + } + Parameter::CenterQGain(center, q, gain) => { + self.set_center_q_gain(F::from_f32(*center), F::from_f32(*q), F::from_f32(*gain)) + } + _ => (), + } + } + + fn set_sample_rate(&mut self, sample_rate: f64) { + self.params.sample_rate = F::from_f64(sample_rate); + self.mode.update(&self.params, &mut self.coefs); + } + + fn tick(&mut self, input: &Frame) -> Frame { + let x0 = F::from_f32(input[0]); + let y0 = self.coefs.b0 * x0 + self.s1; + let fb = F::from_f32(self.shape.shape(y0.to_f32())); + self.s1 = self.s2 + self.coefs.b1 * x0 - fb * self.coefs.a1; + self.s2 = self.coefs.b2 * x0 - fb * self.coefs.a2; + [y0.to_f32()].into() + } + + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { + Routing::Arbitrary(0.0).route(input, self.outputs()) + } +} + +/// Biquad in transposed direct form II with nonlinear state shaping. +#[derive(Clone)] +pub struct DirtyBiquad, S: Shape> { + mode: M, + coefs: BiquadCoefs, + params: BiquadParams, + shape1: S, + shape2: S, + s1: F, + s2: F, +} + +impl, S: Shape> DirtyBiquad { + /// Create new dirty biquad filter. + pub fn new(mode: M, shape: S) -> Self { + let shape1 = shape; + let shape2 = shape1.clone(); + let mut filter = Self { + mode, + coefs: BiquadCoefs::default(), + params: BiquadParams { + sample_rate: F::from_f64(DEFAULT_SR), + center: F::new(440), + q: F::one(), + gain: F::one(), + }, + shape1, + shape2, + s1: F::zero(), + s2: F::zero(), + }; + filter.set_sample_rate(DEFAULT_SR); + filter + } +} + +impl, S: Shape> AudioNode for DirtyBiquad { + const ID: u64 = 89; + type Inputs = M::Inputs; type Outputs = typenum::U1; + fn reset(&mut self) { + self.s1 = F::zero(); + self.s2 = F::zero(); + self.shape1.reset(); + self.shape2.reset(); + } + + fn set_sample_rate(&mut self, sample_rate: f64) { + self.params.sample_rate = F::from_f64(sample_rate); + self.mode.update(&self.params, &mut self.coefs); + } + fn tick(&mut self, input: &Frame) -> Frame { + if M::Inputs::USIZE == 2 { + let center = F::from_f32(input[1]); + if center != self.params.center { + self.params.center = center; + self.mode.update(&self.params, &mut self.coefs); + } + } + if M::Inputs::USIZE == 3 { + let center = F::from_f32(input[1]); + let q = F::from_f32(input[2]); + if squared(center - self.params.center) + squared(q - self.params.q) != F::zero() { + self.params.center = center; + self.params.q = q; + self.mode.update(&self.params, &mut self.coefs); + } + } + if M::Inputs::USIZE == 4 { + let center = F::from_f32(input[1]); + let q = F::from_f32(input[2]); + let gain = F::from_f32(input[3]); + if squared(center - self.params.center) + + squared(q - self.params.q) + + squared(gain - self.params.gain) + != F::zero() + { + self.params.center = center; + self.params.q = q; + self.params.gain = gain; + self.mode.update(&self.params, &mut self.coefs); + } + } + // Transposed Direct Form II with nonlinear state shaping is: + // y0 = b0 * x0 + s1 + // s1 = shape(s2 + b1 * x0 - a1 * y0) + // s2 = shape(b2 * x0 - a2 * y0) let x0 = F::from_f32(input[0]); let y0 = self.coefs.b0 * x0 + self.s1; - self.s1 = self.s2 + self.coefs.b1 * x0 - - F::from_f32(self.shape1.shape(y0.to_f32())) * self.coefs.a1; - self.s2 = self.coefs.b2 * x0 - F::from_f32(self.shape2.shape(y0.to_f32())) * self.coefs.a2; + self.s1 = F::from_f32( + self.shape1 + .shape((self.s2 + self.coefs.b1 * x0 - y0 * self.coefs.a1).to_f32()), + ); + self.s2 = F::from_f32( + self.shape2 + .shape((self.coefs.b2 * x0 - y0 * self.coefs.a2).to_f32()), + ); [y0.to_f32()].into() } - /* + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { + Routing::Arbitrary(0.0).route(input, self.outputs()) + } +} + +/// Biquad in transposed direct form II with nonlinear state shaping, fixed parameters. +#[derive(Clone)] +pub struct FixedDirtyBiquad, S: Shape> { + mode: M, + coefs: BiquadCoefs, + params: BiquadParams, + shape1: S, + shape2: S, + s1: F, + s2: F, +} + +impl, S: Shape> FixedDirtyBiquad { + /// Create new dirty biquad filter. + pub fn new(mode: M, shape: S) -> Self { + let shape1 = shape; + let shape2 = shape1.clone(); + let mut filter = Self { + mode, + coefs: BiquadCoefs::default(), + params: BiquadParams { + sample_rate: F::from_f64(DEFAULT_SR), + center: F::new(440), + q: F::one(), + gain: F::one(), + }, + shape1, + shape2, + s1: F::zero(), + s2: F::zero(), + }; + filter.set_sample_rate(DEFAULT_SR); + filter + } + + /// Set filter `center` or cutoff frequency in Hz. + pub fn set_center(&mut self, center: F) { + self.params.center = center; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter Q. + pub fn set_q(&mut self, q: F) { + self.params.q = q; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter amplitude `gain`. + pub fn set_gain(&mut self, gain: F) { + self.params.gain = gain; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter `center` or cutoff frequency in Hz and Q. + pub fn set_center_q(&mut self, center: F, q: F) { + self.params.center = center; + self.params.q = q; + self.mode.update(&self.params, &mut self.coefs); + } + + /// Set filter `center` or cutoff frequency in Hz, Q and amplitude `gain`. + pub fn set_center_q_gain(&mut self, center: F, q: F, gain: F) { + self.params.center = center; + self.params.q = q; + self.params.gain = gain; + self.mode.update(&self.params, &mut self.coefs); + } +} + +impl, S: Shape> AudioNode for FixedDirtyBiquad { + const ID: u64 = 91; + type Inputs = U1; + type Outputs = U1; + + fn reset(&mut self) { + self.s1 = F::zero(); + self.s2 = F::zero(); + self.shape1.reset(); + self.shape2.reset(); + } + + fn set(&mut self, setting: Setting) { + match setting.parameter() { + Parameter::Center(center) => self.set_center(F::from_f32(*center)), + Parameter::CenterQ(center, q) => { + self.set_center_q(F::from_f32(*center), F::from_f32(*q)) + } + Parameter::CenterQGain(center, q, gain) => { + self.set_center_q_gain(F::from_f32(*center), F::from_f32(*q), F::from_f32(*gain)) + } + _ => (), + } + } + + fn set_sample_rate(&mut self, sample_rate: f64) { + self.params.sample_rate = F::from_f64(sample_rate); + self.mode.update(&self.params, &mut self.coefs); + } + fn tick(&mut self, input: &Frame) -> Frame { let x0 = F::from_f32(input[0]); let y0 = self.coefs.b0 * x0 + self.s1; - self.s1 = F::from_f32(self.shape1.shape((self.s2 + self.coefs.b1 * x0 - y0 * self.coefs.a1).to_f32())); - self.s2 = F::from_f32(self.shape2.shape((self.coefs.b2 * x0 - y0 * self.coefs.a2).to_f32())); + self.s1 = F::from_f32( + self.shape1 + .shape((self.s2 + self.coefs.b1 * x0 - y0 * self.coefs.a1).to_f32()), + ); + self.s2 = F::from_f32( + self.shape2 + .shape((self.coefs.b2 * x0 - y0 * self.coefs.a2).to_f32()), + ); [y0.to_f32()].into() } - */ + + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { + Routing::Arbitrary(0.0).route(input, self.outputs()) + } } diff --git a/src/hacker.rs b/src/hacker.rs index 617feba..2d71354 100644 --- a/src/hacker.rs +++ b/src/hacker.rs @@ -2410,7 +2410,7 @@ pub fn rotate(angle: f32, gain: f32) -> An> { )) } -/// Convert `AudioUnit` `unit` to an `AudioNode` with 32-bit sample type `f32`. +/// Convert `AudioUnit` `unit` to an `AudioNode`. /// The number of inputs and outputs is chosen statically and must match /// the `AudioUnit`. /// - Input(s): from `unit`. @@ -2418,3 +2418,63 @@ pub fn rotate(angle: f32, gain: f32) -> An> { pub fn unit, O: Size>(unit: Box) -> An> { An(Unit::new(unit)) } + +/// Biquad resonator with nonlinear state shaping using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Input 1: center frequency +/// - Input 2: Q +/// - Output 0: filtered audio +/// +/// ### Example +/// ``` +/// use fundsp::hacker::*; +/// let filter = dresonator(Tanh(1.0)); +/// ``` +pub fn dresonator(shape: S) -> An, S>> { + An(DirtyBiquad::new(ResonatorBiquad::new(), shape)) +} + +/// Biquad resonator with nonlinear state shaping with fixed parameters, using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Output 0: filtered audio +pub fn dresonator_hz( + shape: S, + center: f32, + q: f32, +) -> An, S>> { + super::prelude::dresonator_hz(shape, center as f64, q as f64) +} + +/// Biquad resonator with nonlinear feedback using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Input 1: center frequency +/// - Input 2: Q +/// - Output 0: filtered audio +/// +/// ### Example +/// ``` +/// use fundsp::hacker::*; +/// let filter = fresonator(Softsign(1.0)); +/// ``` +pub fn fresonator(shape: S) -> An, S>> { + An(FbBiquad::new(ResonatorBiquad::new(), shape)) +} + +/// Biquad resonator with nonlinear feedback with fixed parameters, using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Output 0: filtered audio +pub fn fresonator_hz( + shape: S, + center: f32, + q: f32, +) -> An, S>> { + super::prelude::fresonator_hz(shape, center as f64, q as f64) +} diff --git a/src/hacker32.rs b/src/hacker32.rs index d824d85..9748e2c 100644 --- a/src/hacker32.rs +++ b/src/hacker32.rs @@ -2410,7 +2410,7 @@ pub fn rotate(angle: f32, gain: f32) -> An> { )) } -/// Convert `AudioUnit` `unit` to an `AudioNode` with 32-bit sample type `f32`. +/// Convert `AudioUnit` `unit` to an `AudioNode`. /// The number of inputs and outputs is chosen statically and must match /// the `AudioUnit`. /// - Input(s): from `unit`. @@ -2418,3 +2418,63 @@ pub fn rotate(angle: f32, gain: f32) -> An> { pub fn unit, O: Size>(unit: Box) -> An> { An(Unit::new(unit)) } + +/// Biquad resonator with nonlinear state shaping using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Input 1: center frequency +/// - Input 2: Q +/// - Output 0: filtered audio +/// +/// ### Example +/// ``` +/// use fundsp::hacker32::*; +/// let filter = dresonator(Tanh(1.0)); +/// ``` +pub fn dresonator(shape: S) -> An, S>> { + An(DirtyBiquad::new(ResonatorBiquad::new(), shape)) +} + +/// Biquad resonator with nonlinear state shaping with fixed parameters, using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Output 0: filtered audio +pub fn dresonator_hz( + shape: S, + center: f32, + q: f32, +) -> An, S>> { + super::prelude::dresonator_hz(shape, center, q) +} + +/// Biquad resonator with nonlinear feedback using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Input 1: center frequency +/// - Input 2: Q +/// - Output 0: filtered audio +/// +/// ### Example +/// ``` +/// use fundsp::hacker32::*; +/// let filter = fresonator(Softsign(1.0)); +/// ``` +pub fn fresonator(shape: S) -> An, S>> { + An(FbBiquad::new(ResonatorBiquad::new(), shape)) +} + +/// Biquad resonator with nonlinear feedback with fixed parameters, using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Output 0: filtered audio +pub fn fresonator_hz( + shape: S, + center: f32, + q: f32, +) -> An, S>> { + super::prelude::fresonator_hz(shape, center, q) +} diff --git a/src/lib.rs b/src/lib.rs index df53bd1..cd08d15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -563,7 +563,11 @@ pub mod wave; pub mod wavetable; // GenericSequence is for Frame::generate. -pub use numeric_array::{self, generic_array::sequence::GenericSequence, typenum}; +pub use numeric_array::{ + self, + generic_array::sequence::{Concat, GenericSequence}, + typenum, +}; pub use funutd; pub use thingbuf; diff --git a/src/net.rs b/src/net.rs index d93f524..4fb9bcc 100644 --- a/src/net.rs +++ b/src/net.rs @@ -304,6 +304,7 @@ impl Net { /// All connections are retained. /// The replacement must have the same number of inputs and outputs /// as the node it is replacing. + /// The ID of the node remains the same. /// Returns the unit that was replaced. /// /// ### Example (Replace Saw Wave With Square Wave) @@ -329,6 +330,7 @@ impl Net { /// All connections are retained. /// The replacement must have the same number of inputs and outputs /// as the node it is replacing. + /// The ID of the node remains the same. /// /// ### Example (Replace Saw Wave With Square Wave Via 1 Second Crossfade) /// ``` diff --git a/src/prelude.rs b/src/prelude.rs index 1818343..316980e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -499,29 +499,29 @@ pub fn highpole_hz(cutoff: F) -> An> { /// Constant-gain bandpass resonator. /// - Input 0: audio /// - Input 1: center frequency (Hz) -/// - Input 2: bandwidth (Hz) +/// - Input 2: Q /// - Output 0: filtered audio /// /// ### Example: Filtered Noise Tone /// ``` /// use fundsp::prelude::*; -/// (noise() | dc((440.0, 5.0))) >> resonator::(); +/// (noise() | dc((440.0, 10.0))) >> resonator::(); /// ``` pub fn resonator() -> An> { - An(Resonator::new(F::new(440), F::new(110))) + An(Resonator::new(F::new(440), F::new(1))) } -/// Constant-gain bandpass resonator with fixed `center` frequency (Hz) and `bandwidth` (Hz). +/// Constant-gain bandpass resonator with fixed `center` frequency (Hz) and Q. /// - Input 0: audio /// - Output 0: filtered audio /// /// ### Example: Filtered Noise Tone /// ``` /// use fundsp::prelude::*; -/// noise() >> resonator_hz::(440.0, 5.0); +/// noise() >> resonator_hz::(440.0, 10.0); /// ``` -pub fn resonator_hz(center: F, bandwidth: F) -> An> { - An(Resonator::new(center, bandwidth)) +pub fn resonator_hz(center: F, q: F) -> An> { + An(Resonator::new(center, q)) } /// An arbitrary biquad filter with coefficients in normalized form. @@ -2855,11 +2855,62 @@ pub fn rotate(angle: f32, gain: f32) -> An> { )) } -/// Convert `AudioUnit` `unit` to an `AudioNode` with 32-bit sample type `f32`. -/// The number of inputs and outputs is chosen statically and must match -/// the `AudioUnit`. +/// Convert `AudioUnit` `unit` to an `AudioNode`. +/// The number of inputs and outputs is chosen statically and must match the `AudioUnit`. /// - Input(s): from `unit`. /// - Output(s): from `unit`. pub fn unit, O: Size>(unit: Box) -> An> { An(Unit::new(unit)) } + +/// Biquad resonator with nonlinear state shaping using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Input 1: center frequency +/// - Input 2: Q +/// - Output 0: filtered audio +pub fn dresonator(shape: S) -> An, S>> { + An(DirtyBiquad::new(ResonatorBiquad::new(), shape)) +} + +/// Biquad resonator with nonlinear state shaping with fixed parameters, using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Output 0: filtered audio +pub fn dresonator_hz( + shape: S, + center: F, + q: F, +) -> An, S>> { + let mut filter = FixedDirtyBiquad::new(ResonatorBiquad::new(), shape); + filter.set_center_q(center, q); + An(filter) +} + +/// Biquad resonator with nonlinear feedback using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Input 1: center frequency +/// - Input 2: Q +/// - Output 0: filtered audio +pub fn fresonator(shape: S) -> An, S>> { + An(FbBiquad::new(ResonatorBiquad::new(), shape)) +} + +/// Biquad resonator with nonlinear feedback with fixed parameters, using waveshaper `shape`. +/// The filter is stable when `shape` is nonexpansive. +/// (The usual waveshapes are nonexpansive up to hardness 1.0). +/// - Input 0: audio +/// - Output 0: filtered audio +pub fn fresonator_hz( + shape: S, + center: F, + q: F, +) -> An, S>> { + let mut filter = FixedFbBiquad::new(ResonatorBiquad::new(), shape); + filter.set_center_q(center, q); + An(filter) +} diff --git a/src/resynth.rs b/src/resynth.rs index cbce50a..c97ba57 100644 --- a/src/resynth.rs +++ b/src/resynth.rs @@ -77,6 +77,7 @@ impl FftWindow { /// Time in seconds at the center (peak) of the window. /// For time varying effects. + /// The first window begins at zero seconds. /// The window is Hann squared shaped. /// Latency is subtracted from stream time. /// Add `latency()` to this if you need stream time. @@ -87,6 +88,7 @@ impl FftWindow { /// Time in seconds at sample `i` of the window. /// For time varying effects. + /// The first window begins at zero seconds. /// There are `length()` samples in total. /// Latency is subtracted from stream time. /// Add `latency()` to this if you need stream time. @@ -95,6 +97,19 @@ impl FftWindow { (self.samples - self.length as u64 + i as u64) as f64 / self.sample_rate as f64 } + /// How many FFT windows there are in a second of audio. + /// The window callback is invoked once for each window. + #[inline] + pub fn windows_per_second(&self) -> f64 { + WINDOWS as f64 * self.sample_rate as f64 / self.length as f64 + } + + /// Time between windows (in seconds) and also the time between two callback calls. + #[inline] + pub fn delta_time(&self) -> f64 { + self.length as f64 / (WINDOWS as f64 * self.sample_rate as f64) + } + /// Get forward vectors for forward FFT. #[inline] pub(crate) fn forward_vectors(&mut self, channel: usize) -> (&Vec, &mut Vec) { @@ -122,6 +137,7 @@ impl FftWindow { } /// Return frequency (in Hz) associated with bin `i`. + /// This is a static value that does not take phase into account. #[inline] pub fn frequency(&self, i: usize) -> f32 { self.sample_rate / self.length() as f32 * i as f32 diff --git a/src/setting.rs b/src/setting.rs index e4d61f2..f2c6bf7 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -22,8 +22,6 @@ pub enum Parameter { CenterQ(f32, f32), /// Set filter center or cutoff frequency (Hz), Q value and amplitude gain. CenterQGain(f32, f32, f32), - /// Set filter center frequency (Hz) and bandwidth (Hz). - CenterBandwidth(f32, f32), /// Set miscellaneous value. Value(f32), /// Set filter coefficient. @@ -91,13 +89,6 @@ impl Setting { address: ArrayVec::new(), } } - /// Create setting for center and bandwidth parameters. - pub fn center_bandwidth(center: f32, bandwidth: f32) -> Self { - Self { - parameter: Parameter::CenterBandwidth(center, bandwidth), - address: ArrayVec::new(), - } - } /// Create setting for constant values. pub fn value(value: f32) -> Self { Self { diff --git a/src/shared.rs b/src/shared.rs index 9f24390..ac60ec5 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -24,6 +24,7 @@ pub trait Atomic: Float { impl Atomic for f32 { type Storage = AtomicU32; + #[inline] fn storage(t: Self) -> Self::Storage { AtomicU32::from(t.to_bits()) } @@ -47,6 +48,7 @@ pub struct Shared { } impl Shared { + #[inline] pub fn new(value: f32) -> Self { Self { value: Arc::new(f32::storage(value)), @@ -85,6 +87,7 @@ pub struct Var { } impl Var { + #[inline] pub fn new(shared: &Shared) -> Self { Self { value: Arc::clone(shared.get_shared()), @@ -92,11 +95,13 @@ impl Var { } /// Set the value of this variable. + #[inline] pub fn set_value(&self, value: f32) { f32::store(&self.value, value) } /// Get the value of this variable. + #[inline] pub fn value(&self) -> f32 { f32::get_stored(&self.value) }