Reworked scope DSP algorithm

This commit is contained in:
Weird Constructor 2022-07-26 06:51:41 +02:00
parent 2f307a3c2e
commit a025c7fbe2
4 changed files with 136 additions and 17 deletions

View file

@ -710,6 +710,53 @@ impl Trigger {
} }
} }
/// Trigger signal detector with custom range.
///
/// Whenever you need to detect a trigger with a custom threshold.
#[derive(Debug, Clone, Copy)]
pub struct CustomTrigger {
triggered: bool,
low_thres: f32,
high_thres: f32,
}
impl CustomTrigger {
/// Create a new trigger detector.
pub fn new(low_thres: f32, high_thres: f32) -> Self {
Self { triggered: false, low_thres, high_thres }
}
pub fn set_threshold(&mut self, low_thres: f32, high_thres: f32) {
self.low_thres = low_thres;
self.high_thres = high_thres;
}
/// Reset the internal state of the trigger detector.
#[inline]
pub fn reset(&mut self) {
self.triggered = false;
}
/// Checks the input signal for a trigger and returns true when the signal
/// surpassed the high threshold and has not fallen below low threshold yet.
#[inline]
pub fn check_trigger(&mut self, input: f32) -> bool {
// println!("TRIG CHECK: {} <> {}", input, self.high_thres);
if self.triggered {
if input <= self.low_thres {
self.triggered = false;
}
false
} else if input > self.high_thres {
self.triggered = true;
true
} else {
false
}
}
}
/// Generates a phase signal from a trigger/gate input signal. /// Generates a phase signal from a trigger/gate input signal.
/// ///
/// This helper allows you to measure the distance between trigger or gate pulses /// This helper allows you to measure the distance between trigger or gate pulses

View file

