diff --git a/src/dsp/biquad.rs b/src/dsp/biquad.rs new file mode 100644 index 0000000..4d42551 --- /dev/null +++ b/src/dsp/biquad.rs @@ -0,0 +1,226 @@ +// 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. +// +// The implementation of this Biquad Filter has been adapted from +// SamiPerttu, Copyright (c) 2020, under the MIT License. +// See also: https://github.com/SamiPerttu/fundsp/blob/master/src/filter.rs +// +// You will find a float type agnostic version in SamiPerttu's code. +// I converted this to pure f32 for no good reason, other than making +// the code more readable (for me). + +use std::f32::consts::*; + +#[derive(Copy, Clone, Debug, Default)] +pub struct BiquadCoefs { + pub a1: f32, + pub a2: f32, + pub b0: f32, + pub b1: f32, + pub b2: f32, +} + +impl BiquadCoefs { + /// Returns settings for a Butterworth lowpass filter. + /// Cutoff is the -3 dB point of the filter in Hz. + #[inline] + pub fn butter_lowpass(sample_rate: f32, cutoff: f32) -> BiquadCoefs { + let f = (cutoff * PI / sample_rate).tan(); + let a0r = 1.0 / (1.0 + SQRT_2 * f + f * f); + let a1 = (2.0 * f * f - 2.0) * a0r; + let a2 = (1.0 - SQRT_2 * f + f * f) * a0r; + let b0 = f * f * a0r; + let b1 = 2.0 * b0; + let b2 = b0; + BiquadCoefs { a1, a2, b0, b1, b2 } + } + + /// Returns 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: f32, center: f32, bandwidth: f32) -> BiquadCoefs { + let r = (-PI * bandwidth / sample_rate).exp(); + let a1 = -2.0 * r * (TAU * center / sample_rate).cos(); + let a2 = r * r; + let b0 = (1.0 - r * r).sqrt() * 0.5; + let b1 = 0.0; + let b2 = -b0; + BiquadCoefs { a1, a2, b0, b1, b2 } + } + +// /// Frequency response at frequency `omega` expressed as fraction of sampling rate. +// pub fn response(&self, omega: f64) -> Complex64 { +// let z1 = Complex64::from_polar(1.0, -TAU * omega); +// let z2 = Complex64::from_polar(1.0, -2.0 * TAU * omega); +// (re(self.b0) + re(self.b1) * z1 + re(self.b2) * z2) +// / (re(1.0) + re(self.a1) * z1 + re(self.a2) * z2) +// } +} + +/// 2nd order IIR filter implemented in normalized Direct Form I. +#[derive(Copy, Clone, Default)] +pub struct Biquad { + coefs: BiquadCoefs, + x1: f32, + x2: f32, + y1: f32, + y2: f32, +} + +impl Biquad { + pub fn new() -> Self { + Default::default() + } + + #[inline] + pub fn coefs(&self) -> &BiquadCoefs { + &self.coefs + } + + #[inline] + pub fn set_coefs(&mut self, coefs: BiquadCoefs) { + self.coefs = coefs; + } + + fn reset(&mut self) { + self.x1 = 0.0; + self.x2 = 0.0; + self.y1 = 0.0; + self.y2 = 0.0; + } + + #[inline] + fn tick(&mut self, input: f32) -> f32 { + let x0 = input; + let y0 = + self.coefs.b0 * x0 + + self.coefs.b1 * self.x1 + + self.coefs.b2 * self.x2 + - self.coefs.a1 * self.y1 + - self.coefs.a2 * self.y2; + self.x2 = self.x1; + self.x1 = x0; + self.y2 = self.y1; + self.y1 = y0; + y0 + + // Transposed Direct Form II would be: + // y0 = b0 * x0 + s1 + // s1 = s2 + b1 * x0 - a1 * y0 + // s2 = b2 * x0 - a2 * y0 + } +} + +#[derive(Copy, Clone)] +pub struct ButterLowpass { + biquad: Biquad, + sample_rate: f32, + cutoff: f32, +} + +impl ButterLowpass { + pub fn new(sample_rate: f32, cutoff: f32) -> Self { + let mut this = ButterLowpass { + biquad: Biquad::new(), + sample_rate, + cutoff: 0.0, + }; + this.set_cutoff(cutoff); + this + } + + pub fn set_cutoff(&mut self, cutoff: f32) { + self.biquad + .set_coefs(BiquadCoefs::butter_lowpass(self.sample_rate, cutoff)); + self.cutoff = cutoff; + } + + fn set_sample_rate(&mut self, srate: f32) { + self.sample_rate = srate; + self.reset(); + self.biquad.reset(); + self.set_cutoff(self.cutoff); + } + + fn reset(&mut self) { + self.biquad.reset(); + self.set_cutoff(self.cutoff); + } + + #[inline] + fn tick(&mut self, input: f32) -> f32 { + self.biquad.tick(input) + } +} + +// Loosely adapted from https://github.com/VCVRack/Befaco/blob/v1/src/ChowDSP.hpp +// Copyright (c) 2019-2020 Andrew Belt and Befaco contributors +// Under GPLv-3.0-or-later +// +// Which was originally taken from https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp +// Copyright (c) 2020 jatinchowdhury18 +/// Implements oversampling with a ratio of 4 and a 4 times cascade +/// of Butterworth lowpass filters. +struct Oversampling4x4 { + filters: [Biquad; 4], + buffer: [f32; 4], +} + +impl Oversampling4x4 { + pub fn new() -> Self { + let mut this = Self { + filters: [Biquad::new(); 4], + buffer: [0.0; 4], + }; + + this.set_sample_rate(44100.0); + + this + } + + pub fn reset(&mut self) { + self.buffer = [0.0; 4]; + for filt in &mut self.filters { + filt.reset(); + } + } + + pub fn set_sample_rate(&mut self, srate: f32) { + let cutoff = 0.98 * (srate / 2.0); + + for filt in &mut self.filters { + filt.set_coefs(BiquadCoefs::butter_lowpass(srate, cutoff)); + } + } + + #[inline] + pub fn upsample(&mut self, v: f32) { + self.buffer[0] = 4.0 * v; + self.buffer[1] = 0.0; + self.buffer[2] = 0.0; + self.buffer[3] = 0.0; + + for s in &mut self.buffer { + for filt in &mut self.filters { + *s = filt.tick(*s); + } + } + } + + #[inline] + pub fn resample_buffer(&mut self) -> &mut [f32; 4] { &mut self.buffer } + + #[inline] + pub fn downsample(&mut self) -> f32 { + let mut ret = 0.0; + for s in &mut self.buffer { + for filt in &mut self.filters { + ret = filt.tick(*s); + } + } + + ret + } +} diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 9ee775e..9fe01f8 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -34,7 +34,10 @@ mod node_sfilter; mod node_mix3; #[allow(non_upper_case_globals)] mod node_bosc; +#[allow(non_upper_case_globals)] +mod node_vosc; +pub mod biquad; pub mod tracker; mod satom; pub mod helpers; @@ -82,6 +85,7 @@ use node_smap::SMap; use node_sfilter::SFilter; use node_mix3::Mix3; use node_bosc::BOsc; +use node_vosc::VOsc; pub const MIDI_MAX_FREQ : f32 = 13289.75; @@ -399,6 +403,15 @@ macro_rules! r_fq { ($x: expr, $coarse: expr) => { } } } +/// The rounding function for vs (v scale) UI knobs +macro_rules! r_vps { ($x: expr, $coarse: expr) => { + if $coarse { + n_vps!((d_vps!($x)).round()) + } else { + n_vps!((d_vps!($x) * 10.0).round() / 10.0) + } +} } + /// The default steps function: macro_rules! stp_d { () => { (20.0, 100.0) } } /// The UI steps to control parameters with a finer fine control: @@ -414,6 +427,11 @@ macro_rules! f_def { ($formatter: expr, $v: expr, $denorm_v: expr) => { write!($formatter, "{:6.3}", $denorm_v) } } +// Default formatting function with very low precision +macro_rules! f_defvlp { ($formatter: expr, $v: expr, $denorm_v: expr) => { + write!($formatter, "{:4.1}", $denorm_v) +} } + macro_rules! f_freq { ($formatter: expr, $v: expr, $denorm_v: expr) => { if ($denorm_v >= 1000.0) { write!($formatter, "{:6.0}Hz", $denorm_v) @@ -448,6 +466,7 @@ macro_rules! f_det { ($formatter: expr, $v: expr, $denorm_v: expr) => { } } } + // norm-fun denorm-min // denorm-fun denorm-max define_exp!{n_gain d_gain 0.0, 2.0} @@ -464,6 +483,8 @@ define_exp!{n_ftme d_ftme 0.25, 1000.0} // to reach more exact "1.0". define_lin!{n_ogin d_ogin 0.0, 2.0} +define_lin!{n_vps d_vps 1.0, 10.0} + // A note about the input-indicies: // // Atoms and Input parameters share the same global ID space @@ -558,6 +579,13 @@ macro_rules! node_list { (2 pw n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) {3 0 wtype setting(0) fa_bosc_wtype 0 3} [0 sig], + vosc => VOsc 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 d n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) + (3 v n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) + (4 vs n_vps d_vps r_vps f_defvlp stp_d 0.0, 1.0, 1.0) + [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_vosc.rs b/src/dsp/node_vosc.rs new file mode 100644 index 0000000..667512b --- /dev/null +++ b/src/dsp/node_vosc.rs @@ -0,0 +1,156 @@ +// 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, DspNode, LedPhaseVals, NodeContext, + GraphAtomData, GraphFun, +}; + +//#[macro_export] +//macro_rules! fa_bosc_wtype { ($formatter: expr, $v: expr, $denorm_v: expr) => { { +// let s = +// match ($v.round() as usize) { +// 0 => "Sin", +// 1 => "Tri", +// 2 => "Saw", +// 3 => "Pulse", +// _ => "?", +// }; +// write!($formatter, "{}", s) +//} } } + +/// A simple amplifier +#[derive(Debug, Clone)] +pub struct VOsc { +// osc: PolyBlepOscillator, + israte: f32, + phase: f32, +} + +impl VOsc { + pub fn new(nid: &NodeId) -> Self { + let init_phase = nid.init_phase(); + + Self { + israte: 1.0 / 44100.0, + phase: init_phase, + } + } + + pub const freq : &'static str = + "VOsc freq\nBase frequency of the oscillator.\n\nRange: (-1..1)\n"; + pub const det : &'static str = + "VOsc 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 d : &'static str = + "VOsc d\n\nRange: (0..1)\n"; + pub const v : &'static str = + "VOsc v\n\nRange: (0..1)\n"; + pub const vs : &'static str = + "VOsc vs\nScaling factor for 'v'.\nRange: (0..1)\n"; + pub const wtype : &'static str = + "VOsc wtype\nWaveform type\nAvailable waveforms:\n\ + Sin - Sine Waveform\n\ + Tri - Triangle Waveform\n\ + Saw - Sawtooth Waveform\n\ + Pulse - Pulse Waveform with configurable pulse width"; + pub const sig : &'static str = + "VOsc sig\nOscillator output\nRange: (-1..1)\n"; + pub const DESC : &'static str = +r#"V Oscillator + +A vector phase shaping oscillator, to create interesting waveforms and +ways to manipulate them. +"#; + pub const HELP : &'static str = +r#"VOsc - Vector Phase Shaping Oscillator + +A vector phase shaping oscillator, to create interesting waveforms and +ways to manipulate them. +"#; + +} + +#[inline] +fn s(p: f32) -> f32 { + -(std::f32::consts::TAU * p).cos() +} + +#[inline] +fn phi_vps(x: f32, v: f32, d: f32) -> f32 { + if x < d { + (v * x) / d + } else { + v + ((1.0 - v) * (x - d))/(1.0 - d) + } +} + +impl DspNode for VOsc { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, srate: f32) { + self.israte = 1.0 / srate; + } + + fn reset(&mut self) { + self.phase = 0.0; +// self.osc.reset(); + } + + #[inline] + fn process( + &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, + _nctx: &NodeContext, + atoms: &[SAtom], inputs: &[ProcBuf], + outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + use crate::dsp::{out, inp, denorm, denorm_offs, at}; + + let freq = inp::VOsc::freq(inputs); + let det = inp::VOsc::det(inputs); + let d = inp::VOsc::d(inputs); + let v = inp::VOsc::v(inputs); + let vs = inp::VOsc::vs(inputs); + let out = out::VOsc::sig(outputs); + + let israte = self.israte; + + for frame in 0..ctx.nframes() { + let freq = denorm_offs::VOsc::freq(freq, det.read(frame), frame); + let v = denorm::VOsc::v(v, frame); + let d = denorm::VOsc::d(d, frame); + let vs = denorm::VOsc::vs(vs, frame); + + let s = s(phi_vps(self.phase, v * vs, d)); + out.write(frame, s); + + self.phase += freq * israte; + self.phase = self.phase.fract(); + } + + ctx_vals[0].set(out.read(ctx.nframes() - 1)); + } + + fn graph_fun() -> Option { + let israte = 1.0 / 128.0; + + Some(Box::new(move |gd: &dyn GraphAtomData, _init: bool, x: f32, _xn: f32| -> f32 { + let v = NodeId::VOsc(0).inp_param("v").unwrap().inp(); + let vs = NodeId::VOsc(0).inp_param("vs").unwrap().inp(); + let d = NodeId::VOsc(0).inp_param("d").unwrap().inp(); + + let v = gd.get_denorm(v as u32); + let vs = gd.get_denorm(vs as u32); + let d = gd.get_denorm(d as u32); + + let s = s(phi_vps(x, v * vs, d)); + (s + 1.0) * 0.5 + })) + } +}