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" diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 1625196..8afa829 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] @@ -455,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 { @@ -559,10 +562,13 @@ 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.wr = (self.wr + 1) % self.data.len(); self.data[self.wr] = input; + self.wr = (self.wr + 1) % self.data.len(); } #[inline] @@ -613,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 b58d024..d6bcb24 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -20,6 +20,10 @@ mod node_fbwr_fbrd; mod node_ad; #[allow(non_upper_case_globals)] mod node_delay; +#[allow(non_upper_case_globals)] +mod node_allp; +#[allow(non_upper_case_globals)] +mod node_noise; pub mod tracker; mod satom; @@ -44,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; @@ -55,6 +60,8 @@ use node_fbwr_fbrd::FbWr; 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; @@ -311,6 +318,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 +391,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". @@ -405,7 +427,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] @@ -463,17 +486,29 @@ 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) - (3 fb n_id d_id r_id f_def stp_d 0.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 -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], + 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], + 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} + [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 +1063,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_allp.rs b/src/dsp/node_allp.rs new file mode 100644 index 0000000..3153c0d --- /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::{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)); + } +} diff --git a/src/dsp/node_delay.rs b/src/dsp/node_delay.rs index 4c9bdde..0af5478 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(), } } @@ -45,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 = @@ -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/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/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(), }; 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/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; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d191f3b..df39c46 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) => { @@ -209,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_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); +} diff --git a/tests/node_delay.rs b/tests/node_delay.rs new file mode 100644 index 0000000..bfe53e1 --- /dev/null +++ b/tests/node_delay.rs @@ -0,0 +1,305 @@ +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), (440, 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), (882, -100), (1323, 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, 100.0); + + let idxs_big = collect_signal_changes(&res.0[..], 70); + + 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), + ]); +}