Implemented VPS oscillator and adapted some oversampling code
This commit is contained in:
parent
3d140b2ead
commit
32b2725f49
3 changed files with 410 additions and 0 deletions
226
src/dsp/biquad.rs
Normal file
226
src/dsp/biquad.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
156
src/dsp/node_vosc.rs
Normal 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
|
||||
}))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue