Implemented VPS oscillator and adapted some oversampling code

This commit is contained in:
Weird Constructor 2021-08-04 06:51:31 +02:00
parent 3d140b2ead
commit 32b2725f49
3 changed files with 410 additions and 0 deletions

226
src/dsp/biquad.rs Normal file
View file

@ -0,0 +1,226 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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
}
}

View file

@ -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)

156
src/dsp/node_vosc.rs Normal file
View file

@ -0,0 +1,156 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<T: NodeAudioContext>(
&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<GraphFun> {
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
}))
}
}