Added a scope probe node, to provide direct signal views from the frontend

This commit is contained in:
Weird Constructor 2022-07-25 05:55:54 +02:00
parent c73199185b
commit 64ece501ad
7 changed files with 147 additions and 2 deletions

View file

@ -535,6 +535,8 @@ mod node_tseq;
mod node_tslfo; mod node_tslfo;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
mod node_vosc; mod node_vosc;
#[allow(non_upper_case_globals)]
mod node_scope;
pub mod biquad; pub mod biquad;
pub mod dattorro; pub mod dattorro;
@ -605,6 +607,7 @@ use node_test::Test;
use node_tseq::TSeq; use node_tseq::TSeq;
use node_tslfo::TsLFO; use node_tslfo::TsLFO;
use node_vosc::VOsc; use node_vosc::VOsc;
use node_scope::Scope;
pub const MIDI_MAX_FREQ: f32 = 13289.75; pub const MIDI_MAX_FREQ: f32 = 13289.75;
@ -1403,6 +1406,9 @@ macro_rules! node_list {
fbrd => FbRd UIType::Generic UICategory::IOUtil fbrd => FbRd UIType::Generic UICategory::IOUtil
(0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) (0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0)
[0 sig], [0 sig],
scope => Scope UIType::Generic UICategory::IOUtil
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
[0 sig],
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)
(1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)

77
src/dsp/node_scope.rs Normal file
View file

@ -0,0 +1,77 @@
// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
//use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger};
use crate::dsp::{
DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};
use crate::nodes::{NodeAudioContext, NodeExecContext};
use crate::UnsyncFloatBuf;
/// A simple signal scope
#[derive(Debug, Clone)]
pub struct Scope {
buf: UnsyncFloatBuf,
idx: usize,
}
impl Scope {
pub fn new(_nid: &NodeId) -> Self {
Self { buf: UnsyncFloatBuf::new_with_len(1), idx: 0 }
}
pub const inp: &'static str = "Scope inp\nSignal input.\nRange: (-1..1)\n";
pub const sig: &'static str =
"Scope sig\nSignal output. The exact same signal that was received on 'inp'!\n";
pub const DESC: &'static str = r#"Signal Oscilloscope Probe
This is a signal oscilloscope probe node. You can have up to 16 of these,
which will be displayed in the GUI.
"#;
pub const HELP: &'static str = r#"Scope - Signal Oscilloscope Probe
You can have up to 16 of these probes in your patch. The received signal will be
forwarded to the GUI and you can inspect the waveform there.
"#;
pub fn set_scope_buffer(&mut self, buf: UnsyncFloatBuf) {
self.buf = buf;
}
}
impl DspNode for Scope {
fn outputs() -> usize {
1
}
fn set_sample_rate(&mut self, _srate: f32) {}
fn reset(&mut self) {}
#[inline]
fn process<T: NodeAudioContext>(
&mut self,
ctx: &mut T,
_ectx: &mut NodeExecContext,
_nctx: &NodeContext,
_atoms: &[SAtom],
inputs: &[ProcBuf],
outputs: &mut [ProcBuf],
ctx_vals: LedPhaseVals,
) {
use crate::dsp::{inp, out};
let inp = inp::Ad::inp(inputs);
let out = out::Scope::sig(outputs);
for frame in 0..ctx.nframes() {
let in_val = inp.read(frame);
self.buf.write(self.idx, in_val);
self.idx = (self.idx + 1) % self.buf.len();
out.write(frame, in_val);
}
let last_frame = ctx.nframes() - 1;
ctx_vals[0].set(out.read(last_frame));
}
}

View file

