Working on the first LFO node
This commit is contained in:
parent
80e9fa0d8e
commit
41441ccb29
4 changed files with 317 additions and 2 deletions
12
src/dsp/dattorro.rs
Normal file
12
src/dsp/dattorro.rs
Normal 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.
|
||||||
|
|
||||||
|
|
|
@ -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_export]
|
||||||
macro_rules! fa_distort { ($formatter: expr, $v: expr, $denorm_v: expr) => { {
|
macro_rules! fa_distort { ($formatter: expr, $v: expr, $denorm_v: expr) => { {
|
||||||
let s =
|
let s =
|
||||||
|
|
|
@ -40,6 +40,8 @@ mod node_vosc;
|
||||||
mod node_biqfilt;
|
mod node_biqfilt;
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
mod node_comb;
|
mod node_comb;
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
mod node_tslfo;
|
||||||
|
|
||||||
pub mod biquad;
|
pub mod biquad;
|
||||||
pub mod tracker;
|
pub mod tracker;
|
||||||
|
@ -97,6 +99,7 @@ use node_bosc::BOsc;
|
||||||
use node_vosc::VOsc;
|
use node_vosc::VOsc;
|
||||||
use node_biqfilt::BiqFilt;
|
use node_biqfilt::BiqFilt;
|
||||||
use node_comb::Comb;
|
use node_comb::Comb;
|
||||||
|
use node_tslfo::TsLfo;
|
||||||
|
|
||||||
pub const MIDI_MAX_FREQ : f32 = 13289.75;
|
pub const MIDI_MAX_FREQ : f32 = 13289.75;
|
||||||
|
|
||||||
|
@ -302,7 +305,7 @@ macro_rules! define_exp {
|
||||||
macro_rules! define_exp4 {
|
macro_rules! define_exp4 {
|
||||||
($n_id: ident $d_id: ident $min: expr, $max: expr) => {
|
($n_id: ident $d_id: ident $min: expr, $max: expr) => {
|
||||||
macro_rules! $n_id { ($x: 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) => {
|
macro_rules! $d_id { ($x: expr) => {
|
||||||
{ let x : f32 = $x * $x * $x * $x; $min * (1.0 - x) + $max * x }
|
{ 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) => {
|
macro_rules! n_pit { ($x: expr) => {
|
||||||
((($x as f32).max(0.01) / 440.0).log2() / 10.0)
|
((($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:
|
/// The default steps function:
|
||||||
macro_rules! stp_d { () => { (20.0, 100.0) } }
|
macro_rules! stp_d { () => { (20.0, 100.0) } }
|
||||||
/// The UI steps to control parameters with a finer fine control:
|
/// 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) => {
|
macro_rules! f_det { ($formatter: expr, $v: expr, $denorm_v: expr) => {
|
||||||
{
|
{
|
||||||
let sign = if $denorm_v < 0.0 { -1.0 } else { 1.0 };
|
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_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_time d_time 0.5, 5000.0}
|
||||||
define_exp!{n_ftme d_ftme 0.1, 1000.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
|
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 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0)
|
||||||
[0 sig],
|
[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)
|
(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)
|
(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)
|
(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}
|
{6 0 mult setting(0) fa_ad_mult 0 2}
|
||||||
[0 sig]
|
[0 sig]
|
||||||
[1 eoet],
|
[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
|
delay => Delay UIType::Generic UICategory::Signal
|
||||||
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
|
(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)
|
(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
118
src/dsp/node_tslfo.rs
Normal 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue