diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index ca9174b..6391a91 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -347,6 +347,9 @@ define_exp!{n_declick d_declick 0.0, 50.0} define_exp!{n_env d_env 0.0, 1000.0} +// Special linear gain factor for the Out node, to be able +// to reach more exact "1.0". +define_lin!{n_ogin d_ogin 0.0, 2.0} // A note about the input-indicies: // @@ -412,7 +415,7 @@ macro_rules! node_list { out => Out UIType::Generic UICategory::IOUtil (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 gain n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) + (2 gain n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) // node_param_idx // | atom_idx format fun // | | name constructor| min max @@ -426,12 +429,14 @@ macro_rules! node_list { [0 sig], ad => Ad UIType::Generic UICategory::CV (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) - (1 atk n_env d_env r_ems f_ms stp_m 0.0, 1.0, 3.0) - (2 dcy n_env d_env r_ems f_ms stp_m 0.0, 1.0, 10.0) - (3 ashp n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) - (4 dshp n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) - {5 0 mult setting(0) fa_ad_mult 0 2} - [0 sig], + (1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (2 atk n_env d_env r_ems f_ms stp_m 0.0, 1.0, 3.0) + (3 dcy n_env d_env r_ems f_ms stp_m 0.0, 1.0, 10.0) + (4 ashp n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) + (5 dshp n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) + {6 0 mult setting(0) fa_ad_mult 0 2} + [0 sig] + [1 eoet], 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 s setting(0) fa_test_s 0 10}, diff --git a/src/dsp/node_ad.rs b/src/dsp/node_ad.rs index ffd26e0..4991a2a 100644 --- a/src/dsp/node_ad.rs +++ b/src/dsp/node_ad.rs @@ -4,6 +4,7 @@ use crate::nodes::{NodeAudioContext, NodeExecContext}; use crate::dsp::{NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals}; +use super::helpers::{Trigger, TrigSignal, sqrt4_to_pow4}; #[macro_export] macro_rules! fa_ad_mult { ($formatter: expr, $v: expr, $denorm_v: expr) => { { @@ -22,25 +23,33 @@ const AD_STAGES : i8 = 2; /// A simple amplifier #[derive(Debug, Clone)] pub struct Ad { - srate: f64, - phase: f64, - last_value: f32, - stage: i8, + inc: f64, + stage: u8, + samples_ms: f64, + value: f64, + last_time: f32, + trig: Trigger, + trig_sig: TrigSignal, } impl Ad { pub fn new(_nid: &NodeId) -> Self { Self { - srate: 44100.0, - phase: 0.0, - last_value: 0.0, - stage: -1, + inc: 0.0, + stage: 0, + samples_ms: 44.1, + value: 0.0, + last_time: -1.0, + trig: Trigger::new(), + trig_sig: TrigSignal::new(), } } pub const inp : &'static str = "Ad inp\nSignal input. If you don't connect this, and set this to 1.0 \ this will act as envelope signal generator. But you can also just \ route a signal directly through this of course.\nRange: (-1..1)\n"; + pub const trig : &'static str = + "Ad trig\nTrigger input that starts the attack phase.\nRange: (0..1)\n"; pub const atk : &'static str = "Ad atk\nAttack time of the envelope. You can extend the maximum \ range of this with the 'mult' setting.\nRange: (0..1)\n"; @@ -63,102 +72,49 @@ impl Ad { you will receive an attenuated signal here. If you set 'inp' to a \ fixed value (for instance 1.0), this will output an envelope signal \ in the range 0.0 to 'inp' (1.0).\nRange: (-1..1)\n"; + pub const eoet : &'static str = + "Ad eoet\nEnd of envelope trigger. This output sends a trigger once \ + the end of the decay stage has been reached.\nRange: (0..1)"; pub const DESC : &'static str = r#"Attack-Decay Envelope -This is a simple envelope offering an attack time and decay time with shape parameter. +This is a simple envelope offering an attack time and decay time with a shape parameter. +You can use it as envelope generator to modulate other inputs or process a signal with it directly. "#; pub const HELP : &'static str = r#"Ad - Attack-Decay Envelope +This simple two stage envelope with attack and decay offers shape parameters +for each stage. The attack and decay times can be extended using the 'mult' +setting. + +The 'inp' can either be used to process a signal, or set the target output +value of the envelope. In the latter case this node is just a simple +envelope generator, with which you can generate control signals to modulate +other inputs. + +With the 'eoet' output you can either trigger other envelopes or via +'FbWr'/'FbRd' retrigger the envelope. "#; } -/* - struct { - srate_per_ms: f64, - value : f64 = 0.0; - inc : f64 = 0.0; - stage = 0; - last_time = 0.0; - target : f64 = 1.0; - shape = 0.5; - } - set_sample_rate(srate) { self.srate_per_ms = srate / 1000.0 } - - // block start: - let mut shape_src = - match stage { - 2 => dcy_shape, - _ => atk_shape, - }; - let mut inc_time_src = - match stage { - 2 => dcy, - _ => atk, - }; - let mut mult : f64 = - if mult == 1 { 10.0 } else if mult == 2 {100.0 } else { 1.0}; - - // each frame: - if stage == 0 { - if trigger(trig_in) { - value = 0.0; - - // transition to stage 1 (attack): - stage = 1; - target = 1.0; - shape_src = atk_shape; - inc_time_src = atk; - last_time = -1.0; - } - } - - let cur_time = denorm(inc_time_src); - if last_time != cur_time { - inc = - (target - value) - / ((cur_time as f64) * mult * srate_per_ms); - } - - value += inc; - shape = read(frame, shape_src).clamp(0.0, 1.0); - - match stage { - 1 => { - if value >= target { - // transition to stage 2 (decay): - stage = 2; - target = 0.0; - shape_src = dcy_shape; - inc_time_src = dcy; - last_time = -1.0; - } - }, - 2 => { - if value <= target { - stage = 0; - eov_trigger.trigger(); - } - }, - _ => {}, - } - - let in_val = inp.read(frame); - out.write( - frame, - in_val - * sqrt4_to_pow4( - value.clamp(0.0, 1.0) as f32, shape)); - trig.write(frame, eov_trigger.next()); -*/ - impl DspNode for Ad { fn outputs() -> usize { 1 } - fn set_sample_rate(&mut self, _srate: f32) { } - fn reset(&mut self) { } + fn set_sample_rate(&mut self, srate: f32) { + self.samples_ms = srate as f64 / 1000.0; + self.trig_sig.set_sample_rate(srate); + } + + fn reset(&mut self) { + self.stage = 0; + self.value = 0.0; + self.inc = 0.0; + self.last_time = -1.0; + self.trig_sig.reset(); + self.trig.reset(); + } #[inline] fn process( @@ -168,15 +124,98 @@ impl DspNode for Ad { { use crate::dsp::{out, inp, denorm, denorm_v, inp_dir, at}; - let out = out::Ad::sig(outputs); + let inp = inp::Ad::inp(inputs); + let trig = inp::Ad::trig(inputs); + let atk = inp::Ad::atk(inputs); + let dcy = inp::Ad::dcy(inputs); + let atk_shape = inp::Ad::ashp(inputs); + let dcy_shape = inp::Ad::dshp(inputs); + let mult = at::Ad::mult(atoms); - let last_frame = ctx.nframes() - 1; + // block start: + let (mut shape_src, mut inc_time_src, mut target) = + match self.stage { + 1 => (atk_shape, atk, 1.0), + 2 => (dcy_shape, dcy, 0.0), + _ => (atk_shape, atk, 0.0), + }; + let mut mult : f64 = + match mult.i() { + 1 => 10.0, + 2 => 100.0, + _ => 1.0, + }; for frame in 0..ctx.nframes() { - out.write(frame, 0.0); + // each frame: + let is_triggered = + self.trig.check_trigger(denorm::Ad::trig(trig, frame)); + + if self.stage == 0 && is_triggered { + // transition to stage 1 (attack): + self.stage = 1; + self.last_time = -1.0; + target = 1.0; + shape_src = atk_shape; + inc_time_src = atk; + } + + let cur_time = denorm::Ad::atk(inc_time_src, frame); + if self.last_time != cur_time { + self.inc = + if cur_time <= 0.0001 { + target - self.value + } else { + (target - self.value) + / ((cur_time as f64) * mult * self.samples_ms) + }; + self.last_time = cur_time; + } + + self.value += self.inc; + let shape = + denorm::Ad::ashp(shape_src, frame) + .clamp(0.0, 1.0); + + match self.stage { + 1 => { + if self.value >= target { + self.stage = 2; + self.last_time = -1.0; + self.value = target; + target = 0.0; + shape_src = dcy_shape; + inc_time_src = dcy; + } + }, + 2 => { + if self.value <= target { + self.stage = 0; + self.last_time = -1.0; + self.value = target; + target = 0.0; + self.trig_sig.trigger(); + } + }, + _ => {}, + } + + let in_val = denorm::Ad::inp(inp, frame); + let out = out::Ad::sig(outputs); + //d// println!("VAL in={}, val={} shp: {}=>{}", in_val, self.value, shape, + //d// sqrt4_to_pow4(1.0, shape)); + out.write( + frame, + in_val + * sqrt4_to_pow4( + self.value.clamp(0.0, 1.0) as f32, + shape)); + + let eoet = out::Ad::eoet(outputs); + eoet.write(frame, self.trig_sig.next()); } - ctx_vals[0].set(0.0); + ctx_vals[0].set(self.value as f32); // ctx_vals[1].set(self.phase / self. + self.stage * ); } } diff --git a/src/dsp/node_out.rs b/src/dsp/node_out.rs index 4870b8c..18625bc 100644 --- a/src/dsp/node_out.rs +++ b/src/dsp/node_out.rs @@ -34,7 +34,9 @@ impl Out { pub const mono : &'static str = "Out mono\nIf set to 'Mono', ch1 will be sent to both output channels.\n(UI only)"; pub const gain : &'static str = - "Out gain\nThe main gain of the synthesizer output, applied to all channels.\nRange: (0..1)"; + "Out gain\nThe main gain of the synthesizer output, applied to all channels. \ + Please note that this is a linear control, to prevent inaccuracies for 1.0. \ + \nRange: (0..1)"; pub const ch1 : &'static str = "Out ch1\nAudio channel 1 (left)\nRange: (-1..1)"; pub const ch2 : &'static str = diff --git a/src/matrix_repr.rs b/src/matrix_repr.rs index 011ea5a..9616e4b 100644 --- a/src/matrix_repr.rs +++ b/src/matrix_repr.rs @@ -469,7 +469,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.7071067690849304]],\"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]}"); + "{\"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]}"); let mut mr2 = MatrixRepr::deserialize(&s).unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e673552..54c110e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -79,6 +79,35 @@ assertion failed: `(left[{}] == right[{}])` } } +#[macro_export] +macro_rules! assert_decimated_slope_feq { + ($vec:expr, $decimate:expr, $cmp_vec:expr) => { + let cmp_vec = $cmp_vec; + let mut res : Vec = vec![]; + let mut prev = 0.0; + for s in $vec.iter() { + let delta = *s - prev; + res.push(delta); + prev = *s; + } + + let res : Vec = res.iter().step_by($decimate).copied().collect(); + + for (i, (s, scmp)) in res.iter().zip(cmp_vec.iter()).enumerate() { + if (s - scmp).abs() > 0.0001 { + panic!(r#" +table_left: {:?} + +table_right: {:?} + +assertion failed: `(left[{}] == right[{}])` + left: `{:?}`, + right: `{:?}`"#, &res[i..], &(cmp_vec[i..]), i, i, s, scmp) + } + } + } +} + #[macro_export] macro_rules! assert_rmsmima { ($rms:expr, $b:expr) => { diff --git a/tests/node_ad.rs b/tests/node_ad.rs new file mode 100644 index 0000000..b48821c --- /dev/null +++ b/tests/node_ad.rs @@ -0,0 +1,41 @@ +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); +}