diff --git a/src/dsp/biquad.rs b/src/dsp/biquad.rs index fe85964..55eff8d 100644 --- a/src/dsp/biquad.rs +++ b/src/dsp/biquad.rs @@ -25,6 +25,11 @@ pub struct BiquadCoefs { // https://github.com/VCVRack/Befaco/blob/v1/src/ChowDSP.hpp#L339 // more coeffs from there ^^^^^^^^^^^^^ ? impl BiquadCoefs { + #[inline] + pub fn new(b0: f32, b1: f32, b2: f32, a1: f32, a2: f32) -> Self { + Self { b0, b1, b2, a1, a2 } + } + /// Returns settings for a Butterworth lowpass filter. /// Cutoff is the -3 dB point of the filter in Hz. #[inline] @@ -113,6 +118,13 @@ impl Biquad { Default::default() } + #[inline] + pub fn new_with(b0: f32, b1: f32, b2: f32, a1: f32, a2: f32) -> Self { + let mut s = Self::new(); + s.set_coefs(BiquadCoefs::new(b0, b1, b2, a1, a2)); + s + } + #[inline] pub fn coefs(&self) -> &BiquadCoefs { &self.coefs diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 2f0a1ad..54037d3 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -969,10 +969,18 @@ impl DelayBuffer { /// Fetch a sample from the delay buffer at the given time. /// /// * `delay_time_ms` - Delay time in milliseconds. + #[inline] pub fn linear_interpolate_at(&self, delay_time_ms: F) -> F { + self.linear_interpolate_at_s((delay_time_ms * self.srate) / f(1000.0)) + } + + /// Fetch a sample from the delay buffer at the given offset. + /// + /// * `s_offs` - Sample offset in samples. + #[inline] + pub fn linear_interpolate_at_s(&self, s_offs: F) -> F { let data = &self.data[..]; let len = data.len(); - let s_offs = (delay_time_ms * self.srate) / f(1000.0); let offs = s_offs.floor().to_usize().unwrap_or(0) % len; let fract = s_offs.fract(); @@ -988,9 +996,16 @@ impl DelayBuffer { /// * `delay_time_ms` - Delay time in milliseconds. #[inline] pub fn cubic_interpolate_at(&self, delay_time_ms: F) -> F { + self.cubic_interpolate_at_s((delay_time_ms * self.srate) / f(1000.0)) + } + + /// Fetch a sample from the delay buffer at the given offset. + /// + /// * `s_offs` - Sample offset in samples. + #[inline] + pub fn cubic_interpolate_at_s(&self, s_offs: F) -> F { let data = &self.data[..]; let len = data.len(); - let s_offs = (delay_time_ms * self.srate) / f(1000.0); let offs = s_offs.floor().to_usize().unwrap_or(0) % len; let fract = s_offs.fract(); @@ -1201,6 +1216,47 @@ impl OnePoleLPF { } } +// Fixed one pole with setable pole and gain. +// Implementation taken from tubonitaub / alec-deason +// from https://github.com/alec-deason/virtual_modular/blob/4025f1ef343c2eb9cd74eac07b5350c1e7ec9c09/src/simd_graph.rs#L4292 +// under MIT License +#[derive(Debug, Copy, Clone, Default)] +pub struct FixedOnePole { + b0: f32, + a1: f32, + y1: f32, + gain: f32, +} + +impl FixedOnePole { + pub fn new(pole: f32, gain: f32) -> Self { + let b0 = + if pole > 0.0 { 1.0 - pole } + else { 1.0 + pole }; + + Self { + b0, + a1: -pole, + y1: 0.0, + gain + } + } + + pub fn reset(&mut self) { + self.y1 = 0.0; + } + + pub fn set_gain(&mut self, gain: f32) { self.gain = gain; } + + pub fn process(&mut self, input: f32) -> f32 { + let output = + self.b0 * self.gain * input + - self.a1 * self.y1; + self.y1 = output; + output + } +} + // one pole hp from valley rack free: // https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Common/DSP/OnePoleFilters.cpp #[inline] diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index eaf67b0..e71a80b 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -52,6 +52,8 @@ mod node_mux9; mod node_cqnt; #[allow(non_upper_case_globals)] mod node_quant; +#[allow(non_upper_case_globals)] +mod node_bowstri; pub mod biquad; pub mod tracker; @@ -121,6 +123,7 @@ use node_rndwk::RndWk; use node_mux9::Mux9; use node_cqnt::CQnt; use node_quant::Quant; +use node_bowstri::BowStri; pub const MIDI_MAX_FREQ : f32 = 13289.75; @@ -822,6 +825,13 @@ macro_rules! node_list { {6 0 dist setting(0) fa_distort 0 3} {7 1 ovrsmpl setting(1) fa_vosc_ovrsmpl 0 1} [0 sig], + bowstri => BowStri UIType::Generic UICategory::Osc + (0 freq n_pit d_pit r_fq f_freq stp_d -1.0, 0.5647131, 440.0) + (1 det n_det d_det r_det f_det stp_f -0.2, 0.2, 0.0) + (2 vel n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) + (3 force n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) + (4 pos n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) + [0 sig], out => Out UIType::Generic UICategory::IOUtil (0 ch1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 ch2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) diff --git a/src/dsp/node_bowstri.rs b/src/dsp/node_bowstri.rs new file mode 100644 index 0000000..f41195d --- /dev/null +++ b/src/dsp/node_bowstri.rs @@ -0,0 +1,195 @@ +// Copyright (c) 2021 Weird Constructor +// This file is a part of HexoDSP. Released under GPL-3.0-or-later. +// See README.md and COPYING for details. + +use crate::nodes::{NodeAudioContext, NodeExecContext}; +use crate::dsp::{ + NodeId, SAtom, ProcBuf, denorm_offs, denorm, + out, inp, DspNode, LedPhaseVals, NodeContext +}; +use crate::dsp::helpers::{FixedOnePole, DelayBuffer}; +use crate::dsp::biquad::{Biquad, BiquadCoefs}; + +// Bowed String instrument oscillator +// Bowed string model, a la Smith (1986), +// after McIntyre, Schumacher, Woodhouse (1983). +// +// This is a digital waveguide model, making its use possibly subject to +// patents held by Stanford University, Yamaha, and others. +// +// Implementation taken from tubonitaub / alec-deason +// from https://github.com/alec-deason/virtual_modular/blob/4025f1ef343c2eb9cd74eac07b5350c1e7ec9c09/src/simd_graph.rs#L3926 +// or +// under MIT License +// +// Which is a reimplementation of this implementation: +// https://github.com/thestk/stk/blob/38970124ecda9d78a74a375426ed5fb9c09840a2/src/Bowed.cpp#L32 +// By Perry R. Cook and Gary P. Scavone, 1995--2019. +// Contributions by Esteban Maestre, 2011. +#[derive(Debug, Clone)] +struct BowedString { + srate: f32, + nut_to_bow: DelayBuffer, + bow_to_bridge: DelayBuffer, + string_filter: FixedOnePole, + body_filters: [Biquad; 6], +} + +impl BowedString { + pub fn new() -> Self { + let mut s = Self { + srate: 44100.0, + nut_to_bow: DelayBuffer::new(), + bow_to_bridge: DelayBuffer::new(), + string_filter: FixedOnePole::new(0.0, 0.0), + body_filters: [ + Biquad::new_with(1.0, 1.5667, 0.3133, -0.5509, -0.3925), + Biquad::new_with(1.0, -1.9537, 0.9542, -1.6357, 0.8697), + Biquad::new_with(1.0, -1.6683, 0.8852, -1.7674, 0.8735), + Biquad::new_with(1.0, -1.8585, 0.9653, -1.8498, 0.9516), + Biquad::new_with(1.0, -1.9299, 0.9621, -1.9354, 0.9590), + Biquad::new_with(1.0, -1.9800, 0.9888, -1.9867, 0.9923), + ], + }; + s.set_sample_rate(s.srate); + s + } + + pub fn set_sample_rate(&mut self, sample_rate: f32) { + self.srate = sample_rate; + self.string_filter = + FixedOnePole::new( + 0.75 - (0.2 * (22050.0 / sample_rate)), + 0.95); + } + + pub fn reset(&mut self) { + self.nut_to_bow.reset(); + self.bow_to_bridge.reset(); + self.string_filter.reset(); + + for f in self.body_filters.iter_mut() { + f.reset(); + } + } + + #[inline] + pub fn process(&mut self, + freq: f32, bow_velocity: f32, bow_force: f32, pos: f32 + ) -> f32 + { + let total_l = self.srate / freq.max(20.0); + let bow_position = ((pos + 1.0) / 2.0).clamp(0.01, 0.99); + + let bow_nut_l = total_l * (1.0 - bow_position); + let bow_bridge_l = total_l * bow_position; + + let nut = -self.nut_to_bow.linear_interpolate_at_s(bow_nut_l); + let brid = self.bow_to_bridge.linear_interpolate_at_s(bow_bridge_l); + let bridge = -self.string_filter.process(brid); + + let dv = bow_velocity - (nut + bridge); + + let phat = + ((dv + 0.001) * bow_force + 0.75) + .powf(-4.0) + .clamp(0.0, 0.98); + + self.bow_to_bridge.feed(nut + phat*dv); + self.nut_to_bow.feed(bridge + phat*dv); + + let mut output = bridge; + for f in self.body_filters.iter_mut() { + output = f.tick(output); + } + + output + } +} + +/// A sine oscillator +#[derive(Debug, Clone)] +pub struct BowStri { + bstr: Box, +} + +const TWOPI : f32 = 2.0 * std::f32::consts::PI; + +impl BowStri { + pub fn new(_nid: &NodeId) -> Self { + Self { + bstr: Box::new(BowedString::new()), + } + } + pub const freq : &'static str = + "BowStri freq\nFrequency of the bowed string oscillator.\n\nRange: (-1..1)\n"; + pub const det : &'static str = + "BowStri det\nDetune the oscillator in semitones and cents. \ + the input of this value is rounded to semitones on coarse input. \ + Fine input lets you detune in cents (rounded). \ + A signal sent to this port is not rounded.\n\ + Note: The signal input allows detune +-10 octaves.\ + \nRange: (Knob -0.2 .. 0.2) / (Signal -1.0 .. 1.0)\n"; + pub const vel : &'static str = + "BowStri vel\n\n\nRange: (-1..1)\n"; + pub const force : &'static str = + "BowStri force\n\n\nRange: (-1..1)\n"; + pub const pos : &'static str = + "BowStri pos\n\n\nRange: (-1..1)\n"; + pub const sig : &'static str = + "BowStri sig\nOscillator signal output.\n\nRange: (-1..1)\n"; + + pub const DESC : &'static str = +r#"Bowed String Oscillator + +This is an oscillator that simulates a bowed string. +"#; + + pub const HELP : &'static str = +r#"BowStri - A Bowed String Oscillator + +"#; +} + +impl DspNode for BowStri { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, srate: f32) { + self.bstr.set_sample_rate(srate); + } + + fn reset(&mut self) { + self.bstr.reset(); + } + + #[inline] + fn process( + &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, + _nctx: &NodeContext, + _atoms: &[SAtom], inputs: &[ProcBuf], + outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + let o = out::BowStri::sig(outputs); + let freq = inp::BowStri::freq(inputs); + let det = inp::BowStri::det(inputs); + let vel = inp::BowStri::vel(inputs); + let force = inp::BowStri::force(inputs); + let pos = inp::BowStri::pos(inputs); + + let mut last_val = 0.0; + for frame in 0..ctx.nframes() { + let freq = denorm_offs::BowStri::freq(freq, det.read(frame), frame); + + let out = + self.bstr.process( + freq, + denorm::BowStri::vel(vel, frame), + denorm::BowStri::force(force, frame), + denorm::BowStri::pos(pos, frame)); + last_val = out; + o.write(frame, out); + } + + ctx_vals[0].set(last_val); + } +}