@ -9,6 +9,7 @@ pub use crate::monitor::MON_SIG_CNT;
pub use crate::nodes::MinMaxMonitorSamples; pub use crate::nodes::MinMaxMonitorSamples;
use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES}; use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES};
pub use crate::CellDir; pub use crate::CellDir;
use crate::UnsyncFloatBuf;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@ -581,6 +582,10 @@ impl Matrix {
self.config.get_pattern_data(tracker_id) self.config.get_pattern_data(tracker_id)
} }
pub fn get_scope_buffer(&self, scope: usize) -> Option<UnsyncFloatBuf> {
self.config.get_scope_buffer(scope)
}
/// Checks if pattern data updates need to be sent to the /// Checks if pattern data updates need to be sent to the
/// DSP thread. /// DSP thread.
pub fn check_pattern_data(&mut self, tracker_id: usize) { pub fn check_pattern_data(&mut self, tracker_id: usize) {

View file

@ -3,6 +3,8 @@
// See README.md and COPYING for details. // See README.md and COPYING for details.
pub const MAX_ALLOCATED_NODES: usize = 256; pub const MAX_ALLOCATED_NODES: usize = 256;
pub const MAX_SCOPES: usize = 16;
pub const SCOPE_SAMPLES: usize = 512;
pub const MAX_INPUTS: usize = 32; pub const MAX_INPUTS: usize = 32;
pub const MAX_SMOOTHERS: usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs pub const MAX_SMOOTHERS: usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs
pub const MAX_AVAIL_TRACKERS: usize = 128; pub const MAX_AVAIL_TRACKERS: usize = 128;

View file

@ -3,14 +3,15 @@
// See README.md and COPYING for details. // See README.md and COPYING for details.
use super::{ use super::{
FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS, FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES,
MAX_INPUTS, UNUSED_MONITOR_IDX, MAX_AVAIL_TRACKERS, MAX_INPUTS, UNUSED_MONITOR_IDX, MAX_SCOPES, SCOPE_SAMPLES
}; };
use crate::dsp::tracker::{PatternData, Tracker}; use crate::dsp::tracker::{PatternData, Tracker};
use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom}; use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom};
use crate::monitor::{new_monitor_processor, MinMaxMonitorSamples, Monitor, MON_SIG_CNT}; use crate::monitor::{new_monitor_processor, MinMaxMonitorSamples, Monitor, MON_SIG_CNT};
use crate::nodes::drop_thread::DropThread; use crate::nodes::drop_thread::DropThread;
use crate::util::AtomicFloat; use crate::util::AtomicFloat;
use crate::unsync_float_buf::UnsyncFloatBuf;
use crate::SampleLibrary; use crate::SampleLibrary;
use ringbuf::{Producer, RingBuffer}; use ringbuf::{Producer, RingBuffer};
@ -177,6 +178,8 @@ pub struct NodeConfigurator {
pub(crate) node2idx: HashMap<NodeId, usize>, pub(crate) node2idx: HashMap<NodeId, usize>,
/// Holding the tracker sequencers /// Holding the tracker sequencers
pub(crate) trackers: Vec<Tracker>, pub(crate) trackers: Vec<Tracker>,
/// Holding the scope buffers:
pub(crate) scopes: Vec<UnsyncFloatBuf>,
/// The shared parts of the [NodeConfigurator] /// The shared parts of the [NodeConfigurator]
/// and the [crate::nodes::NodeExecutor]. /// and the [crate::nodes::NodeExecutor].
pub(crate) shared: SharedNodeConf, pub(crate) shared: SharedNodeConf,
@ -279,6 +282,7 @@ impl NodeConfigurator {
atom_values: std::collections::HashMap::new(), atom_values: std::collections::HashMap::new(),
node2idx: HashMap::new(), node2idx: HashMap::new(),
trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS],
scopes: vec![UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES); MAX_SCOPES],
}, },
shared_exec, shared_exec,
) )
@ -638,6 +642,10 @@ impl NodeConfigurator {
} }
} }
pub fn get_scope_buffer(&self, scope: usize) -> Option<UnsyncFloatBuf> {
self.scopes.get(scope).cloned()
}
pub fn get_pattern_data(&self, tracker_id: usize) -> Option<Arc<Mutex<PatternData>>> { pub fn get_pattern_data(&self, tracker_id: usize) -> Option<Arc<Mutex<PatternData>>> {
if tracker_id >= self.trackers.len() { if tracker_id >= self.trackers.len() {
return None; return None;
@ -677,6 +685,12 @@ impl NodeConfigurator {
} }
} }
if let Node::Scope { node } = &mut node {
if let Some(buf) = self.scopes.get(ni.instance()) {
node.set_scope_buffer(buf.clone());
}
}
for i in 0..self.nodes.len() { for i in 0..self.nodes.len() {
if let NodeId::Nop = self.nodes[i].0.to_id() { if let NodeId::Nop = self.nodes[i].0.to_id() {
index = Some(i); index = Some(i);

View file

@ -47,6 +47,11 @@ impl UnsyncFloatBuf {
pub fn read(&self, idx: usize) -> f32 { pub fn read(&self, idx: usize) -> f32 {
self.0.read(idx) self.0.read(idx)
} }
/// Length of the buffer.
pub fn len(&self) -> usize {
self.0.len()
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -87,6 +92,10 @@ impl UnsyncFloatBufImpl {
0.0 0.0
} }
} }
fn len(&self) -> usize {
self.len
}
} }
#[cfg(test)] #[cfg(test)]

32
tests/node_scope.rs Normal file
View file

@ -0,0 +1,32 @@
// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
mod common;
use common::*;
use hexodsp::nodes::SCOPE_SAMPLES;
#[test]
fn check_node_scope_1() {
let (node_conf, mut node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 3, 3);
let mut chain = MatrixCellChain::new(CellDir::B);
chain.node_inp("scope", "inp").place(&mut matrix, 0, 0).unwrap();
matrix.sync().unwrap();
let scope = NodeId::Scope(0);
let inp_p = scope.inp_param("inp").unwrap();
matrix.set_param(inp_p, SAtom::param(1.0));
let _res = run_for_ms(&mut node_exec, 11.0);
let scope = matrix.get_scope_buffer(0).unwrap();
let mut v = vec![];
for x in 0..SCOPE_SAMPLES {
v.push(scope.read(x));
}
assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]);
}