From b57dc2a4b7a4f22a6e3916a0a3b041cbe8c588d7 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 21 Jun 2021 19:58:04 +0200 Subject: [PATCH 01/15] with jack pls --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4fea557..f01e9cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ hound = "3.4.0" [dev-dependencies] microfft = "0.3.1" num-complex = "0.2" -#jack = "0.6.6" +jack = "0.6.6" [lib] path = "src/lib.rs" From adfcd3f431fa0a1c1f956db1b7ae2720e9b75e7f Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 22 Jun 2021 05:04:11 +0200 Subject: [PATCH 02/15] wrote basic delay tests --- src/dsp/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index b58d024..4da3ca6 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -463,9 +463,9 @@ macro_rules! node_list { [0 sig] [1 eoet], delay => Delay UIType::Generic UICategory::Signal - (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, 0.0) (1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) - (2 time n_time d_time r_tms f_ms stp_m 0.0, 1.0, 0.5) + (2 time n_time d_time r_tms f_ms stp_m 0.0, 1.0, 250.0) (3 fb n_id d_id r_id f_def stp_d 0.0, 1.0, 0.0) (4 mix n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {5 0 mode setting(0) fa_delay_mode 0 1} From 1e0caab0204c35f96c0269f7faf9a81138f00384 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 23 Jun 2021 05:07:32 +0200 Subject: [PATCH 03/15] finished writing tests for the delay node --- src/dsp/mod.rs | 15 ++- src/dsp/node_test.rs | 62 +++++++-- tests/common/mod.rs | 21 +++ tests/node_delay.rs | 296 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 379 insertions(+), 15 deletions(-) create mode 100644 tests/node_delay.rs diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 4da3ca6..6e8ac74 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -466,14 +466,16 @@ macro_rules! node_list { (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) (2 time n_time d_time r_tms f_ms stp_m 0.0, 1.0, 250.0) - (3 fb n_id d_id r_id f_def stp_d 0.0, 1.0, 0.0) + (3 fb n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (4 mix n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {5 0 mode setting(0) fa_delay_mode 0 1} [0 sig], test => Test UIType::Generic UICategory::IOUtil (0 f n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) - {1 0 p param(0.0) fa_test_s 0 10} - [0 sig], + {1 0 p param(0.0) fa_test_s 0 10} + {2 1 trig param(0.0) fa_test_s 0 0} + [0 sig] + [1 tsig], } } } @@ -1028,6 +1030,13 @@ macro_rules! make_node_info_enum { })+ } + #[allow(non_snake_case)] + pub mod out_idx { + $(pub mod $variant { + $(#[inline] pub fn $out() -> usize { $out_idx })* + })+ + } + mod ni { $( #[derive(Debug, Clone)] diff --git a/src/dsp/node_test.rs b/src/dsp/node_test.rs index 0c003c4..e8f21c9 100644 --- a/src/dsp/node_test.rs +++ b/src/dsp/node_test.rs @@ -4,6 +4,7 @@ use crate::nodes::{NodeAudioContext, NodeExecContext}; use crate::dsp::{NodeId, SAtom, ProcBuf, GraphFun, GraphAtomData, DspNode, LedPhaseVals}; +use crate::dsp::helpers::{TrigSignal}; #[macro_export] macro_rules! fa_test_s { ($formatter: expr, $v: expr, $denorm_v: expr) => { { @@ -28,26 +29,39 @@ macro_rules! fa_test_s { ($formatter: expr, $v: expr, $denorm_v: expr) => { { /// A simple amplifier #[derive(Debug, Clone)] pub struct Test { + trig_sig: TrigSignal, + trigger: bool, } impl Test { + pub fn new(_nid: &NodeId) -> Self { + Self { + trigger: false, + trig_sig: TrigSignal::new(), + } + } + + pub const f : &'static str = "F Test"; + pub const p : &'static str = "Test p\nAn unsmoothed parameter for automated tests."; + pub const trig: &'static str = "Test trig\nA trigger input, that will create a short pulse on the 'tsig' output.\nRange: (-1..1)"; + pub const sig : &'static str = "Test sig\nThe output of p as signal"; + pub const tsig : &'static str = "Test tsig\nA short trigger pulse will be generated when the 'trig' input is triggered."; + pub const DESC : &'static str = r#""#; pub const HELP : &'static str = r#""#; - pub fn new(_nid: &NodeId) -> Self { - Self { - } - } - pub const f : &'static str = "F Test"; - pub const p : &'static str = "Test p\nJust an unsmoothed parameter for tests."; - pub const sig : &'static str = "Test sig\nThe output of p as signal"; } impl DspNode for Test { - fn outputs() -> usize { 1 } + fn outputs() -> usize { 2 } - fn set_sample_rate(&mut self, _srate: f32) { } - fn reset(&mut self) { } + fn set_sample_rate(&mut self, srate: f32) { + self.trig_sig.set_sample_rate(srate); + } + + fn reset(&mut self) { + self.trig_sig.reset(); + } #[inline] fn process( @@ -55,12 +69,36 @@ impl DspNode for Test { atoms: &[SAtom], _params: &[ProcBuf], _inputs: &[ProcBuf], outputs: &mut [ProcBuf], _led: LedPhaseVals) { - use crate::dsp::{out, at}; + use crate::dsp::{out_idx, at}; let p = at::Test::p(atoms); - let out = out::Test::sig(outputs); + let trig = at::Test::trig(atoms); + let tsig = out_idx::Test::tsig(); + + let (out, tsig) = outputs.split_at_mut(tsig); + let out = &mut out[0]; + let tsig = &mut tsig[0]; + + let mut trigger = trig.i(); + if !self.trigger && trigger > 0 { + self.trigger = true; + + } else if !self.trigger && trigger == 0 { + self.trigger = false; + + } else if self.trigger { + trigger = 0; + } + for frame in 0..ctx.nframes() { + if trigger > 0 { + self.trig_sig.trigger(); + trigger = 0; + } + out.write(frame, p.f()); + let t = self.trig_sig.next(); + tsig.write(frame, t); } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d191f3b..257ad8e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -194,6 +194,27 @@ assertion failed: `(left[{}] == right[{}])` } } +#[allow(dead_code)] +pub fn collect_signal_changes(inp: &[f32], thres: i64) -> Vec<(usize, i64)> { + let mut idxs = vec![]; + let mut last_sig = 0.0; + for i in 0..inp.len() { + if (inp[i] - last_sig).abs() > 0.1 { + idxs.push((i, (inp[i] * 100.0).floor() as i64)); + last_sig = inp[i]; + } + } + + let mut idxs_big = vec![]; + for v in idxs.iter() { + if v.1.abs() > thres { + idxs_big.push(*v); + } + } + + return idxs_big; +} + #[macro_export] macro_rules! assert_rmsmima { ($rms:expr, $b:expr) => { diff --git a/tests/node_delay.rs b/tests/node_delay.rs new file mode 100644 index 0000000..d81b7aa --- /dev/null +++ b/tests/node_delay.rs @@ -0,0 +1,296 @@ +mod common; +use common::*; + +#[test] +fn check_node_delay_1() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let ad = NodeId::Ad(0); + let sin = NodeId::Sin(0); + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(ad) + .input(ad.inp("inp"), None, None) + .out(None, None, ad.out("sig"))); + matrix.place(0, 2, Cell::empty(dly) + .input(dly.inp("inp"), None, None) + .out(None, None, dly.out("sig"))); + matrix.place(0, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + matrix.sync().unwrap(); + + pset_d(&mut matrix, ad, "atk", 50.0); + pset_d(&mut matrix, ad, "dcy", 50.0); + pset_n(&mut matrix, ad, "trig", 1.0); + + let res = run_for_ms(&mut node_exec, 500.0); + // 441 decimation => 10ms resolution + assert_decimated_feq!(res.0, 441, vec![ + // 10ms smoothing time + 0.0, + // burst of sine for 100ms: + 0.018363932, -0.124816686, 0.21992423, -0.19471036, 0.00002711302, + 0.27546832, -0.35064548, 0.25555965, -0.0991776, 0.000008648983, + // 150ms silence: + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + // delayed burst of sine for 100ms: + 0.015279313, -0.119179465, 0.22757527, -0.22698581, 0.05398392, + 0.22569486, -0.3332433, 0.26348564, -0.11514694, 0.008539479, + // silence afterwards: + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 + ]); +} + +#[test] +fn check_node_delay_2() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(0, 2, Cell::empty(dly) + .out(None, None, dly.out("sig"))); + matrix.place(0, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + matrix.sync().unwrap(); + + pset_d(&mut matrix, dly, "time", 31.0); + pset_d(&mut matrix, dly, "inp", 1.0); + + let res = run_for_ms(&mut node_exec, 150.0); + // 441 decimation => 10ms resolution + assert_decimated_feq!(res.0, 441, vec![ + // 10ms smoothing time for "inp" + 0.001133, + // 30ms delaytime just mixing the 0.5: + 0.5, 0.5, 0.5, + // the delayed smoothing ramp (10ms): + 0.9513, + // the delay + input signal: + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 + ]); +} + +#[test] +fn check_node_delay_time_mod() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let sin = NodeId::Sin(0); + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(1, 1, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(1, 2, Cell::empty(dly) + .input(dly.inp("inp"), None, dly.inp("time")) + .out(None, None, dly.out("sig"))); + matrix.place(1, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, dly, "mix", 1.0); + pset_d(&mut matrix, dly, "time", 100.0); + + // skip delay time: + run_for_ms(&mut node_exec, 100.0); + + let fft = run_and_get_fft4096_now(&mut node_exec, 600); + assert_eq!(fft[0], (431, 614)); + assert_eq!(fft[1], (441, 1012)); + + let sin2 = NodeId::Sin(1); + matrix.place(0, 3, Cell::empty(sin2) + .out(sin2.out("sig"), None, None)); + + matrix.sync().unwrap(); + pset_d(&mut matrix, sin2, "freq", 0.5); + + // let everything settle down and the delay buffer fill with stuff: + run_for_ms(&mut node_exec, 5000.0); + + // skip some time to let everything settle: + run_for_ms(&mut node_exec, 670.0); + + let fft = run_and_get_fft4096_now(&mut node_exec, 110); + // Expect a sine sweep over a + // range of low frequencies: + assert_eq!(fft[0], (108, 111)); + assert_eq!(fft[5], (312, 110)); + assert_eq!(fft[10], (700, 110)); + + // Sweep upwards: + run_for_ms(&mut node_exec, 300.0); + let fft = run_and_get_fft4096_now(&mut node_exec, 122); + assert_eq!(fft[0], (2509, 123)); + assert_eq!(fft[8], (2821, 123)); + + // Sweep at mostly highest point: + run_for_ms(&mut node_exec, 700.0); + let fft = run_and_get_fft4096_now(&mut node_exec, 300); + assert_eq!(fft[0], (6417, 309)); + assert_eq!(fft[4], (6471, 407)); +} + + +#[test] +fn check_node_delay_trig() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let test = NodeId::Test(0); + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(1, 1, Cell::empty(test) + .out(None, None, test.out("tsig"))); + matrix.place(0, 3, Cell::empty(test) + .out(test.out("sig"), None, None)); + matrix.place(1, 2, Cell::empty(dly) + .input(dly.inp("inp"), None, dly.inp("trig")) + .out(None, None, dly.out("sig"))); + matrix.place(1, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, dly, "mix", 1.0); + pset_n(&mut matrix, dly, "mode", 1.0); + pset_d(&mut matrix, dly, "time", 5.0); + + // Trigger the delay 2 times, with an interval of 20ms: + pset_n(&mut matrix, test, "p", 1.0); + run_for_ms(&mut node_exec, 10.0); + pset_n(&mut matrix, test, "p", 0.0); + run_for_ms(&mut node_exec, 10.0); + pset_n(&mut matrix, test, "p", 1.0); + run_for_ms(&mut node_exec, 10.0); + pset_n(&mut matrix, test, "p", 0.0); + run_for_ms(&mut node_exec, 10.0); + + // Now the delay should have a 20ms delay time. + + // Emit the trigger signal: + pset_n(&mut matrix, test, "trig", 1.0); + + let res = run_for_ms(&mut node_exec, 30.0); + + let mut idx_first_non_zero = 99999; + for i in 0..res.0.len() { + if res.0[i] > 0.0 { + idx_first_non_zero = i; + break; + } + } + + // We expect the signal to be delayed by 20ms: + assert_eq!(idx_first_non_zero, (44100 * 20) / 1000); +} + + +#[test] +fn check_node_delay_fb() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let test = NodeId::Test(0); + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(1, 1, Cell::empty(test) + .out(None, None, test.out("tsig"))); + matrix.place(1, 2, Cell::empty(dly) + .input(dly.inp("inp"), None, None) + .out(None, None, dly.out("sig"))); + matrix.place(1, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + + pset_n(&mut matrix, dly, "mix", 1.0); + pset_d(&mut matrix, dly, "time", 5.0); + pset_n(&mut matrix, dly, "fb", 0.5); + + matrix.sync().unwrap(); + + // Emit the trigger signal: + pset_n(&mut matrix, test, "trig", 1.0); + + let res = run_for_ms(&mut node_exec, 100.0); + + let idxs_big = collect_signal_changes(&res.0[..], 50); + + // We expect the signal to be delayed by 20ms: + assert_eq!(idxs_big, vec![(220, 106), (441, 53)]); +} + +#[test] +fn check_node_delay_fb_neg() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let test = NodeId::Test(0); + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(1, 1, Cell::empty(test) + .out(None, None, test.out("tsig"))); + matrix.place(1, 2, Cell::empty(dly) + .input(dly.inp("inp"), None, None) + .out(None, None, dly.out("sig"))); + matrix.place(1, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + + pset_n(&mut matrix, dly, "mix", 1.0); + pset_d(&mut matrix, dly, "time", 10.0); + pset_n(&mut matrix, dly, "fb", -1.0); + + matrix.sync().unwrap(); + + // Emit the trigger signal: + pset_n(&mut matrix, test, "trig", 1.0); + + let res = run_for_ms(&mut node_exec, 40.0); + + let idxs_big = collect_signal_changes(&res.0[..], 70); + + assert_eq!(idxs_big, vec![(441, 100), (883, -100), (1325, 100)]); +} + + +#[test] +fn check_node_delay_fb_pos() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let test = NodeId::Test(0); + let dly = NodeId::Delay(0); + let out = NodeId::Out(0); + matrix.place(1, 1, Cell::empty(test) + .out(None, None, test.out("tsig"))); + matrix.place(1, 2, Cell::empty(dly) + .input(dly.inp("inp"), None, None) + .out(None, None, dly.out("sig"))); + matrix.place(1, 3, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + + pset_n(&mut matrix, dly, "mix", 1.0); + pset_d(&mut matrix, dly, "time", 10.0); + pset_n(&mut matrix, dly, "fb", 1.0); + + matrix.sync().unwrap(); + + // Emit the trigger signal: + pset_n(&mut matrix, test, "trig", 1.0); + + let res = run_for_ms(&mut node_exec, 40.0); + + let idxs_big = collect_signal_changes(&res.0[..], 70); + + assert_eq!(idxs_big, vec![(441, 100), (883, 100), (1325, 100)]); +} From 3f8bee1301157e82adac9cfccb15ecf2838a4e25 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 23 Jun 2021 05:11:24 +0200 Subject: [PATCH 04/15] fixed a bug in the delay line, that made it 1 sample off, getting incrementally worse --- src/dsp/helpers.rs | 2 +- src/dsp/node_delay.rs | 17 ++++------------- tests/node_delay.rs | 18 ++++++++++++++---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 1625196..2d5ac15 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -561,8 +561,8 @@ impl DelayBuffer { #[inline] pub fn feed(&mut self, input: f32) { - self.wr = (self.wr + 1) % self.data.len(); self.data[self.wr] = input; + self.wr = (self.wr + 1) % self.data.len(); } #[inline] diff --git a/src/dsp/node_delay.rs b/src/dsp/node_delay.rs index 4c9bdde..a73e2d6 100644 --- a/src/dsp/node_delay.rs +++ b/src/dsp/node_delay.rs @@ -21,7 +21,6 @@ macro_rules! fa_delay_mode { ($formatter: expr, $v: expr, $denorm_v: expr) => { #[derive(Debug, Clone)] pub struct Delay { buffer: Box, - fb_sample: f32, clock: TriggerSampleClock, } @@ -29,7 +28,6 @@ impl Delay { pub fn new(_nid: &NodeId) -> Self { Self { buffer: Box::new(DelayBuffer::new()), - fb_sample: 0.0, clock: TriggerSampleClock::new(), } } @@ -108,43 +106,36 @@ impl DspNode for Delay { let mix = inp::Delay::mix(inputs); let out = out::Delay::sig(outputs); - let mut fb_s = self.fb_sample; - if mode.i() == 0 { for frame in 0..ctx.nframes() { let dry = inp.read(frame); - buffer.feed(dry + fb_s * denorm::Delay::fb(fb, frame)); let out_sample = buffer.cubic_interpolate_at( denorm::Delay::time(time, frame)); + buffer.feed(dry + out_sample * denorm::Delay::fb(fb, frame)); + out.write(frame, crossfade(dry, out_sample, denorm::Delay::mix(mix, frame).clamp(0.0, 1.0))); - - fb_s = out_sample; } } else { for frame in 0..ctx.nframes() { let dry = inp.read(frame); - buffer.feed(dry + fb_s * denorm::Delay::fb(fb, frame)); let clock_samples = self.clock.next(denorm::Delay::trig(trig, frame)); - let out_sample = buffer.at(clock_samples as usize); + buffer.feed(dry + out_sample * denorm::Delay::fb(fb, frame)); + out.write(frame, crossfade(dry, out_sample, denorm::Delay::mix(mix, frame).clamp(0.0, 1.0))); - - fb_s = out_sample; } } - self.fb_sample = fb_s; - let last_frame = ctx.nframes() - 1; ctx_vals[0].set(out.read(last_frame)); } diff --git a/tests/node_delay.rs b/tests/node_delay.rs index d81b7aa..b4ae726 100644 --- a/tests/node_delay.rs +++ b/tests/node_delay.rs @@ -225,7 +225,7 @@ fn check_node_delay_fb() { let idxs_big = collect_signal_changes(&res.0[..], 50); // We expect the signal to be delayed by 20ms: - assert_eq!(idxs_big, vec![(220, 106), (441, 53)]); + assert_eq!(idxs_big, vec![(220, 106), (440, 53)]); } #[test] @@ -258,7 +258,7 @@ fn check_node_delay_fb_neg() { let idxs_big = collect_signal_changes(&res.0[..], 70); - assert_eq!(idxs_big, vec![(441, 100), (883, -100), (1325, 100)]); + assert_eq!(idxs_big, vec![(441, 100), (882, -100), (1323, 100)]); } @@ -288,9 +288,19 @@ fn check_node_delay_fb_pos() { // Emit the trigger signal: pset_n(&mut matrix, test, "trig", 1.0); - let res = run_for_ms(&mut node_exec, 40.0); + let res = run_for_ms(&mut node_exec, 100.0); let idxs_big = collect_signal_changes(&res.0[..], 70); - assert_eq!(idxs_big, vec![(441, 100), (883, 100), (1325, 100)]); + assert_eq!(idxs_big, vec![ + (441, 100), + (441 + 1 * 441, 100), + (441 + 2 * 441, 100), + (441 + 3 * 441, 100), + (441 + 4 * 441, 100), + (441 + 5 * 441, 100), + (441 + 6 * 441, 100), + (441 + 7 * 441, 100), + (441 + 8 * 441, 100), + ]); } From bdbfd016080af451bb18f8fd73cb3d8ada4b98a2 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 23 Jun 2021 05:11:39 +0200 Subject: [PATCH 05/15] make a note for DelayBuffer::feed --- src/dsp/helpers.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 2d5ac15..c682f4b 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -559,6 +559,9 @@ impl DelayBuffer { self.wr = 0; } + /// Feed one sample into the delay line and increment the write pointer. + /// Please note: For sample accurate feedback you need to retrieve the + /// output of the delay line before feeding in a new signal. #[inline] pub fn feed(&mut self, input: f32) { self.data[self.wr] = input; From 40c5c2c9c9069ab004e056bc5e345124f2f40dca Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 23 Jun 2021 05:12:37 +0200 Subject: [PATCH 06/15] fix documentation for delay feedback --- src/dsp/node_delay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsp/node_delay.rs b/src/dsp/node_delay.rs index a73e2d6..0af5478 100644 --- a/src/dsp/node_delay.rs +++ b/src/dsp/node_delay.rs @@ -43,7 +43,7 @@ impl Delay { likings.\nRange: (0..1)"; pub const fb : &'static str = "Delay fb\nThe feedback amount of the delay output to it's input. \ - \nRange: (0..1)"; + \nRange: (-1..1)"; pub const mix : &'static str = "Delay mix\nThe dry/wet mix of the delay.\nRange: (0..1)"; pub const mode : &'static str = From 845df99ab544b67f6c46e250c42f454577d38e3c Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 23 Jun 2021 05:13:50 +0200 Subject: [PATCH 07/15] updated splitmix64 implementation --- src/dsp/helpers.rs | 48 ++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index c682f4b..abe4a59 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -108,29 +108,29 @@ impl RandGen { } +// Copyright 2018 Developers of the Rand project. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. //- splitmix64 (http://xoroshiro.di.unimi.it/splitmix64.c) -//""" -// Written in 2015 by Sebastiano Vigna (vigna@acm.org) // -// To the extent possible under law, the author has dedicated all copyright -// and related and neighboring rights to this software to the public domain -// worldwide. This software is distributed without any warranty. -// -// See . -//""" -// -// Written by Alexander Stocko -// -// To the extent possible under law, the author has dedicated all copyright -// and related and neighboring rights to this software to the public domain -// worldwide. This software is distributed without any warranty. -// -// See - -/// The `SplitMix64` random number generator. +/// A splitmix64 random number generator. +/// +/// The splitmix algorithm is not suitable for cryptographic purposes, but is +/// very fast and has a 64 bit state. +/// +/// The algorithm used here is translated from [the `splitmix64.c` +/// reference source code](http://xoshiro.di.unimi.it/splitmix64.c) by +/// Sebastiano Vigna. For `next_u32`, a more efficient mixing function taken +/// from [`dsiutils`](http://dsiutils.di.unimi.it/) is used. #[derive(Copy, Clone)] pub struct SplitMix64(pub u64); +const PHI: u64 = 0x9e3779b97f4a7c15; + impl SplitMix64 { pub fn new(seed: u64) -> Self { Self(seed) } pub fn new_from_i64(seed: i64) -> Self { @@ -139,13 +139,11 @@ impl SplitMix64 { #[inline] pub fn next_u64(&mut self) -> u64 { - use std::num::Wrapping as w; - - let mut z = w(self.0) + w(0x9E37_79B9_7F4A_7C15_u64); - self.0 = z.0; - z = (z ^ (z >> 30)) * w(0xBF58_476D_1CE4_E5B9_u64); - z = (z ^ (z >> 27)) * w(0x94D0_49BB_1331_11EB_u64); - (z ^ (z >> 31)).0 + self.0 = self.0.wrapping_add(PHI); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) } #[inline] From de15c3cfd7cb75d9d10fa4771d358c3ddf301add Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 27 Jun 2021 07:48:42 +0200 Subject: [PATCH 08/15] there is no limit for audio files, it's up to you to kill your ram. --- src/sample_lib.rs | 3 --- tests/common/mod.rs | 6 ++++++ tests/node_delay.rs | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sample_lib.rs b/src/sample_lib.rs index 77c40ae..34c96dc 100644 --- a/src/sample_lib.rs +++ b/src/sample_lib.rs @@ -36,9 +36,6 @@ impl SampleLibrary { /// Returns an SAtom reference that you can clone and send directly /// to the sampling node of your choice. /// - /// The maximum length of the sample is `44100 * 10` samples, which - /// is the equivalent of roughly 1.7 MB. - /// /// Keep in mind that blocking on I/O in the UI might not be desireable. pub fn load<'a>(&'a mut self, path: &str) -> Result<&'a SAtom, SampleLoadError> { if self.loaded_samples.get(path).is_some() { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 257ad8e..df39c46 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -230,6 +230,12 @@ macro_rules! assert_minmax_of_rms { } } +#[allow(unused)] +pub fn pset_s(matrix: &mut Matrix, nid: NodeId, parm: &str, set: i64) { + let p = nid.inp_param(parm).unwrap(); + matrix.set_param(p, SAtom::setting(set)); +} + #[allow(unused)] pub fn pset_n(matrix: &mut Matrix, nid: NodeId, parm: &str, v_norm: f32) { let p = nid.inp_param(parm).unwrap(); diff --git a/tests/node_delay.rs b/tests/node_delay.rs index b4ae726..bfe53e1 100644 --- a/tests/node_delay.rs +++ b/tests/node_delay.rs @@ -261,7 +261,6 @@ fn check_node_delay_fb_neg() { assert_eq!(idxs_big, vec![(441, 100), (882, -100), (1323, 100)]); } - #[test] fn check_node_delay_fb_pos() { let (node_conf, mut node_exec) = new_node_engine(); From 9397b978dcbc59927947a8cb9343ae5384c8c79f Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 27 Jun 2021 23:38:14 +0200 Subject: [PATCH 09/15] added trigger input --- src/dsp/helpers.rs | 5 ++++ src/dsp/mod.rs | 3 +- src/dsp/node_tseq.rs | 66 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index abe4a59..560bc52 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -453,6 +453,11 @@ impl TriggerPhaseClock { self.clock_samples = 0; } + #[inline] + pub fn sync(&mut self) { + self.clock_phase = 0.0; + } + #[inline] pub fn next_phase(&mut self, clock_limit: f64, trigger_in: f32) -> f64 { if self.prev_trigger { diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 6e8ac74..256e410 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -405,7 +405,8 @@ macro_rules! node_list { [0 sig], tseq => TSeq UIType::Generic UICategory::CV (0 clock n_id d_id r_id f_def stp_d 0.0, 1.0, 0.0) - {1 0 cmode setting(1) fa_tseq_cmode 0 2} + (1 trig n_id n_id r_id f_def stp_d -1.0, 1.0, 0.0) + {2 0 cmode setting(1) fa_tseq_cmode 0 2} [0 trk1] [1 trk2] [2 trk3] diff --git a/src/dsp/node_tseq.rs b/src/dsp/node_tseq.rs index f4118b3..d400b18 100644 --- a/src/dsp/node_tseq.rs +++ b/src/dsp/node_tseq.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. use crate::nodes::{NodeAudioContext, NodeExecContext}; -use crate::dsp::helpers::TriggerPhaseClock; +use crate::dsp::helpers::{TriggerPhaseClock, Trigger}; use crate::dsp::{NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals}; use crate::dsp::tracker::TrackerBackend; @@ -22,12 +22,18 @@ macro_rules! fa_tseq_cmode { ($formatter: expr, $v: expr, $denorm_v: expr) => { } } } +#[derive(Debug)] +pub struct TSeqTime { + clock: TriggerPhaseClock, + trigger: Trigger, +} + /// A tracker based sequencer #[derive(Debug)] pub struct TSeq { backend: Option>, - clock: TriggerPhaseClock, srate: f64, + time: Box, } impl Clone for TSeq { @@ -39,7 +45,10 @@ impl TSeq { Self { backend: None, srate: 48000.0, - clock: TriggerPhaseClock::new(), + time: Box::new(TSeqTime { + clock: TriggerPhaseClock::new(), + trigger: Trigger::new(), + }), } } @@ -49,6 +58,8 @@ impl TSeq { pub const clock : &'static str = "TSeq clock\nClock input\nRange: (0..1)\n"; + pub const trig : &'static str = + "TSeq trig\nSynchronization trigger which restarts the sequence.\nRange: (-1..1)\n"; pub const cmode : &'static str = "TSeq cmode\n'clock' input signal mode:\n\ - RowT: Trigger = advance row\n\ @@ -89,12 +100,25 @@ impl TSeq { pub const HELP : &'static str = r#"Tracker (based) Sequencer +This sequencer gets it's speed from the clock source. The 'clock' +signal can be interpreted in different modes. But if you want to +run multiple sequencers in parallel, you want to synchronize them. +For this you can use the 'trig' input, it resets the played row to +the beginning of the sequence every time a trigger is received. + +Alternatively you can run the sequencer clock using the phase mode. +With that the phase (0..1) signal on the 'clock' input determines the +exact play head position in the pattern. With this you just need to +synchronize the phase generators for different sequencers. + +For an idea how to chain multiple tracker sequencers, see the next page. + This tracker provides 6 columns that each can have one of the following types: - Note column: for specifying pitches. - Step column: for specifying non interpolated CV signals. -- Value column: for specifying linearily interpolated CV signals. +- Value column: for specifying linearly interpolated CV signals. - Gate column: for specifying gates, with probability and ratcheting. Step, value and gate cells can be set to 4096 (0xFFF) different values @@ -102,6 +126,10 @@ or contain nothing at all. For step and value columns these values are mapped to the 0.0-1.0 CV signal range, with 0xFFF being 1.0 and 0x000 being 0.0. +On the next page you can read about the gate cells and the gate outputs. +---page--- +Gate Input and Output + The gate cells are differently coded: - 0x00F: The least significant nibble controls the gate length. @@ -109,7 +137,7 @@ The gate cells are differently coded: - 0x0F0: The second nibble controls ratcheting, with 0x0F0 being one gate per row, and 0x000 being 16 gates per row. - 0xF00: The most significant nibble controls probability of the - whole gate cell. With 0xF00 meaing the gate will always be + whole gate cell. With 0xF00 meaning the gate will always be triggered, and 0x000 means that the gate is only triggered with 6% probability. 50% is 0x070. @@ -118,7 +146,7 @@ column type: - Step gat1-gat6: Like note columns, this will output a 1.0 for the whole row if a step value is set. With two step values directly - following each other no 0.0 will be emitted inbetween + following each other no 0.0 will be emitted in between the rows. This means if you want to drive an envelope with release phase with this signal, you need to make space for the release phase. @@ -127,6 +155,12 @@ column type: - Value gat1-gat6: Outputs a 1.0 value for the duration of the last row. You can use this to trigger other things once the sequence has been played. + +Tip: + If you want to use the end of a tracker sequence as trigger for + something else, eg. switching to a different 'tseq' and restart + it using it's 'trig' input, you will need to use the gate output + of a value column and invert it. "#; } @@ -139,7 +173,8 @@ impl DspNode for TSeq { fn reset(&mut self) { self.backend = None; - self.clock.reset(); + self.time.clock.reset(); + self.time.trigger.reset(); } #[inline] @@ -148,8 +183,9 @@ impl DspNode for TSeq { atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) { - use crate::dsp::{out, inp, at}; + use crate::dsp::{out, inp, at, denorm}; let clock = inp::TSeq::clock(inputs); + let trig = inp::TSeq::trig(inputs); let cmode = at::TSeq::cmode(atoms); let backend = @@ -163,13 +199,21 @@ impl DspNode for TSeq { [0.0; MAX_BLOCK_SIZE]; let cmode = cmode.i(); - let plen = backend.pattern_len() as f64; + let plen = backend.pattern_len().max(1) as f64; + + let time = &mut self.time; for frame in 0..ctx.nframes() { + if time.trigger.check_trigger( + denorm::TSeq::trig(trig, frame)) + { + time.clock.sync(); + } + let phase = match cmode { - 0 => self.clock.next_phase(plen, clock.read(frame)) / plen, - 1 => self.clock.next_phase(1.0, clock.read(frame)), + 0 => time.clock.next_phase(plen, clock.read(frame)) / plen, + 1 => time.clock.next_phase(1.0, clock.read(frame)), 2 | _ => (clock.read(frame).abs() as f64).fract(), }; From a92ad0de35269c6f1e7b0120b639e23080320f33 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 28 Jun 2021 03:48:02 +0200 Subject: [PATCH 10/15] wrote test for tseq trig --- tests/basics.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/tests/basics.rs b/tests/basics.rs index 0817ffa..7b941aa 100644 --- a/tests/basics.rs +++ b/tests/basics.rs @@ -747,10 +747,8 @@ fn check_matrix_tseq() { .input(out.inp("ch1"), None, None)); matrix.sync().unwrap(); - let freq_param = sin.inp_param("freq").unwrap(); - matrix.set_param(freq_param, SAtom::param(-0.978)); - let cmode_param = tsq.inp_param("cmode").unwrap(); - matrix.set_param(cmode_param, SAtom::setting(1)); + pset_n(&mut matrix, sin, "freq", -0.978); + pset_s(&mut matrix, tsq, "cmode", 1); let pat = matrix.get_pattern_data(0).unwrap(); { @@ -782,13 +780,13 @@ fn check_matrix_tseq() { assert_float_eq!(samples[9], 0.42228); // switch to row trigger: - matrix.set_param(cmode_param, SAtom::setting(0)); + pset_s(&mut matrix, tsq, "cmode", 0); let samples = run_and_undersample(&mut node_exec, 2000.0, 5); assert_vec_feq!(samples, vec![0.70411, 0.90413, 0.99306, 0.97972, 0.966387]); // set to phase mode: - matrix.set_param(cmode_param, SAtom::setting(2)); + pset_s(&mut matrix, tsq, "cmode", 2); let samples = run_and_undersample(&mut node_exec, 1000.0, 5); assert_float_eq!(samples[0], 0.2491); @@ -798,6 +796,73 @@ fn check_matrix_tseq() { assert_float_eq!(samples[4], 0.8104); } +#[test] +fn check_matrix_tseq_trig() { + use hexodsp::dsp::tracker::UIPatternModel; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let tsq = NodeId::TSeq(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(tsq) + .input(tsq.inp("clock"), None, None) + .out(None, None, tsq.out("trk1"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, sin, "freq", -0.978); + pset_s(&mut matrix, tsq, "cmode", 1); + + let pat = matrix.get_pattern_data(0).unwrap(); + { + let mut pr = pat.borrow_mut(); + pr.set_rows(16); + pr.set_cell_value(0, 0, 0xFFF); + pr.set_cell_value(15, 0, 0x000); + } + + for _ in 0..10 { + matrix.check_pattern_data(0); + } + + // We let the clock mode tune in: + run_and_undersample(&mut node_exec, 10000.0, 1); + + // Take some real samples: + let samples = run_and_undersample(&mut node_exec, 2000.0, 10); + + assert_float_eq!(samples[0], 0.3157); + assert_float_eq!(samples[1], 0.209); + assert_float_eq!(samples[2], 0.1024); + assert_float_eq!(samples[3], 0.0648); + assert_float_eq!(samples[4], 0.95566); + assert_float_eq!(samples[5], 0.84899); + assert_float_eq!(samples[6], 0.74231); + assert_float_eq!(samples[7], 0.6356); + assert_float_eq!(samples[8], 0.5289); + assert_float_eq!(samples[9], 0.42228); + + pset_n(&mut matrix, tsq, "trig", 1.0); + + // Take some real samples: + let samples = run_and_undersample(&mut node_exec, 2000.0, 10); + + assert_float_eq!(samples[0], 0.3157); + // trigger hits: + assert_float_eq!(samples[1], 0.9639); + assert_float_eq!(samples[2], 0.8572); + assert_float_eq!(samples[3], 0.7506); + assert_float_eq!(samples[4], 0.6439); + assert_float_eq!(samples[5], 0.5372); + assert_float_eq!(samples[6], 0.4305); + assert_float_eq!(samples[7], 0.3239); +} + #[test] fn check_matrix_tseq_gate() { use hexodsp::dsp::tracker::UIPatternModel; From 85ad5e79559f29cf68392de535f8e2859b1e39d8 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 28 Jun 2021 05:10:46 +0200 Subject: [PATCH 11/15] helpers for comb and allpass, and added allpass node --- src/dsp/helpers.rs | 67 ++++++++++++++++++++++++++ src/dsp/mod.rs | 25 +++++++++- src/dsp/node_allp.rs | 110 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/dsp/node_allp.rs diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 560bc52..8afa829 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -619,6 +619,73 @@ impl DelayBuffer { } } +/// Default size of the delay buffer: 1 seconds at 8 times 48kHz +const DEFAULT_ALLPASS_COMB_SAMPLES : usize = 8 * 48000; + +#[derive(Debug, Clone)] +pub struct AllPass { + delay: DelayBuffer, +} + +impl AllPass { + pub fn new() -> Self { + Self { + delay: DelayBuffer::new_with_size(DEFAULT_ALLPASS_COMB_SAMPLES), + } + } + + pub fn set_sample_rate(&mut self, srate: f32) { + self.delay.set_sample_rate(srate); + } + + pub fn reset(&mut self) { + self.delay.reset(); + } + + #[inline] + pub fn next(&mut self, time: f32, g: f32, v: f32) -> f32 { + let s = self.delay.cubic_interpolate_at(time); + self.delay.feed(v + s * g); + s + -1.0 * g * v + } +} + +#[derive(Debug, Clone)] +pub struct Comb { + delay: DelayBuffer, +} + +impl Comb { + pub fn new() -> Self { + Self { + delay: DelayBuffer::new_with_size(DEFAULT_ALLPASS_COMB_SAMPLES), + } + } + + pub fn set_sample_rate(&mut self, srate: f32) { + self.delay.set_sample_rate(srate); + } + + pub fn reset(&mut self) { + self.delay.reset(); + } + + #[inline] + pub fn next_feedback(&mut self, time: f32, g: f32, v: f32) -> f32 { + let s = self.delay.cubic_interpolate_at(time); + self.delay.feed(v + s * g); + v + } + + #[inline] + pub fn next_feedforward(&mut self, time: f32, g: f32, v: f32) -> f32 { + let s = self.delay.cubic_interpolate_at(time); + self.delay.feed(v); + v + s * g + } +} + + // translated from Odin 2 Synthesizer Plugin // Copyright (C) 2020 TheWaveWarden // under GPLv3 or any later diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 256e410..be0669f 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -20,6 +20,8 @@ mod node_fbwr_fbrd; mod node_ad; #[allow(non_upper_case_globals)] mod node_delay; +#[allow(non_upper_case_globals)] +mod node_allp; pub mod tracker; mod satom; @@ -55,6 +57,7 @@ use node_fbwr_fbrd::FbWr; use node_fbwr_fbrd::FbRd; use node_ad::Ad; use node_delay::Delay; +use node_allp::AllP; pub const MIDI_MAX_FREQ : f32 = 13289.75; @@ -311,6 +314,20 @@ macro_rules! r_tms { ($x: expr, $coarse: expr) => { } } } +/// The rounding function for milliseconds knobs +macro_rules! r_fms { ($x: expr, $coarse: expr) => { + if $coarse { + if d_ftme!($x) > 1000.0 { + n_ftme!((d_ftme!($x) / 100.0).round() * 100.0) + } else if d_ftme!($x) > 100.0 { + n_ftme!((d_ftme!($x) / 10.0).round() * 10.0) + } else { + n_ftme!((d_ftme!($x)).round()) + } + } else { + n_ftme!((d_ftme!($x) * 10.0).round() / 10.0) + } +} } /// The default steps function: macro_rules! stp_d { () => { (20.0, 100.0) } } @@ -370,7 +387,8 @@ define_exp!{n_declick d_declick 0.0, 50.0} define_exp!{n_env d_env 0.0, 1000.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.25, 1000.0} // Special linear gain factor for the Out node, to be able // to reach more exact "1.0". @@ -471,6 +489,11 @@ macro_rules! node_list { (4 mix n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {5 0 mode setting(0) fa_delay_mode 0 1} [0 sig], + allp => AllP UIType::Generic UICategory::Signal + (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (1 time n_ftme d_ftme r_fms f_ms stp_m 0.0, 1.0, 25.0) + (2 g n_id d_id r_id f_def stp_d -1.0, 1.0, 0.7) + [0 sig], test => Test UIType::Generic UICategory::IOUtil (0 f n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {1 0 p param(0.0) fa_test_s 0 10} diff --git a/src/dsp/node_allp.rs b/src/dsp/node_allp.rs new file mode 100644 index 0000000..b6ca616 --- /dev/null +++ b/src/dsp/node_allp.rs @@ -0,0 +1,110 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::{NodeAudioContext, NodeExecContext}; +use crate::dsp::{NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals}; +use crate::dsp::helpers::AllPass; + +/// A simple amplifier +#[derive(Debug, Clone)] +pub struct AllP { + allpass: Box, +} + +impl AllP { + pub fn new(_nid: &NodeId) -> Self { + Self { + allpass: Box::new(AllPass::new()), + } + } + + pub const inp : &'static str = + "AllP inp\nThe signal input for the allpass filter.\nRange: (-1..1)"; + pub const g : &'static str = + "AllP g\nThe internal factor for the allpass filter.\nRange: (-1..1)"; + pub const time : &'static str = + "AllP time\nThe allpass delay time.\nRange: (0..1)"; + pub const sig : &'static str = + "AllP sig\nThe output of allpass filter.\nRange: (-1..1)"; + + pub const DESC : &'static str = +r#"Simple Single Allpass Filter + +This is an allpass filter that can be used to build reverbs +or anything you might find it useful for. +"#; +pub const HELP : &'static str = +r#"AllP - A Simple Single Allpass Filter + +This is an allpass filter that can be used to build reverbs +or anything you might find it useful for. + +Typical arrangements are (Schroeder Reverb): + + t=4.5ms + g=0.7 -> Comb + AllP -> AllP -> AllP -> -> Comb + t=42ms t=13.5ms -> Comb + g=0.7 g=0.7 -> Comb + +Or: + + Comb -> t=0.48ms + Comb -> g=0.7 + Comb -> AllP -> AllP -> AllP + Comb -> t=5ms t=1.68ms + g=0.7 g=0.7 + +Typical values for the comb filters are in the range g=0.6 to 0.9 +and time in the range of 30ms to 250ms. + +Feel free to deviate from this and experiment around. + +Building your own reverbs is fun! + +(And don't forget that you can create feedback +using the FbWr and FbRd nodes!) +"#; +} + +impl DspNode for AllP { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, srate: f32) { + self.allpass.set_sample_rate(srate); + } + + fn reset(&mut self) { + self.allpass.reset(); + } + + #[inline] + fn process( + &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, + atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf], + outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + use crate::dsp::{at, out, inp, denorm}; + + let inp = inp::AllP::inp(inputs); + let time = inp::AllP::time(inputs); + let g = inp::AllP::g(inputs); + let out = out::AllP::sig(outputs); + + let ap = &mut *self.allpass; + + for frame in 0..ctx.nframes() { + let v = inp.read(frame); + + out.write(frame, + ap.next( + denorm::AllP::time(time, frame), + denorm::AllP::g(g, frame), + v)); + } + + let last_frame = ctx.nframes() - 1; + ctx_vals[0].set(out.read(last_frame)); + } +} From 1020684e136228304f957b2e93cd5b399fd6f94e Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 28 Jun 2021 19:33:02 +0200 Subject: [PATCH 12/15] fix warnings --- src/dsp/node_allp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dsp/node_allp.rs b/src/dsp/node_allp.rs index b6ca616..3153c0d 100644 --- a/src/dsp/node_allp.rs +++ b/src/dsp/node_allp.rs @@ -82,10 +82,10 @@ impl DspNode for AllP { #[inline] fn process( &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, - atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf], + _atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) { - use crate::dsp::{at, out, inp, denorm}; + use crate::dsp::{out, inp, denorm}; let inp = inp::AllP::inp(inputs); let time = inp::AllP::time(inputs); From 143f204ac88ea67369e4163c4e165bd5bb817ab3 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 29 Jun 2021 05:08:43 +0200 Subject: [PATCH 13/15] finished allpass tests --- tests/node_allp.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/node_allp.rs diff --git a/tests/node_allp.rs b/tests/node_allp.rs new file mode 100644 index 0000000..7a03bdf --- /dev/null +++ b/tests/node_allp.rs @@ -0,0 +1,60 @@ +mod common; +use common::*; + +#[test] +fn check_node_allp() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 4, 4); + + let test = NodeId::Test(0); + let ap = NodeId::AllP(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(test) + .out(None, None, test.out("tsig"))); + matrix.place(0, 1, Cell::empty(ap) + .input(ap.inp("inp"), None, None) + .out(None, None, ap.out("sig"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None) + .out(None, None, None)); + matrix.place(1, 0, Cell::empty(test) + .out(None, None, test.out("tsig"))); + matrix.place(1, 1, Cell::empty(out) + .input(out.inp("ch2"), None, None) + .out(None, None, None)); + pset_d(&mut matrix, ap, "time", 3.0); + matrix.sync().unwrap(); + + pset_s(&mut matrix, test, "trig", 1); + + let res = run_for_ms(&mut node_exec, 20.0); + + // the original signal on ch2: 2ms trigger up: + let mut v = vec![1.0; (2.0 * 44.1_f32).ceil() as usize]; + v.append(&mut vec![0.0; (18.0 * 44.1_f32).ceil() as usize]); + assert_vec_feq!(res.1, v); + + // now signal on ch1 from the allpass: + // starts with original signal * -0.7 + let mut v = vec![-0.7; (2.0 * 44.1_f32).ceil() as usize]; + // silence for 1ms, which is the internal delay of the allpass + v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 3]); + + // allpass feedback of the original signal for 2ms: + // XXX: the smearing before and after the allpass is due to the + // cubic interpolation! + v.append(&mut vec![-0.03150302, 0.25802, 1.0735]); + v.append(&mut vec![1.0; (2.0 * 44.1_f32).ceil() as usize - 3]); + v.append(&mut vec![1.0315, 0.7419, -0.0735]); + // 1ms allpass silence like before: + v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 6]); + + // 2ms the previous 1.0 * 0.7 fed back into the filter, + // including even more smearing due to cubic interpolation: + v.append(&mut vec![0.0006, -0.0120, 0.0106, 0.3444, 0.7801, 0.6962]); + v.append(&mut vec![0.7; (2.0 * 44.1_f32).floor() as usize - 5]); + v.append(&mut vec![0.6993, 0.712, 0.6893, 0.3555, -0.0801, 0.0037]); + + //d// println!("res={:?}", res.1); + assert_vec_feq!(res.0, v); +} From 087983b386b575fd043c7be380bf489892c3d737 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 30 Jun 2021 03:53:30 +0200 Subject: [PATCH 14/15] boilerplate for a noise oscillator --- src/dsp/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index be0669f..1c78acf 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -22,6 +22,8 @@ mod node_ad; mod node_delay; #[allow(non_upper_case_globals)] mod node_allp; +#[allow(non_upper_case_globals)] +mod node_noise; pub mod tracker; mod satom; @@ -46,6 +48,7 @@ use crate::fa_sampl_pmode; use crate::fa_sampl_dir; use crate::fa_ad_mult; use crate::fa_delay_mode; +use crate::fa_noise_mode; use node_amp::Amp; use node_sin::Sin; @@ -58,6 +61,7 @@ use node_fbwr_fbrd::FbRd; use node_ad::Ad; use node_delay::Delay; use node_allp::AllP; +use node_noise::Noise; pub const MIDI_MAX_FREQ : f32 = 13289.75; @@ -494,6 +498,11 @@ macro_rules! node_list { (1 time n_ftme d_ftme r_fms f_ms stp_m 0.0, 1.0, 25.0) (2 g n_id d_id r_id f_def stp_d -1.0, 1.0, 0.7) [0 sig], + noise => Noise UIType::Generic UICategory::IOUtil + (0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (1 offs n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + {2 0 mode setting(0) fa_noise_mode 0 1} + [0 sig], test => Test UIType::Generic UICategory::IOUtil (0 f n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {1 0 p param(0.0) fa_test_s 0 10} From dd89eebbd50c33f5606789bcb4acfce8f3d27520 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 30 Jun 2021 03:55:26 +0200 Subject: [PATCH 15/15] added to noise to oscillators --- src/dsp/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 1c78acf..d6bcb24 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -498,7 +498,7 @@ macro_rules! node_list { (1 time n_ftme d_ftme r_fms f_ms stp_m 0.0, 1.0, 25.0) (2 g n_id d_id r_id f_def stp_d -1.0, 1.0, 0.7) [0 sig], - noise => Noise UIType::Generic UICategory::IOUtil + noise => Noise UIType::Generic UICategory::Osc (0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 offs n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) {2 0 mode setting(0) fa_noise_mode 0 1}