Working on the first LFO node

This commit is contained in:
Weird Constructor 2021-08-06 05:49:03 +02:00
parent 80e9fa0d8e
commit 41441ccb29
4 changed files with 317 additions and 2 deletions

12
src/dsp/dattorro.rs Normal file
View file

@ -0,0 +1,12 @@
// 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.
// This file contains a reverb implementation that is based
// on Jon Dattorro's 1997 reverb algorithm. It's also largely
// based on the C++ implementation from ValleyAudio / ValleyRackFree
//
// ValleyRackFree Copyright (C) 2020, Valley Audio Soft, Dale Johnson
// Adapted under the GPL-3.0-or-later License.

View file

@ -1653,6 +1653,98 @@ impl VPSOscillator {
}
}
// Adapted from https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Common/DSP/LFO.hpp
//
// ValleyRackFree Copyright (C) 2020, Valley Audio Soft, Dale Johnson
// Adapted under the GPL-3.0-or-later License.
/// An LFO with a variable reverse point, which can go from reverse Saw, to Tri
/// and to Saw, depending on the reverse point.
#[derive(Debug, Clone, Copy)]
pub struct TriSawLFO {
/// The (inverse) sample rate. Eg. 1.0 / 44100.0.
israte: f64,
/// The current oscillator phase.
phase: f64,
/// The point from where the falling edge will be used.
rev: f64,
/// Whether the LFO is currently rising
rising: bool,
/// The frequency.
freq: f64,
/// Precomputed rise/fall rate of the LFO.
rise_r: f64,
fall_r: f64,
}
impl TriSawLFO {
pub fn new() -> Self {
let mut this = Self {
israte: 1.0 / 44100.0,
phase: 0.0,
rev: 0.5,
rising: true,
freq: 1.0,
fall_r: 0.0,
rise_r: 0.0,
};
this.recalc();
this
}
#[inline]
fn recalc(&mut self) {
self.rev = self.rev.clamp(0.0001, 0.999);
self.rise_r = 1.0 / self.rev;
self.fall_r = -1.0 / (1.0 - self.rev);
}
pub fn set_sample_rate(&mut self, srate: f32) {
self.israte = 1.0 / (srate as f64);
self.recalc();
}
pub fn reset(&mut self) {
self.phase = 0.0;
self.rev = 0.5;
self.rising = true;
}
#[inline]
pub fn set(&mut self, freq: f32, rev: f32) {
self.freq = freq as f64;
self.rev = rev as f64;
self.recalc();
}
#[inline]
pub fn next_unipolar(&mut self) -> f64 {
if self.phase >= 1.0 {
self.phase -= 1.0;
self.rising = true;
}
if self.phase >= self.rev {
self.rising = false;
}
let s =
if self.rising {
self.phase * self.rise_r
} else {
self.phase * self.fall_r - self.fall_r
};
self.phase += self.freq * self.israte;
s
}
#[inline]
pub fn next_bipolar(&mut self) -> f64 {
(self.next_unipolar() * 2.0) - 1.0
}
}
#[macro_export]
macro_rules! fa_distort { ($formatter: expr, $v: expr, $denorm_v: expr) => { {
let s =

View file

@ -40,6 +40,8 @@ mod node_vosc;
mod node_biqfilt;
#[allow(non_upper_case_globals)]
mod node_comb;
#[allow(non_upper_case_globals)]
mod node_tslfo;
pub mod biquad;
pub mod tracker;
@ -97,6 +99,7 @@ use node_bosc::BOsc;
use node_vosc::VOsc;
use node_biqfilt::BiqFilt;
use node_comb::Comb;
use node_tslfo::TsLfo;
pub const MIDI_MAX_FREQ : f32 = 13289.75;
@ -302,7 +305,7 @@ macro_rules! define_exp {
macro_rules! define_exp4 {
($n_id: ident $d_id: ident $min: expr, $max: expr) => {
macro_rules! $n_id { ($x: expr) => {
(($x - $min) / ($max - $min)).abs().sqrt().sqrt()
(($x - $min) / ($max - $min) as f32).abs().sqrt().sqrt()
} }
macro_rules! $d_id { ($x: expr) => {
{ let x : f32 = $x * $x * $x * $x; $min * (1.0 - x) + $max * x }
@ -310,6 +313,18 @@ macro_rules! define_exp4 {
}
}
macro_rules! define_exp6 {
($n_id: ident $d_id: ident $min: expr, $max: expr) => {
macro_rules! $n_id { ($x: expr) => {
(($x - $min) / ($max - $min) as f32).abs().powf(1.0 / 6.0)
} }
macro_rules! $d_id { ($x: expr) => {
{ let x : f32 = ($x).powf(6.0); $min * (1.0 - x) + $max * x }
} }
}
}
macro_rules! n_pit { ($x: expr) => {
((($x as f32).max(0.01) / 440.0).log2() / 10.0)
} }
@ -423,6 +438,62 @@ macro_rules! r_vps { ($x: expr, $coarse: expr) => {
}
} }
/// The rounding function for LFO time knobs
macro_rules! r_lfot { ($x: expr, $coarse: expr) => {
if $coarse {
let denv = d_lfot!($x);
if denv < 10.0 {
let hz = 1000.0 / denv;
let hz = (hz / 10.0).round() * 10.0;
n_lfot!(1000.0 / hz)
} else if denv < 250.0 {
n_lfot!((denv / 5.0).round() * 5.0)
} else if denv < 1500.0 {
n_lfot!((denv / 50.0).round() * 50.0)
} else if denv < 5000.0 {
n_lfot!((denv / 500.0).round() * 500.0)
} else if denv < 15000.0 {
n_lfot!((denv / 1000.0).round() * 1000.0)
} else {
n_lfot!((denv / 5000.0).round() * 5000.0)
}
} else {
let denv = d_lfot!($x);
let o =
if denv < 10.0 {
let hz = 1000.0 / denv;
let hz = hz.round();
n_lfot!(1000.0 / hz)
} else if denv < 100.0 {
n_lfot!(denv.round())
} else if denv < 1000.0 {
n_lfot!((denv / 5.0).round() * 5.0)
} else if denv < 2500.0 {
n_lfot!((denv / 10.0).round() * 10.0)
} else if denv < 25000.0 {
n_lfot!((denv / 100.0).round() * 100.0)
} else {
n_lfot!((denv / 500.0).round() * 500.0)
};
println!("ROUND C {} => {}", d_lfot!($x), d_lfot!(o));
o
}
} }
/// The default steps function:
macro_rules! stp_d { () => { (20.0, 100.0) } }
/// The UI steps to control parameters with a finer fine control:
@ -468,6 +539,22 @@ macro_rules! f_ms { ($formatter: expr, $v: expr, $denorm_v: expr) => {
}
} }
macro_rules! f_lfot { ($formatter: expr, $v: expr, $denorm_v: expr) => {
if $denorm_v < 10.0 {
write!($formatter, "{:5.1}Hz", 1000.0 / $denorm_v)
} else if $denorm_v < 500.0 {
write!($formatter, "{:4.1}ms", $denorm_v)
} else if $denorm_v < 5000.0 {
write!($formatter, "{:4.0}ms", $denorm_v)
} else {
write!($formatter, "{:5.2}s", $denorm_v / 1000.0)
}
} }
macro_rules! f_det { ($formatter: expr, $v: expr, $denorm_v: expr) => {
{
let sign = if $denorm_v < 0.0 { -1.0 } else { 1.0 };
@ -492,6 +579,7 @@ define_exp!{n_declick d_declick 0.0, 50.0}
define_exp!{n_env d_env 0.0, 1000.0}
define_exp6!{n_lfot d_lfot 0.1,300000.0}
define_exp!{n_time d_time 0.5, 5000.0}
define_exp!{n_ftme d_ftme 0.1, 1000.0}
@ -622,7 +710,7 @@ macro_rules! node_list {
fbrd => FbRd UIType::Generic UICategory::IOUtil
(0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0)
[0 sig],
ad => Ad UIType::Generic UICategory::CV
ad => Ad UIType::Generic UICategory::Mod
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0)
(1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(2 atk n_env d_env r_ems f_ms stp_m 0.0, 1.0, 3.0)
@ -632,6 +720,11 @@ macro_rules! node_list {
{6 0 mult setting(0) fa_ad_mult 0 2}
[0 sig]
[1 eoet],
tslfo => TsLfo UIType::Generic UICategory::Mod
(0 time n_lfot d_lfot r_lfot f_lfot stp_f 0.0, 1.0, 1000.0)
(1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(2 rev n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5)
[0 sig],
delay => Delay UIType::Generic UICategory::Signal
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)

118
src/dsp/node_tslfo.rs Normal file
View file

@ -0,0 +1,118 @@
// 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,
GraphAtomData, GraphFun, NodeContext,
};
use super::helpers::{TriSawLFO, Trigger};
#[derive(Debug, Clone)]
pub struct TsLfo {
lfo: Box<TriSawLFO>,
trig: Trigger,
}
impl TsLfo {
pub fn new(_nid: &NodeId) -> Self {
Self {
lfo: Box::new(TriSawLFO::new()),
trig: Trigger::new(),
}
}
pub const time : &'static str =
"TsLfo time\nThe frequency or period time of the LFO, goes all the \
way from 0.1ms up to 30s.\nRange: (0..1)\n";
pub const trig : &'static str =
"TsLfo trig\nTriggers a phase reset of the LFO.\nRange: (0..1)\n";
pub const rev : &'static str =
"TsLfo rev\nThe reverse point of the LFO waveform. At 0.5 the LFO \
will follow a triangle waveform. At 0.0 or 1.0 the LFO waveform will \
be (almost) a (reversed) saw tooth. Node: A perfect sawtooth can not be \
achieved with this oscillator, as there will always be a minimal \
rise/fall time.\nRange: (0..1)\n";
pub const sig : &'static str =
"TsLfo sig\nThe LFO output.\nRange: (0..1)";
pub const DESC : &'static str =
r#"TriSaw LFO
This simple LFO has a configurable waveform. You can blend between triangular to sawtooth waveforms using the 'rev' parameter.
"#;
pub const HELP : &'static str =
r#"TsLfo - TriSaw LFO
This simple LFO has a configurable waveform. You can blend between
triangular to sawtooth waveforms using the 'rev' parameter.
Using the 'trig' input you can reset the LFO phase, which allows to use it
kind of like an envelope.
"#;
}
impl DspNode for TsLfo {
fn outputs() -> usize { 1 }
fn set_sample_rate(&mut self, srate: f32) {
self.lfo.set_sample_rate(srate);
}
fn reset(&mut self) {
self.lfo.reset();
self.trig.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, at};
let time = inp::TsLfo::time(inputs);
let trig = inp::TsLfo::trig(inputs);
let rev = inp::TsLfo::rev(inputs);
let out = out::TsLfo::sig(outputs);
let mut lfo = &mut *self.lfo;
for frame in 0..ctx.nframes() {
if self.trig.check_trigger(denorm::TsLfo::trig(trig, frame)) {
lfo.reset();
}
let time_ms = denorm::TsLfo::time(time, frame).clamp(0.1, 300000.0);
lfo.set(
1000.0 / time_ms,
denorm::TsLfo::rev(rev, frame));
out.write(frame, lfo.next_unipolar() as f32);
}
ctx_vals[0].set(out.read(ctx.nframes() - 1));
}
fn graph_fun() -> Option<GraphFun> {
let mut lfo = TriSawLFO::new();
Some(Box::new(move |gd: &dyn GraphAtomData, init: bool, _x: f32, xn: f32| -> f32 {
if init {
lfo.reset();
let time_idx = NodeId::TsLfo(0).inp_param("time").unwrap().inp();
let rev_idx = NodeId::TsLfo(0).inp_param("rev").unwrap().inp();
let time = gd.get_norm(time_idx as u32).sqrt();
let rev = gd.get_norm(rev_idx as u32);
lfo.set(0.2 * (1.0 - time) + time * 1.0, rev);
}
lfo.next_unipolar() as f32
}))
}
}