diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 65f2a21..528f31a 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -276,8 +276,8 @@ macro_rules! node_list { sampl => Sampl UIType::Generic UICategory::Osc (0 freq n_pit d_pit -1.0, 1.0, 440.0) (1 trig n_id n_id -1.0, 1.0, 0.0) - (2 spos n_id n_id -1.0, 1.0, 0.0) - (3 epos n_id n_id -1.0, 1.0, 0.0) + (2 offs n_id n_id 0.0, 1.0, 0.0) + (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} diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index 6963028..0495eec 100644 --- a/src/dsp/node_sampl.rs +++ b/src/dsp/node_sampl.rs @@ -32,10 +32,10 @@ impl Sampl { pub const trig : &'static str = "Sampl trig\nThe trigger input causes a resync of the playback phase \ and triggers the playback if the 'pmode' is 'OneShot'"; - pub const spos : &'static str = - "Sampl spos\nStart position offset.\nRange: (-1..1)\n"; - pub const epos : &'static str = - "Sampl epos\nEnd position offset.\nRange: (-1..1)\n"; + pub const offs : &'static str = + "Sampl offs\nStart position offset.\nRange: (0..1)\n"; + pub const len : &'static str = + "Sampl len\nLength of the sample, after the offset has been applied.\nRange: (0..1)\n"; pub const sample : &'static str = "Sampl sample\nThe audio sample that is played back.\nRange: (-1..1)\n"; @@ -59,6 +59,7 @@ impl Sampl { #[inline] fn next_sample(&mut self, sr_factor: f64, speed: f64, sample_data: &[f32]) -> f32 { let sd_len = sample_data.len(); + if sd_len < 1 { return 0.0; } let i = self.phase.floor() as usize + sd_len; @@ -89,30 +90,46 @@ 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_loop(&mut self, inputs: &[ProcBuf], nframes: usize, sample_data: &[f32], out: &mut ProcBuf) { - let freq = inp::Sampl::freq(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; - - out.write(frame, - self.next_sample( - sr_factor, playback_speed as f64, sample_data)); - } - } - - #[inline] - fn play_oneshot(&mut self, inputs: &[ProcBuf], nframes: usize, - sample_data: &[f32], out: &mut ProcBuf) + fn play(&mut self, inputs: &[ProcBuf], nframes: usize, + sample_data: &[f32], out: &mut ProcBuf, do_loop: bool) { let freq = inp::Sampl::freq(inputs); let trig = inp::Sampl::trig(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..]; @@ -120,6 +137,16 @@ impl Sampl { let mut is_playing = self.is_playing; + if do_loop { + is_playing = true; + } + + let mut prev_offs = -10.0; + let mut prev_len = -10.0; + + let mut start_idx = 0; + let mut end_idx_plus1 = sample_data.len(); + for frame in 0..nframes { let trig_val = denorm::Sampl::trig(trig, frame); let triggered = self.trig.check_trigger(trig_val); @@ -135,12 +162,36 @@ impl Sampl { let prev_phase = self.phase; - let s = self.next_sample( - sr_factor, playback_speed as f64, sample_data); + let cur_offs = + denorm::Sampl::offs(offs, frame) + .abs().min(0.999999); + if prev_offs != cur_offs { + start_idx = + (sample_data.len() as f32 * cur_offs) + .floor() as usize; + prev_offs = cur_offs; + } + + let cur_len = + 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)) + .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)]); + out.write(frame, s); - // played past end => stop playing. - if prev_phase > self.phase { + if !do_loop && prev_phase > self.phase { + // played past end => stop playing. is_playing = false; } } else { @@ -171,11 +222,20 @@ impl DspNode for Sampl { let out = out::Sampl::sig(outputs); if let SAtom::AudioSample((_, Some(sample_data))) = sample { - if pmode.i() == 0 { - self.play_loop(inputs, ctx.nframes(), &sample_data[..], out); - } else { - self.play_oneshot(inputs, ctx.nframes(), &sample_data[..], out); + // 3 is for sample-sample-rate and at least 2 audio samples. + if sample_data.len() < 3 { + for frame in 0..ctx.nframes() { + out.write(frame, 0.0); + } + return; } + + self.play( + inputs, + ctx.nframes(), + &sample_data[..], + out, + pmode.i() == 0); } else { for frame in 0..ctx.nframes() { out.write(frame, 0.0); diff --git a/tests/basics.rs b/tests/basics.rs index ba6fca4..e623202 100644 --- a/tests/basics.rs +++ b/tests/basics.rs @@ -1,211 +1,5 @@ -use hexodsp::matrix::*; -use hexodsp::nodes::new_node_engine; -use hexodsp::dsp::*; - -use hound; -//use num_complex::Complex; -use microfft; - -macro_rules! assert_float_eq { - ($a:expr, $b:expr) => { - if ($a - $b).abs() > 0.0001 { - panic!(r#"assertion failed: `(left == right)` - left: `{:?}`, - right: `{:?}`"#, $a, $b) - } - } -} - -const SAMPLE_RATE : f32 = 44100.0; - -fn save_wav(name: &str, buf: &[f32]) { - let spec = hound::WavSpec { - channels: 1, - sample_rate: SAMPLE_RATE as u32, - bits_per_sample: 16, - sample_format: hound::SampleFormat::Int, - }; - - let mut writer = hound::WavWriter::create(name, spec).unwrap(); - for s in buf.iter() { - let amp = i16::MAX as f32; - writer.write_sample((amp * s) as i16).unwrap(); - } -} - -fn run_no_input(node_exec: &mut hexodsp::nodes::NodeExecutor, seconds: f32) -> (Vec, Vec) { - run_realtime_no_input(node_exec, seconds, false) -} - -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) -} - -fn calc_rms_mimax_each_ms(buf: &[f32], ms: f32) -> Vec<(f32, f32, f32)> { - let ms_samples = ms * SAMPLE_RATE / 1000.0; - let len_ms = ms_samples as usize; - - let mut idx = 0; - let mut res = vec![]; - loop { - if (idx + len_ms) > buf.len() { - break; - } - - let mut max = -1000.0; - let mut min = 1000.0; - for s in buf[idx..(idx + len_ms)].iter() { - max = s.max(max); - min = s.min(min); - } - - let rms : f32 = - buf[idx..(idx + len_ms)] - .iter() - .map(|s: &f32| s * s).sum::() - / ms_samples; - - res.push((rms, min, max)); - - idx += len_ms; - } - - res -} - -fn run_and_undersample( - node_exec: &mut hexodsp::nodes::NodeExecutor, - run_len_ms: f32, samples: usize) -> Vec -{ - let (out_l, _out_r) = run_no_input(node_exec, run_len_ms / 1000.0); - - let sample_interval = out_l.len() / samples; - let mut out_samples = vec![]; - - for i in 0..samples { - let idx = i * sample_interval; - out_samples.push(out_l[idx]); - } - - out_samples -} - -fn run_and_get_each_rms_mimax( - node_exec: &mut hexodsp::nodes::NodeExecutor, - len_ms: f32) -> Vec<(f32, f32, f32)> -{ - let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); - calc_rms_mimax_each_ms(&out_l[..], len_ms) -} - -fn run_and_get_first_rms_mimax( - node_exec: &mut hexodsp::nodes::NodeExecutor, - len_ms: f32) -> (f32, f32, f32) -{ - let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); - let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], len_ms); - rms_mimax[0] -} - -fn run_and_get_l_rms_mimax( - node_exec: &mut hexodsp::nodes::NodeExecutor, - len_ms: f32) -> (f32, f32, f32) -{ - let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); - let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], len_ms); - rms_mimax[1] -} - -fn run_and_get_fft4096( - node_exec: &mut hexodsp::nodes::NodeExecutor, - thres: u32, - offs_ms: f32) -> Vec<(u16, u32)> -{ - let min_samples_for_fft = 4096.0; - let offs_samples = (offs_ms * (SAMPLE_RATE / 1000.0)).ceil(); - let min_len_samples = - offs_samples - // 2.0 * for safety margin - + 2.0 * min_samples_for_fft; - let run_len_s = min_len_samples / SAMPLE_RATE; - let (mut out_l, _out_r) = run_no_input(node_exec, run_len_s); - fft_thres_at_ms(&mut out_l[..], FFT::F4096, thres, offs_ms) -} - -#[allow(unused)] -enum FFT { - F16, - F32, - F64, - F128, - F512, - F1024, - F2048, - F4096, -} - -fn fft_thres_at_ms(buf: &mut [f32], size: FFT, amp_thres: u32, ms_idx: f32) -> Vec<(u16, u32)> { - let ms_sample_offs = ms_idx * (SAMPLE_RATE / 1000.0); - let fft_nbins = match size { - FFT::F16 => 16, - FFT::F32 => 32, - FFT::F64 => 64, - FFT::F128 => 128, - FFT::F512 => 512, - FFT::F1024 => 1024, - FFT::F2048 => 2048, - FFT::F4096 => 4096, - }; - let len = fft_nbins; - - let idx = ms_sample_offs as usize; - let mut res = vec![]; - - if (idx + len) > buf.len() { - return res; - } - - // Hann window: - for (i, s) in buf[idx..(idx + len)].iter_mut().enumerate() { - let w = - 0.5 - * (1.0 - - ((2.0 * std::f32::consts::PI * i as f32) - / (fft_nbins as f32 - 1.0)) - .cos()); - *s *= w; - } - - let spec = - match size { - FFT::F16 => - microfft::real::rfft_16(&mut buf[idx..(idx + len)]), - FFT::F32 => - microfft::real::rfft_32(&mut buf[idx..(idx + len)]), - FFT::F64 => - microfft::real::rfft_64(&mut buf[idx..(idx + len)]), - FFT::F128 => - microfft::real::rfft_128(&mut buf[idx..(idx + len)]), - FFT::F512 => - microfft::real::rfft_512(&mut buf[idx..(idx + len)]), - FFT::F1024 => - microfft::real::rfft_1024(&mut buf[idx..(idx + len)]), - FFT::F2048 => - microfft::real::rfft_2048(&mut buf[idx..(idx + len)]), - FFT::F4096 => - microfft::real::rfft_4096(&mut buf[idx..(idx + len)]), - }; - let amplitudes: Vec<_> = spec.iter().map(|c| c.norm() as u32).collect(); - - for (i, amp) in amplitudes.iter().enumerate() { - if *amp >= amp_thres { - let freq = (i as f32 * SAMPLE_RATE) / fft_nbins as f32; - res.push((freq.round() as u16, *amp)); - } - } - - res -} +mod common; +use common::*; #[test] fn check_matrix_sine() { @@ -1122,230 +916,3 @@ fn check_matrix_output_feedback() { assert_float_eq!(fo_amp.1, 0.11627); } - -#[test] -fn check_node_sampl_1() { - 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 freq_p = smpl.inp_param("freq").unwrap(); - 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); - - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (441, 940)); - - matrix.set_param(freq_p, SAtom::param(0.1)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (894, 988)); - - matrix.set_param(freq_p, SAtom::param(-0.1)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (226, 966)); - - matrix.set_param(freq_p, SAtom::param(-0.2)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (108, 953)); - - matrix.set_param(freq_p, SAtom::param(-0.4)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (22, 818)); - - matrix.set_param(freq_p, SAtom::param(-0.5)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (11, 964)); - - matrix.set_param(freq_p, SAtom::param(0.2)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (1776, 877)); - - matrix.set_param(freq_p, SAtom::param(0.4)); - let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (7127, 1029)); -} - -#[test] -fn check_node_sampl_reload() { - { - let (node_conf, _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(); - matrix.set_param(sample_p, SAtom::audio_unloaded("tests/sample_sin.wav")); - - hexodsp::save_patch_to_file(&mut matrix, "check_matrix_serialize.hxy") - .unwrap(); - } - - { - let (node_conf, mut node_exec) = new_node_engine(); - let mut matrix = Matrix::new(node_conf, 3, 3); - - hexodsp::load_patch_from_file( - &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 fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); - assert_eq!(fft[0], (441, 940)); - } -} - -#[test] -fn check_node_sampl_load_err() { - 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(); - 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); - - 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\" }))"); -} - -#[test] -fn check_node_sampl_trigger() { - 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 trig_p = smpl.inp_param("trig").unwrap(); - 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); - - 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 (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); -} - -#[test] -fn check_node_sampl_trigger_reset_phase() { - 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 trig_p = smpl.inp_param("trig").unwrap(); - - let mut test_sample_ramp = vec![0.0; 44101]; - test_sample_ramp[0] = 44100.0; - for i in 0..(test_sample_ramp.len() - 1) { - test_sample_ramp[i + 1] = - (i as f32) / ((test_sample_ramp.len() - 2) as f32) - } - - matrix.set_param(sample_p, - SAtom::audio( - "1second_ramp.wav", - std::sync::Arc::new(test_sample_ramp))); - 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); - - 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); - - // 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); - - // retrigger the phase sample - 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]; - // this is the start of the phase - assert_float_eq!(min, 0.0); - // 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); -} - - diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..d17d8a9 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,211 @@ +pub use hexodsp::matrix::*; +pub use hexodsp::nodes::new_node_engine; +pub use hexodsp::dsp::*; + +use hound; +//use num_complex::Complex; +use microfft; + +pub const SAMPLE_RATE : f32 = 44100.0; +pub const SAMPLE_RATE_US : usize = 44100; + +#[macro_export] +macro_rules! assert_float_eq { + ($a:expr, $b:expr) => { + if ($a - $b).abs() > 0.0001 { + panic!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, $a, $b) + } + } +} + +pub fn save_wav(name: &str, buf: &[f32]) { + let spec = hound::WavSpec { + channels: 1, + sample_rate: SAMPLE_RATE as u32, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + let mut writer = hound::WavWriter::create(name, spec).unwrap(); + for s in buf.iter() { + let amp = i16::MAX as f32; + writer.write_sample((amp * s) as i16).unwrap(); + } +} + +pub fn run_no_input(node_exec: &mut hexodsp::nodes::NodeExecutor, seconds: f32) -> (Vec, Vec) { + run_realtime_no_input(node_exec, seconds, 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) +} + +pub fn calc_rms_mimax_each_ms(buf: &[f32], ms: f32) -> Vec<(f32, f32, f32)> { + let ms_samples = ms * SAMPLE_RATE / 1000.0; + let len_ms = ms_samples as usize; + + let mut idx = 0; + let mut res = vec![]; + loop { + if (idx + len_ms) > buf.len() { + break; + } + + let mut max = -1000.0; + let mut min = 1000.0; + for s in buf[idx..(idx + len_ms)].iter() { + max = s.max(max); + min = s.min(min); + } + + let rms : f32 = + buf[idx..(idx + len_ms)] + .iter() + .map(|s: &f32| s * s).sum::() + / ms_samples; + + res.push((rms, min, max)); + + idx += len_ms; + } + + res +} + +pub fn run_and_undersample( + node_exec: &mut hexodsp::nodes::NodeExecutor, + run_len_ms: f32, samples: usize) -> Vec +{ + let (out_l, _out_r) = run_no_input(node_exec, run_len_ms / 1000.0); + + let sample_interval = out_l.len() / samples; + let mut out_samples = vec![]; + + for i in 0..samples { + let idx = i * sample_interval; + out_samples.push(out_l[idx]); + } + + out_samples +} + +pub fn run_and_get_each_rms_mimax( + node_exec: &mut hexodsp::nodes::NodeExecutor, + len_ms: f32) -> Vec<(f32, f32, f32)> +{ + let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); + calc_rms_mimax_each_ms(&out_l[..], len_ms) +} + +pub fn run_and_get_first_rms_mimax( + node_exec: &mut hexodsp::nodes::NodeExecutor, + len_ms: f32) -> (f32, f32, f32) +{ + let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], len_ms); + rms_mimax[0] +} + +pub fn run_and_get_l_rms_mimax( + node_exec: &mut hexodsp::nodes::NodeExecutor, + len_ms: f32) -> (f32, f32, f32) +{ + let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], len_ms); + rms_mimax[1] +} + +pub fn run_and_get_fft4096( + node_exec: &mut hexodsp::nodes::NodeExecutor, + thres: u32, + offs_ms: f32) -> Vec<(u16, u32)> +{ + let min_samples_for_fft = 4096.0; + let offs_samples = (offs_ms * (SAMPLE_RATE / 1000.0)).ceil(); + let min_len_samples = + offs_samples + // 2.0 * for safety margin + + 2.0 * min_samples_for_fft; + let run_len_s = min_len_samples / SAMPLE_RATE; + let (mut out_l, _out_r) = run_no_input(node_exec, run_len_s); + fft_thres_at_ms(&mut out_l[..], FFT::F4096, thres, offs_ms) +} + +#[allow(unused)] +pub enum FFT { + F16, + F32, + F64, + F128, + F512, + F1024, + F2048, + F4096, +} + +pub fn fft_thres_at_ms(buf: &mut [f32], size: FFT, amp_thres: u32, ms_idx: f32) -> Vec<(u16, u32)> { + let ms_sample_offs = ms_idx * (SAMPLE_RATE / 1000.0); + let fft_nbins = match size { + FFT::F16 => 16, + FFT::F32 => 32, + FFT::F64 => 64, + FFT::F128 => 128, + FFT::F512 => 512, + FFT::F1024 => 1024, + FFT::F2048 => 2048, + FFT::F4096 => 4096, + }; + let len = fft_nbins; + + let idx = ms_sample_offs as usize; + let mut res = vec![]; + + if (idx + len) > buf.len() { + return res; + } + + // Hann window: + for (i, s) in buf[idx..(idx + len)].iter_mut().enumerate() { + let w = + 0.5 + * (1.0 + - ((2.0 * std::f32::consts::PI * i as f32) + / (fft_nbins as f32 - 1.0)) + .cos()); + *s *= w; + } + + let spec = + match size { + FFT::F16 => + microfft::real::rfft_16(&mut buf[idx..(idx + len)]), + FFT::F32 => + microfft::real::rfft_32(&mut buf[idx..(idx + len)]), + FFT::F64 => + microfft::real::rfft_64(&mut buf[idx..(idx + len)]), + FFT::F128 => + microfft::real::rfft_128(&mut buf[idx..(idx + len)]), + FFT::F512 => + microfft::real::rfft_512(&mut buf[idx..(idx + len)]), + FFT::F1024 => + microfft::real::rfft_1024(&mut buf[idx..(idx + len)]), + FFT::F2048 => + microfft::real::rfft_2048(&mut buf[idx..(idx + len)]), + FFT::F4096 => + microfft::real::rfft_4096(&mut buf[idx..(idx + len)]), + }; + let amplitudes: Vec<_> = spec.iter().map(|c| c.norm() as u32).collect(); + + for (i, amp) in amplitudes.iter().enumerate() { + if *amp >= amp_thres { + let freq = (i as f32 * SAMPLE_RATE) / fft_nbins as f32; + res.push((freq.round() as u16, *amp)); + } + } + + res +} + diff --git a/tests/node_sampl.rs b/tests/node_sampl.rs new file mode 100644 index 0000000..34ddb4c --- /dev/null +++ b/tests/node_sampl.rs @@ -0,0 +1,370 @@ +mod common; +use common::*; + +#[test] +fn check_node_sampl_1() { + 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 freq_p = smpl.inp_param("freq").unwrap(); + 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); + + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (441, 940)); + + matrix.set_param(freq_p, SAtom::param(0.1)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (894, 988)); + + matrix.set_param(freq_p, SAtom::param(-0.1)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (226, 966)); + + matrix.set_param(freq_p, SAtom::param(-0.2)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (108, 953)); + + matrix.set_param(freq_p, SAtom::param(-0.4)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (22, 818)); + + matrix.set_param(freq_p, SAtom::param(-0.5)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (11, 964)); + + matrix.set_param(freq_p, SAtom::param(0.2)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (1776, 877)); + + matrix.set_param(freq_p, SAtom::param(0.4)); + let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (7127, 1029)); +} + +#[test] +fn check_node_sampl_reload() { + { + let (node_conf, _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(); + matrix.set_param(sample_p, SAtom::audio_unloaded("tests/sample_sin.wav")); + + hexodsp::save_patch_to_file(&mut matrix, "check_matrix_serialize.hxy") + .unwrap(); + } + + { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + hexodsp::load_patch_from_file( + &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 fft = run_and_get_fft4096(&mut node_exec, 800, 20.0); + assert_eq!(fft[0], (441, 940)); + } +} + +#[test] +fn check_node_sampl_load_err() { + 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(); + 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); + + 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\" }))"); +} + +#[test] +fn check_node_sampl_trigger() { + 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 trig_p = smpl.inp_param("trig").unwrap(); + 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); + + 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 (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); +} + +fn create_1sec_ramp() -> SAtom { + let mut test_sample_ramp = vec![0.0; (SAMPLE_RATE_US) + 1]; + test_sample_ramp[0] = SAMPLE_RATE; + for i in 0..(test_sample_ramp.len() - 1) { + test_sample_ramp[i + 1] = + (i as f32) / ((test_sample_ramp.len() - 2) as f32) + } + + SAtom::audio( + "1second_ramp.wav", + std::sync::Arc::new(test_sample_ramp)) +} + +#[test] +fn check_node_sampl_trigger_reset_phase() { + 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 trig_p = smpl.inp_param("trig").unwrap(); + + matrix.set_param(sample_p, create_1sec_ramp()); + // One Shot Mode + 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); + + 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); + + // 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); + + // retrigger the phase sample + 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]; + // this is the start of the phase + assert_float_eq!(min, 0.0); + // 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); +} + +#[test] +fn check_node_sampl_trigger_loop_reset_phase() { + 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 trig_p = smpl.inp_param("trig").unwrap(); + + matrix.set_param(sample_p, create_1sec_ramp()); + // Loop mode: + matrix.set_param(pmode_p, SAtom::setting(0)); + + 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); + + 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); +} + +#[test] +fn check_node_sampl_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 offs_p = smpl.inp_param("offs").unwrap(); + let len_p = smpl.inp_param("len").unwrap(); + + matrix.set_param(sample_p, create_1sec_ramp()); + matrix.set_param(pmode_p, SAtom::setting(0)); + + // Select part 0.5 to 0.75 of the sample: + matrix.set_param(offs_p, SAtom::param(0.5)); + 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); + + 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); +} + + +#[test] +fn check_node_sampl_offs_len_zero_crash() { + 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 offs_p = smpl.inp_param("offs").unwrap(); + let len_p = smpl.inp_param("len").unwrap(); + let trig_p = smpl.inp_param("trig").unwrap(); + + matrix.set_param(sample_p, create_1sec_ramp()); + matrix.set_param(pmode_p, SAtom::setting(0)); + + // Select part 0.5 to 0.75 of the sample: + matrix.set_param(offs_p, SAtom::param(1.0)); + matrix.set_param(len_p, SAtom::param(0.0)); + + 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); + + // Select part 0.5 to 0.75 of the sample: + matrix.set_param(offs_p, SAtom::param(0.9)); + matrix.set_param(len_p, SAtom::param(0.0)); + + 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); + + // Select part 0.5 to 0.75 of the sample: + matrix.set_param(offs_p, SAtom::param(1.0)); + matrix.set_param(len_p, SAtom::param(0.0)); + + 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); +}