diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index d78ff00..642f1c3 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -705,6 +705,69 @@ impl TriggerSampleClock { } } +#[derive(Debug, Clone, Copy)] +pub struct SlewValue { + slew_count: u64, + current: F, + target: F, + inc: F, + sr_ms: F, +} + +impl SlewValue { + pub fn new() -> Self { + Self { + slew_count: 0, + current: f(0.0), + target: f(0.0), + inc: f(0.0), + sr_ms: f(44100.0 / 1000.0), + } + } + + pub fn reset(&mut self) { + self.slew_count = 0; + self.current = f(0.0); + self.target = f(0.0); + self.inc = f(0.0); + } + + pub fn set_sample_rate(&mut self, srate: F) { + self.sr_ms = srate / f(1000.0); + } + + #[inline] + pub fn set_target(&mut self, target: F, slew_time_ms: F) { + self.target = target; + + // 0.02ms, thats a fraction of a sample at 44.1kHz + if slew_time_ms < f(0.02) { + self.current = self.target; + self.slew_count = 0; + + } else { + let slew_samples = slew_time_ms * self.sr_ms; + self.slew_count = slew_samples.to_u64().unwrap_or(0); + self.inc = (self.target - self.current) / slew_samples; + } + } + + #[inline] + pub fn value(&self) -> F { self.current } + + #[inline] + pub fn next(&mut self) -> F { + if self.slew_count > 0 { + self.current = self.current + self.inc; + self.slew_count -= 1; + } else { + self.current = self.target; + } + + self.current + } +} + /// Default size of the delay buffer: 5 seconds at 8 times 48kHz const DEFAULT_DELAY_BUFFER_SAMPLES : usize = 8 * 48000 * 5; diff --git a/src/dsp/node_rndwk.rs b/src/dsp/node_rndwk.rs index 163260b..b08f96d 100644 --- a/src/dsp/node_rndwk.rs +++ b/src/dsp/node_rndwk.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. use crate::nodes::{NodeAudioContext, NodeExecContext}; -use crate::dsp::helpers::{Rng, Trigger}; +use crate::dsp::helpers::{Rng, Trigger, SlewValue}; use crate::dsp::{ NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals, NodeContext, GraphAtomData, GraphFun, @@ -12,12 +12,8 @@ use crate::dsp::{ /// A triggered random walker #[derive(Debug, Clone)] pub struct RndWk { - sr_ms: f32, rng: Rng, - target: f32, - target_inc: f32, - slew_count: u64, - current: f32, + slew_val: SlewValue, trig: Trigger, } @@ -30,34 +26,59 @@ impl RndWk { Self { rng, - sr_ms: 44100.0 / 1000.0, - target: 0.0, - target_inc: 0.0, - slew_count: 0, - current: 0.0, - trig: Trigger::new(), + trig: Trigger::new(), + slew_val: SlewValue::new(), } } pub const trig : &'static str = - "RndWk trig\n\n\nRange: (-1..1)"; + "RndWk trig\nThis trigger generates a new random number within \ + the current 'min'/'max' range.\nRange: (-1..1)"; pub const step : &'static str = - "RndWk step\n\nRange: (-1..1)"; + "RndWk step\nThis is the maximum possible step size of the \ + random number drawn upon 'trig'. Setting this to 0.0 will disable \ + the randomness.\nThe minimum step size can be defined \ + by the 'offs' parameter.\nRange: (0..1)"; pub const offs : &'static str = - "RndWk offs\n\nRange: (-1..1)"; + "RndWk offs\nThe minimum step size and direction that is done on each 'trig'.\ + Depending on the size of the 'offs' and the 'min'/'max' range, \ + this might result in the output value being close to the limits \ + of that range.\nRange: (-1..1)"; pub const min : &'static str = - "RndWk min\n\nRange: (0..1)"; + "RndWk min\nThe minimum of the new target value. If a value is drawn \ + that is outside of this range, it will be reflected back into it.\ + \nRange: (0..1)"; pub const max : &'static str = - "RndWk max\n\nRange: (0..1)"; + "RndWk max\nThe maximum of the new target value. If a value is drawn \ + that is outside of this range, it will be reflected back into it.\ + \nRange: (0..1)"; pub const slewt : &'static str = - "RndWk slewt\n\nRange: (0..1)"; + "RndWk slewt\nThe slew time, the time it takes to reach the \ + new target value. This can be used to smooth off rough transitions and \ + clicky noises.\nRange: (0..1)"; pub const sig : &'static str = "RndWk sig\nOscillator output\nRange: (-1..1)\n"; pub const DESC : &'static str = r#"Random Walker + +This modulator generates a random number by walking a pre defined maximum random 'step' width. For smoother transitions a slew time is integrated. "#; pub const HELP : &'static str = r#"RndWk - Random Walker + +This modulator generates a random number by walking a pre defined +maximum random 'step' width. The newly generated target value will always +be folded within the defined 'min'/'max' range. The 'offs' parameter defines a +minimal step width each 'trig' has to change the target value. + +For smoother transitions, if you want to modulate an audio signal with this, +a slew time ('slewt') is integrated. + +You can disable all randomness by setting 'step' to 0.0. + +Tip: Interesting and smooth results can be achieved if you set 'slewt' +to a longer time than the interval in that you trigger 'trig'. It will smooth +off the step widths and the overall motion even more. "#; } @@ -66,14 +87,11 @@ impl DspNode for RndWk { fn outputs() -> usize { 1 } fn set_sample_rate(&mut self, srate: f32) { - self.sr_ms = srate / 1000.0; + self.slew_val.set_sample_rate(srate); } fn reset(&mut self) { - self.target = 0.0; - self.current = 0.0; - self.target_inc = 0.0; - self.slew_count = 0; + self.slew_val.reset(); self.trig.reset(); } @@ -81,7 +99,7 @@ impl DspNode for RndWk { fn process( &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, _nctx: &NodeContext, - atoms: &[SAtom], inputs: &[ProcBuf], + _atoms: &[SAtom], inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) { use crate::dsp::{out, inp, denorm, denorm_offs, at}; @@ -106,34 +124,18 @@ impl DspNode for RndWk { let step = denorm::RndWk::step(step, frame).clamp(-1.0, 1.0); let offs = denorm::RndWk::offs(offs, frame).clamp(-1.0, 1.0); - self.target = - self.current + let target = + self.slew_val.value() + ((self.rng.next() * 2.0 * step) - step) + offs; - self.target = ((self.target - min) % delta).abs() + min; + let target = ((target - min) % delta).abs() + min; let slew_time_ms = denorm::RndWk::slewt(slewt, frame); - if slew_time_ms < 0.01 { - self.current = self.target; - self.slew_count = 0; - - } else { - let slew_samples = slew_time_ms * self.sr_ms; - self.slew_count = slew_samples as u64; - self.target_inc = (self.target - self.current) / slew_samples; - } + self.slew_val.set_target(target, slew_time_ms); } - if self.slew_count > 0 { - self.current += self.target_inc; - self.slew_count -= 1; - } else { - self.target_inc = 0.0; - self.current = self.target; - } - - out.write(frame, self.current); + out.write(frame, self.slew_val.next()); } ctx_vals[0].set(out.read(ctx.nframes() - 1)); diff --git a/tests/node_rndwk.rs b/tests/node_rndwk.rs new file mode 100644 index 0000000..c6e84d1 --- /dev/null +++ b/tests/node_rndwk.rs @@ -0,0 +1,197 @@ +// 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. + +mod common; +use common::*; + +#[test] +fn check_node_rndwk_def_trig() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let rwk = NodeId::RndWk(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(rwk) + .out(None, None, rwk.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 40, vec![ + 0.0, // start value + // 10ms ramp: + 0.0049022376, 0.015222744, 0.025543215, 0.035863716, 0.04618426, + 0.056504805, 0.066825345, 0.07714589, 0.08746643, 0.09778698, + 0.10810752, + // end value: + 0.11378352, 0.11378352, 0.11378352, 0.11378352, 0.11378352, + ]); + + pset_n(&mut matrix, rwk, "trig", 0.0); + pset_d_wait(&mut matrix, &mut node_exec, rwk, "slewt", 1.0); + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 15, vec![ + 0.11378352, 0.11378352, // last value + 0.1436584, 0.19344981, 0.24324122, // 1ms ramp 15 * 3 => ~44.1 samples + 0.26017055, 0.26017055, // end value + ]); +} + +#[test] +fn check_node_rndwk_step() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let rwk = NodeId::RndWk(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(rwk) + .out(None, None, rwk.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + pset_d(&mut matrix, rwk, "step", 1.0); + matrix.sync().unwrap(); + + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 60, vec![ + 0.0, // start value + // 10ms ramp: + 0.050312463, 0.12771615, 0.20512024, 0.28252393, 0.35992712, + 0.4373303, 0.51473385, + // end value + // which is 5.0 * 0.11378352 + // (the first random sample, see previous test) + 0.56891763, 0.56891763, + ]); +} + +#[test] +fn check_node_rndwk_offs() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let rwk = NodeId::RndWk(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(rwk) + .out(None, None, rwk.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + pset_d(&mut matrix, rwk, "offs", 0.3); + matrix.sync().unwrap(); + + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 60, vec![ + 0.0, // start value + // 10ms ramp: + 0.03659311, 0.0928901, 0.14918698, 0.20548387, + 0.26178095, 0.31807873, 0.3743765, + // end value + // which is 0.11378352 + 0.3 + // (the first random sample, see previous test) + 0.41378355, + 0.41378355, + 0.41378355, + ]); +} + +#[test] +fn check_node_rndwk_offs_neg() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let rwk = NodeId::RndWk(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(rwk) + .out(None, None, rwk.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + pset_d(&mut matrix, rwk, "offs", -0.2); + matrix.sync().unwrap(); + + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 60, vec![ + 0.0, // start value + // 10ms ramp: + 0.007624589, 0.019354708, 0.03108479, 0.042814985, 0.05454518, + 0.06627537, 0.07800557, + // end value + // which is (0.11378352 - 0.2).abs() + 0.08621648, 0.08621648, + ]); +} + +#[test] +fn check_node_rndwk_max() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let rwk = NodeId::RndWk(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(rwk) + .out(None, None, rwk.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + pset_d(&mut matrix, rwk, "step", 1.0); // => first sample is 0.56891763 + pset_d(&mut matrix, rwk, "max", 0.5); + matrix.sync().unwrap(); + + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 60, vec![ + 0.0, // start value + // 10ms ramp: + 0.006094757, 0.015471312, 0.024847867, 0.03422442, 0.043600976, + 0.052977532, 0.062354088, + // end value + // which is 0.5 - 0.56891763 + 0.06891763, 0.06891763, + ]); +} + +#[test] +fn check_node_rndwk_min() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let rwk = NodeId::RndWk(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(rwk) + .out(None, None, rwk.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + pset_d(&mut matrix, rwk, "step", 1.0); // => first sample is 0.56891763 + pset_d(&mut matrix, rwk, "max", 1.0); + pset_d(&mut matrix, rwk, "min", 0.75); // wraps first sample to 0.93108237 + matrix.sync().unwrap(); + + pset_n(&mut matrix, rwk, "trig", 1.0); + run_for_ms(&mut node_exec, 7.0); // wait for trigger... + + let (out_l, _) = run_for_ms(&mut node_exec, 20.0); + assert_decimated_feq!(out_l, 60, vec![ + 0.0, // start value + // 10ms ramp: + 0.08234063, 0.20901868, 0.33569613, 0.4623733, 0.5890517, + 0.71573067, 0.8424096, + // end value + 0.93108237, 0.93108237, 0.93108237, 0.93108237, 0.93108237, 0.93108237, + ]); +} diff --git a/tests/node_tslfo.rs b/tests/node_tslfo.rs index 6fa16d8..3e15db0 100644 --- a/tests/node_tslfo.rs +++ b/tests/node_tslfo.rs @@ -10,7 +10,7 @@ fn check_node_tslfo_1() { let (node_conf, mut node_exec) = new_node_engine(); let mut matrix = Matrix::new(node_conf, 3, 3); - let tsl = NodeId::TsLfo(0); + let tsl = NodeId::TsLFO(0); let out = NodeId::Out(0); matrix.place(0, 0, Cell::empty(tsl) .out(None, None, tsl.out("sig"))); @@ -62,7 +62,7 @@ fn check_node_tslfo_trig_slopes() { let (node_conf, mut node_exec) = new_node_engine(); let mut matrix = Matrix::new(node_conf, 3, 3); - let tsl = NodeId::TsLfo(0); + let tsl = NodeId::TsLFO(0); let out = NodeId::Out(0); matrix.place(0, 0, Cell::empty(tsl) .out(None, None, tsl.out("sig")));