diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 93d932d..5441e9f 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -30,6 +30,8 @@ mod node_map; mod node_smap; #[allow(non_upper_case_globals)] mod node_sfilter; +#[allow(non_upper_case_globals)] +mod node_mix3; pub mod tracker; mod satom; @@ -75,6 +77,7 @@ use node_noise::Noise; use node_map::Map; use node_smap::SMap; use node_sfilter::SFilter; +use node_mix3::Mix3; pub const MIDI_MAX_FREQ : f32 = 13289.75; @@ -484,6 +487,15 @@ macro_rules! node_list { (2 att n_att d_att r_id f_def stp_d 0.0, 1.0, 1.0) {3 0 neg_att setting(1) fa_amp_neg_att 0 1} [0 sig], + mix3 => Mix3 UIType::Generic UICategory::NtoM + (0 ch1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (1 ch2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (2 ch3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (3 gain1 n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) + (4 gain2 n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) + (5 gain3 n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) + (6 ogain n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) + [0 sig], smap => SMap UIType::Generic UICategory::CV (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 min n_id d_id r_s f_def stp_d -1.0, 1.0, -1.0) @@ -1033,6 +1045,19 @@ macro_rules! make_node_info_enum { } } + pub fn inp_name_by_idx(&self, idx: u8) -> Option<&'static str> { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match idx { + $($in_idx => Some(stringify!($para)),)* + $($in_at_idx => Some(stringify!($atom)),)* + _ => None, + } + }),+ + } + } + pub fn out_name_by_idx(&self, idx: u8) -> Option<&'static str> { match self { NodeId::$v1 => None, diff --git a/src/dsp/node_mix3.rs b/src/dsp/node_mix3.rs new file mode 100644 index 0000000..8334366 --- /dev/null +++ b/src/dsp/node_mix3.rs @@ -0,0 +1,98 @@ +// 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, NodeContext}; + +//#[macro_export] +//macro_rules! fa_amp_neg_att { ($formatter: expr, $v: expr, $denorm_v: expr) => { { +// let s = +// match ($v.round() as usize) { +// 0 => "Allow", +// 1 => "Clip", +// _ => "?", +// }; +// write!($formatter, "{}", s) +//} } } + +/// A simple amplifier +#[derive(Debug, Clone)] +pub struct Mix3 { +} + +impl Mix3 { + pub fn new(_nid: &NodeId) -> Self { + Self { + } + } + pub const ch1 : &'static str = + "Mix3 inp\nChannel 1 Signal input\nRange: (-1..1)\n"; + pub const ch2 : &'static str = + "Mix3 inp2\nChannel 2 Signal input\nRange: (-1..1)\n"; + pub const ch3 : &'static str = + "Mix3 inp3\nChannel 3 Signal input\nRange: (-1..1)\n"; + pub const gain1 : &'static str = + "Mix3 gain1\nChannel 1 gain\nRange: (0..1)"; + pub const gain2 : &'static str = + "Mix3 gain2\nChannel 2 gain\nRange: (0..1)"; + pub const gain3 : &'static str = + "Mix3 gain3\nChannel 3 gain\nRange: (0..1)"; + pub const ogain : &'static str = + "Mix3 ogain\nOutput gain of the sum\nRange: (0..1)"; + pub const sig : &'static str = + "Mix3 sig\nMixed signal output\nRange: (-1..1)\n"; + pub const DESC : &'static str = +r#"3 Ch. Signal Mixer + +A very simple 3 channel signal mixer. +You can mix anything, from audio signals to control signals. +"#; + pub const HELP : &'static str = +r#"Mix3 - 3 Channel Signal Mixer + +Just a small 3 channel mixer to create a sum of multiple signals. +You can mix anything, from audio signals to control signals. + +There is even a convenient output gain knob, +to turn down the output. +"#; + +} + +impl DspNode for Mix3 { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, _srate: f32) { } + fn reset(&mut self) { } + + #[inline] + fn process( + &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, + _nctx: &NodeContext, + _atoms: &[SAtom], inputs: &[ProcBuf], + outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + use crate::dsp::{out, inp, denorm}; + + let inp1 = inp::Mix3::ch1(inputs); + let inp2 = inp::Mix3::ch2(inputs); + let inp3 = inp::Mix3::ch3(inputs); + let g1 = inp::Mix3::gain1(inputs); + let g2 = inp::Mix3::gain2(inputs); + let g3 = inp::Mix3::gain3(inputs); + let og = inp::Mix3::ogain(inputs); + let out = out::Mix3::sig(outputs); + + for frame in 0..ctx.nframes() { + let sum = + inp1.read(frame) * denorm::Mix3::gain1(g1, frame) + + inp2.read(frame) * denorm::Mix3::gain2(g2, frame) + + inp3.read(frame) * denorm::Mix3::gain3(g3, frame); + out.write(frame, sum * denorm::Mix3::ogain(og, frame)); + } + + ctx_vals[0].set( + out.read(ctx.nframes() - 1)); + } +} diff --git a/src/matrix.rs b/src/matrix.rs index 428bd06..1fc3420 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -98,7 +98,7 @@ impl Cell { /// let repr = some_cell.to_repr(); /// assert_eq!( /// repr.serialize().to_string(), - /// "[\"sin\",0,0,0,[-1,0,0],[-1,0,0]]"); + /// "[\"sin\",0,0,0,[-1,\"freq\",\"freq\"],[-1,\"sig\",\"sig\"]]"); ///``` pub fn to_repr(&self) -> CellRepr { CellRepr { @@ -518,7 +518,7 @@ impl Matrix { /// /// let mut serialized = matrix.to_repr().serialize().to_string(); /// - /// assert!(serialized.find("\"sin\",2,0,0,[-1,-1,-1],[-1,0,-1]").is_some()); + /// assert!(serialized.find("\"sin\",2,0,0,[-1,-1,-1],[-1,\"sig\",-1]").is_some()); /// assert!(serialized.find("\"freq\",-0.100").is_some()); ///``` /// diff --git a/src/matrix_repr.rs b/src/matrix_repr.rs index 2c706a3..d2f91d2 100644 --- a/src/matrix_repr.rs +++ b/src/matrix_repr.rs @@ -27,6 +27,49 @@ fn deserialize_node_id(v: &Value, i1: usize, i2: usize) Ok(nid.to_instance(v[i2].as_i64().unwrap_or(0) as usize)) } +fn inp2idx(node_id: NodeId, v: &Value) -> i16 { + let inp = v.as_i64().unwrap_or(-2) as i16; + + if inp > -2 { inp } + else { + v.as_str() + .map(|s| node_id.inp(s).map(|i| i as i16).unwrap_or(-1)) + .unwrap_or(-1) + } +} + +fn out2idx(node_id: NodeId, v: &Value) -> i16 { + let out = v.as_i64().unwrap_or(-2) as i16; + + if out > -2 { out } + else { + v.as_str() + .map(|s| node_id.out(s).map(|i| i as i16).unwrap_or(-1)) + .unwrap_or(-1) + } +} + +fn inp_idx2value(node_id: NodeId, idx: i16) -> Value { + if idx == -1 { + json!(-1) + } else { + node_id.inp_name_by_idx(idx as u8) + .map(|n| json!(n)) + .unwrap_or_else(|| json!(-1)) + } +} + +fn out_idx2value(node_id: NodeId, idx: i16) -> Value { + if idx == -1 { + json!(-1) + } else { + node_id.out_name_by_idx(idx as u8) + .map(|n| json!(n)) + .unwrap_or_else(|| json!(-1)) + } +} + + impl CellRepr { pub fn serialize(&self) -> Value { json!([ @@ -34,25 +77,31 @@ impl CellRepr { self.node_id.instance(), self.x, self.y, - [self.inp[0], self.inp[1], self.inp[2]], - [self.out[0], self.out[1], self.out[2]], + [inp_idx2value(self.node_id, self.inp[0]), + inp_idx2value(self.node_id, self.inp[1]), + inp_idx2value(self.node_id, self.inp[2])], + [out_idx2value(self.node_id, self.out[0]), + out_idx2value(self.node_id, self.out[1]), + out_idx2value(self.node_id, self.out[2])], ]) } pub fn deserialize(v: &Value) -> Result { + let node_id = deserialize_node_id(v, 0, 1)?; + Ok(Self { - node_id: deserialize_node_id(v, 0, 1)?, + node_id, x: v[2].as_i64().unwrap_or(0) as usize, y: v[3].as_i64().unwrap_or(0) as usize, inp: [ - v[4][0].as_i64().unwrap_or(-1) as i16, - v[4][1].as_i64().unwrap_or(-1) as i16, - v[4][2].as_i64().unwrap_or(-1) as i16, + inp2idx(node_id, &v[4][0]), + inp2idx(node_id, &v[4][1]), + inp2idx(node_id, &v[4][2]) ], out: [ - v[5][0].as_i64().unwrap_or(-1) as i16, - v[5][1].as_i64().unwrap_or(-1) as i16, - v[5][2].as_i64().unwrap_or(-1) as i16, + out2idx(node_id, &v[5][0]), + out2idx(node_id, &v[5][1]), + out2idx(node_id, &v[5][2]) ], }) } @@ -499,8 +548,7 @@ mod tests { let s = mr.serialize(); assert_eq!(s, - "{\"VERSION\":1,\"atoms\":[[\"out\",0,\"mono\",[\"i\",0]]],\"cells\":[[\"sin\",2,0,0,[-1,-1,-1],[-1,0,-1]],[\"out\",0,1,0,[-1,0,-1],[-1,-1,0]]],\"params\":[[\"out\",0,\"ch1\",0.0],[\"out\",0,\"ch2\",0.0],[\"sin\",0,\"det\",0.0],[\"sin\",1,\"det\",0.0],[\"sin\",2,\"det\",0.0],[\"sin\",0,\"freq\",0.0],[\"sin\",1,\"freq\",0.0],[\"sin\",2,\"freq\",-0.10000000149011612],[\"out\",0,\"gain\",0.5]],\"patterns\":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],\"props\":[]}"); - + "{\"VERSION\":1,\"atoms\":[[\"out\",0,\"mono\",[\"i\",0]]],\"cells\":[[\"sin\",2,0,0,[-1,-1,-1],[-1,\"sig\",-1]],[\"out\",0,1,0,[-1,\"ch1\",-1],[-1,-1,-1]]],\"params\":[[\"out\",0,\"ch1\",0.0],[\"out\",0,\"ch2\",0.0],[\"sin\",0,\"det\",0.0],[\"sin\",1,\"det\",0.0],[\"sin\",2,\"det\",0.0],[\"sin\",0,\"freq\",0.0],[\"sin\",1,\"freq\",0.0],[\"sin\",2,\"freq\",-0.10000000149011612],[\"out\",0,\"gain\",0.5]],\"patterns\":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],\"props\":[]}"); let mut mr2 = MatrixRepr::deserialize(&s).unwrap(); let s2 = mr2.serialize(); @@ -546,7 +594,7 @@ mod tests { fn check_cell_repr() { let cell = Cell::empty(NodeId::Out(2)) - .input(Some(2), Some(0), Some(3)) + .input(Some(2), Some(0), Some(1)) .out(Some(11), Some(4), Some(1)); let cr = cell.to_repr(); @@ -747,4 +795,44 @@ mod tests { assert_eq!(matrix.get_prop("test").unwrap().i(), 31337); } } + + #[test] + fn check_matrix_repr_old_format2new() { + let old_format = + "{\"VERSION\":1,\"atoms\":[[\"out\",0,\"mono\",[\"i\",0]]],\ + \"cells\":[[\"sin\",2,0,0,[-1,-1,-1],[-1,0,-1]],[\"out\",0,1,0,\ + [-1,0,-1],[-1,-1,0]]],\"params\":[[\"out\",0,\"ch1\",0.0],\ + [\"out\",0,\"ch2\",0.0],[\"sin\",0,\"det\",0.0],[\"sin\",1,\ + \"det\",0.0],[\"sin\",2,\"det\",0.0],[\"sin\",0,\"freq\",0.0],\ + [\"sin\",1,\"freq\",0.0],[\"sin\",2,\"freq\",\ + -0.10000000149011612],[\"out\",0,\"gain\",0.5]],\"patterns\":\ + [null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null],\"props\":[]}"; + + let mut mr = MatrixRepr::deserialize(old_format).unwrap(); + let new_format = mr.serialize().to_string(); + + assert_eq!(new_format, + "{\"VERSION\":1,\"atoms\":[[\"out\",0,\"mono\",[\"i\",0]]],\"cells\":[[\"sin\",2,0,\ + 0,[-1,-1,-1],[-1,\"sig\",-1]],[\"out\",0,1,0,[-1,\"ch1\",-1],[-1,-1,-1]]],\"params\":[[\"o\ + ut\",0,\"ch1\",0.0],[\"out\",0,\"ch2\",0.0],[\"sin\",0,\"det\",0.0],[\"sin\",1,\"det\",0.0],\ + [\"sin\",2,\"det\",0.0],[\"sin\",0,\"freq\",0.0],[\"sin\",1,\"freq\",0.0],[\"sin\",2,\"freq\ + \",-0.10000000149011612],[\"out\",0,\"gain\",0.5]],\"patterns\":[null,null,null,null,null,nul\ + l,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,nu\ + ll,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null\ + ,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,n\ + ull,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,nul\ + l,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,\ + null,null,null,null,null,null,null,null,null,null,null,null],\"props\":[]}"); + } } diff --git a/tests/node_mix3.rs b/tests/node_mix3.rs new file mode 100644 index 0000000..f9762de --- /dev/null +++ b/tests/node_mix3.rs @@ -0,0 +1,396 @@ +mod common; +use common::*; + +#[test] +fn check_node_ad_1() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(ad) + .out(None, None, ad.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let trig_p = ad.inp_param("trig").unwrap(); + + matrix.set_param(trig_p, SAtom::param(1.0)); + let res = run_for_ms(&mut node_exec, 25.0); + assert_decimated_slope_feq!(res.0, 50, vec![ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + // 44.1 per ms, attack is default 3.0ms (roughly 3 * 50 samples): + 0.007558584, 0.007558584, 0.007558584, + // 44.1 per ms, decay is default 10.0ms (=> roughly 9 * 50 samples): + -0.002267599, -0.0022675395, -0.002267599, -0.0022675395, + -0.0022675693, -0.0022675693, -0.0022675842, -0.0022675693, + -0.0022675726, + 0.0, 0.0, 0.0, 0.0 + ]); + + matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 10.0); + matrix.set_param(trig_p, SAtom::param(1.0)); + let res = run_for_ms(&mut node_exec, 25.0); + //d// println!("RES: {:?}", res); + let start = res.0[330]; + assert_float_eq!(start, 0.0075585); + let peak = res.0[330 + ((44.1_f64 * 3.0).floor() as usize)]; + assert_float_eq!(peak, 1.0); +} + +#[test] +fn check_node_ad_retrig() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let test = NodeId::Test(0); + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(test) + .out(None, None, test.out("sig"))); + matrix.place(0, 1, Cell::empty(ad) + .input(ad.inp("trig"), None, None) + .out(None, None, ad.out("sig"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let trig_p = test.inp_param("p").unwrap(); + + matrix.set_param(trig_p, SAtom::param(1.0)); + let res = run_for_ms(&mut node_exec, 25.0); + assert_decimated_slope_feq!(res.0, 50, vec![ + // XXX: Direct trigger! + // Due to Test node outputting an unsmoothed value! + + // 44.1 per ms, attack is default 3.0ms (roughly 3 * 50 samples): + 0.007558584, 0.007558584, 0.007558584, + // 44.1 per ms, decay is default 10.0ms (=> roughly 9 * 50 samples): + -0.002267599, -0.0022675395, -0.002267599, -0.0022675395, + -0.0022675693, -0.0022675693, -0.0022675842, -0.0022675693, + -0.0022675726, + 0.0, 0.0, 0.0, 0.0 + ]); + + + matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 0.1); + matrix.set_param(trig_p, SAtom::param(1.0)); + let res = run_for_ms(&mut node_exec, 1.5); + assert_decimated_feq!(res.0, 2, vec![ + 0.0075585, 0.022675736, 0.03779289, 0.05291005, 0.068027206, 0.08314436, + 0.09826152, 0.113378674, 0.12849583, 0.143613, 0.15873015, + 0.1738473, 0.18896446, 0.20408161, 0.21919878, 0.23431593, + 0.24943309, 0.26455024, 0.2796674, 0.29478455, 0.3099017, + 0.32501888, 0.34013602, 0.3552532, 0.37037033, 0.3854875, + 0.40060467, 0.4157218, 0.43083897, 0.4459561, 0.46107328, + 0.47619045, 0.4913076 + ]); + + // Reset trigger + matrix.set_param(trig_p, SAtom::param(0.0)); + let res = run_for_ms(&mut node_exec, 0.1); + assert_slope_feq!(res.0, vec![0.00755; 3]); + + // Retrigger attack (should do nothing) + matrix.set_param(trig_p, SAtom::param(1.0)); + let res = run_for_ms(&mut node_exec, 0.1); + assert_slope_feq!(res.0, vec![0.00755; 7]); + + // Wait into decay phase + matrix.set_param(trig_p, SAtom::param(0.0)); + let res = run_for_ms(&mut node_exec, 1.4); + let mut v = vec![0.00755; 57]; + v.append(&mut vec![0.002267, -0.002267, -0.002267]); + assert_slope_feq!(res.0, v); + + // Decay some more + let res = run_for_ms(&mut node_exec, 0.8); + assert_slope_feq!(res.0, vec![-0.002267; 100]); + + // Retrigger right in the decay phase + matrix.set_param(trig_p, SAtom::param(1.0)); + let res = run_for_ms(&mut node_exec, 1.0); + assert_slope_feq!(res.0, vec![ + // Re-attack until we are at 1.0 again + 0.007558584, 0.007558584, 0.007558584, 0.0075585246, 0.007558584, + 0.007558584, 0.007558584, 0.007558584, 0.007558584, 0.007558584, + 0.0007558465, + // Restart decay after 1.0 was reached: + -0.002267599, -0.0022675395, -0.002267599, + -0.0022675395, -0.002267599, -0.0022675395, -0.002267599, + -0.002267599, -0.0022675395, -0.002267599, -0.0022675395, + -0.002267599, -0.0022675395, -0.002267599, -0.002267599, + -0.0022675395, -0.002267599, -0.0022675395, -0.002267599, + -0.0022675395, -0.002267599, -0.002267599, -0.0022675395, + -0.002267599, -0.0022675395, -0.002267599, -0.0022675395, + -0.002267599, -0.002267599, -0.0022675395, -0.002267599, -0.0022675395 + ]); +} + +#[test] +fn check_node_ad_inp_sin() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let ad = NodeId::Ad(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(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let trig_p = ad.inp_param("trig").unwrap(); + let atk_p = ad.inp_param("atk").unwrap(); + let dcy_p = ad.inp_param("dcy").unwrap(); + + // check if we have any frequencies resembling 440Hz + matrix.set_param(trig_p, SAtom::param(1.0)); + run_for_ms(&mut node_exec, 7.0); + + let fft = run_and_get_fft4096_now(&mut node_exec, 6); + assert_eq!(fft[0], (420, 6)); + assert_eq!(fft[1], (431, 6)); + assert_eq!(fft[2], (441, 6)); + assert_eq!(fft[3], (452, 6)); + assert_eq!(fft[4], (463, 6)); + + // Next we test if lengthening the attack has + // effect on the captured frequencies. + matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 8.0); + + matrix.set_param(atk_p, SAtom::param(atk_p.norm(40.0))); + matrix.set_param(trig_p, SAtom::param(1.0)); + run_for_ms(&mut node_exec, 7.0); + + let fft = run_and_get_fft4096_now(&mut node_exec, 300); + assert_eq!(fft[0], (431, 322)); + assert_eq!(fft[1], (441, 360)); + + matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 8.0); + + // Next we test if lengthening the decay too has + // effect on the captured frequencies. + matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 8.0); + + matrix.set_param(dcy_p, SAtom::param(dcy_p.norm(40.0))); + matrix.set_param(trig_p, SAtom::param(1.0)); + run_for_ms(&mut node_exec, 7.0); + + let fft = run_and_get_fft4096_now(&mut node_exec, 300); + assert_eq!(fft[0], (431, 489)); + assert_eq!(fft[1], (441, 647)); + assert_eq!(fft[2], (452, 398)); + + matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 8.0); +} + +#[test] +fn check_node_ad_shp_log() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(ad) + .out(None, None, ad.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, ad, "trig", 1.0); + pset_n(&mut matrix, ad, "ashp", 1.0); + pset_n(&mut matrix, ad, "dshp", 1.0); + + let res = run_for_ms(&mut node_exec, 25.0); + assert_decimated_slope_feq!(res.0, 50, vec![ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + // 44.1 per ms, attack is default 3.0ms (roughly 3 * 50 samples): + 0.009243369, 0.003936231, 0.0020142794, + // 44.1 per ms, decay is default 10.0ms (=> roughly 9 * 50 samples): + -0.0006071329, -0.00067061186, -0.000752151, -0.0008612871, + -0.0010163188, -0.0012571216, -0.0016934872, -0.0027971864, + -0.027684271, + 0.0, 0.0, 0.0, 0.0 + ]); +} + +#[test] +fn check_node_ad_shp_exp() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(ad) + .out(None, None, ad.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, ad, "trig", 1.0); + pset_n(&mut matrix, ad, "ashp", 0.0); + pset_n(&mut matrix, ad, "dshp", 0.0); + + let res = run_for_ms(&mut node_exec, 25.0); + assert_decimated_slope_feq!(res.0, 50, vec![ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + // 44.1 per ms, attack is default 3.0ms (roughly 3 * 50 samples): + 0.0009576017, 0.0044435486, 0.023418367, + // 44.1 per ms, decay is default 10.0ms (=> roughly 9 * 50 samples): + -0.0068960786, -0.004632175, -0.002927363, -0.0017025322, + -0.00087817386, -0.0003750762, -0.000113890506, -0.0000153047, + -0.0000000017186217, + 0.0, 0.0, 0.0, 0.0 + ]); +} + +#[test] +fn check_node_ad_eoet() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(ad) + .out(None, None, ad.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.place(1, 0, Cell::empty(ad) + .out(None, None, ad.out("eoet"))); + matrix.place(1, 1, Cell::empty(out) + .input(out.inp("ch2"), None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, ad, "trig", 1.0); + let res = run_for_ms(&mut node_exec, 25.0); + // just make sure we are running an env: + assert_decimated_slope_feq!(res.0, 50, vec![ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + // 44.1 per ms, attack is default 3.0ms (roughly 3 * 50 samples): + 0.007558584, 0.007558584, 0.007558584, + // 44.1 per ms, decay is default 10.0ms (=> roughly 9 * 50 samples): + -0.002267599, -0.0022675395, -0.002267599, -0.0022675395, + -0.0022675693, -0.0022675693, -0.0022675842, -0.0022675693, + -0.0022675726, + 0.0, // <- EOET expected here + 0.0, 0.0, 0.0 + ]); + + // check if trigger appears: + assert_decimated_feq!(res.1, 50, vec![ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + // 44.1 per ms, attack is default 3.0ms (roughly 3 * 50 samples): + 0.0, 0.0, 0.0, + // 44.1 per ms, decay is default 10.0ms (=> roughly 9 * 50 samples): + 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, + 0.0, + 1.0, // <- End of envelope! + 0.0, 0.0, 0.0 + ]); +} + + +#[test] +fn check_node_ad_atk_dcy() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let test = NodeId::Test(0); + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(test) + .out(None, None, test.out("sig"))); + matrix.place(0, 1, Cell::empty(ad) + .input(ad.inp("trig"), None, None) + .out(None, None, ad.out("sig"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + pset_d(&mut matrix, ad, "atk", 20.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); + let res = run_for_ms(&mut node_exec, 10.0); + assert_decimated_slope_feq!(res.0, 10, vec![0.001133787; 50]); + + pset_d(&mut matrix, ad, "atk", 50.0); + let res = run_for_ms(&mut node_exec, 20.0); + assert_decimated_slope_feq!(res.0, 40, vec![ + // Slope is getting less and less steep, as expected: + 0.0011277795, 0.0010179877, 0.00092345476, 0.00084143877, + 0.0007699132, 0.0007072091, 0.0006517768, 0.00060266256, + 0.00055885315, 0.0005196929, 0.00048446655, 0.00045353174, + // Slope does not change after the "atk" change has been smoothed + 0.00045353174, 0.00045353174, 0.00045353174, 0.00045347214, + 0.00045353174, 0.00045353174, 0.00045353174, 0.00045353174, + 0.00045347214, 0.00045353174, + // attack phase ended, and now we decay: + -0.002267599 + ]); + + // check if decay stays stable: + let res = run_for_ms(&mut node_exec, 2.0); + assert_decimated_slope_feq!(res.0, 40, vec![-0.002267599; 3]); + + pset_d(&mut matrix, ad, "dcy", 200.0); + let res = run_for_ms(&mut node_exec, 20.0); + assert_decimated_slope_feq!(res.0, 40, vec![ + // Slope is getting less and less steep, as expected: + -0.002197802, -0.0012806058, -0.00083732605, -0.0005899668, + -0.00043797493, -0.00033789873, -0.00026863813, -0.00021868944, + -0.00018143654, -0.00015294552, -0.00013071299, + // Slope does not change after the "dcy" change has been smoothed + -0.000113368034, -0.000113368034, -0.00011339784, -0.00011339784, + -0.000113368034, -0.000113368034, -0.00011339784, -0.000113368034, + -0.000113368034, -0.00011339784, -0.000113368034, -0.000113368034 + ]); +} + +#[test] +fn check_node_ad_mult() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(ad) + .out(None, None, ad.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + pset_n(&mut matrix, ad, "trig", 1.0); + pset_n(&mut matrix, ad, "mult", 2.0); + let res = run_for_ms(&mut node_exec, 2000.0); + assert_decimated_slope_feq_fine!(res.0, 1800, vec![ + 0.0, + // looong attack: + 0.00007558, 0.00007558, 0.00007558, 0.00007558, + 0.00007558, 0.00007558, 0.00007558, + // looong decay: + -0.000022709, -0.000022709, -0.000022709, -0.000022709, -0.000022709, + -0.000022709, -0.000022709, -0.000022709, -0.000022709, -0.000022709, + -0.000022709, -0.000022709, -0.000022709, -0.000022709, -0.000022709, + -0.000022709, -0.000022709, -0.000022709, -0.000022709, -0.000022709, + -0.000022709, -0.000022709, -0.000022709, -0.000022709, -0.000022709, + 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, + ]); +} +