@ -1232,6 +1232,7 @@ macro_rules! f_det {
// norm-fun denorm-min // norm-fun denorm-min
// denorm-fun denorm-max // denorm-fun denorm-max
define_exp! {n_gain d_gain 0.0, 2.0} define_exp! {n_gain d_gain 0.0, 2.0}
define_exp! {n_xgin d_xgin 0.0, 10.0}
define_exp! {n_att d_att 0.0, 1.0} define_exp! {n_att d_att 0.0, 1.0}
define_exp! {n_declick d_declick 0.0, 50.0} define_exp! {n_declick d_declick 0.0, 50.0}
@ -1417,9 +1418,9 @@ macro_rules! node_list {
(6 off1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (6 off1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(7 off2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (7 off2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(8 off3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (8 off3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(9 gain1 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) (9 gain1 n_xgin d_xgin r_id f_def stp_d 0.0, 1.0, 1.0)
(10 gain2 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) (10 gain2 n_xgin d_xgin r_id f_def stp_d 0.0, 1.0, 1.0)
(11 gain3 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) (11 gain3 n_xgin d_xgin r_id f_def stp_d 0.0, 1.0, 1.0)
{12 0 tsrc setting(0) mode fa_scope_tsrc 0 1}, {12 0 tsrc setting(0) mode fa_scope_tsrc 0 1},
ad => Ad UIType::Generic UICategory::Mod ad => Ad UIType::Generic UICategory::Mod
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0)

View file

@ -1,8 +1,14 @@
// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com> // Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
// This file is a part of HexoDSP. Released under GPL-3.0-or-later. // This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details. // See README.md and COPYING for details.
//
// This code was inspired by VCV Rack's scope:
// https://github.com/VCVRack/Fundamental/blob/v2/src/Scope.cpp
// Which is/was under the license GPL-3.0-or-later.
// Copyright by Andrew Belt, 2021
//use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger}; //use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger};
use crate::dsp::helpers::CustomTrigger;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::SCOPE_SAMPLES; use crate::nodes::SCOPE_SAMPLES;
use crate::nodes::{NodeAudioContext, NodeExecContext}; use crate::nodes::{NodeAudioContext, NodeExecContext};
@ -10,7 +16,7 @@ use crate::ScopeHandle;
use std::sync::Arc; use std::sync::Arc;
#[macro_export] #[macro_export]
macro_rules! fa_scope_tsrc { macro_rules! fa_scope_tsrc {
($formatter: expr, $v: expr, $denorm_v: expr) => {{ ($formatter: expr, $v: expr, $denorm_v: expr) => {{
let s = match ($v.round() as usize) { let s = match ($v.round() as usize) {
0 => "Off", 0 => "Off",
@ -27,24 +33,37 @@ macro_rules! fa_scope_tsrc {
pub struct Scope { pub struct Scope {
handle: Arc<ScopeHandle>, handle: Arc<ScopeHandle>,
idx: usize, idx: usize,
frame_count: usize,
srate_ms: f32,
trig: CustomTrigger,
} }
impl Scope { impl Scope {
pub fn new(_nid: &NodeId) -> Self { pub fn new(_nid: &NodeId) -> Self {
Self { handle: ScopeHandle::new_shared(), idx: 0 } Self {
handle: ScopeHandle::new_shared(),
idx: 0,
srate_ms: 44.1,
frame_count: 0,
trig: CustomTrigger::new(0.0, 0.0001),
}
} }
pub const in1: &'static str = "Scope in1\nSignal input 1.\nRange: (-1..1)\n"; pub const in1: &'static str = "Scope in1\nSignal input 1.\nRange: (-1..1)\n";
pub const in2: &'static str = "Scope in2\nSignal input 2.\nRange: (-1..1)\n"; pub const in2: &'static str = "Scope in2\nSignal input 2.\nRange: (-1..1)\n";
pub const in3: &'static str = "Scope in3\nSignal input 3.\nRange: (-1..1)\n"; pub const in3: &'static str = "Scope in3\nSignal input 3.\nRange: (-1..1)\n";
pub const time: &'static str = "Scope time\nDisplayed time range of the oscilloscope view.\nRange: (0..1)\n"; pub const time: &'static str =
"Scope time\nDisplayed time range of the oscilloscope view.\nRange: (0..1)\n";
pub const trig: &'static str = "Scope trig\nExternal trigger input. Only active if 'tsrc' is set to 'Extern'. 'thrsh' applies also for external triggers.\nRange: (-1..1)\n"; pub const trig: &'static str = "Scope trig\nExternal trigger input. Only active if 'tsrc' is set to 'Extern'. 'thrsh' applies also for external triggers.\nRange: (-1..1)\n";
pub const thrsh: &'static str = "Scope thrsh\nTrigger threshold. If the threshold is passed by the signal from low to high the signal recording will be reset. Either for internal or for external triggering. Trigger is only active if 'tsrc' is not 'Off'.\nRange: (-1..1)\n"; pub const thrsh: &'static str = "Scope thrsh\nTrigger threshold. If the threshold is passed by the signal from low to high the signal recording will be reset. Either for internal or for external triggering. Trigger is only active if 'tsrc' is not 'Off'.\nRange: (-1..1)\n";
pub const off1: &'static str = "Scope off1\nVisual offset of signal input 1.\nRange: (-1..1)\n"; pub const off1: &'static str = "Scope off1\nVisual offset of signal input 1.\nRange: (-1..1)\n";
pub const off2: &'static str = "Scope off2\nVisual offset of signal input 2.\nRange: (-1..1)\n"; pub const off2: &'static str = "Scope off2\nVisual offset of signal input 2.\nRange: (-1..1)\n";
pub const off3: &'static str = "Scope off3\nVisual offset of signal input 3.\nRange: (-1..1)\n"; pub const off3: &'static str = "Scope off3\nVisual offset of signal input 3.\nRange: (-1..1)\n";
pub const gain1: &'static str = "Scope gain1\nVisual amplification/attenuation of the signal input 1.\nRange: (0..1)\n"; pub const gain1: &'static str =
pub const gain2: &'static str = "Scope gain2\nVisual amplification/attenuation of the signal input 2.\nRange: (0..1)\n"; "Scope gain1\nVisual amplification/attenuation of the signal input 1.\nRange: (0..1)\n";
pub const gain3: &'static str = "Scope gain3\nVisual amplification/attenuation of the signal input 3.\nRange: (0..1)\n"; pub const gain2: &'static str =
"Scope gain2\nVisual amplification/attenuation of the signal input 2.\nRange: (0..1)\n";
pub const gain3: &'static str =
"Scope gain3\nVisual amplification/attenuation of the signal input 3.\nRange: (0..1)\n";
pub const tsrc: &'static str = "Scope tsrc\nTriggering allows you to capture fast signals or pinning fast waveforms into the scope view for better inspection.\nRange: (-1..1)\n"; pub const tsrc: &'static str = "Scope tsrc\nTriggering allows you to capture fast signals or pinning fast waveforms into the scope view for better inspection.\nRange: (-1..1)\n";
pub const DESC: &'static str = r#"Signal Oscilloscope Probe pub const DESC: &'static str = r#"Signal Oscilloscope Probe
@ -80,7 +99,9 @@ impl DspNode for Scope {
1 1
} }
fn set_sample_rate(&mut self, _srate: f32) {} fn set_sample_rate(&mut self, srate: f32) {
self.srate_ms = srate / 1000.0;
}
fn reset(&mut self) {} fn reset(&mut self) {}
@ -95,22 +116,65 @@ impl DspNode for Scope {
_outputs: &mut [ProcBuf], _outputs: &mut [ProcBuf],
ctx_vals: LedPhaseVals, ctx_vals: LedPhaseVals,
) { ) {
use crate::dsp::inp; use crate::dsp::{denorm, inp};
let in1 = inp::Scope::in1(inputs); let in1 = inp::Scope::in1(inputs);
let in2 = inp::Scope::in2(inputs); let in2 = inp::Scope::in2(inputs);
let in3 = inp::Scope::in3(inputs); let in3 = inp::Scope::in3(inputs);
let time = inp::Scope::time(inputs);
let thrsh = inp::Scope::thrsh(inputs);
let inputs = [in1, in2, in3]; let inputs = [in1, in2, in3];
self.handle.set_active_from_mask(nctx.in_connected); self.handle.set_active_from_mask(nctx.in_connected);
for frame in 0..ctx.nframes() { let time = denorm::Scope::time(time, 0);
for (i, input) in inputs.iter().enumerate() { let samples_per_block = (time * self.srate_ms) / SCOPE_SAMPLES as f32;
let in_val = input.read(frame); let threshold = denorm::Scope::thrsh(thrsh, 0);
self.handle.write(i, self.idx, in_val); self.trig.set_threshold(threshold, threshold + 0.001);
}
self.idx = (self.idx + 1) % SCOPE_SAMPLES; let trigger_input = in1;
if samples_per_block < 1.0 {
let copy_count = ((1.0 / samples_per_block) as usize).min(SCOPE_SAMPLES);
for frame in 0..ctx.nframes() {
if self.idx < SCOPE_SAMPLES {
for (i, input) in inputs.iter().enumerate() {
let in_val = input.read(frame);
self.handle.write_copies(i, self.idx, copy_count, in_val);
}
self.idx = self.idx.saturating_add(copy_count);
}
if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) {
self.frame_count = 0;
self.idx = 0;
}
}
} else {
let samples_per_block = samples_per_block as usize;
for frame in 0..ctx.nframes() {
if self.idx < SCOPE_SAMPLES {
if self.frame_count >= samples_per_block {
for (i, input) in inputs.iter().enumerate() {
let in_val = input.read(frame);
self.handle.write(i, self.idx, in_val);
}
self.idx = self.idx.saturating_add(1);
self.frame_count = 0;
}
self.frame_count += 1;
}
if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) {
self.frame_count = 0;
self.idx = 0;
}
}
} }
let last_frame = ctx.nframes() - 1; let last_frame = ctx.nframes() - 1;

View file

@ -27,6 +27,13 @@ impl ScopeHandle {
}) })
} }
pub fn write_copies(&self, buf_idx: usize, idx: usize, copies: usize, v: f32) {
let end = (idx + copies).min(SCOPE_SAMPLES);
for i in idx..end {
self.bufs[buf_idx % 3][i % SCOPE_SAMPLES].set(v);
}
}
pub fn write(&self, buf_idx: usize, idx: usize, v: f32) { pub fn write(&self, buf_idx: usize, idx: usize, v: f32) {
self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v); self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v);
} }