diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 528f31a..498df89 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -280,7 +280,7 @@ macro_rules! node_list { (3 len n_id n_id 0.0, 1.0, 1.0) {4 0 sample audio_unloaded("") 0 0} {5 1 pmode setting(0) 0 1} - {6 2 dclick setting(1) 0 1} + {6 2 dclick setting(0) 0 1} [0 sig], sin => Sin UIType::Generic UICategory::Osc (0 freq n_pit d_pit -1.0, 1.0, 440.0) diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index 0495eec..85a57b6 100644 --- a/src/dsp/node_sampl.rs +++ b/src/dsp/node_sampl.rs @@ -7,6 +7,8 @@ use crate::dsp::{SAtom, ProcBuf, DspNode, LedPhaseVals}; use crate::dsp::{out, at, inp, denorm}; //, inp, denorm, denorm_v, inp_dir, at}; use super::helpers::Trigger; +const RAMP_TIME_MS : f64 = 3.14; + /// A simple amplifier #[derive(Debug, Clone)] pub struct Sampl { @@ -90,41 +92,10 @@ impl Sampl { (((a * f) - b_neg) * f + c) * f + x0 } -// #[inline] -// fn play_loop(&mut self, inputs: &[ProcBuf], nframes: usize, sample_data: &[f32], out: &mut ProcBuf) { -// let freq = inp::Sampl::freq(inputs); -// let offs = inp::Sampl::offs(inputs); -// let len = inp::Sampl::len(inputs); -// -// let sample_srate = sample_data[0] as f64; -// let sample_data = &sample_data[1..]; -// let sr_factor = sample_srate / self.srate; -// -// for frame in 0..nframes { -// let playback_speed = -// denorm::Sampl::freq(freq, frame) / 440.0; -// -// let start_idx = -// (sample_data.len() as f32 -// * denorm::Sampl::offs(offs, frame).abs().min(0.999999)) -// .floor() as usize; -// -// let end_idx_plus1 = -// ((sample_data.len() - start_idx) as f32 -// * denorm::Sampl::len(offs, frame).abs().min(0.999999)) -// .ceil() as usize; -// -// out.write(frame, -// self.next_sample( -// sr_factor, -// playback_speed as f64, -// sample_data[start_idx..end_idx_plus1])); -// } -// } -// #[inline] fn play(&mut self, inputs: &[ProcBuf], nframes: usize, - sample_data: &[f32], out: &mut ProcBuf, do_loop: bool) + sample_data: &[f32], out: &mut ProcBuf, do_loop: bool, + declick: bool) { let freq = inp::Sampl::freq(inputs); let trig = inp::Sampl::trig(inputs); @@ -135,6 +106,9 @@ impl Sampl { let sample_data = &sample_data[1..]; let sr_factor = sample_srate / self.srate; + let ramp_sample_count = ((RAMP_TIME_MS * self.srate) / 1000.0).ceil() as usize; + let ramp_inc = 1000.0 / (RAMP_TIME_MS * self.srate); + let mut is_playing = self.is_playing; if do_loop { @@ -163,8 +137,7 @@ impl Sampl { let prev_phase = self.phase; let cur_offs = - denorm::Sampl::offs(offs, frame) - .abs().min(0.999999); + denorm::Sampl::offs(offs, frame).abs().min(0.999999); if prev_offs != cur_offs { start_idx = (sample_data.len() as f32 * cur_offs) @@ -173,20 +146,41 @@ impl Sampl { } let cur_len = - denorm::Sampl::len(len, frame) - .abs().min(0.999999); + denorm::Sampl::len(len, frame).abs().min(0.999999); if prev_len != cur_len { end_idx_plus1 = - ((sample_data.len() - start_idx) as f32 - * denorm::Sampl::len(len, frame).abs().min(0.999999)) + ((sample_data.len() - start_idx) as f32 * cur_len) .ceil() as usize; prev_len = cur_len; } - let s = self.next_sample( - sr_factor, - playback_speed as f64, - &sample_data[start_idx..(start_idx + end_idx_plus1)]); + let sample_slice = + &sample_data[start_idx..(start_idx + end_idx_plus1)]; + + // next_sample mutates self.phase, so we need the current phase + // that is used for looking up the sample from the audio data. + let sample_idx = self.phase.floor() as usize; + + let mut s = + self.next_sample( + sr_factor, + playback_speed as f64, + sample_slice); + + if declick { + let samples_to_end = sample_data.len() - sample_idx; + + let ramp_atten_factor = + if sample_idx < ramp_sample_count { + sample_idx as f64 * ramp_inc + } else if samples_to_end < ramp_sample_count { + 1.0 - (samples_to_end as f64 * ramp_inc) + } else { + 1.0 + }; + + s *= ramp_atten_factor as f32; + } out.write(frame, s); @@ -219,6 +213,7 @@ impl DspNode for Sampl { { let sample = at::Sampl::sample(atoms); let pmode = at::Sampl::pmode(atoms); + let dclick = at::Sampl::dclick(atoms); let out = out::Sampl::sig(outputs); if let SAtom::AudioSample((_, Some(sample_data))) = sample { @@ -235,7 +230,8 @@ impl DspNode for Sampl { ctx.nframes(), &sample_data[..], out, - pmode.i() == 0); + pmode.i() == 0, + dclick.i() == 1); } else { for frame in 0..ctx.nframes() { out.write(frame, 0.0); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d17d8a9..51e1cf5 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,6 +7,7 @@ use hound; use microfft; pub const SAMPLE_RATE : f32 = 44100.0; +#[allow(dead_code)] pub const SAMPLE_RATE_US : usize = 44100; #[macro_export] @@ -20,6 +21,59 @@ macro_rules! assert_float_eq { } } +#[macro_export] +macro_rules! assert_fpair_eq { + ($a:expr, $b:expr) => { + if ($a.0 - $b.0).abs() > 0.0001 { + panic!(r#"assertion failed: `(left.0 == right.0)` + left: `{:?}`, + right: `{:?}`"#, $a.0, $b.0) + } + if ($a.1 - $b.1).abs() > 0.0001 { + panic!(r#"assertion failed: `(left.1 == right.1)` + left: `{:?}`, + right: `{:?}`"#, $a.1, $b.1) + } + } +} + +#[macro_export] +macro_rules! assert_f3tupl_eq { + ($a:expr, $b:expr) => { + if ($a.0 - $b.0).abs() > 0.0001 { + panic!(r#"assertion failed: `(left.0 == right.0)` + left: `{:?}`, + right: `{:?}`"#, $a.0, $b.0) + } + if ($a.1 - $b.1).abs() > 0.0001 { + panic!(r#"assertion failed: `(left.1 == right.1)` + left: `{:?}`, + right: `{:?}`"#, $a.1, $b.1) + } + if ($a.2 - $b.2).abs() > 0.0001 { + panic!(r#"assertion failed: `(left.2 == right.2)` + left: `{:?}`, + right: `{:?}`"#, $a.2, $b.2) + } + } +} + +#[macro_export] +macro_rules! assert_rmsmima { + ($rms:expr, $b:expr) => { + assert_f3tupl_eq!($rms, $b); + } +} + +#[macro_export] +macro_rules! assert_minmax_of_rms { + ($rms:expr, $b:expr) => { + let (_, min, max) = $rms; + assert_fpair_eq!((min, max), $b); + } +} + +#[allow(dead_code)] pub fn save_wav(name: &str, buf: &[f32]) { let spec = hound::WavSpec { channels: 1, @@ -39,6 +93,11 @@ pub fn run_no_input(node_exec: &mut hexodsp::nodes::NodeExecutor, seconds: f32) run_realtime_no_input(node_exec, seconds, false) } +#[allow(dead_code)] +pub fn run_for_ms(node_exec: &mut hexodsp::nodes::NodeExecutor, ms: f32) -> (Vec, Vec) { + run_realtime_no_input(node_exec, ms / 1000.0, false) +} + pub fn run_realtime_no_input(node_exec: &mut hexodsp::nodes::NodeExecutor, seconds: f32, sleep_a_bit: bool) -> (Vec, Vec) { node_exec.test_run(seconds, sleep_a_bit) } @@ -75,6 +134,7 @@ pub fn calc_rms_mimax_each_ms(buf: &[f32], ms: f32) -> Vec<(f32, f32, f32)> { res } +#[allow(dead_code)] pub fn run_and_undersample( node_exec: &mut hexodsp::nodes::NodeExecutor, run_len_ms: f32, samples: usize) -> Vec @@ -92,6 +152,7 @@ pub fn run_and_undersample( out_samples } +#[allow(dead_code)] pub fn run_and_get_each_rms_mimax( node_exec: &mut hexodsp::nodes::NodeExecutor, len_ms: f32) -> Vec<(f32, f32, f32)> @@ -100,6 +161,7 @@ pub fn run_and_get_each_rms_mimax( calc_rms_mimax_each_ms(&out_l[..], len_ms) } +#[allow(dead_code)] pub fn run_and_get_first_rms_mimax( node_exec: &mut hexodsp::nodes::NodeExecutor, len_ms: f32) -> (f32, f32, f32) diff --git a/tests/node_sampl.rs b/tests/node_sampl.rs index 34ddb4c..57cae97 100644 --- a/tests/node_sampl.rs +++ b/tests/node_sampl.rs @@ -19,9 +19,7 @@ fn check_node_sampl_1() { matrix.set_param(sample_p, SAtom::audio_unloaded("tests/sample_sin.wav")); let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); - assert_float_eq!(rms, 0.505); - assert_float_eq!(min, -0.9998); - assert_float_eq!(max, 1.0); + assert_rmsmima!((rms, min, max), (0.505, -0.9998, 1.0)); let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); assert_eq!(fft[0], (441, 940)); @@ -84,10 +82,8 @@ fn check_node_sampl_reload() { &mut matrix, "check_matrix_serialize.hxy").unwrap(); - let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); - assert_float_eq!(rms, 0.505); - assert_float_eq!(min, -0.9998); - assert_float_eq!(max, 1.0); + let rmsmima = run_and_get_l_rms_mimax(&mut node_exec, 50.0); + assert_rmsmima!(rmsmima, (0.505, -0.9998, 1.0)); let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); assert_eq!(fft[0], (441, 940)); @@ -111,9 +107,9 @@ fn check_node_sampl_load_err() { matrix.set_param(sample_p, SAtom::audio_unloaded("tests/sample_NOSIN.wav")); let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); - assert_float_eq!(rms, 0.0); - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.0); + assert_rmsmima!( + (rms, min, max), + (0.0, 0.0, 0.0)); let err = matrix.pop_error(); assert_eq!(err.unwrap(), "Sample Loading Error\nCouldn't load sample 'tests/sample_NOSIN.wav':\nLoadError(IoError(Os { code: 2, kind: NotFound, message: \"No such file or directory\" }))"); @@ -138,21 +134,24 @@ fn check_node_sampl_trigger() { matrix.set_param(sample_p, SAtom::audio_unloaded("tests/sample_sin.wav")); matrix.set_param(pmode_p, SAtom::setting(1)); - let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 10.0); - assert_float_eq!(rms, 0.0); - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.0); + let rmsmima = run_and_get_l_rms_mimax(&mut node_exec, 10.0); + assert_rmsmima!(rmsmima, (0.0, 0.0, 0.0)); matrix.set_param(trig_p, (1.0).into()); - let (rms, min, max) = run_and_get_first_rms_mimax(&mut node_exec, 10.0); - assert_float_eq!(rms, 0.1136); - assert_float_eq!(min, -0.9998); - assert_float_eq!(max, 1.0); + let rmsmima = run_and_get_first_rms_mimax(&mut node_exec, 10.0); + assert_rmsmima!(rmsmima, (0.1136, -0.9998, 1.0)); - let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 20.0); - assert_float_eq!(rms, 0.0); - assert_float_eq!(min, -0.0); - assert_float_eq!(max, 0.0); + let rmsmima = run_and_get_l_rms_mimax(&mut node_exec, 20.0); + assert_rmsmima!(rmsmima, (0.0, 0.0, 0.0)); +} + +fn create_1sec_const(s: f32) -> SAtom { + let mut test_sample_ramp = vec![s; (SAMPLE_RATE_US) + 1]; + test_sample_ramp[0] = SAMPLE_RATE; + + SAtom::audio( + "1second_const.wav", + std::sync::Arc::new(test_sample_ramp)) } fn create_1sec_ramp() -> SAtom { @@ -196,20 +195,13 @@ fn check_node_sampl_trigger_reset_phase() { matrix.set_param(trig_p, (1.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 100.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.092496); - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.19252); - assert_float_eq!(max, 0.29250); + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.092496)); + assert_minmax_of_rms!(rmsvec[2], (0.19252, 0.29250)); // lower trigger level, for retrigger later matrix.set_param(trig_p, (0.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 10.0); - - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.31252); - assert_float_eq!(max, 0.32250); + assert_minmax_of_rms!(rmsvec[2], (0.31252, 0.32250)); // retrigger the phase sample matrix.set_param(trig_p, (1.0).into()); @@ -221,13 +213,8 @@ fn check_node_sampl_trigger_reset_phase() { // this is the last value of the previous triggering assert_float_eq!(max, 0.32998); - let (_rms, min, max) = rmsvec[1]; - assert_float_eq!(min, 0.09251); - assert_float_eq!(max, 0.19249); - - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.19252); - assert_float_eq!(max, 0.29250); + assert_minmax_of_rms!(rmsvec[1], (0.09251, 0.19249)); + assert_minmax_of_rms!(rmsvec[2], (0.19252, 0.29250)); } #[test] @@ -253,21 +240,13 @@ fn check_node_sampl_trigger_loop_reset_phase() { matrix.set_param(trig_p, (0.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 100.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.09999); - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.2); - assert_float_eq!(max, 0.2999); + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.0999)); + assert_minmax_of_rms!(rmsvec[2], (0.2, 0.2999)); matrix.set_param(trig_p, (1.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 100.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.3074); - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.1925); - assert_float_eq!(max, 0.2925); + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.3074)); + assert_minmax_of_rms!(rmsvec[2], (0.1925, 0.2925)); } #[test] @@ -296,23 +275,13 @@ fn check_node_sampl_offs_len() { matrix.set_param(len_p, SAtom::param(0.5)); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 50.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0011133); - assert_float_eq!(max, 0.54999); - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.6); - assert_float_eq!(max, 0.65); + assert_minmax_of_rms!(rmsvec[0], (0.001113, 0.54999)); + assert_minmax_of_rms!(rmsvec[2], (0.6, 0.65)); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 50.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.65); - assert_float_eq!(max, 0.6999); - let (_rms, min, max) = rmsvec[1]; - assert_float_eq!(min, 0.70); - assert_float_eq!(max, 0.75); - let (_rms, min, max) = rmsvec[2]; - assert_float_eq!(min, 0.5); - assert_float_eq!(max, 0.55); + assert_minmax_of_rms!(rmsvec[0], (0.65, 0.6999)); + assert_minmax_of_rms!(rmsvec[1], (0.70, 0.75)); + assert_minmax_of_rms!(rmsvec[2], (0.5, 0.55)); } @@ -344,9 +313,7 @@ fn check_node_sampl_offs_len_zero_crash() { matrix.set_param(trig_p, (1.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 50.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 1.0); + assert_minmax_of_rms!(rmsvec[0], (0.0, 1.0)); // Select part 0.5 to 0.75 of the sample: matrix.set_param(offs_p, SAtom::param(0.9)); @@ -354,9 +321,7 @@ fn check_node_sampl_offs_len_zero_crash() { matrix.set_param(trig_p, (1.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 50.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.0); + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.0)); // Select part 0.5 to 0.75 of the sample: matrix.set_param(offs_p, SAtom::param(1.0)); @@ -364,7 +329,107 @@ fn check_node_sampl_offs_len_zero_crash() { matrix.set_param(trig_p, (1.0).into()); let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 50.0); - let (_rms, min, max) = rmsvec[0]; - assert_float_eq!(min, 0.0); - assert_float_eq!(max, 0.0); + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.0)); +} + +#[test] +fn check_node_sampl_declick() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let smpl = NodeId::Sampl(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(smpl) + .out(None, None, smpl.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let sample_p = smpl.inp_param("sample").unwrap(); + let pmode_p = smpl.inp_param("pmode").unwrap(); + let dclick_p = smpl.inp_param("dclick").unwrap(); + let trig_p = smpl.inp_param("trig").unwrap(); + + matrix.set_param(sample_p, create_1sec_const(1.0)); + // One Shot Mode + matrix.set_param(pmode_p, SAtom::setting(1)); + matrix.set_param(dclick_p, SAtom::setting(0)); + matrix.set_param(trig_p, (1.0).into()); + let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 5.0); + + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.0)); + assert_minmax_of_rms!(rmsvec[1], (0.0, 1.0)); + assert_minmax_of_rms!(rmsvec[2], (1.0, 1.0)); + + // reset trigger: + matrix.set_param(trig_p, (0.0).into()); + run_for_ms(&mut node_exec, 1000.0); + + matrix.set_param(dclick_p, SAtom::setting(1)); + matrix.set_param(trig_p, (1.0).into()); + // let the trigger appear in the sampler: + run_for_ms(&mut node_exec, 7.5); + // now the de-click should run: + let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 1.0); + + assert_minmax_of_rms!(rmsvec[0], (0.0, 0.3105)); + assert_minmax_of_rms!(rmsvec[1], (0.3177, 0.6282)); + assert_minmax_of_rms!(rmsvec[2], (0.6354, 0.9460)); +} + + +#[test] +fn check_node_sampl_declick_offs_len() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let smpl = NodeId::Sampl(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(smpl) + .out(None, None, smpl.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let sample_p = smpl.inp_param("sample").unwrap(); + let pmode_p = smpl.inp_param("pmode").unwrap(); + let dclick_p = smpl.inp_param("dclick").unwrap(); + let trig_p = smpl.inp_param("trig").unwrap(); + let offs_p = smpl.inp_param("offs").unwrap(); + let len_p = smpl.inp_param("len").unwrap(); + + matrix.set_param(sample_p, create_1sec_const(1.0)); + // One Shot Mode + matrix.set_param(pmode_p, SAtom::setting(1)); + matrix.set_param(dclick_p, SAtom::setting(1)); + matrix.set_param(trig_p, (1.0).into()); + matrix.set_param(offs_p, SAtom::param(0.9)); + matrix.set_param(len_p, SAtom::param(0.06)); + + // trigger: + run_for_ms(&mut node_exec, 7.5); + + let res = run_for_ms(&mut node_exec, 12.0); + println!("RMS: {:#?}", res.0); + + assert!(false); + +// assert_minmax_of_rms!(rmsvec[0], (0.0, 2.0)); +// assert_minmax_of_rms!(rmsvec[1], (0.0, 1.0)); +// assert_minmax_of_rms!(rmsvec[2], (1.0, 1.0)); +// +// // reset trigger: +// matrix.set_param(trig_p, (0.0).into()); +// run_for_ms(&mut node_exec, 1000.0); +// +// matrix.set_param(dclick_p, SAtom::setting(1)); +// matrix.set_param(trig_p, (1.0).into()); +// // let the trigger appear in the sampler: +// run_for_ms(&mut node_exec, 7.5); +// // now the de-click should run: +// let rmsvec = run_and_get_each_rms_mimax(&mut node_exec, 1.0); +// +// assert_minmax_of_rms!(rmsvec[0], (0.0, 0.3105)); +// assert_minmax_of_rms!(rmsvec[1], (0.3177, 0.6282)); +// assert_minmax_of_rms!(rmsvec[2], (0.6354, 0.9460)); }