Merge branch 'master' of ssh://hentai-kamen.m8geil.de:19022/WeirdConstructor/HexoDSP

This commit is contained in:
Weird Constructor 2021-06-30 06:22:40 +02:00
commit 8c656a8ce3
12 changed files with 832 additions and 80 deletions

View file

@ -23,7 +23,7 @@ hound = "3.4.0"
[dev-dependencies] [dev-dependencies]
microfft = "0.3.1" microfft = "0.3.1"
num-complex = "0.2" num-complex = "0.2"
#jack = "0.6.6" jack = "0.6.6"
[lib] [lib]
path = "src/lib.rs" path = "src/lib.rs"

View file

@ -108,29 +108,29 @@ impl RandGen {
} }
// Copyright 2018 Developers of the Rand project.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, 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) //- 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 /// A splitmix64 random number generator.
// and related and neighboring rights to this software to the public domain ///
// worldwide. This software is distributed without any warranty. /// The splitmix algorithm is not suitable for cryptographic purposes, but is
// /// very fast and has a 64 bit state.
// See <http://creativecommons.org/publicdomain/zero/1.0/>. ///
//""" /// The algorithm used here is translated from [the `splitmix64.c`
// /// reference source code](http://xoshiro.di.unimi.it/splitmix64.c) by
// Written by Alexander Stocko <as@coder.gg> /// Sebastiano Vigna. For `next_u32`, a more efficient mixing function taken
// /// from [`dsiutils`](http://dsiutils.di.unimi.it/) is used.
// 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 <LICENSE or http://creativecommons.org/publicdomain/zero/1.0/>
/// The `SplitMix64` random number generator.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct SplitMix64(pub u64); pub struct SplitMix64(pub u64);
const PHI: u64 = 0x9e3779b97f4a7c15;
impl SplitMix64 { impl SplitMix64 {
pub fn new(seed: u64) -> Self { Self(seed) } pub fn new(seed: u64) -> Self { Self(seed) }
pub fn new_from_i64(seed: i64) -> Self { pub fn new_from_i64(seed: i64) -> Self {
@ -139,13 +139,11 @@ impl SplitMix64 {
#[inline] #[inline]
pub fn next_u64(&mut self) -> u64 { pub fn next_u64(&mut self) -> u64 {
use std::num::Wrapping as w; self.0 = self.0.wrapping_add(PHI);
let mut z = self.0;
let mut z = w(self.0) + w(0x9E37_79B9_7F4A_7C15_u64); z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
self.0 = z.0; z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
z = (z ^ (z >> 30)) * w(0xBF58_476D_1CE4_E5B9_u64); z ^ (z >> 31)
z = (z ^ (z >> 27)) * w(0x94D0_49BB_1331_11EB_u64);
(z ^ (z >> 31)).0
} }
#[inline] #[inline]
@ -455,6 +453,11 @@ impl TriggerPhaseClock {
self.clock_samples = 0; self.clock_samples = 0;
} }
#[inline]
pub fn sync(&mut self) {
self.clock_phase = 0.0;
}
#[inline] #[inline]
pub fn next_phase(&mut self, clock_limit: f64, trigger_in: f32) -> f64 { pub fn next_phase(&mut self, clock_limit: f64, trigger_in: f32) -> f64 {
if self.prev_trigger { if self.prev_trigger {
@ -559,10 +562,13 @@ impl DelayBuffer {
self.wr = 0; 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] #[inline]
pub fn feed(&mut self, input: f32) { pub fn feed(&mut self, input: f32) {
self.wr = (self.wr + 1) % self.data.len();
self.data[self.wr] = input; self.data[self.wr] = input;
self.wr = (self.wr + 1) % self.data.len();
} }
#[inline] #[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 // translated from Odin 2 Synthesizer Plugin
// Copyright (C) 2020 TheWaveWarden // Copyright (C) 2020 TheWaveWarden
// under GPLv3 or any later // under GPLv3 or any later

View file

@ -20,6 +20,10 @@ mod node_fbwr_fbrd;
mod node_ad; mod node_ad;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
mod node_delay; mod node_delay;
#[allow(non_upper_case_globals)]
mod node_allp;
#[allow(non_upper_case_globals)]
mod node_noise;
pub mod tracker; pub mod tracker;
mod satom; mod satom;
@ -44,6 +48,7 @@ use crate::fa_sampl_pmode;
use crate::fa_sampl_dir; use crate::fa_sampl_dir;
use crate::fa_ad_mult; use crate::fa_ad_mult;
use crate::fa_delay_mode; use crate::fa_delay_mode;
use crate::fa_noise_mode;
use node_amp::Amp; use node_amp::Amp;
use node_sin::Sin; use node_sin::Sin;
@ -55,6 +60,8 @@ use node_fbwr_fbrd::FbWr;
use node_fbwr_fbrd::FbRd; use node_fbwr_fbrd::FbRd;
use node_ad::Ad; use node_ad::Ad;
use node_delay::Delay; use node_delay::Delay;
use node_allp::AllP;
use node_noise::Noise;
pub const MIDI_MAX_FREQ : f32 = 13289.75; 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: /// The default steps function:
macro_rules! stp_d { () => { (20.0, 100.0) } } macro_rules! stp_d { () => { (20.0, 100.0) } }
@ -371,6 +392,7 @@ define_exp!{n_declick d_declick 0.0, 50.0}
define_exp!{n_env d_env 0.0, 1000.0} define_exp!{n_env d_env 0.0, 1000.0}
define_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 // Special linear gain factor for the Out node, to be able
// to reach more exact "1.0". // to reach more exact "1.0".
@ -405,7 +427,8 @@ macro_rules! node_list {
[0 sig], [0 sig],
tseq => TSeq UIType::Generic UICategory::CV tseq => TSeq UIType::Generic UICategory::CV
(0 clock n_id d_id r_id f_def stp_d 0.0, 1.0, 0.0) (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] [0 trk1]
[1 trk2] [1 trk2]
[2 trk3] [2 trk3]
@ -463,17 +486,29 @@ macro_rules! node_list {
[0 sig] [0 sig]
[1 eoet], [1 eoet],
delay => Delay UIType::Generic UICategory::Signal delay => Delay UIType::Generic UICategory::Signal
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 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) (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) (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) (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} {5 0 mode setting(0) fa_delay_mode 0 1}
[0 sig], [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 test => Test UIType::Generic UICategory::IOUtil
(0 f n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) (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} {1 0 p param(0.0) fa_test_s 0 10}
[0 sig], {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 { mod ni {
$( $(
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

110
src/dsp/node_allp.rs Normal file
View file

@ -0,0 +1,110 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<AllPass>,
}
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<T: NodeAudioContext>(
&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));
}
}

View file

@ -21,7 +21,6 @@ macro_rules! fa_delay_mode { ($formatter: expr, $v: expr, $denorm_v: expr) => {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Delay { pub struct Delay {
buffer: Box<DelayBuffer>, buffer: Box<DelayBuffer>,
fb_sample: f32,
clock: TriggerSampleClock, clock: TriggerSampleClock,
} }
@ -29,7 +28,6 @@ impl Delay {
pub fn new(_nid: &NodeId) -> Self { pub fn new(_nid: &NodeId) -> Self {
Self { Self {
buffer: Box::new(DelayBuffer::new()), buffer: Box::new(DelayBuffer::new()),
fb_sample: 0.0,
clock: TriggerSampleClock::new(), clock: TriggerSampleClock::new(),
} }
} }
@ -45,7 +43,7 @@ impl Delay {
likings.\nRange: (0..1)"; likings.\nRange: (0..1)";
pub const fb : &'static str = pub const fb : &'static str =
"Delay fb\nThe feedback amount of the delay output to it's input. \ "Delay fb\nThe feedback amount of the delay output to it's input. \
\nRange: (0..1)"; \nRange: (-1..1)";
pub const mix : &'static str = pub const mix : &'static str =
"Delay mix\nThe dry/wet mix of the delay.\nRange: (0..1)"; "Delay mix\nThe dry/wet mix of the delay.\nRange: (0..1)";
pub const mode : &'static str = pub const mode : &'static str =
@ -108,43 +106,36 @@ impl DspNode for Delay {
let mix = inp::Delay::mix(inputs); let mix = inp::Delay::mix(inputs);
let out = out::Delay::sig(outputs); let out = out::Delay::sig(outputs);
let mut fb_s = self.fb_sample;
if mode.i() == 0 { if mode.i() == 0 {
for frame in 0..ctx.nframes() { for frame in 0..ctx.nframes() {
let dry = inp.read(frame); let dry = inp.read(frame);
buffer.feed(dry + fb_s * denorm::Delay::fb(fb, frame));
let out_sample = let out_sample =
buffer.cubic_interpolate_at( buffer.cubic_interpolate_at(
denorm::Delay::time(time, frame)); denorm::Delay::time(time, frame));
buffer.feed(dry + out_sample * denorm::Delay::fb(fb, frame));
out.write(frame, out.write(frame,
crossfade(dry, out_sample, crossfade(dry, out_sample,
denorm::Delay::mix(mix, frame).clamp(0.0, 1.0))); denorm::Delay::mix(mix, frame).clamp(0.0, 1.0)));
fb_s = out_sample;
} }
} else { } else {
for frame in 0..ctx.nframes() { for frame in 0..ctx.nframes() {
let dry = inp.read(frame); let dry = inp.read(frame);
buffer.feed(dry + fb_s * denorm::Delay::fb(fb, frame));
let clock_samples = let clock_samples =
self.clock.next(denorm::Delay::trig(trig, frame)); self.clock.next(denorm::Delay::trig(trig, frame));
let out_sample = buffer.at(clock_samples as usize); let out_sample = buffer.at(clock_samples as usize);
buffer.feed(dry + out_sample * denorm::Delay::fb(fb, frame));
out.write(frame, out.write(frame,
crossfade(dry, out_sample, crossfade(dry, out_sample,
denorm::Delay::mix(mix, frame).clamp(0.0, 1.0))); 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; let last_frame = ctx.nframes() - 1;
ctx_vals[0].set(out.read(last_frame)); ctx_vals[0].set(out.read(last_frame));
} }

View file

@ -4,6 +4,7 @@
use crate::nodes::{NodeAudioContext, NodeExecContext}; use crate::nodes::{NodeAudioContext, NodeExecContext};
use crate::dsp::{NodeId, SAtom, ProcBuf, GraphFun, GraphAtomData, DspNode, LedPhaseVals}; use crate::dsp::{NodeId, SAtom, ProcBuf, GraphFun, GraphAtomData, DspNode, LedPhaseVals};
use crate::dsp::helpers::{TrigSignal};
#[macro_export] #[macro_export]
macro_rules! fa_test_s { ($formatter: expr, $v: expr, $denorm_v: expr) => { { 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 /// A simple amplifier
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Test { pub struct Test {
trig_sig: TrigSignal,
trigger: bool,
} }
impl Test { 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 DESC : &'static str = r#""#;
pub const HELP : &'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 { impl DspNode for Test {
fn outputs() -> usize { 1 } fn outputs() -> usize { 2 }
fn set_sample_rate(&mut self, _srate: f32) { } fn set_sample_rate(&mut self, srate: f32) {
fn reset(&mut self) { } self.trig_sig.set_sample_rate(srate);
}
fn reset(&mut self) {
self.trig_sig.reset();
}
#[inline] #[inline]
fn process<T: NodeAudioContext>( fn process<T: NodeAudioContext>(
@ -55,12 +69,36 @@ impl DspNode for Test {
atoms: &[SAtom], _params: &[ProcBuf], _inputs: &[ProcBuf], atoms: &[SAtom], _params: &[ProcBuf], _inputs: &[ProcBuf],
outputs: &mut [ProcBuf], _led: LedPhaseVals) outputs: &mut [ProcBuf], _led: LedPhaseVals)
{ {
use crate::dsp::{out, at}; use crate::dsp::{out_idx, at};
let p = at::Test::p(atoms); 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() { for frame in 0..ctx.nframes() {
if trigger > 0 {
self.trig_sig.trigger();
trigger = 0;
}
out.write(frame, p.f()); out.write(frame, p.f());
let t = self.trig_sig.next();
tsig.write(frame, t);
} }
} }

View file

@ -3,7 +3,7 @@
// See README.md and COPYING for details. // See README.md and COPYING for details.
use crate::nodes::{NodeAudioContext, NodeExecContext}; 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::{NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals};
use crate::dsp::tracker::TrackerBackend; 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 /// A tracker based sequencer
#[derive(Debug)] #[derive(Debug)]
pub struct TSeq { pub struct TSeq {
backend: Option<Box<TrackerBackend>>, backend: Option<Box<TrackerBackend>>,
clock: TriggerPhaseClock,
srate: f64, srate: f64,
time: Box<TSeqTime>,
} }
impl Clone for TSeq { impl Clone for TSeq {
@ -39,7 +45,10 @@ impl TSeq {
Self { Self {
backend: None, backend: None,
srate: 48000.0, srate: 48000.0,
time: Box::new(TSeqTime {
clock: TriggerPhaseClock::new(), clock: TriggerPhaseClock::new(),
trigger: Trigger::new(),
}),
} }
} }
@ -49,6 +58,8 @@ impl TSeq {
pub const clock : &'static str = pub const clock : &'static str =
"TSeq clock\nClock input\nRange: (0..1)\n"; "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 = pub const cmode : &'static str =
"TSeq cmode\n'clock' input signal mode:\n\ "TSeq cmode\n'clock' input signal mode:\n\
- RowT: Trigger = advance row\n\ - RowT: Trigger = advance row\n\
@ -89,12 +100,25 @@ impl TSeq {
pub const HELP : &'static str = pub const HELP : &'static str =
r#"Tracker (based) Sequencer 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 This tracker provides 6 columns that each can have one of the following
types: types:
- Note column: for specifying pitches. - Note column: for specifying pitches.
- Step column: for specifying non interpolated CV signals. - 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. - Gate column: for specifying gates, with probability and ratcheting.
Step, value and gate cells can be set to 4096 (0xFFF) different values 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 are mapped to the 0.0-1.0 CV signal range, with 0xFFF being 1.0
and 0x000 being 0.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: The gate cells are differently coded:
- 0x00F: The least significant nibble controls the gate length. - 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 - 0x0F0: The second nibble controls ratcheting, with 0x0F0 being one
gate per row, and 0x000 being 16 gates per row. gate per row, and 0x000 being 16 gates per row.
- 0xF00: The most significant nibble controls probability of the - 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 triggered, and 0x000 means that the gate is only triggered with
6% probability. 50% is 0x070. 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 - 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 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 the rows. This means if you want to drive an envelope
with release phase with this signal, you need to make with release phase with this signal, you need to make
space for the release phase. 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. - 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 You can use this to trigger other things once the
sequence has been played. 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) { fn reset(&mut self) {
self.backend = None; self.backend = None;
self.clock.reset(); self.time.clock.reset();
self.time.trigger.reset();
} }
#[inline] #[inline]
@ -148,8 +183,9 @@ impl DspNode for TSeq {
atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf], atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf],
outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) 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 clock = inp::TSeq::clock(inputs);
let trig = inp::TSeq::trig(inputs);
let cmode = at::TSeq::cmode(atoms); let cmode = at::TSeq::cmode(atoms);
let backend = let backend =
@ -163,13 +199,21 @@ impl DspNode for TSeq {
[0.0; MAX_BLOCK_SIZE]; [0.0; MAX_BLOCK_SIZE];
let cmode = cmode.i(); 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() { for frame in 0..ctx.nframes() {
if time.trigger.check_trigger(
denorm::TSeq::trig(trig, frame))
{
time.clock.sync();
}
let phase = let phase =
match cmode { match cmode {
0 => self.clock.next_phase(plen, clock.read(frame)) / plen, 0 => time.clock.next_phase(plen, clock.read(frame)) / plen,
1 => self.clock.next_phase(1.0, clock.read(frame)), 1 => time.clock.next_phase(1.0, clock.read(frame)),
2 | _ => (clock.read(frame).abs() as f64).fract(), 2 | _ => (clock.read(frame).abs() as f64).fract(),
}; };

View file

@ -36,9 +36,6 @@ impl SampleLibrary {
/// Returns an SAtom reference that you can clone and send directly /// Returns an SAtom reference that you can clone and send directly
/// to the sampling node of your choice. /// 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. /// 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> { pub fn load<'a>(&'a mut self, path: &str) -> Result<&'a SAtom, SampleLoadError> {
if self.loaded_samples.get(path).is_some() { if self.loaded_samples.get(path).is_some() {

View file

@ -747,10 +747,8 @@ fn check_matrix_tseq() {
.input(out.inp("ch1"), None, None)); .input(out.inp("ch1"), None, None));
matrix.sync().unwrap(); matrix.sync().unwrap();
let freq_param = sin.inp_param("freq").unwrap(); pset_n(&mut matrix, sin, "freq", -0.978);
matrix.set_param(freq_param, SAtom::param(-0.978)); pset_s(&mut matrix, tsq, "cmode", 1);
let cmode_param = tsq.inp_param("cmode").unwrap();
matrix.set_param(cmode_param, SAtom::setting(1));
let pat = matrix.get_pattern_data(0).unwrap(); let pat = matrix.get_pattern_data(0).unwrap();
{ {
@ -782,13 +780,13 @@ fn check_matrix_tseq() {
assert_float_eq!(samples[9], 0.42228); assert_float_eq!(samples[9], 0.42228);
// switch to row trigger: // 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); 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]); assert_vec_feq!(samples, vec![0.70411, 0.90413, 0.99306, 0.97972, 0.966387]);
// set to phase mode: // 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); let samples = run_and_undersample(&mut node_exec, 1000.0, 5);
assert_float_eq!(samples[0], 0.2491); assert_float_eq!(samples[0], 0.2491);
@ -798,6 +796,73 @@ fn check_matrix_tseq() {
assert_float_eq!(samples[4], 0.8104); 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] #[test]
fn check_matrix_tseq_gate() { fn check_matrix_tseq_gate() {
use hexodsp::dsp::tracker::UIPatternModel; use hexodsp::dsp::tracker::UIPatternModel;

View file

@ -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_export]
macro_rules! assert_rmsmima { macro_rules! assert_rmsmima {
($rms:expr, $b:expr) => { ($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)] #[allow(unused)]
pub fn pset_n(matrix: &mut Matrix, nid: NodeId, parm: &str, v_norm: f32) { pub fn pset_n(matrix: &mut Matrix, nid: NodeId, parm: &str, v_norm: f32) {
let p = nid.inp_param(parm).unwrap(); let p = nid.inp_param(parm).unwrap();

60
tests/node_allp.rs Normal file
View file

@ -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);
}

305
tests/node_delay.rs Normal file
View file

@ -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),
]);
}