diff --git a/src/dsp/dattorro.rs b/src/dsp/dattorro.rs new file mode 100644 index 0000000..7f6d9a7 --- /dev/null +++ b/src/dsp/dattorro.rs @@ -0,0 +1,12 @@ +// 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. + +// 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. + + diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 85df357..e2d9456 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -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 = diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 3845221..45ac053 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -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) diff --git a/src/dsp/node_tslfo.rs b/src/dsp/node_tslfo.rs new file mode 100644 index 0000000..0421cf7 --- /dev/null +++ b/src/dsp/node_tslfo.rs @@ -0,0 +1,118 @@ +// 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, + GraphAtomData, GraphFun, NodeContext, +}; +use super::helpers::{TriSawLFO, Trigger}; + +#[derive(Debug, Clone)] +pub struct TsLfo { + lfo: Box, + 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( + &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 { + 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 + })) + } +}