This commit is contained in:
Dimas Leenman 2022-08-07 11:59:32 +02:00
commit 22aa19cf9f
54 changed files with 5288 additions and 3539 deletions

18
CHANGELOG.md Normal file
View file

@ -0,0 +1,18 @@
0.2.0 (unreleased)
==================
* Documentation: Added a guide in the hexodsp::dsp module documentation
about implementing new DSP nodes.
* Bugfix: TriSawLFO (TsLFO) node did output too high values if the `rev`
parameter was changed or modulated at runtime.
* Bugfix: Found a bug in cubic interpolation in the sample player and
similar bugs in the delay line (and all-pass & comb filters). Refactored
the cubic interpolation and tested it seperately now.
* Feature: Matrix::get\_connections() returns information about the connections
to the adjacent cells.
* Feature: Added the MatrixCellChain abstraction for easy creation of DSP
chains on the hexagonal Matrix.
* Feature: Added Scope DSP node and NodeConfigurator/Matrix API for retrieving
the scope handles for access to it's capture buffers.
* Feature: Added WBlockDSP visual programming language utilizing the `synfx-dsp-jit` crate.
* Change: Moved DSP code over to `synfx-dsp` crate.

View file

@ -8,8 +8,8 @@ description = "Comprehensive DSP graph and synthesis library for developing a mo
keywords = ["audio", "music", "real-time", "synthesis", "synthesizer", "dsp", "sound"]
categories = ["multimedia::audio", "multimedia", "algorithms", "mathematics"]
#[features]
#default = [ "hexotk" ]
[features]
default = [ "synfx-dsp-jit" ]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
@ -18,11 +18,14 @@ ringbuf = "0.2.2"
triple_buffer = "5.0.6"
lazy_static = "1.4.0"
hound = "3.4.0"
num-traits = "0.2.14"
synfx-dsp-jit = { git = "https://github.com/WeirdConstructor/synfx-dsp-jit", optional = true }
synfx-dsp = { git = "https://github.com/WeirdConstructor/synfx-dsp" }
#synfx-dsp-jit = { path = "../synfx-dsp-jit", optional = true }
#synfx-dsp = "0.5.1"
[dev-dependencies]
num-complex = "0.2"
jack = "0.6.6"
jack = "0.10.0"
rustfft = "6.0.0"
cpal = "0.13.5"
anyhow = "1.0.58"

View file

@ -1,6 +1,5 @@
# hexodsp
## HexoDSP - Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth.
This project contains the complete DSP backend of the modular
@ -8,7 +7,7 @@ synthesizer [HexoSynth](https://github.com/WeirdConstructor/HexoSynth).
It's aimed to provide a toolkit for everyone who wants to develop
a synthesizer in Rust. You can use it to quickly define a DSP graph
that you can change at runtime. It comes with a large collection
that you can change at runtime. It comes with a (growing) collection
of already developed DSP modules/nodes, such as oscillators, filters,
amplifiers, envelopes and sequencers.
@ -16,14 +15,50 @@ The DSP graph API also provides multiple kinds of feedback to track what the
signals in the DSP threads look like. From monitoring the inputs and outputs of
single nodes to get the current output value of all nodes.
There is also an (optional) JIT compiler for defining custom pieces of DSP code
that runs at native speed in a DSP graph module/node.
Here a short list of features:
* Runtime changeable DSP graph
* Serialization and loading of the DSP graph and the parameters
* Full monitoring and feedback introspection into the running DSP graph
* Provides a wide variety of modules
* Extensible framework for quickly developing new nodes at compile time
* A comprehensive automated test suite
* Runtime changeable DSP graph.
* Serialization and loading of the DSP graph and the parameters.
* Full monitoring and feedback introspection into the running DSP graph.
* Provides a wide variety of modules.
* (Optional) JIT (Just In Time) compiled custom DSP code for integrating your own
DSP algorithms at runtime. One possible frontend language is the visual
"BlockCode" programming language in HexoSynth.
* Extensible framework for quickly adding new nodes to HexoDSP.
* A comprehensive automated test suite covering all modules in HexoDSP.
And following DSP nodes:
| Category | Name | Function |
|-|-|-|
| IO Util | Out | Audio output (to DAW or Jack) |
| Osc | Sampl | Sample player |
| Osc | Sin | Sine oscillator |
| Osc | BOsc | Basic bandlimited waveform oscillator (waveforms: Sin, Tri, Saw, Pulse/Square) |
| Osc | VOsc | Vector phase shaping oscillator |
| Osc | Noise | Noise oscillator |
| Signal | Amp | Amplifier/Attenuator |
| Signal | SFilter | Simple collection of filters, useable for synthesis |
| Signal | Delay | Single tap signal delay |
| Signal | PVerb | Reverb node, based on Dattorros plate reverb algorithm |
| Signal | AllP | All-Pass filter based on internal delay line feedback |
| Signal | Comb | Comb filter |
| Signal | Code | JIT (Just In Time) compiled piece of custom DSP code. |
| N-\>M | Mix3 | 3 channel mixer |
| N-\>M | Mux9 | 9 channel to 1 output multiplexer/switch |
| Ctrl | SMap | Simple control signal mapper |
| Ctrl | Map | Control signal mapper |
| Ctrl | CQnt | Control signal pitch quantizer |
| Ctrl | Quant | Pitch signal quantizer |
| Mod | TSeq | Tracker/pattern sequencer |
| Mod | Ad | Attack-Decay envelope |
| Mod | TsLFO | Tri/Saw waveform low frequency oscillator (LFO) |
| Mod | RndWk | Random walker, a Sample & Hold noise generator |
| IO Util | FbWr / FbRd | Utility modules for feedback in patches |
| IO Util | Scope | Oscilloscope for up to 3 channels |
### API Examples
@ -74,9 +109,9 @@ This is a short overview of the API provided by the
hexagonal Matrix API, which is the primary API used
inside [HexoSynth](https://github.com/WeirdConstructor/HexoSynth).
This only showcases the non-realtime generation of audio
samples. For a real time application of this library please
refer to the examples that come with this library.
This only showcases the direct generation of audio samples, without any audio
device playing it. For a real time application of this library please refer to
the examples that come with this library.
```rust
use hexodsp::*;
@ -84,13 +119,16 @@ use hexodsp::*;
let (node_conf, mut node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 3, 3);
let sin = NodeId::Sin(0);
let amp = NodeId::Amp(0);
let out = NodeId::Out(0);
matrix.place(0, 0, Cell::empty(sin)
.out(None, None, sin.out("sig")));
matrix.place(0, 1, Cell::empty(amp)
.input(amp.inp("inp"), None, None));
.input(amp.inp("inp"), None, None)
.out(None, None, amp.out("sig")));
matrix.place(0, 2, Cell::empty(out)
.input(out.inp("inp"), None, None));
matrix.sync().unwrap();
let gain_p = amp.inp_param("gain").unwrap();
@ -101,9 +139,33 @@ let (out_l, out_r) = node_exec.test_run(0.11, true);
// samples now.
```
#### Simplified Hexagonal Matrix API
There is also a simplified version for easier setup of DSP chains
on the hexagonal grid, using the [crate::MatrixCellChain] abstraction:
```rust
use hexodsp::*;
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_out("sin", "sig")
.node_io("amp", "inp", "sig")
.set_atom("gain", SAtom::param(0.25))
.node_inp("out", "ch1")
.place(&mut matrix, 0, 0);
matrix.sync().unwrap();
let (out_l, out_r) = node_exec.test_run(0.11, true);
// out_l and out_r contain two channels of audio
// samples now.
```
### State of Development
As of 2021-05-18: The architecture and it's functionality have been mostly
As of 2022-07-30: The architecture and it's functionality have been mostly
feature complete by now. The only part that is still lacking is the collection
of modules/nodes, this is the area of current development. Adding lots of
nodes.
@ -159,6 +221,9 @@ devote for project coordination. So please don't be offended if your issue rots
in the GitHub issue tracker, or your pull requests is left dangling around
for ages.
If you want to contribute new DSP nodes/modules to HexoDSP/HexoSynth,
please look into the guide at the start of the [crate::dsp] module.
I might merge pull requests if I find the time and think that the contributions
are in line with my vision.

View file

@ -147,11 +147,6 @@ impl jack::NotificationHandler for Notifications {
println!("JACK: freewheel mode is {}", if is_enabled { "on" } else { "off" });
}
fn buffer_size(&mut self, _: &jack::Client, sz: jack::Frames) -> jack::Control {
println!("JACK: buffer size changed to {}", sz);
jack::Control::Continue
}
fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control {
println!("JACK: sample rate changed to {}", srate);
let mut ne = self.node_exec.lock().unwrap();
@ -215,16 +210,6 @@ impl jack::NotificationHandler for Notifications {
println!("JACK: xrun occurred");
jack::Control::Continue
}
fn latency(&mut self, _: &jack::Client, mode: jack::LatencyType) {
println!(
"JACK: {} latency has changed",
match mode {
jack::LatencyType::Capture => "capture",
jack::LatencyType::Playback => "playback",
}
);
}
}
// This function starts the Jack backend and

321
src/chain_builder.rs Normal file
View file

@ -0,0 +1,321 @@
// Copyright (c) 2021-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.
/*! Defines an API for easy DSP chain building with the hexagonal [crate::Matrix].
The [crate::MatrixCellChain] abstractions allows very easy placement of DSP signal chains:
```
use hexodsp::*;
let mut chain = MatrixCellChain::new(CellDir::BR);
chain.node_out("sin", "sig")
.set_denorm("freq", 220.0)
.node_io("amp", "inp", "sig")
.set_denorm("att", 0.5)
.node_inp("out", "ch1");
// use crate::nodes::new_node_engine;
let (node_conf, _node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 7, 7);
chain.place(&mut matrix, 2, 2).expect("no error in this case");
```
*/
use crate::{Cell, CellDir, Matrix, NodeId, ParamId, SAtom};
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct MatrixChainLink {
cell: Cell,
dir: CellDir,
params: Vec<(ParamId, SAtom)>,
}
/// A DSP chain builder for the [crate::Matrix].
///
/// This is an extremely easy API to create and place new DSP chains into the [crate::Matrix].
/// It can be used by frontends to place DSP chains on user request or it can be used
/// by test cases to quickly fill the hexagonal Matrix.
///
///```
/// use hexodsp::*;
/// let mut chain = MatrixCellChain::new(CellDir::BR);
/// chain.node_out("sin", "sig")
/// .set_denorm("freq", 220.0)
/// .node_io("amp", "inp", "sig")
/// .set_denorm("att", 0.5)
/// .node_inp("out", "ch1");
///
/// // use crate::nodes::new_node_engine;
/// let (node_conf, _node_exec) = new_node_engine();
/// let mut matrix = Matrix::new(node_conf, 7, 7);
///
/// chain.place(&mut matrix, 2, 2).expect("no error in this case");
///```
#[derive(Debug, Clone)]
pub struct MatrixCellChain {
chain: Vec<MatrixChainLink>,
error: Option<ChainError>,
dir: CellDir,
param_idx: usize,
}
/// Error type for the [crate::MatrixCellChain].
#[derive(Debug, Clone)]
pub enum ChainError {
UnknownNodeId(String),
UnknownOutput(NodeId, String),
UnknownInput(NodeId, String),
}
impl MatrixCellChain {
/// Create a new [MatrixCellChain] with the given placement direction.
///
/// The direction is used to guide the placement of the cells.
pub fn new(dir: CellDir) -> Self {
Self { dir, chain: vec![], error: None, param_idx: 0 }
}
fn output_dir(&self) -> CellDir {
if self.dir.is_output() {
self.dir
} else {
self.dir.flip()
}
}
fn input_dir(&self) -> CellDir {
if self.dir.is_input() {
self.dir
} else {
self.dir.flip()
}
}
/// Sets the current parameter cell by chain index.
pub fn params_for_idx(&mut self, idx: usize) -> &mut Self {
self.param_idx = idx;
if self.param_idx >= self.chain.len() {
self.param_idx = self.chain.len();
}
self
}
/// Sets the denormalized value of the current parameter cell's parameter.
///
/// The current parameter cell is set automatically when a new node is added.
/// Alternatively you can use [MatrixCellChain::params_for_idx] to set the current
/// parameter cell.
pub fn set_denorm(&mut self, param: &str, denorm: f32) -> &mut Self {
let link = self.chain.get_mut(self.param_idx).expect("Correct parameter idx");
if let Some(pid) = link.cell.node_id().inp_param(param) {
link.params.push((pid, SAtom::param(pid.norm(denorm as f32))));
} else {
self.error = Some(ChainError::UnknownInput(link.cell.node_id(), param.to_string()));
}
self
}
/// Sets the atom value of the current parameter cell's parameter.
///
/// The current parameter cell is set automatically when a new node is added.
/// Alternatively you can use [MatrixCellChain::params_for_idx] to set the current
/// parameter cell.
pub fn set_atom(&mut self, param: &str, at: SAtom) -> &mut Self {
let link = self.chain.get_mut(self.param_idx).expect("Correct parameter idx");
if let Some(pid) = link.cell.node_id().inp_param(param) {
link.params.push((pid, at));
} else {
self.error = Some(ChainError::UnknownInput(link.cell.node_id(), param.to_string()));
}
self
}
/// Utility function for creating [crate::Cell] for this chain.
pub fn spawn_cell_from_node_id_name(&mut self, node_id_name: &str) -> Option<Cell> {
let node_id = NodeId::from_str(node_id_name);
if node_id == NodeId::Nop && node_id_name != "nop" {
return None;
}
Some(Cell::empty(node_id))
}
/// Utility function to add a pre-built [crate::Cell] as next link.
///
/// This also sets the current parameter cell.
pub fn add_link(&mut self, cell: Cell) {
self.chain.push(MatrixChainLink { dir: self.dir, cell, params: vec![] });
self.param_idx = self.chain.len() - 1;
}
/// Place a new node in the chain without any inputs or outputs. This is of limited
/// use in this API, but might makes a few corner cases easier in test cases.
pub fn node(&mut self, node_id: &str) -> &mut Self {
if let Some(cell) = self.spawn_cell_from_node_id_name(node_id) {
self.add_link(cell);
} else {
self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
}
self
}
/// Place a new node in the chain with the given output assigned.
pub fn node_out(&mut self, node_id: &str, out: &str) -> &mut Self {
if let Some(mut cell) = self.spawn_cell_from_node_id_name(node_id) {
if let Err(()) = cell.set_output_by_name(out, self.output_dir()) {
self.error = Some(ChainError::UnknownOutput(cell.node_id(), out.to_string()));
}
self.add_link(cell);
} else {
self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
}
self
}
/// Place a new node in the chain with the given input assigned.
pub fn node_inp(&mut self, node_id: &str, inp: &str) -> &mut Self {
if let Some(mut cell) = self.spawn_cell_from_node_id_name(node_id) {
if let Err(()) = cell.set_input_by_name(inp, self.input_dir()) {
self.error = Some(ChainError::UnknownInput(cell.node_id(), inp.to_string()));
}
self.add_link(cell);
} else {
self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
}
self
}
/// Place a new node in the chain with the given input and output assigned.
pub fn node_io(&mut self, node_id: &str, inp: &str, out: &str) -> &mut Self {
if let Some(mut cell) = self.spawn_cell_from_node_id_name(node_id) {
if let Err(()) = cell.set_input_by_name(inp, self.input_dir()) {
self.error = Some(ChainError::UnknownInput(cell.node_id(), inp.to_string()));
}
if let Err(()) = cell.set_output_by_name(out, self.output_dir()) {
self.error = Some(ChainError::UnknownOutput(cell.node_id(), out.to_string()));
}
self.add_link(cell);
} else {
self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
}
self
}
/// Places the chain into the matrix at the given position.
///
/// If any error occured while building the chain (such as bad input/output names
/// or unknown parameters), it will be returned here.
pub fn place(
&mut self,
matrix: &mut Matrix,
at_x: usize,
at_y: usize,
) -> Result<(), ChainError> {
if let Some(err) = self.error.take() {
return Err(err);
}
let mut last_unused = HashMap::new();
let mut pos = (at_x, at_y);
for link in self.chain.iter() {
let (x, y) = pos;
let mut cell = link.cell.clone();
let node_id = cell.node_id();
let node_name = node_id.name();
let node_id = if let Some(i) = last_unused.get(node_name).cloned() {
last_unused.insert(node_name.to_string(), i + 1);
node_id.to_instance(i + 1)
} else {
let node_id = matrix.get_unused_instance_node_id(node_id);
last_unused.insert(node_name.to_string(), node_id.instance());
node_id
};
cell.set_node_id_keep_ios(node_id);
matrix.place(x, y, cell);
let offs = link.dir.as_offs(pos.0);
pos.0 = (pos.0 as i32 + offs.0) as usize;
pos.1 = (pos.1 as i32 + offs.1) as usize;
}
for link in self.chain.iter() {
for (pid, at) in link.params.iter() {
matrix.set_param(*pid, at.clone());
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_matrix_chain_builder_1() {
use crate::nodes::new_node_engine;
let (node_conf, _node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 7, 7);
let mut chain = MatrixCellChain::new(CellDir::B);
chain
.node_out("sin", "sig")
.set_denorm("freq", 220.0)
.node_io("amp", "inp", "sig")
.set_denorm("att", 0.5)
.node_inp("out", "ch1");
chain.params_for_idx(0).set_atom("det", SAtom::param(0.1));
chain.place(&mut matrix, 2, 2).expect("no error in this case");
matrix.sync().expect("Sync ok");
let cell_sin = matrix.get(2, 2).unwrap();
assert_eq!(cell_sin.node_id(), NodeId::Sin(0));
let cell_amp = matrix.get(2, 3).unwrap();
assert_eq!(cell_amp.node_id(), NodeId::Amp(0));
let cell_out = matrix.get(2, 4).unwrap();
assert_eq!(cell_out.node_id(), NodeId::Out(0));
assert_eq!(
format!("{:?}", matrix.get_param(&NodeId::Sin(0).inp_param("freq").unwrap()).unwrap()),
"Param(-0.1)"
);
assert_eq!(
format!("{:?}", matrix.get_param(&NodeId::Sin(0).inp_param("det").unwrap()).unwrap()),
"Param(0.1)"
);
assert_eq!(
format!("{:?}", matrix.get_param(&NodeId::Amp(0).inp_param("att").unwrap()).unwrap()),
"Param(0.70710677)"
);
}
}

View file

@ -1,272 +0,0 @@
// Copyright (c) 2021 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.
//
// The implementation of this Biquad Filter has been adapted from
// SamiPerttu, Copyright (c) 2020, under the MIT License.
// See also: https://github.com/SamiPerttu/fundsp/blob/master/src/filter.rs
//
// You will find a float type agnostic version in SamiPerttu's code.
// I converted this to pure f32 for no good reason, other than making
// the code more readable (for me).
use std::f32::consts::*;
#[derive(Copy, Clone, Debug, Default)]
pub struct BiquadCoefs {
pub a1: f32,
pub a2: f32,
pub b0: f32,
pub b1: f32,
pub b2: f32,
}
// TODO:
// https://github.com/VCVRack/Befaco/blob/v1/src/ChowDSP.hpp#L339
// more coeffs from there ^^^^^^^^^^^^^ ?
impl BiquadCoefs {
#[inline]
pub fn new(b0: f32, b1: f32, b2: f32, a1: f32, a2: f32) -> Self {
Self { b0, b1, b2, a1, a2 }
}
/// Returns settings for a Butterworth lowpass filter.
/// Cutoff is the -3 dB point of the filter in Hz.
#[inline]
pub fn butter_lowpass(sample_rate: f32, cutoff: f32) -> BiquadCoefs {
let f = (cutoff * PI / sample_rate).tan();
let a0r = 1.0 / (1.0 + SQRT_2 * f + f * f);
let a1 = (2.0 * f * f - 2.0) * a0r;
let a2 = (1.0 - SQRT_2 * f + f * f) * a0r;
let b0 = f * f * a0r;
let b1 = 2.0 * b0;
let b2 = b0;
BiquadCoefs { a1, a2, b0, b1, b2 }
}
/// Returns the Q for cascading a butterworth filter:
fn calc_cascaded_butter_q(order: usize, casc_idx: usize) -> f32 {
let order = order as f32;
let casc_idx = casc_idx as f32;
let b = -2.0 * ((2.0 * casc_idx + order - 1.0) * PI / (2.0 * order)).cos();
1.0 / b
}
/// Returns settings for a lowpass filter with a specific q
#[inline]
pub fn lowpass(sample_rate: f32, q: f32, cutoff: f32) -> BiquadCoefs {
let f = (cutoff * PI / sample_rate).tan();
let a0r = 1.0 / (1.0 + f / q + f * f);
/*
float norm = 1.f / (1.f + K / Q + K * K);
this->b[0] = K * K * norm;
this->b[1] = 2.f * this->b[0];
this->b[2] = this->b[0];
this->a[1] = 2.f * (K * K - 1.f) * norm;
this->a[2] = (1.f - K / Q + K * K) * norm;
*/
let b0 = f * f * a0r;
let b1 = 2.0 * b0;
let b2 = b0;
let a1 = 2.0 * (f * f - 1.0) * a0r;
let a2 = (1.0 - f / q + f * f) * a0r;
BiquadCoefs { a1, a2, b0, b1, b2 }
}
/// Returns settings for a constant-gain bandpass resonator.
/// The center frequency is given in Hz.
/// Bandwidth is the difference in Hz between -3 dB points of the filter response.
/// The overall gain of the filter is independent of bandwidth.
pub fn resonator(sample_rate: f32, center: f32, bandwidth: f32) -> BiquadCoefs {
let r = (-PI * bandwidth / sample_rate).exp();
let a1 = -2.0 * r * (TAU * center / sample_rate).cos();
let a2 = r * r;
let b0 = (1.0 - r * r).sqrt() * 0.5;
let b1 = 0.0;
let b2 = -b0;
BiquadCoefs { a1, a2, b0, b1, b2 }
}
// /// Frequency response at frequency `omega` expressed as fraction of sampling rate.
// pub fn response(&self, omega: f64) -> Complex64 {
// let z1 = Complex64::from_polar(1.0, -TAU * omega);
// let z2 = Complex64::from_polar(1.0, -2.0 * TAU * omega);
// (re(self.b0) + re(self.b1) * z1 + re(self.b2) * z2)
// / (re(1.0) + re(self.a1) * z1 + re(self.a2) * z2)
// }
}
/// 2nd order IIR filter implemented in normalized Direct Form I.
#[derive(Debug, Copy, Clone, Default)]
pub struct Biquad {
coefs: BiquadCoefs,
x1: f32,
x2: f32,
y1: f32,
y2: f32,
}
impl Biquad {
pub fn new() -> Self {
Default::default()
}
#[inline]
pub fn new_with(b0: f32, b1: f32, b2: f32, a1: f32, a2: f32) -> Self {
let mut s = Self::new();
s.set_coefs(BiquadCoefs::new(b0, b1, b2, a1, a2));
s
}
#[inline]
pub fn coefs(&self) -> &BiquadCoefs {
&self.coefs
}
#[inline]
pub fn set_coefs(&mut self, coefs: BiquadCoefs) {
self.coefs = coefs;
}
pub fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
#[inline]
pub fn tick(&mut self, input: f32) -> f32 {
let x0 = input;
let y0 = self.coefs.b0 * x0 + self.coefs.b1 * self.x1 + self.coefs.b2 * self.x2
- self.coefs.a1 * self.y1
- self.coefs.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x0;
self.y2 = self.y1;
self.y1 = y0;
y0
// Transposed Direct Form II would be:
// y0 = b0 * x0 + s1
// s1 = s2 + b1 * x0 - a1 * y0
// s2 = b2 * x0 - a2 * y0
}
}
#[derive(Copy, Clone)]
pub struct ButterLowpass {
biquad: Biquad,
sample_rate: f32,
cutoff: f32,
}
#[allow(dead_code)]
impl ButterLowpass {
pub fn new(sample_rate: f32, cutoff: f32) -> Self {
let mut this = ButterLowpass { biquad: Biquad::new(), sample_rate, cutoff: 0.0 };
this.set_cutoff(cutoff);
this
}
pub fn set_cutoff(&mut self, cutoff: f32) {
self.biquad.set_coefs(BiquadCoefs::butter_lowpass(self.sample_rate, cutoff));
self.cutoff = cutoff;
}
fn set_sample_rate(&mut self, srate: f32) {
self.sample_rate = srate;
self.reset();
self.biquad.reset();
self.set_cutoff(self.cutoff);
}
fn reset(&mut self) {
self.biquad.reset();
self.set_cutoff(self.cutoff);
}
#[inline]
fn tick(&mut self, input: f32) -> f32 {
self.biquad.tick(input)
}
}
// Loosely adapted from https://github.com/VCVRack/Befaco/blob/v1/src/ChowDSP.hpp
// Copyright (c) 2019-2020 Andrew Belt and Befaco contributors
// Under GPLv-3.0-or-later
//
// Which was originally taken from https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp
// Copyright (c) 2020 jatinchowdhury18
/// Implements oversampling with a ratio of N and a 4 times cascade
/// of Butterworth lowpass filters (~48dB?).
#[derive(Debug, Copy, Clone)]
pub struct Oversampling<const N: usize> {
filters: [Biquad; 4],
buffer: [f32; N],
}
impl<const N: usize> Oversampling<N> {
pub fn new() -> Self {
let mut this = Self { filters: [Biquad::new(); 4], buffer: [0.0; N] };
this.set_sample_rate(44100.0);
this
}
pub fn reset(&mut self) {
self.buffer = [0.0; N];
for filt in &mut self.filters {
filt.reset();
}
}
pub fn set_sample_rate(&mut self, srate: f32) {
let cutoff = 0.98 * (0.5 * srate);
let ovr_srate = (N as f32) * srate;
let filters_len = self.filters.len();
for (i, filt) in self.filters.iter_mut().enumerate() {
let q = BiquadCoefs::calc_cascaded_butter_q(2 * 4, filters_len - i);
filt.set_coefs(BiquadCoefs::lowpass(ovr_srate, q, cutoff));
}
}
#[inline]
pub fn upsample(&mut self, v: f32) {
self.buffer.fill(0.0);
self.buffer[0] = (N as f32) * v;
for s in &mut self.buffer {
for filt in &mut self.filters {
*s = filt.tick(*s);
}
}
}
#[inline]
pub fn resample_buffer(&mut self) -> &mut [f32; N] {
&mut self.buffer
}
#[inline]
pub fn downsample(&mut self) -> f32 {
let mut ret = 0.0;
for s in &mut self.buffer {
ret = *s;
for filt in &mut self.filters {
ret = filt.tick(ret);
}
}
ret
}
}

View file

@ -1,443 +0,0 @@
// Copyright (c) 2021 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.
// This file contains a reverb implementation that is based
// on Jon Dattorro's 1997 reverb algorithm. It's also largely
// based on the C++ implementation from ValleyAudio / ValleyRackFree
//
// ValleyRackFree Copyright (C) 2020, Valley Audio Soft, Dale Johnson
// Adapted under the GPL-3.0-or-later License.
//
// See also: https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Plateau/Dattorro.cpp
// and: https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Plateau/Dattorro.hpp
//
// And: https://ccrma.stanford.edu/~dattorro/music.html
// And: https://ccrma.stanford.edu/~dattorro/EffectDesignPart1.pdf
use crate::dsp::helpers::crossfade;
const DAT_SAMPLE_RATE: f64 = 29761.0;
const DAT_SAMPLES_PER_MS: f64 = DAT_SAMPLE_RATE / 1000.0;
const DAT_INPUT_APF_TIMES_MS: [f64; 4] = [
141.0 / DAT_SAMPLES_PER_MS,
107.0 / DAT_SAMPLES_PER_MS,
379.0 / DAT_SAMPLES_PER_MS,
277.0 / DAT_SAMPLES_PER_MS,
];
const DAT_LEFT_APF1_TIME_MS: f64 = 672.0 / DAT_SAMPLES_PER_MS;
const DAT_LEFT_APF2_TIME_MS: f64 = 1800.0 / DAT_SAMPLES_PER_MS;
const DAT_RIGHT_APF1_TIME_MS: f64 = 908.0 / DAT_SAMPLES_PER_MS;
const DAT_RIGHT_APF2_TIME_MS: f64 = 2656.0 / DAT_SAMPLES_PER_MS;
const DAT_LEFT_DELAY1_TIME_MS: f64 = 4453.0 / DAT_SAMPLES_PER_MS;
const DAT_LEFT_DELAY2_TIME_MS: f64 = 3720.0 / DAT_SAMPLES_PER_MS;
const DAT_RIGHT_DELAY1_TIME_MS: f64 = 4217.0 / DAT_SAMPLES_PER_MS;
const DAT_RIGHT_DELAY2_TIME_MS: f64 = 3163.0 / DAT_SAMPLES_PER_MS;
const DAT_LEFT_TAPS_TIME_MS: [f64; 7] = [
266.0 / DAT_SAMPLES_PER_MS,
2974.0 / DAT_SAMPLES_PER_MS,
1913.0 / DAT_SAMPLES_PER_MS,
1996.0 / DAT_SAMPLES_PER_MS,
1990.0 / DAT_SAMPLES_PER_MS,
187.0 / DAT_SAMPLES_PER_MS,
1066.0 / DAT_SAMPLES_PER_MS,
];
const DAT_RIGHT_TAPS_TIME_MS: [f64; 7] = [
353.0 / DAT_SAMPLES_PER_MS,
3627.0 / DAT_SAMPLES_PER_MS,
1228.0 / DAT_SAMPLES_PER_MS,
2673.0 / DAT_SAMPLES_PER_MS,
2111.0 / DAT_SAMPLES_PER_MS,
335.0 / DAT_SAMPLES_PER_MS,
121.0 / DAT_SAMPLES_PER_MS,
];
const DAT_LFO_FREQS_HZ: [f64; 4] = [0.1, 0.15, 0.12, 0.18];
const DAT_INPUT_DIFFUSION1: f64 = 0.75;
const DAT_INPUT_DIFFUSION2: f64 = 0.625;
const DAT_PLATE_DIFFUSION1: f64 = 0.7;
const DAT_PLATE_DIFFUSION2: f64 = 0.5;
const DAT_LFO_EXCURSION_MS: f64 = 16.0 / DAT_SAMPLES_PER_MS;
const DAT_LFO_EXCURSION_MOD_MAX: f64 = 16.0;
use crate::dsp::helpers::{AllPass, DCBlockFilter, DelayBuffer, OnePoleHPF, OnePoleLPF, TriSawLFO};
#[derive(Debug, Clone)]
pub struct DattorroReverb {
last_scale: f64,
inp_dc_block: [DCBlockFilter<f64>; 2],
out_dc_block: [DCBlockFilter<f64>; 2],
lfos: [TriSawLFO<f64>; 4],
input_hpf: OnePoleHPF<f64>,
input_lpf: OnePoleLPF<f64>,
pre_delay: DelayBuffer<f64>,
input_apfs: [(AllPass<f64>, f64, f64); 4],
apf1: [(AllPass<f64>, f64, f64); 2],
hpf: [OnePoleHPF<f64>; 2],
lpf: [OnePoleLPF<f64>; 2],
apf2: [(AllPass<f64>, f64, f64); 2],
delay1: [(DelayBuffer<f64>, f64); 2],
delay2: [(DelayBuffer<f64>, f64); 2],
left_sum: f64,
right_sum: f64,
dbg_count: usize,
}
pub trait DattorroReverbParams {
/// Time for the pre-delay of the reverb. Any sensible `ms` that fits
/// into a delay buffer of 5 seconds.
fn pre_delay_time_ms(&self) -> f64;
/// The size of the reverb, values go from 0.0 to 1.0.
fn time_scale(&self) -> f64;
/// High-pass input filter cutoff freq in Hz, range: 0.0 to 22000.0
fn input_high_cutoff_hz(&self) -> f64;
/// Low-pass input filter cutoff freq in Hz, range: 0.0 to 22000.0
fn input_low_cutoff_hz(&self) -> f64;
/// High-pass reverb filter cutoff freq in Hz, range: 0.0 to 22000.0
fn reverb_high_cutoff_hz(&self) -> f64;
/// Low-pass reverb filter cutoff freq in Hz, range: 0.0 to 22000.0
fn reverb_low_cutoff_hz(&self) -> f64;
/// Modulation speed factor, range: 0.0 to 1.0
fn mod_speed(&self) -> f64;
/// Modulation depth from the LFOs, range: 0.0 to 1.0
fn mod_depth(&self) -> f64;
/// Modulation shape (from saw to tri to saw), range: 0.0 to 1.0
fn mod_shape(&self) -> f64;
/// The mix between output from the pre-delay and the input diffusion.
/// range: 0.0 to 1.0. Default should be 1.0
fn input_diffusion_mix(&self) -> f64;
/// The amount of plate diffusion going on, range: 0.0 to 1.0
fn diffusion(&self) -> f64;
/// Internal tank decay time, range: 0.0 to 1.0
fn decay(&self) -> f64;
}
impl DattorroReverb {
pub fn new() -> Self {
let mut this = Self {
last_scale: 1.0,
inp_dc_block: [DCBlockFilter::new(); 2],
out_dc_block: [DCBlockFilter::new(); 2],
lfos: [TriSawLFO::new(); 4],
input_hpf: OnePoleHPF::new(),
input_lpf: OnePoleLPF::new(),
pre_delay: DelayBuffer::new(),
input_apfs: Default::default(),
apf1: Default::default(),
hpf: [OnePoleHPF::new(); 2],
lpf: [OnePoleLPF::new(); 2],
apf2: Default::default(),
delay1: Default::default(),
delay2: Default::default(),
left_sum: 0.0,
right_sum: 0.0,
dbg_count: 0,
};
this.reset();
this
}
pub fn reset(&mut self) {
self.input_lpf.reset();
self.input_hpf.reset();
self.input_lpf.set_freq(22000.0);
self.input_hpf.set_freq(0.0);
self.input_apfs[0] = (AllPass::new(), DAT_INPUT_APF_TIMES_MS[0], DAT_INPUT_DIFFUSION1);
self.input_apfs[1] = (AllPass::new(), DAT_INPUT_APF_TIMES_MS[1], DAT_INPUT_DIFFUSION1);
self.input_apfs[2] = (AllPass::new(), DAT_INPUT_APF_TIMES_MS[2], DAT_INPUT_DIFFUSION2);
self.input_apfs[3] = (AllPass::new(), DAT_INPUT_APF_TIMES_MS[3], DAT_INPUT_DIFFUSION2);
self.apf1[0] = (AllPass::new(), DAT_LEFT_APF1_TIME_MS, -DAT_PLATE_DIFFUSION1);
self.apf1[1] = (AllPass::new(), DAT_RIGHT_APF1_TIME_MS, -DAT_PLATE_DIFFUSION1);
self.apf2[0] = (AllPass::new(), DAT_LEFT_APF2_TIME_MS, -DAT_PLATE_DIFFUSION2);
self.apf2[1] = (AllPass::new(), DAT_RIGHT_APF2_TIME_MS, -DAT_PLATE_DIFFUSION2);
self.delay1[0] = (DelayBuffer::new(), DAT_LEFT_DELAY1_TIME_MS);
self.delay1[1] = (DelayBuffer::new(), DAT_RIGHT_DELAY1_TIME_MS);
self.delay2[0] = (DelayBuffer::new(), DAT_LEFT_DELAY2_TIME_MS);
self.delay2[1] = (DelayBuffer::new(), DAT_RIGHT_DELAY2_TIME_MS);
self.lpf[0].reset();
self.lpf[1].reset();
self.lpf[0].set_freq(10000.0);
self.lpf[1].set_freq(10000.0);
self.hpf[0].reset();
self.hpf[1].reset();
self.hpf[0].set_freq(0.0);
self.hpf[1].set_freq(0.0);
self.lfos[0].set(DAT_LFO_FREQS_HZ[0], 0.5);
self.lfos[0].set_phase_offs(0.0);
self.lfos[0].reset();
self.lfos[1].set(DAT_LFO_FREQS_HZ[1], 0.5);
self.lfos[1].set_phase_offs(0.25);
self.lfos[1].reset();
self.lfos[2].set(DAT_LFO_FREQS_HZ[2], 0.5);
self.lfos[2].set_phase_offs(0.5);
self.lfos[2].reset();
self.lfos[3].set(DAT_LFO_FREQS_HZ[3], 0.5);
self.lfos[3].set_phase_offs(0.75);
self.lfos[3].reset();
self.inp_dc_block[0].reset();
self.inp_dc_block[1].reset();
self.out_dc_block[0].reset();
self.out_dc_block[1].reset();
self.pre_delay.reset();
self.left_sum = 0.0;
self.right_sum = 0.0;
self.set_time_scale(1.0);
}
#[inline]
pub fn set_time_scale(&mut self, scale: f64) {
if (self.last_scale - scale).abs() > std::f64::EPSILON {
let scale = scale.max(0.1);
self.last_scale = scale;
self.apf1[0].1 = DAT_LEFT_APF1_TIME_MS * scale;
self.apf1[1].1 = DAT_RIGHT_APF1_TIME_MS * scale;
self.apf2[0].1 = DAT_LEFT_APF2_TIME_MS * scale;
self.apf2[1].1 = DAT_RIGHT_APF2_TIME_MS * scale;
self.delay1[0].1 = DAT_LEFT_DELAY1_TIME_MS * scale;
self.delay1[1].1 = DAT_RIGHT_DELAY1_TIME_MS * scale;
self.delay2[0].1 = DAT_LEFT_DELAY2_TIME_MS * scale;
self.delay2[1].1 = DAT_RIGHT_DELAY2_TIME_MS * scale;
}
}
pub fn set_sample_rate(&mut self, srate: f64) {
self.inp_dc_block[0].set_sample_rate(srate);
self.inp_dc_block[1].set_sample_rate(srate);
self.out_dc_block[0].set_sample_rate(srate);
self.out_dc_block[1].set_sample_rate(srate);
self.lfos[0].set_sample_rate(srate);
self.lfos[1].set_sample_rate(srate);
self.lfos[2].set_sample_rate(srate);
self.lfos[3].set_sample_rate(srate);
self.input_hpf.set_sample_rate(srate);
self.input_lpf.set_sample_rate(srate);
self.pre_delay.set_sample_rate(srate);
self.input_apfs[0].0.set_sample_rate(srate);
self.input_apfs[1].0.set_sample_rate(srate);
self.input_apfs[2].0.set_sample_rate(srate);
self.input_apfs[3].0.set_sample_rate(srate);
self.apf1[0].0.set_sample_rate(srate);
self.apf1[1].0.set_sample_rate(srate);
self.apf2[0].0.set_sample_rate(srate);
self.apf2[1].0.set_sample_rate(srate);
self.hpf[0].set_sample_rate(srate);
self.hpf[1].set_sample_rate(srate);
self.lpf[0].set_sample_rate(srate);
self.lpf[1].set_sample_rate(srate);
self.delay1[0].0.set_sample_rate(srate);
self.delay1[1].0.set_sample_rate(srate);
self.delay2[0].0.set_sample_rate(srate);
self.delay2[1].0.set_sample_rate(srate);
}
#[inline]
fn calc_apf_delay_times(
&mut self,
params: &mut dyn DattorroReverbParams,
) -> (f64, f64, f64, f64) {
let left_apf1_delay_ms = self.apf1[0].1
+ (self.lfos[0].next_bipolar() as f64
* DAT_LFO_EXCURSION_MS
* DAT_LFO_EXCURSION_MOD_MAX
* params.mod_depth());
let right_apf1_delay_ms = self.apf1[1].1
+ (self.lfos[1].next_bipolar() as f64
* DAT_LFO_EXCURSION_MS
* DAT_LFO_EXCURSION_MOD_MAX
* params.mod_depth());
let left_apf2_delay_ms = self.apf2[0].1
+ (self.lfos[2].next_bipolar() as f64
* DAT_LFO_EXCURSION_MS
* DAT_LFO_EXCURSION_MOD_MAX
* params.mod_depth());
let right_apf2_delay_ms = self.apf2[1].1
+ (self.lfos[3].next_bipolar() as f64
* DAT_LFO_EXCURSION_MS
* DAT_LFO_EXCURSION_MOD_MAX
* params.mod_depth());
(left_apf1_delay_ms, right_apf1_delay_ms, left_apf2_delay_ms, right_apf2_delay_ms)
}
pub fn process(
&mut self,
params: &mut dyn DattorroReverbParams,
input_l: f64,
input_r: f64,
) -> (f64, f64) {
// Some parameter setup...
let timescale = 0.1 + (4.0 - 0.1) * params.time_scale();
self.set_time_scale(timescale);
self.hpf[0].set_freq(params.reverb_high_cutoff_hz());
self.hpf[1].set_freq(params.reverb_high_cutoff_hz());
self.lpf[0].set_freq(params.reverb_low_cutoff_hz());
self.lpf[1].set_freq(params.reverb_low_cutoff_hz());
let mod_speed = params.mod_speed();
let mod_speed = mod_speed * mod_speed;
let mod_speed = mod_speed * 99.0 + 1.0;
self.lfos[0].set(DAT_LFO_FREQS_HZ[0] * mod_speed, params.mod_shape());
self.lfos[1].set(DAT_LFO_FREQS_HZ[1] * mod_speed, params.mod_shape());
self.lfos[2].set(DAT_LFO_FREQS_HZ[2] * mod_speed, params.mod_shape());
self.lfos[3].set(DAT_LFO_FREQS_HZ[3] * mod_speed, params.mod_shape());
self.apf1[0].2 = -DAT_PLATE_DIFFUSION1 * params.diffusion();
self.apf1[1].2 = -DAT_PLATE_DIFFUSION1 * params.diffusion();
self.apf2[0].2 = DAT_PLATE_DIFFUSION2 * params.diffusion();
self.apf2[1].2 = DAT_PLATE_DIFFUSION2 * params.diffusion();
let (left_apf1_delay_ms, right_apf1_delay_ms, left_apf2_delay_ms, right_apf2_delay_ms) =
self.calc_apf_delay_times(params);
// Parameter setup done!
// Input into their corresponding DC blockers
let input_r = self.inp_dc_block[0].next(input_r);
let input_l = self.inp_dc_block[1].next(input_l);
// Sum of DC outputs => LPF => HPF
self.input_lpf.set_freq(params.input_low_cutoff_hz());
self.input_hpf.set_freq(params.input_high_cutoff_hz());
let out_lpf = self.input_lpf.process(input_r + input_l);
let out_hpf = self.input_hpf.process(out_lpf);
// HPF => Pre-Delay
let out_pre_delay = if params.pre_delay_time_ms() < 0.1 {
out_hpf
} else {
self.pre_delay.next_cubic(params.pre_delay_time_ms(), out_hpf)
};
// Pre-Delay => 4 All-Pass filters
let mut diffused = out_pre_delay;
for (apf, time, g) in &mut self.input_apfs {
diffused = apf.next(*time, *g, diffused);
}
// Mix between diffused and pre-delayed intput for further processing
let tank_feed = crossfade(out_pre_delay, diffused, params.input_diffusion_mix());
// First tap for the output
self.left_sum += tank_feed;
self.right_sum += tank_feed;
// Calculate tank decay of the left/right signal channels.
let decay = 1.0 - params.decay().clamp(0.1, 0.9999);
let decay = 1.0 - (decay * decay);
// Left Sum => APF1 => Delay1 => LPF => HPF => APF2 => Delay2
// And then send this over to the right sum.
let left = self.left_sum;
let left = self.apf1[0].0.next(left_apf1_delay_ms, self.apf1[0].2, left);
let left_apf_tap = left;
let left = self.delay1[0].0.next_cubic(self.delay1[0].1, left);
let left = self.lpf[0].process(left);
let left = self.hpf[0].process(left);
let left = left * decay;
let left = self.apf2[0].0.next(left_apf2_delay_ms, self.apf2[0].2, left);
let left = self.delay2[0].0.next_cubic(self.delay2[0].1, left);
// if self.dbg_count % 48 == 0 {
// println!("APFS dcy={:8.6}; {:8.6} {:8.6} {:8.6} {:8.6} | {:8.6} {:8.6} {:8.6} {:8.6}",
// decay,
// self.apf1[0].2,
// self.apf1[1].2,
// self.apf2[0].2,
// self.apf2[1].2,
// left_apf1_delay_ms, right_apf1_delay_ms,
// left_apf2_delay_ms, right_apf2_delay_ms);
// println!("DELY1/2 {:8.6} / {:8.6} | {:8.6} / {:8.6}",
// self.delay1[0].1,
// self.delay2[0].1,
// self.delay1[1].1,
// self.delay2[1].1);
// }
// Right Sum => APF1 => Delay1 => LPF => HPF => APF2 => Delay2
// And then send this over to the left sum.
let right = self.right_sum;
let right = self.apf1[1].0.next(right_apf1_delay_ms, self.apf1[1].2, right);
let right_apf_tap = right;
let right = self.delay1[1].0.next_cubic(self.delay1[1].1, right);
let right = self.lpf[1].process(right);
let right = self.hpf[1].process(right);
let right = right * decay;
let right = self.apf2[1].0.next(right_apf2_delay_ms, self.apf2[1].2, right);
let right = self.delay2[1].0.next_cubic(self.delay2[1].1, right);
self.right_sum = left * decay;
self.left_sum = right * decay;
let mut left_accum = left_apf_tap;
left_accum += self.delay1[0].0.tap_n(DAT_LEFT_TAPS_TIME_MS[0]);
left_accum += self.delay1[0].0.tap_n(DAT_LEFT_TAPS_TIME_MS[1]);
left_accum -= self.apf2[0].0.delay_tap_n(DAT_LEFT_TAPS_TIME_MS[2]);
left_accum += self.delay2[0].0.tap_n(DAT_LEFT_TAPS_TIME_MS[3]);
left_accum -= self.delay1[1].0.tap_n(DAT_LEFT_TAPS_TIME_MS[4]);
left_accum -= self.apf2[1].0.delay_tap_n(DAT_LEFT_TAPS_TIME_MS[5]);
left_accum -= self.delay2[1].0.tap_n(DAT_LEFT_TAPS_TIME_MS[6]);
let mut right_accum = right_apf_tap;
right_accum += self.delay1[1].0.tap_n(DAT_RIGHT_TAPS_TIME_MS[0]);
right_accum += self.delay1[1].0.tap_n(DAT_RIGHT_TAPS_TIME_MS[1]);
right_accum -= self.apf2[1].0.delay_tap_n(DAT_RIGHT_TAPS_TIME_MS[2]);
right_accum += self.delay2[1].0.tap_n(DAT_RIGHT_TAPS_TIME_MS[3]);
right_accum -= self.delay1[0].0.tap_n(DAT_RIGHT_TAPS_TIME_MS[4]);
right_accum -= self.apf2[0].0.delay_tap_n(DAT_RIGHT_TAPS_TIME_MS[5]);
right_accum -= self.delay2[0].0.tap_n(DAT_RIGHT_TAPS_TIME_MS[6]);
let left_out = self.out_dc_block[0].next(left_accum);
let right_out = self.out_dc_block[1].next(right_accum);
self.dbg_count += 1;
(left_out * 0.5, right_out * 0.5)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,489 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// Copyright (c) 2021-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.
/*!
# HexoDSP DSP nodes and DSP code
## How to Add a New DSP Node
When adding a new node to HexoDSP, I recommend working through the following checklist:
- [ ] Implement boilerplate in node_yourname.rs
- [ ] Add input parameter and output signal definition to dsp/mod.rs
- [ ] Document boilerplate in node_yourname.rs
- [ ] DSP implementation
- [ ] Parameter fine tuning
- [ ] DSP tests for all (relevant) params
- [ ] Ensure Documentation is properly formatted for the GUI
- [ ] Format the source using `cargo fmt`
- [ ] Add CHANGELOG.md entry in HexoSynth
- [ ] Add CHANGELOG.md entry in HexoDSP
- [ ] Add table entry in README.md in HexoSynth
- [ ] Add table entry in README.md in HexoDSP
The boilerplate can be a bit daunting. But it pays off, because HexoSynth will give
you a GUI for your DSP code for free at the end.
Generally I recommend starting out small. Define your new node with minimal parameters
until you get the hang of all the things involved to make it compile in the first place.
**Be aware that new DSP nodes need to meet these quality guidelines to be included:**
- Clean Rust code that I can understand and maintain. You can use `cargo fmt` (rustfmt) to
format the code.
- Does not drag in huge dependency trees. One rationale here is,
that I don't want the sound of a HexoSynth patch to change (significantly) because
some upstream crate decided to change their DSP code. To have optimal
control over this, I would love to have all the DSP code
contained in HexoDSP. Make sure to link the repository the code comes
from though. If you add dependencies for your DSP node, make sure that it's
characteristics are properly covered by the automated tests. So that problems become
visible in case upstream breaks or changes it's DSP code. If DSP code changes just slightly,
the test cases of course need to be changed accordingly.
- Come with automated smoke tests like all the other nodes, most test
signal min/max/rms over time, as well as the frequency spectrum
where applicable.
- It's parameters have proper denormalized mappings, like `0.5 => 4000 Hz` or `0.3 => 200ms`.
- Provide short descriptions for the node and it's parameters.
- Provide a comprehensive longer help text with (more details further down in this guide):
- What this node is about
- How to use it
- How the parameters work in combination
- Suggestions which combinations with other nodes might be interesting
- If applicable: provide a graph function for visualizing what it does.
### Boilerplate
- I recommend copying an existing node code, like `node_ad.rs` for instance.
- In this file `mod.rs` copy it's entry in the `node_list` macro definition.
- Copy the `tests/node_ad.rs` file to have a starting point for the automated testing.
Also keep in mind looking in other tests, about how they test things. Commonly used
macros are found in the ´tests/common/´ module.
A `node_list` macro entry looks like this:
```ignore
// node_id => node_label UIType UICategory
// | | / |
// / /------/ / |
// / | / |
xxx => Xxx UIType::Generic UICategory::Signal
// node_param_idx
// name denorm round format steps norm norm denorm
// norm_fun fun fun fun def min max default
(0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(1 gain n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0)
(2 att n_att d_att r_id f_def stp_d 0.0, 1.0, 1.0)
// node_param_idx UI widget type (mode, knob, sample)
// | atom_idx | format fun
// | | name constructor| | min max
// | | | | def|ult_v|lue | /
// | | | | | | | | |
{3 0 mono setting(0) mode fa_out_mono 0 1},
[0 sig],
```
The first entries, encapsulated in ´( )´ are the input ports or also called input parameters.
Input parameters can be connected to outputs of other DSP nodes. In contrast to the ´{ }´ encapsulated
so called _Atom parameters_. The data type for these is the [SAtom] datatype. And these parameters
can not be automated.
You can freely choose parameter names like eg. `inp` or `gain` and
pick names that suit the parameter semantics best. But I would like you to keep the naming
consistent with the rest of HexoDSP nodes if that is suitable to the DSP node.
There are some implicit conventions in HexoDSP for naming though:
- `inp` for single channel signal input
- `ch1`, `ch2`, ... for multiple channels
- `sig` for signal output
- `trig` for receiving a single trigger signal
- `t_*` if multiple trigger signals are expected
- If you have `freq` inputs, consider also adding `det` for detuning that frequency input.
But only if you think this makes sense in the context of the DSP node.
The macros in the node list definition like `n_gain`, `d_pit`, `r_fq` and so on
are all macros that are defined in the HexoDSP crate. You can create your own
normalization/denormalization, rounding, step and formatting function macros if
the existing ones don't suit the DSP node's needs.
### Signal Ranges in HexoDSP
The HexoDSP graph, or rather the nodes, operate with the raw normalized (audio)
signal range [-1, 1]. There is a second range that is also common in HexoDSP,
which is the control signal range [0, 1]. Following this convention will help combinding
HexoDSP nodes with each other. The existing normalization/denormalization functions for the
node list declaration already encode most of the conventions in HexoDSP, but here is a short
incomplete overview of common value mappings to the normalized signal ranges:
- Frequencies are usually using the `n_pit` and `d_pit` mappings. Where 0.0 is 440Hz
and the next octave is at 0.1 with 880Hz and the octave before that is at -0.1 with 220Hz.
This means one octave per 0.1 signal value.
- Triggers have to rise above the "high" threshold of 0.5 to be recognized, and the signal has to
fall below 0.25 to be detected as "low" again. Same works for gates.
### Node Documentation
**Attention: Defining the documentation for your DSP node is not optional. It's required to make
it compile in the first place!**
Every DSP node must come with online documentation. The online documentation is what makes the
node usable in the first place. It's the only hint for the user to learn how to use this node.
Keep in mind that the user is not an engineer, but usually a musician. They want to make music
and know how to use a parameter.
Every input parameter and atom parameter requires you to define a documenation entry in the
corresponding ´node_*.rs´ module/file. And also a _DESC_ and _HELP_ entry.
Here an example from ´node_ad.rs´:
```ignore
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";
```
The general format of the parameter documentation should be:
```ignore
"<Node name> <parameter name>\n
A short description what this paramter means/does and relates to others.\n
Range: <range>\n"
```
Keep the description of the paramter short and concise. Look at the space available
in HexoSynth where this is displayed. If you want to write more elaborate documentation
for a paramter, write it in the `HELP` entry.
The _range_ relates to the DSP signal range this paramter is supposed to receive.
This should either be the unipolar range (0..1) or the bipolar range (-1..1). Other
ranges should be avoided, because everything in HexoDSP is supposed to be fine with
receiving values in those ranges.
Next you need to document the node itself, how it works what it does and so on...
For this there are two entries:
```ignore
pub const DESC: &'static str = r#"Attack-Decay Envelope
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
...
"#;
```
_DESC_ should only contain a short description of the node. It's space is as limited as the
space for the parameter description. It will be autowrapped.
_HELP_ can be a multiple pages long detailed description of the node. Keep the
width of the lines below a certain limit (below 80 usually). Or else it will be
truncated in the help text window in HexoSynth. As inspiration what should be in
the help documentation:
- What the node does (even if it repeats mostly what _DESC_ already says)
- How the input paramters relate to each other.
- What the different atom settings (if any) mean.
- Which other DSP nodes this node is commonly combined with.
- Interesting or even uncommon uses of this DSP node.
- Try to inspire the user to experiment.
### Node Code Structure
For non trivial DSP nodes, the DSP code itself should be separate from it's `dsp/node_*.rs`
file. That file should only care about interfacing the DSP code with HexoDSP, but not implement
all the complicated DSP code. It's good practice to factor out the DSP code into
a separate module or file. It is preferable to add your custom DSP code to the `synfx-dsp`
crate [synfx-dsp](https://github.com/WeirdConstructor/synfx-dsp).
Look at `node_tslfo.rs` for instance. It wires up the `TriSawLFO` from `synfx-dsp`
to the HexoDSP node interface.
```ignore
// node_tslfo.rs
use synfx_dsp::{TriSawLFO, Trigger};
#[derive(Debug, Clone)]
pub struct TsLFO {
lfo: Box<TriSawLFO<f64>>,
trig: Trigger,
}
// ...
impl DspNode for TsLFO {
// ...
#[inline]
fn process<T: NodeAudioContext>(&mut self, /* ... */) {
// ...
let lfo = &mut *self.lfo;
for frame in 0..ctx.nframes() {
// ...
out.write(frame, lfo.next_unipolar() as f32);
}
// ...
}
}
```
The code for `TriSawLFO` in `synfx-dsp` is then independent and reusable else where.
### Node Parameter/Inputs
When implementing your node, you want to access the parameters or inputs of your DSP node.
This is done using the buffer access modules in `dsp/mod.rs` that are defined using the
`node_list` macro. Let me give you a short overview using `node_sin.rs` as an example:
```ignore
#[inline]
fn process<T: NodeAudioContext>(
&mut self,
ctx: &mut T, // DSP execution context holding the DSP graph input and output buffers.
_ectx: &mut NodeExecContext, // Contains special stuff, like the FeedbackBuffers
_nctx: &NodeContext, // Holds context info about the node, for instance which ports
// are connected.
_atoms: &[SAtom], // An array holding the Atom parameters
inputs: &[ProcBuf], // An array holding the input parameter buffers, containing
// either outputs from other DSP nodes or smoothed parameter
// settings from the GUI/frontend.
outputs: &mut [ProcBuf], // An array holding the output buffers.
ctx_vals: LedPhaseVals, // Values for visual aids in the GUI (the hextile LED)
) {
use crate::dsp::{denorm_offs, inp, out};
let o = out::Sin::sig(outputs);
let freq = inp::Sin::freq(inputs);
let det = inp::Sin::det(inputs);
let isr = 1.0 / self.srate;
let mut last_val = 0.0;
for frame in 0..ctx.nframes() {
let freq = denorm_offs::Sampl::freq(freq, det.read(frame), frame);
// ...
}
// ...
}
```
There are three buffer/parameter function access modules loaded in this example:
```ignore
use crate::dsp::{denorm_offs, inp, out};
```
`inp` holds a sub module for each of the available nodes. That means: `inp::Sin`, `inp::Ad`, ...
Those submodules each have a function that returns the corresponding buffer from the `inputs`
vector of buffers. That means `inp::Sin::det(inputs)` gives you a reference to a [ProcBuf]
you can read the normalized signal inputs (range -1 to 1) from.
It works similarly with `out::Sin::sig`, which provides you with a [ProcBuf] reference to
write your output to.
`denorm_offs` is a special module, that offers you functions to access the denormalized
value of a specific input parameter with a modulation offset.
Commonly you want to use the `denorm` module to access the denormalized values. That means
values in human understandable form and that can be used in your DSP arithmetics more easily.
For instance `denorm::TsLFO::time` from `node_tslfo.rs`:
```ignore
use crate::dsp::{denorm, inp, out};
let time = inp::TsLFO::time(inputs);
for frame in 0..ctx.nframes() {
let time_ms = denorm::TsLFO::time(time, frame).clamp(0.1, 300000.0);
// ...
}
```
`denorm::TsLFO::time` is a function that takes the [ProcBuf] with the raw normalized
input signal samples and returns the denormalized values in milliseconds for a specific
frame.
To get a hang of all the possibilities I suggest diving a bit into the other node source code
a bit.
### Node Beautification
To make nodes responsive in HexoSynth the `DspNode::process` function receives the [LedPhaseVals].
These should be written after the inner loop that calculates the samples. The first context value
is the _LED value_, it should be in the range between -1 and 1. The most easy way to set it is
by using the last written sample from your loop:
```ignore
ctx_vals[0].set(out.read(ctx.nframes() - 1));
```
But consider giving it a more meaningful value if possible. The `node_ad.rs` sets the LED value
to the internal phase value of the envelope instead of it's output.
The second value in [LedPhaseVals] is the _Phase value_. It usually has special meaning for the
node specific visualizations. Such as TSeq emits the position of the playhead for instance.
The CQnt quantizer emits the most recently activated key to the GUI using the Phase value.
Consider also providing a visualization graph if possible. You can look eg at `node_ad.rs`
or `node_tslfo.rs` or many others how to provide a visualization graph function:
```ignore
impl DspNode for TsLFO {
fn graph_fun() -> Option<GraphFun> {
Some(Box::new(|gd: &dyn GraphAtomData, _init: bool, x: f32, xn: f32| -> f32 {
// ...
}))
}
}
```
Let me explain the callback function parameters quickly:
- `gd: &dyn GraphAtomData` this trait object gives you access to the input paramters of
this node. And also the LED and Phase values.
- `init: bool` allows you to detect when the first sample of the graph is being drawn/requested.
You can use this to reset any state that is carried with the callback.
- `x: f32` is the current X position of the graph. Use this to derive the Y value which
must be returned from the callback.
- `xn: f32` is the next value for X. This is useful for determining if your function might
reaches a min or max between _x_ and _xn_, so you could for instance return the min or max
value now instead of the next sample.
### Automated Testing of Your Node
First lets discuss shortly why automated tests are necessary. HexoDSP has an automated test
suite to check if any changes on the internal DSP helpers break something. Or if some
changes on some DSP node accidentally broke something. Or if a platform behaves weirdly.
Or even if upstream crates that are included broke or changed something essential.
A few things you can test your DSP code for:
- Is non 0.0 signal emitted?
- Is the signal inside the -1..1 or 0..1 range?
- Does the signal level change in expected ways if the input parameters are changed?
- Does the frequency spectrum peak at expected points in the FFT output?
- Does the frequency spectrum change to expected points in the FFT output when an input parameter
changed?
Try to nail down the characteristics of your DSP node with a few tests as well as possible.
For the FFT and other tests there are helper functions in `tests/common/mod.rs`
The start of your `tests/node_*.rs` file usually should look like this:
```ignore
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 mut chain = MatrixCellChain::new(CellDir::B);
chain.node_out("ad", "sig")
.node_inp("out", "ch1")
.place(&mut matrix, 0, 0).unwrap();
matrix.sync().unwrap();
let ad = NodeId::Ad(0);
// ...
}
```
Lets dissect this a bit. The beginning of each test case should setup an instance of the DSP engine
of HexoDSP using [crate::new_node_engine]. It returns a [crate::NodeConfigurator] and a [crate::NodeExecutor].
The first is responsible for setting up the DSP graph and modifying it at runtime.
The latter ([crate::NodeExecutor]) is responsible for executing the DSP graph and generate output samples.
```ignore
let (node_conf, mut node_exec) = new_node_engine();
```
The [crate::Matrix] abstraction encapsulates the configurator and provides you an interface
to layout the nodes in a hexagonal grid. It is currently the easiest API for using HexoDSP.
The two parameters to _new_ are the width and height of the hex grid.
```ignore
let mut matrix = Matrix::new(node_conf, 3, 3);
```
Next step is to create a DSP chain of nodes and place that onto the hexagonal matrix.
Luckily a simpler API has been created with the [crate::MatrixCellChain], that lets
you build DSP chains on the fly using only names of the nodes and the corresponding
input/output ports:
```ignore
// Create a new cell chain that points in to the given direction (CellDir::B => to bottom).
let mut chain = MatrixCellChain::new(CellDir::B);
chain.node_out("ad", "sig") // Add a Node::Ad(0) cell, with the "sig" output set
.node_inp("out", "ch1") // Add a Node::Out(0) cell, with the "ch1" input set
.place(&mut matrix, 0, 0).unwrap();
```
After placing the new cells, we need to synchronize it with the audio backend:
```ignore
matrix.sync().unwrap();
```
The `sync` is necessary to update the DSP graph.
Next you usually want to define short variable names for the [NodeId] that refer to the DSP
node instances:
```ignore
let ad = NodeId::Ad(0);
```
The [NodeId] interface offers you functions to get the input parameter index from
a name like `out.inp("ch1")` or the output port index from a name: `ad.out("sig")`.
You can have multiple instances for a node. The number in the parenthesis are
the instance index of that node.
After you have setup everything for the test, you usually want to modify a paramter
and look at the values the graph returns.
```ignore
#[test]
fn check_node_ad_1() {
// ...
// matrix setup code above
// ...
let ad = NodeId::Ad(0);
// Fetch parameter id:
let trig_p = ad.inp_param("trig").unwrap();
// Set parameter:
matrix.set_param(trig_p, SAtom::param(1.0));
/// Run the DSP graph for 25 milliseconds of audio.
let res = run_for_ms(&mut node_exec, 25.0);
// `res` now contains two vectors. one for first channel "ch1"
// and one for the second channel "ch2".
assert_decimated_slope_feq!(
res.0,
// ....
)
}
```
***Attention: This is important to keep in mind:*** After using `matrix.set_param(...)` to
set a paramter, keep in mind that the parameter values will be smoothed. That means it will
take a few milliseconds until `trig_p` reaches the 1.0. In case of the Ad node that means
the trigger threshold won't be triggered at the first sample, but a few milliseconds
later!
*/
#[allow(non_upper_case_globals)]
mod node_ad;
#[allow(non_upper_case_globals)]
@ -15,6 +497,8 @@ mod node_bosc;
#[allow(non_upper_case_globals)]
mod node_bowstri;
#[allow(non_upper_case_globals)]
mod node_code;
#[allow(non_upper_case_globals)]
mod node_comb;
#[allow(non_upper_case_globals)]
mod node_cqnt;
@ -43,6 +527,8 @@ mod node_rndwk;
#[allow(non_upper_case_globals)]
mod node_sampl;
#[allow(non_upper_case_globals)]
mod node_scope;
#[allow(non_upper_case_globals)]
mod node_sfilter;
#[allow(non_upper_case_globals)]
mod node_sin;
@ -57,17 +543,14 @@ mod node_tslfo;
#[allow(non_upper_case_globals)]
mod node_vosc;
pub mod biquad;
pub mod dattorro;
pub mod helpers;
mod satom;
pub mod tracker;
use crate::nodes::NodeAudioContext;
use crate::nodes::NodeExecContext;
use crate::util::AtomicFloat;
use std::sync::Arc;
use synfx_dsp::AtomicFloat;
pub type LedPhaseVals<'a> = &'a [Arc<AtomicFloat>];
@ -83,7 +566,6 @@ use crate::fa_cqnt;
use crate::fa_cqnt_omax;
use crate::fa_cqnt_omin;
use crate::fa_delay_mode;
use crate::fa_distort;
use crate::fa_map_clip;
use crate::fa_mux9_in_cnt;
use crate::fa_noise_mode;
@ -92,12 +574,14 @@ use crate::fa_quant;
use crate::fa_sampl_dclick;
use crate::fa_sampl_dir;
use crate::fa_sampl_pmode;
use crate::fa_scope_tsrc;
use crate::fa_sfilter_type;
use crate::fa_smap_clip;
use crate::fa_smap_mode;
use crate::fa_test_s;
use crate::fa_tseq_cmode;
use crate::fa_vosc_ovrsmpl;
use synfx_dsp::fa_distort;
use node_ad::Ad;
use node_allp::AllP;
@ -105,6 +589,7 @@ use node_amp::Amp;
use node_biqfilt::BiqFilt;
use node_bosc::BOsc;
use node_bowstri::BowStri;
use node_code::Code;
use node_comb::Comb;
use node_cqnt::CQnt;
use node_delay::Delay;
@ -120,6 +605,7 @@ use node_pverb::PVerb;
use node_quant::Quant;
use node_rndwk::RndWk;
use node_sampl::Sampl;
use node_scope::Scope;
use node_sfilter::SFilter;
use node_sin::Sin;
use node_smap::SMap;
@ -733,6 +1219,22 @@ macro_rules! f_lfot {
};
}
macro_rules! f_lfoms {
($formatter: expr, $v: expr, $denorm_v: expr) => {
if $denorm_v < 10.0 {
write!($formatter, "{:5.3}ms", $denorm_v)
} else if $denorm_v < 250.0 {
write!($formatter, "{:4.1}ms", $denorm_v)
} else if $denorm_v < 1500.0 {
write!($formatter, "{:4.0}ms", $denorm_v)
} else if $denorm_v < 10000.0 {
write!($formatter, "{:5.2}s", $denorm_v / 1000.0)
} else {
write!($formatter, "{:5.1}s", $denorm_v / 1000.0)
}
};
}
macro_rules! f_det {
($formatter: expr, $v: expr, $denorm_v: expr) => {{
let sign = if $denorm_v < 0.0 { -1.0 } else { 1.0 };
@ -750,6 +1252,7 @@ macro_rules! f_det {
// norm-fun denorm-min
// denorm-fun denorm-max
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_declick d_declick 0.0, 50.0}
@ -868,11 +1371,21 @@ macro_rules! node_list {
[9 gat4]
[10 gat5]
[11 gat6],
code => Code UIType::Generic UICategory::Signal
(0 in1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(1 in2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(2 alpha n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(3 beta n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(4 delta n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(5 gamma n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
[0 sig]
[1 sig1]
[2 sig2],
sampl => Sampl UIType::Generic UICategory::Osc
(0 freq n_pit d_pit r_fq f_def stp_d -1.0, 0.564713133, 440.0)
(1 trig n_id n_id r_id f_def stp_d -1.0, 1.0, 0.0)
(2 offs n_id n_id r_id f_def stp_d 0.0, 1.0, 0.0)
(3 len n_id n_id r_id f_def stp_d 0.0, 1.0, 1.0)
(1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(2 offs n_id d_id r_id f_def stp_d 0.0, 1.0, 0.0)
(3 len n_id d_id r_id f_def stp_d 0.0, 1.0, 1.0)
(4 dcms n_declick d_declick r_dc_ms f_ms stp_m 0.0, 1.0, 3.0)
(5 det n_det d_det r_det f_det stp_f -0.2, 0.2, 0.0)
{6 0 sample audio_unloaded("") sample f_def 0 0}
@ -925,6 +1438,20 @@ macro_rules! node_list {
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 sig],
scope => Scope UIType::Generic UICategory::IOUtil
(0 in1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(1 in2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(2 in3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(3 time n_lfot d_lfot r_lfot f_lfoms stp_f 0.0, 1.0, 1000.0)
(4 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(5 thrsh 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)
(8 off3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0)
(9 gain1 n_xgin d_xgin 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_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 2},
ad => Ad UIType::Generic UICategory::Mod
(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)
@ -1079,7 +1606,7 @@ fn rand_node_satisfies_spec(nid: NodeId, sel: RandNodeSelector) -> bool {
}
pub fn get_rand_node_id(count: usize, sel: RandNodeSelector) -> Vec<NodeId> {
let mut sm = crate::dsp::helpers::SplitMix64::new_time_seed();
let mut sm = synfx_dsp::SplitMix64::new_time_seed();
let mut out = vec![];
let mut cnt = 0;
@ -1459,7 +1986,7 @@ macro_rules! make_node_info_enum {
1 => 0.05,
2 => 0.1,
// 0.25 just to protect against sine cancellation
_ => crate::dsp::helpers::rand_01() * 0.25
_ => synfx_dsp::rand_01() * 0.25
}
}

View file

@ -2,7 +2,7 @@
// 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 synfx_dsp::{sqrt4_to_pow4, TrigSignal, Trigger};
use crate::dsp::{
DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::AllPass;
use synfx_dsp::AllPass;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::biquad::*;
use synfx_dsp::*;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::PolyBlepOscillator;
use synfx_dsp::PolyBlepOscillator;
use crate::dsp::{
DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

View file

@ -2,8 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::biquad::Biquad;
use crate::dsp::helpers::{DelayBuffer, FixedOnePole};
use synfx_dsp::{DelayBuffer, FixedOnePole, Biquad};
use crate::dsp::{
denorm, denorm_offs, inp, out, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

142
src/dsp/node_code.rs Normal file
View file

@ -0,0 +1,142 @@
// Copyright (c) 2021 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 crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};
#[cfg(feature = "synfx-dsp-jit")]
use synfx_dsp_jit::engine::CodeEngineBackend;
//use crate::dsp::MAX_BLOCK_SIZE;
/// A WBlockDSP code execution node for JIT'ed DSP code
pub struct Code {
#[cfg(feature = "synfx-dsp-jit")]
backend: Option<Box<CodeEngineBackend>>,
srate: f64,
}
impl std::fmt::Debug for Code {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "Code")
}
}
impl Clone for Code {
fn clone(&self) -> Self {
Self::new(&NodeId::Nop)
}
}
impl Code {
pub fn new(_nid: &NodeId) -> Self {
Self {
#[cfg(feature = "synfx-dsp-jit")]
backend: None,
srate: 48000.0,
}
}
#[cfg(feature = "synfx-dsp-jit")]
pub fn set_backend(&mut self, backend: CodeEngineBackend) {
self.backend = Some(Box::new(backend));
}
pub const in1: &'static str = "Code in1\nInput Signal 1\nRange: (-1..1)\n";
pub const in2: &'static str = "Code in2\nInput Signal 1\nRange: (-1..1)\n";
pub const alpha: &'static str = "Code alpha\nInput Parameter Alpha\nRange: (-1..1)\n";
pub const beta: &'static str = "Code alpha\nInput Parameter Alpha\nRange: (-1..1)\n";
pub const delta: &'static str = "Code alpha\nInput Parameter Alpha\nRange: (-1..1)\n";
pub const gamma: &'static str = "Code alpha\nInput Parameter Alpha\nRange: (-1..1)\n";
pub const sig: &'static str = "Code sig\nReturn output\nRange: (-1..1)\n";
pub const sig1: &'static str = "Code sig1\nSignal channel 1 output\nRange: (-1..1)\n";
pub const sig2: &'static str = "Code sig2\nSignal channel 2 output\nRange: (-1..1)\n";
pub const DESC: &'static str = "WBlockDSP Code Execution\n\n\
This node executes just in time compiled code as fast as machine code. \
Use this to implement real time DSP code yourself.";
pub const HELP: &'static str = r#"WBlockDSP Code Execution
Do it!
"#;
}
impl DspNode for Code {
fn outputs() -> usize {
3
}
fn set_sample_rate(&mut self, srate: f32) {
self.srate = srate as f64;
#[cfg(feature = "synfx-dsp-jit")]
if let Some(backend) = self.backend.as_mut() {
backend.set_sample_rate(srate);
}
}
fn reset(&mut self) {
#[cfg(feature = "synfx-dsp-jit")]
if let Some(backend) = self.backend.as_mut() {
backend.clear();
}
}
#[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_idx};
let in1 = inp::Code::in1(inputs);
let in2 = inp::Code::in2(inputs);
let a = inp::Code::alpha(inputs);
let b = inp::Code::beta(inputs);
let d = inp::Code::delta(inputs);
let g = inp::Code::gamma(inputs);
let out_i = out_idx::Code::sig1();
let (sig, sig1) = outputs.split_at_mut(out_i);
let (sig1, sig2) = sig1.split_at_mut(1);
let sig = &mut sig[0];
let sig1 = &mut sig1[0];
let sig2 = &mut sig2[0];
#[cfg(feature = "synfx-dsp-jit")]
{
let backend = if let Some(backend) = &mut self.backend {
backend
} else {
return;
};
backend.process_updates();
let mut ret = 0.0;
let mut s1 = 0.0;
#[allow(unused_assignments)]
let mut s2 = 0.0;
for frame in 0..ctx.nframes() {
(s1, s2, ret) = backend.process(
in1.read(frame),
in2.read(frame),
a.read(frame),
b.read(frame),
d.read(frame),
g.read(frame),
);
sig.write(frame, ret);
sig1.write(frame, s1);
sig2.write(frame, s2);
}
ctx_vals[0].set(ret);
ctx_vals[1].set(s1);
}
}
}

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers;
use synfx_dsp;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};
@ -21,12 +21,12 @@ macro_rules! fa_comb_mode {
/// A simple amplifier
#[derive(Debug, Clone)]
pub struct Comb {
comb: Box<helpers::Comb>,
comb: Box<synfx_dsp::Comb>,
}
impl Comb {
pub fn new(_nid: &NodeId) -> Self {
Self { comb: Box::new(helpers::Comb::new()) }
Self { comb: Box::new(synfx_dsp::Comb::new()) }
}
pub const inp: &'static str = "Comb inp\nThe signal input for the comb filter.\nRange: (-1..1)";

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::{ChangeTrig, CtrlPitchQuantizer};
use synfx_dsp::{ChangeTrig, CtrlPitchQuantizer};
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::{crossfade, DelayBuffer, TriggerSampleClock};
use synfx_dsp::{crossfade, DelayBuffer, TriggerSampleClock};
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::Trigger;
use synfx_dsp::Trigger;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::Rng;
use synfx_dsp::Rng;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,8 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use super::dattorro::{DattorroReverb, DattorroReverbParams};
use super::helpers::crossfade;
use synfx_dsp::{DattorroReverb, DattorroReverbParams, crossfade};
use crate::dsp::{denorm, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::{ChangeTrig, Quantizer};
use synfx_dsp::{ChangeTrig, Quantizer};
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::{Rng, SlewValue, Trigger};
use synfx_dsp::{Rng, SlewValue, Trigger};
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use super::helpers::Trigger;
use synfx_dsp::{cubic_interpolate, Trigger};
use crate::dsp::{at, denorm, denorm_offs, inp, out}; //, inp, denorm, denorm_v, inp_dir, at};
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};
@ -142,72 +142,24 @@ be provided on the 'trig' input port. The 'trig' input also works in
impl Sampl {
#[allow(clippy::many_single_char_names)]
#[inline]
fn next_sample_rev(&mut self, sr_factor: f64, speed: f64, sample_data: &[f32]) -> f32 {
fn next_sample(
&mut self,
sr_factor: f64,
speed: f64,
sample_data: &[f32],
reverse: bool,
) -> f32 {
let sd_len = sample_data.len();
if sd_len < 1 {
return 0.0;
}
let j = self.phase.floor() as usize % sd_len;
let i = ((sd_len - 1) - j) + sd_len;
let i = self.phase.floor() as usize % sd_len;
let f = self.phase.fract();
self.phase = j as f64 + f + sr_factor * speed;
self.phase = i as f64 + f + sr_factor * speed;
// Hermite interpolation, take from
// https://github.com/eric-wood/delay/blob/main/src/delay.rs#L52
//
// Thanks go to Eric Wood!
//
// For the interpolation code:
// MIT License, Copyright (c) 2021 Eric Wood
let xm1 = sample_data[(i + 1) % sd_len];
let x0 = sample_data[i % sd_len];
let x1 = sample_data[(i - 1) % sd_len];
let x2 = sample_data[(i - 2) % sd_len];
let c = (x1 - xm1) * 0.5;
let v = x0 - x1;
let w = c + v;
let a = w + v + (x2 - x0) * 0.5;
let b_neg = w + a;
let f = (1.0 - f) as f32;
(((a * f) - b_neg) * f + c) * f + x0
}
#[allow(clippy::many_single_char_names)]
#[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;
let f = self.phase.fract();
self.phase = (i % sd_len) as f64 + f + sr_factor * speed;
// Hermite interpolation, take from
// https://github.com/eric-wood/delay/blob/main/src/delay.rs#L52
//
// Thanks go to Eric Wood!
//
// For the interpolation code:
// MIT License, Copyright (c) 2021 Eric Wood
let xm1 = sample_data[(i - 1) % sd_len];
let x0 = sample_data[i % sd_len];
let x1 = sample_data[(i + 1) % sd_len];
let x2 = sample_data[(i + 2) % sd_len];
let c = (x1 - xm1) * 0.5;
let v = x0 - x1;
let w = c + v;
let a = w + v + (x2 - x0) * 0.5;
let b_neg = w + a;
let f = f as f32;
(((a * f) - b_neg) * f + c) * f + x0
let (i, f) = if reverse { (((sd_len - 1) - i), 1.0 - f) } else { (i, f) };
cubic_interpolate(&sample_data[..], sd_len, i, f as f32)
}
#[allow(clippy::float_cmp)]
@ -294,11 +246,8 @@ impl Sampl {
// that is used for looking up the sample from the audio data.
let sample_idx = self.phase.floor() as usize;
let mut s = if reverse {
self.next_sample_rev(sr_factor, playback_speed as f64, sample_slice)
} else {
self.next_sample(sr_factor, playback_speed as f64, sample_slice)
};
let mut s =
self.next_sample(sr_factor, playback_speed as f64, sample_slice, reverse);
if declick {
let samples_to_end = sample_slice.len() - sample_idx;

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

@ -0,0 +1,230 @@
// 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.
//
// 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 synfx_dsp::CustomTrigger;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::SCOPE_SAMPLES;
use crate::nodes::{NodeAudioContext, NodeExecContext};
use crate::ScopeHandle;
use std::sync::Arc;
#[macro_export]
macro_rules! fa_scope_tsrc {
($formatter: expr, $v: expr, $denorm_v: expr) => {{
let s = match ($v.round() as usize) {
0 => "Off",
1 => "Intern",
2 => "Extern",
_ => "?",
};
write!($formatter, "{}", s)
}};
}
/// A simple signal scope
#[derive(Debug, Clone)]
pub struct Scope {
handle: Arc<ScopeHandle>,
idx: usize,
frame_time: f32,
srate_ms: f32,
cur_mm: Box<[(f32, f32); 3]>,
trig: CustomTrigger,
}
impl Scope {
pub fn new(_nid: &NodeId) -> Self {
Self {
handle: ScopeHandle::new_shared(),
idx: 0,
srate_ms: 44.1,
frame_time: 0.0,
cur_mm: Box::new([(0.0, 0.0); 3]),
trig: CustomTrigger::new(0.0, 0.0001),
}
}
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 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 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 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 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 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. You can let the scope freeze and manually recapture waveforms by setting 'tsrc' to 'Extern' and hitting the 'trig' button manually.\nRange: (-1..1)\n";
pub const DESC: &'static str = r#"Signal Oscilloscope Probe
This is a signal oscilloscope probe node, you can capture up to 3 signals. You can enable internal or external triggering for capturing signals or pinning fast waveforms.
"#;
pub const HELP: &'static str = r#"Scope - Signal Oscilloscope Probe
You can have up to 8 different scopes in your patch. That means you can
in record up to 24 signals for displaying them in the scope view.
The received signal will be forwarded to the GUI and you can inspect
the waveform there.
You can enable an internal trigger with the 'tsrc' setting set to 'Intern'.
'Intern' here means that the signal input 1 'in1' is used as trigger signal.
The 'thrsh' parameter is the trigger detection parameter. That means, if your
signal passes that threshold in negative to positive direction, the signal
recording will be reset to that point.
You can also route in an external trigger to capture signals with the 'trig'
input and 'tsrc' set to 'Extern'. Of course you can also hit the 'trig' button
manually to recapture a waveform.
The inputs 'off1', 'off2' and 'off3' define a vertical offset of the signal
waveform in the scope view. Use 'gain1', 'gain2' and 'gain3' for scaling
the input signals up/down.
"#;
pub fn set_scope_handle(&mut self, handle: Arc<ScopeHandle>) {
self.handle = handle;
}
}
impl DspNode for Scope {
fn outputs() -> usize {
1
}
fn set_sample_rate(&mut self, srate: f32) {
self.srate_ms = srate / 1000.0;
}
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::{at, denorm, inp};
let in1 = inp::Scope::in1(inputs);
let in2 = inp::Scope::in2(inputs);
let in3 = inp::Scope::in3(inputs);
let time = inp::Scope::time(inputs);
let thrsh = inp::Scope::thrsh(inputs);
let trig = inp::Scope::trig(inputs);
let tsrc = at::Scope::tsrc(atoms);
let input_bufs = [in1, in2, in3];
self.handle.set_active_from_mask(nctx.in_connected);
self.handle.set_offs_gain(
0,
denorm::Scope::off1(inp::Scope::off1(inputs), 0),
denorm::Scope::gain1(inp::Scope::gain1(inputs), 0),
);
self.handle.set_offs_gain(
1,
denorm::Scope::off2(inp::Scope::off2(inputs), 0),
denorm::Scope::gain2(inp::Scope::gain2(inputs), 0),
);
self.handle.set_offs_gain(
2,
denorm::Scope::off3(inp::Scope::off3(inputs), 0),
denorm::Scope::gain3(inp::Scope::gain3(inputs), 0),
);
let time = denorm::Scope::time(time, 0).clamp(0.1, 1000.0 * 300.0);
let samples_per_block = (time * self.srate_ms) / SCOPE_SAMPLES as f32;
let time_per_block = time / SCOPE_SAMPLES as f32;
let sample_time = 1.0 / self.srate_ms;
let threshold = denorm::Scope::thrsh(thrsh, 0);
self.trig.set_threshold(threshold, threshold + 0.0001);
let trigger_input = if tsrc.i() == 2 { trig } else { in1 };
let trigger_disabled = tsrc.i() == 0;
self.handle.set_threshold(if trigger_disabled { None } else { Some(threshold) });
//d// println!("TIME time={}; st={}; tpb={}; frame_time={}", time, sample_time, time_per_block, self.frame_time);
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 input_bufs.iter().enumerate() {
let in_val = input.read(frame);
self.handle.write_oversampled(i, self.idx, copy_count, in_val);
}
self.idx = self.idx.saturating_add(copy_count);
}
if self.idx >= SCOPE_SAMPLES {
if self.trig.check_trigger(trigger_input.read(frame)) {
self.frame_time = 0.0;
self.idx = 0;
} else if trigger_disabled {
self.frame_time = 0.0;
self.idx = 0;
}
}
}
} else {
let cur_mm = self.cur_mm.as_mut();
// let samples_per_block = samples_per_block as usize;
for frame in 0..ctx.nframes() {
if self.idx < SCOPE_SAMPLES {
for (i, input) in input_bufs.iter().enumerate() {
let in_val = input.read(frame);
cur_mm[i].0 = cur_mm[i].0.max(in_val);
cur_mm[i].1 = cur_mm[i].1.min(in_val);
}
if self.frame_time >= time_per_block {
for i in 0..input_bufs.len() {
self.handle.write(i, self.idx, cur_mm[i]);
}
*cur_mm = [(-99999.0, 99999.0); 3];
self.idx = self.idx.saturating_add(1);
self.frame_time -= time_per_block;
}
self.frame_time += sample_time;
}
if self.idx >= SCOPE_SAMPLES {
if self.trig.check_trigger(trigger_input.read(frame)) {
*cur_mm = [(-99999.0, 99999.0); 3];
self.frame_time = 0.0;
self.idx = 0;
} else if trigger_disabled {
*cur_mm = [(-99999.0, 99999.0); 3];
self.idx = 0;
}
}
}
}
let last_frame = ctx.nframes() - 1;
ctx_vals[0].set(
(in1.read(last_frame) + in2.read(last_frame) + in3.read(last_frame)).clamp(-1.0, 1.0),
);
}
}

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::{
use synfx_dsp::{
process_1pole_highpass, process_1pole_lowpass, process_1pole_tpt_highpass,
process_1pole_tpt_lowpass, process_hal_chamberlin_svf, process_simper_svf,
process_stilson_moog,

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::fast_sin;
use synfx_dsp::fast_sin;
use crate::dsp::{
denorm_offs, inp, out, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::TrigSignal;
use synfx_dsp::TrigSignal;
use crate::dsp::{
DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::helpers::{Trigger, TriggerPhaseClock};
use synfx_dsp::{Trigger, TriggerPhaseClock};
use crate::dsp::tracker::TrackerBackend;
use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
use crate::nodes::{NodeAudioContext, NodeExecContext};

View file

@ -2,7 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use super::helpers::{TriSawLFO, Trigger};
use synfx_dsp::{TriSawLFO, Trigger};
use crate::dsp::{
DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

View file

@ -2,8 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use crate::dsp::biquad::Oversampling;
use crate::dsp::helpers::{apply_distortion, VPSOscillator};
use synfx_dsp::{Oversampling, apply_distortion, VPSOscillator};
use crate::dsp::{
DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};

View file

@ -4,7 +4,7 @@
use super::MAX_COLS;
use super::MAX_PATTERN_LEN;
use crate::dsp::helpers::SplitMix64;
use synfx_dsp::SplitMix64;
pub struct PatternSequencer {
rows: usize,

View file

@ -1,17 +1,15 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// Copyright (c) 2021-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.
/*!
# HexoDSP - Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth.
/*!# HexoDSP - Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth.
This project contains the complete DSP backend of the modular
synthesizer [HexoSynth](https://github.com/WeirdConstructor/HexoSynth).
It's aimed to provide a toolkit for everyone who wants to develop
a synthesizer in Rust. You can use it to quickly define a DSP graph
that you can change at runtime. It comes with a large collection
that you can change at runtime. It comes with a (growing) collection
of already developed DSP modules/nodes, such as oscillators, filters,
amplifiers, envelopes and sequencers.
@ -19,14 +17,50 @@ The DSP graph API also provides multiple kinds of feedback to track what the
signals in the DSP threads look like. From monitoring the inputs and outputs of
single nodes to get the current output value of all nodes.
There is also an (optional) JIT compiler for defining custom pieces of DSP code
that runs at native speed in a DSP graph module/node.
Here a short list of features:
* Runtime changeable DSP graph
* Serialization and loading of the DSP graph and the parameters
* Full monitoring and feedback introspection into the running DSP graph
* Provides a wide variety of modules
* Extensible framework for quickly developing new nodes at compile time
* A comprehensive automated test suite
* Runtime changeable DSP graph.
* Serialization and loading of the DSP graph and the parameters.
* Full monitoring and feedback introspection into the running DSP graph.
* Provides a wide variety of modules.
* (Optional) JIT (Just In Time) compiled custom DSP code for integrating your own
DSP algorithms at runtime. One possible frontend language is the visual
"BlockCode" programming language in HexoSynth.
* Extensible framework for quickly adding new nodes to HexoDSP.
* A comprehensive automated test suite covering all modules in HexoDSP.
And following DSP nodes:
| Category | Name | Function |
|-|-|-|
| IO Util | Out | Audio output (to DAW or Jack) |
| Osc | Sampl | Sample player |
| Osc | Sin | Sine oscillator |
| Osc | BOsc | Basic bandlimited waveform oscillator (waveforms: Sin, Tri, Saw, Pulse/Square) |
| Osc | VOsc | Vector phase shaping oscillator |
| Osc | Noise | Noise oscillator |
| Signal | Amp | Amplifier/Attenuator |
| Signal | SFilter | Simple collection of filters, useable for synthesis |
| Signal | Delay | Single tap signal delay |
| Signal | PVerb | Reverb node, based on Dattorros plate reverb algorithm |
| Signal | AllP | All-Pass filter based on internal delay line feedback |
| Signal | Comb | Comb filter |
| Signal | Code | JIT (Just In Time) compiled piece of custom DSP code. |
| N-\>M | Mix3 | 3 channel mixer |
| N-\>M | Mux9 | 9 channel to 1 output multiplexer/switch |
| Ctrl | SMap | Simple control signal mapper |
| Ctrl | Map | Control signal mapper |
| Ctrl | CQnt | Control signal pitch quantizer |
| Ctrl | Quant | Pitch signal quantizer |
| Mod | TSeq | Tracker/pattern sequencer |
| Mod | Ad | Attack-Decay envelope |
| Mod | TsLFO | Tri/Saw waveform low frequency oscillator (LFO) |
| Mod | RndWk | Random walker, a Sample & Hold noise generator |
| IO Util | FbWr / FbRd | Utility modules for feedback in patches |
| IO Util | Scope | Oscilloscope for up to 3 channels |
## API Examples
@ -77,9 +111,9 @@ This is a short overview of the API provided by the
hexagonal Matrix API, which is the primary API used
inside [HexoSynth](https://github.com/WeirdConstructor/HexoSynth).
This only showcases the non-realtime generation of audio
samples. For a real time application of this library please
refer to the examples that come with this library.
This only showcases the direct generation of audio samples, without any audio
device playing it. For a real time application of this library please refer to
the examples that come with this library.
```rust
use hexodsp::*;
@ -87,13 +121,16 @@ use hexodsp::*;
let (node_conf, mut node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 3, 3);
let sin = NodeId::Sin(0);
let amp = NodeId::Amp(0);
let out = NodeId::Out(0);
matrix.place(0, 0, Cell::empty(sin)
.out(None, None, sin.out("sig")));
matrix.place(0, 1, Cell::empty(amp)
.input(amp.inp("inp"), None, None));
.input(amp.inp("inp"), None, None)
.out(None, None, amp.out("sig")));
matrix.place(0, 2, Cell::empty(out)
.input(out.inp("inp"), None, None));
matrix.sync().unwrap();
let gain_p = amp.inp_param("gain").unwrap();
@ -104,9 +141,33 @@ let (out_l, out_r) = node_exec.test_run(0.11, true);
// samples now.
```
### Simplified Hexagonal Matrix API
There is also a simplified version for easier setup of DSP chains
on the hexagonal grid, using the [crate::MatrixCellChain] abstraction:
```rust
use hexodsp::*;
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_out("sin", "sig")
.node_io("amp", "inp", "sig")
.set_atom("gain", SAtom::param(0.25))
.node_inp("out", "ch1")
.place(&mut matrix, 0, 0);
matrix.sync().unwrap();
let (out_l, out_r) = node_exec.test_run(0.11, true);
// out_l and out_r contain two channels of audio
// samples now.
```
## State of Development
As of 2021-05-18: The architecture and it's functionality have been mostly
As of 2022-07-30: The architecture and it's functionality have been mostly
feature complete by now. The only part that is still lacking is the collection
of modules/nodes, this is the area of current development. Adding lots of
nodes.
@ -162,6 +223,9 @@ devote for project coordination. So please don't be offended if your issue rots
in the GitHub issue tracker, or your pull requests is left dangling around
for ages.
If you want to contribute new DSP nodes/modules to HexoDSP/HexoSynth,
please look into the guide at the start of the [crate::dsp] module.
I might merge pull requests if I find the time and think that the contributions
are in line with my vision.
@ -247,6 +311,7 @@ projects and authors, I can't relicense it.
*/
pub mod cell_dir;
pub mod chain_builder;
#[allow(unused_macros, non_snake_case)]
pub mod dsp;
pub mod log;
@ -255,9 +320,12 @@ pub mod matrix_repr;
pub mod monitor;
pub mod nodes;
pub mod sample_lib;
pub mod scope_handle;
pub mod wblockdsp;
mod util;
pub use cell_dir::CellDir;
pub use chain_builder::MatrixCellChain;
pub use dsp::{NodeId, NodeInfo, ParamId, SAtom};
pub use log::log;
pub use matrix::{Cell, Matrix};
@ -265,6 +333,7 @@ pub use matrix_repr::load_patch_from_file;
pub use matrix_repr::save_patch_to_file;
pub use nodes::{new_node_engine, NodeConfigurator, NodeExecutor};
pub use sample_lib::{SampleLibrary, SampleLoadError};
pub use scope_handle::ScopeHandle;
pub struct Context<'a, 'b, 'c, 'd> {
pub nframes: usize,

View file

@ -1,4 +1,4 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// Copyright (c) 2021-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.
@ -9,6 +9,8 @@ pub use crate::monitor::MON_SIG_CNT;
pub use crate::nodes::MinMaxMonitorSamples;
use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES};
pub use crate::CellDir;
use crate::ScopeHandle;
use crate::wblockdsp::{BlockFun, BlockFunSnapshot, BlkJITCompileError};
use std::collections::{HashMap, HashSet};
@ -149,6 +151,10 @@ impl Cell {
self.out3 = None;
}
pub fn set_node_id_keep_ios(&mut self, node_id: NodeId) {
self.node_id = node_id;
}
pub fn label<'a>(&self, buf: &'a mut [u8]) -> Option<&'a str> {
use std::io::Write;
let mut cur = std::io::Cursor::new(buf);
@ -261,6 +267,40 @@ impl Cell {
}
}
/// This is a helper function to quickly set an input by name and direction.
///
///```
/// use hexodsp::*;
///
/// let mut cell = Cell::empty(NodeId::Sin(0));
/// cell.set_input_by_name("freq", CellDir::T).unwrap();
///```
pub fn set_input_by_name(&mut self, name: &str, dir: CellDir) -> Result<(), ()> {
if let Some(idx) = self.node_id.inp(name) {
self.set_io_dir(dir, idx as usize);
Ok(())
} else {
Err(())
}
}
/// This is a helper function to quickly set an output by name and direction.
///
///```
/// use hexodsp::*;
///
/// let mut cell = Cell::empty(NodeId::Sin(0));
/// cell.set_output_by_name("sig", CellDir::B).unwrap();
///```
pub fn set_output_by_name(&mut self, name: &str, dir: CellDir) -> Result<(), ()> {
if let Some(idx) = self.node_id.out(name) {
self.set_io_dir(dir, idx as usize);
Ok(())
} else {
Err(())
}
}
pub fn input(mut self, i1: Option<u8>, i2: Option<u8>, i3: Option<u8>) -> Self {
self.in1 = i1;
self.in2 = i2;
@ -275,13 +315,13 @@ impl Cell {
self
}
/// Finds the first free input (one without an adjacent cell). If any free input
/// has an assigned input, that edge is returned.
/// Finds the first free input or output (one without an adjacent cell). If any free input/output
/// has an assigned input, that edge is returned before any else.
/// With `dir` you can specify input with `CellDir::T`, output with `CellDir::B`
/// and any with `CellDir::C`.
pub fn find_first_adjacent_free(
&self,
m: &mut Matrix,
m: &Matrix,
dir: CellDir,
) -> Option<(CellDir, Option<u8>)> {
let mut free_ports = vec![];
@ -320,7 +360,7 @@ impl Cell {
/// and any with `CellDir::C`.
pub fn find_all_adjacent_free(
&self,
m: &mut Matrix,
m: &Matrix,
dir: CellDir,
) -> Vec<(CellDir, (usize, usize))> {
let mut free_ports = vec![];
@ -341,11 +381,34 @@ impl Cell {
}
}
free_ports.to_vec()
free_ports
}
/// Finds all dangling ports in the specified direction.
/// With `dir` you can specify input with `CellDir::T`, output with `CellDir::B`
/// and any with `CellDir::C`.
pub fn find_unconnected_ports(&self, m: &Matrix, dir: CellDir) -> Vec<CellDir> {
let mut unused_ports = vec![];
let options: &[CellDir] = if dir == CellDir::C {
&[CellDir::T, CellDir::TL, CellDir::BL, CellDir::TR, CellDir::BR, CellDir::B]
} else if dir.is_input() {
&[CellDir::T, CellDir::TL, CellDir::BL]
} else {
&[CellDir::TR, CellDir::BR, CellDir::B]
};
for dir in options {
if self.is_port_dir_connected(m, *dir).is_none() {
unused_ports.push(*dir);
}
}
unused_ports
}
/// If the port is connected, it will return the position of the other cell.
pub fn is_port_dir_connected(&self, m: &mut Matrix, dir: CellDir) -> Option<(usize, usize)> {
pub fn is_port_dir_connected(&self, m: &Matrix, dir: CellDir) -> Option<(usize, usize)> {
if self.has_dir_set(dir) {
if let Some(new_pos) = dir.offs_pos((self.x as usize, self.y as usize)) {
if let Some(dst_cell) = m.get(new_pos.0, new_pos.1) {
@ -388,7 +451,7 @@ pub trait MatrixObserver {
/// Not called, when [MatrixObserver::update_all] tells you that
/// everything has changed.
fn update_prop(&self, key: &str);
/// Called when a new cell is monitored via [MatrixObserver::monitor_cell].
/// Called when a new cell is monitored via [Matrix::monitor_cell].
/// Not called, when [MatrixObserver::update_all] tells you that
/// everything has changed.
fn update_monitor(&self, cell: &Cell);
@ -516,16 +579,37 @@ impl Matrix {
self.config.filtered_out_fb_for(ni, out)
}
/// Retrieve the oscilloscope handle for the scope index `scope`.
pub fn get_pattern_data(&self, tracker_id: usize) -> Option<Arc<Mutex<PatternData>>> {
self.config.get_pattern_data(tracker_id)
}
/// Checks if pattern data updates need to be sent to the
/// DSP thread.
/// Retrieve a handle to the tracker pattern data of the tracker `tracker_id`.
pub fn get_scope_handle(&self, scope: usize) -> Option<Arc<ScopeHandle>> {
self.config.get_scope_handle(scope)
}
/// Checks if there are any updates to send for the pattern data that belongs to the
/// tracker `tracker_id`. Call this repeatedly, eg. once per frame in a GUI, in case the user
/// modified the pattern data. It will make sure that the modifications are sent to the
/// audio thread.
pub fn check_pattern_data(&mut self, tracker_id: usize) {
self.config.check_pattern_data(tracker_id)
}
/// Checks the block function for the id `id`. If the block function did change,
/// updates are then sent to the audio thread.
/// See also [Matrix::get_block_function].
pub fn check_block_function(&mut self, id: usize) -> Result<(), BlkJITCompileError> {
self.config.check_block_function(id)
}
/// Retrieve a handle to the block function `id`. In case you modify the block function,
/// make sure to call [Matrix::check_block_function].
pub fn get_block_function(&self, id: usize) -> Option<Arc<Mutex<BlockFun>>> {
self.config.get_block_function(id)
}
/// Saves the state of the hexagonal grid layout.
/// This is usually used together with [Matrix::check]
/// and [Matrix::restore_matrix] to try if changes on
@ -760,9 +844,21 @@ impl Matrix {
tracker_id += 1;
}
let mut block_funs: Vec<Option<BlockFunSnapshot>> = vec![];
let mut bf_id = 0;
while let Some(bf) = self.get_block_function(bf_id) {
block_funs.push(if bf.lock().unwrap().is_unset() {
None
} else {
Some(bf.lock().unwrap().save_snapshot())
});
bf_id += 1;
}
let properties = self.properties.iter().map(|(k, v)| (k.to_string(), v.clone())).collect();
MatrixRepr { cells, params, atoms, patterns, properties, version: 2 }
MatrixRepr { cells, params, atoms, patterns, block_funs, properties, version: 2 }
}
/// Loads the matrix from a previously my [Matrix::to_repr]
@ -794,6 +890,14 @@ impl Matrix {
}
}
for (bf_id, block_fun) in repr.block_funs.iter().enumerate() {
if let Some(block_fun) = block_fun {
if let Some(bf) = self.get_block_function(bf_id) {
bf.lock().unwrap().load_snapshot(block_fun);
}
}
}
let ret = self.sync();
if let Some(obs) = &self.observer {
@ -978,6 +1082,53 @@ impl Matrix {
}
}
/// Retrieves the immediate connections to adjacent cells and returns a list.
/// Returns none if there is no cell at the given position.
///
/// Returns a vector with pairs of this content:
///
/// (
/// (center_cell, center_connection_dir, center_node_io_index),
/// (
/// other_cell,
/// other_connection_dir,
/// other__node_io_index,
/// (other_cell_x, other_cell_y)
/// )
/// )
pub fn get_connections(
&self,
x: usize,
y: usize,
) -> Option<Vec<((Cell, CellDir, u8), (Cell, CellDir, u8, (usize, usize)))>> {
let this_cell = self.get(x, y)?;
let mut ret = vec![];
for edge in 0..6 {
let dir = CellDir::from(edge);
if let Some(node_io_idx) = this_cell.local_port_idx(dir) {
if let Some((nx, ny)) = dir.offs_pos((x, y)) {
if !(nx < self.w && ny < self.h) {
continue;
}
if let Some(other_cell) = self.get(nx, ny) {
if let Some(other_node_io_idx) = other_cell.local_port_idx(dir.flip()) {
ret.push((
(*this_cell, dir, node_io_idx),
(*other_cell, dir.flip(), other_node_io_idx, (nx, ny)),
));
}
}
}
}
}
Some(ret)
}
pub fn for_each<F: FnMut(usize, usize, &Cell)>(&self, mut f: F) {
for x in 0..self.w {
for y in 0..self.h {
@ -1347,6 +1498,44 @@ mod tests {
);
}
#[test]
fn check_matrix_get_connections() {
use crate::nodes::new_node_engine;
let (node_conf, _node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 3, 3);
matrix.place(0, 0, Cell::empty(NodeId::Sin(0)).out(None, Some(0), None));
matrix.place(
1,
0,
Cell::empty(NodeId::Sin(1)).input(None, Some(0), None).out(None, None, Some(0)),
);
matrix.place(1, 1, Cell::empty(NodeId::Sin(2)).input(Some(0), None, None));
matrix.sync().unwrap();
let res = matrix.get_connections(1, 0);
let res = res.expect("Found connected cells");
let (_src_cell, src_dir, src_io_idx) = res[0].0;
let (_dst_cell, dst_dir, dst_io_idx, (nx, ny)) = res[0].1;
assert_eq!(src_dir, CellDir::B, "Found first connection at bottom");
assert_eq!(src_io_idx, 0, "Correct output port");
assert_eq!(dst_dir, CellDir::T, "Found first connection at bottom");
assert_eq!(dst_io_idx, 0, "Correct output port");
assert_eq!((nx, ny), (1, 1), "Correct other position");
let (_src_cell, src_dir, src_io_idx) = res[1].0;
let (_dst_cell, dst_dir, dst_io_idx, (nx, ny)) = res[1].1;
assert_eq!(src_dir, CellDir::TL, "Found first connection at bottom");
assert_eq!(src_io_idx, 0, "Correct output port");
assert_eq!(dst_dir, CellDir::BR, "Found first connection at bottom");
assert_eq!(dst_io_idx, 0, "Correct output port");
assert_eq!((nx, ny), (0, 0), "Correct other position");
}
#[test]
fn check_matrix_param_is_used() {
use crate::nodes::new_node_engine;
@ -1542,7 +1731,9 @@ mod tests {
prog.prog[2].to_string(),
"Op(i=1 out=(1-2|1) in=(2-4|1) at=(0-0) mod=(1-3) cpy=(o0 => i2) mod=1)"
);
assert_eq!(prog.prog[3].to_string(), "Op(i=2 out=(2-3|0) in=(4-6|3) at=(0-0) mod=(3-5) cpy=(o1 => i4) cpy=(o3 => i5) mod=3 mod=4)");
assert_eq!(
prog.prog[3].to_string(),
"Op(i=2 out=(2-3|0) in=(4-6|3) at=(0-0) mod=(3-5) cpy=(o1 => i4) cpy=(o3 => i5) mod=3 mod=4)");
}
#[test]

View file

@ -4,6 +4,7 @@
use crate::dsp::{NodeId, ParamId, SAtom};
use serde_json::{json, Value};
use crate::wblockdsp::BlockFunSnapshot;
#[derive(Debug, Clone, Copy)]
pub struct CellRepr {
@ -187,6 +188,7 @@ pub struct MatrixRepr {
pub atoms: Vec<(ParamId, SAtom)>,
pub patterns: Vec<Option<PatternRepr>>,
pub properties: Vec<(String, SAtom)>,
pub block_funs: Vec<Option<BlockFunSnapshot>>,
pub version: i64,
}
@ -289,8 +291,9 @@ impl MatrixRepr {
let atoms = vec![];
let patterns = vec![];
let properties = vec![];
let block_funs = vec![];
Self { cells, params, atoms, patterns, properties, version: 2 }
Self { cells, params, atoms, patterns, block_funs, properties, version: 2 }
}
pub fn write_to_file(&mut self, filepath: &str) -> std::io::Result<()> {
@ -398,6 +401,17 @@ impl MatrixRepr {
}
}
let block_funs = &v["block_funs"];
if let Value::Array(block_funs) = block_funs {
for p in block_funs.iter() {
m.block_funs.push(if p.is_object() {
Some(BlockFunSnapshot::deserialize(&p)?)
} else {
None
});
}
}
Ok(m)
}
@ -468,6 +482,15 @@ impl MatrixRepr {
v["patterns"] = patterns;
let mut block_funs = json!([]);
if let Value::Array(block_funs) = &mut block_funs {
for p in self.block_funs.iter() {
block_funs.push(if let Some(p) = p { p.serialize() } else { Value::Null });
}
}
v["block_funs"] = block_funs;
v.to_string()
}
}

View file

@ -3,9 +3,12 @@
// See README.md and COPYING for details.
pub const MAX_ALLOCATED_NODES: usize = 256;
pub const MAX_SCOPES: usize = 8;
pub const SCOPE_SAMPLES: usize = 512;
pub const MAX_INPUTS: usize = 32;
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_CODE_ENGINES: usize = 32;
pub const MAX_FB_DELAYS: usize = 256; // 256 feedback delays, thats roughly 1.2MB RAM
pub const FB_DELAY_TIME_US: usize = 3140; // 3.14ms (should be enough for MAX_BLOCK_SIZE)
// This means, until 384000 sample rate the times are accurate.
@ -87,7 +90,7 @@ pub fn new_node_engine() -> (NodeConfigurator, NodeExecutor) {
// XXX: This is one of the earliest and most consistent points
// in runtime to do this kind of initialization:
crate::dsp::helpers::init_cos_tab();
synfx_dsp::init_cos_tab();
(nc, ne)
}

View file

@ -3,20 +3,24 @@
// See README.md and COPYING for details.
use super::{
FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS,
MAX_INPUTS, UNUSED_MONITOR_IDX,
FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES,
MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX,
};
use crate::wblockdsp::*;
use crate::dsp::tracker::{PatternData, Tracker};
use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom};
use crate::monitor::{new_monitor_processor, MinMaxMonitorSamples, Monitor, MON_SIG_CNT};
use crate::nodes::drop_thread::DropThread;
use crate::util::AtomicFloat;
#[cfg(feature = "synfx-dsp-jit")]
use synfx_dsp_jit::engine::CodeEngine;
use crate::SampleLibrary;
use crate::ScopeHandle;
use ringbuf::{Producer, RingBuffer};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use synfx_dsp::AtomicFloat;
use triple_buffer::Output;
/// A NodeInstance describes the input/output/atom ports of a Node
@ -177,6 +181,16 @@ pub struct NodeConfigurator {
pub(crate) node2idx: HashMap<NodeId, usize>,
/// Holding the tracker sequencers
pub(crate) trackers: Vec<Tracker>,
/// Holding the scope buffers:
pub(crate) scopes: Vec<Arc<ScopeHandle>>,
/// Holding the WBlockDSP code engine backends:
#[cfg(feature = "synfx-dsp-jit")]
pub(crate) code_engines: Vec<CodeEngine>,
/// Holds the block functions that are JIT compiled to DSP code
/// for the `Code` nodes. The code is then sent via the [CodeEngine]
/// in [NodeConfigurator::check_block_function].
#[cfg(feature = "synfx-dsp-jit")]
pub(crate) block_functions: Vec<(u64, Arc<Mutex<BlockFun>>)>,
/// The shared parts of the [NodeConfigurator]
/// and the [crate::nodes::NodeExecutor].
pub(crate) shared: SharedNodeConf,
@ -263,6 +277,22 @@ impl NodeConfigurator {
let (shared, shared_exec) = SharedNodeConf::new();
let mut scopes = vec![];
scopes.resize_with(MAX_SCOPES, || ScopeHandle::new_shared());
#[cfg(feature = "synfx-dsp-jit")]
let (code_engines, block_functions) = {
let code_engines = vec![CodeEngine::new_stdlib(); MAX_AVAIL_CODE_ENGINES];
let lang = setup_hxdsp_block_language(code_engines[0].get_lib());
let mut block_functions = vec![];
block_functions.resize_with(MAX_AVAIL_CODE_ENGINES, || {
(0, Arc::new(Mutex::new(BlockFun::new(lang.clone()))))
});
(code_engines, block_functions)
};
(
NodeConfigurator {
nodes,
@ -279,6 +309,11 @@ impl NodeConfigurator {
atom_values: std::collections::HashMap::new(),
node2idx: HashMap::new(),
trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS],
#[cfg(feature = "synfx-dsp-jit")]
code_engines,
#[cfg(feature = "synfx-dsp-jit")]
block_functions,
scopes,
},
shared_exec,
)
@ -638,6 +673,12 @@ impl NodeConfigurator {
}
}
/// Retrieve the oscilloscope handle for the scope index `scope`.
pub fn get_scope_handle(&self, scope: usize) -> Option<Arc<ScopeHandle>> {
self.scopes.get(scope).cloned()
}
/// Retrieve a handle to the tracker pattern data of the tracker `tracker_id`.
pub fn get_pattern_data(&self, tracker_id: usize) -> Option<Arc<Mutex<PatternData>>> {
if tracker_id >= self.trackers.len() {
return None;
@ -646,6 +687,10 @@ impl NodeConfigurator {
Some(self.trackers[tracker_id].data())
}
/// Checks if there are any updates to send for the pattern data that belongs to the
/// tracker `tracker_id`. Call this repeatedly, eg. once per frame in a GUI, in case the user
/// modified the pattern data. It will make sure that the modifications are sent to the
/// audio thread.
pub fn check_pattern_data(&mut self, tracker_id: usize) {
if tracker_id >= self.trackers.len() {
return;
@ -654,6 +699,49 @@ impl NodeConfigurator {
self.trackers[tracker_id].send_one_update();
}
/// Checks the block function for the id `id`. If the block function did change,
/// updates are then sent to the audio thread.
/// See also [NodeConfigurator::get_block_function].
pub fn check_block_function(&mut self, id: usize) -> Result<(), BlkJITCompileError> {
#[cfg(feature = "synfx-dsp-jit")]
if let Some(cod) = self.code_engines.get_mut(id) {
cod.query_returns();
}
#[cfg(feature = "synfx-dsp-jit")]
if let Some((generation, block_fun)) = self.block_functions.get_mut(id) {
if let Ok(block_fun) = block_fun.lock() {
if *generation != block_fun.generation() {
*generation = block_fun.generation();
let mut compiler = Block2JITCompiler::new(block_fun.block_language());
let ast = compiler.compile(&block_fun)?;
if let Some(cod) = self.code_engines.get_mut(id) {
match cod.upload(ast) {
Err(e) => return Err(BlkJITCompileError::JITCompileError(e)),
Ok(()) => (),
}
}
}
}
}
Ok(())
}
/// Retrieve a handle to the block function `id`. In case you modify the block function,
/// make sure to call [NodeConfigurator::check_block_function].
pub fn get_block_function(&self, id: usize) -> Option<Arc<Mutex<BlockFun>>> {
#[cfg(feature = "synfx-dsp-jit")]
{
self.block_functions.get(id).map(|pair| pair.1.clone())
}
#[cfg(not(feature = "synfx-dsp-jit"))]
{
None
}
}
pub fn delete_nodes(&mut self) {
self.node2idx.clear();
self.nodes.fill_with(|| (NodeInfo::from_node_id(NodeId::Nop), None));
@ -677,6 +765,20 @@ impl NodeConfigurator {
}
}
#[cfg(feature = "synfx-dsp-jit")]
if let Node::Code { node } = &mut node {
let code_idx = ni.instance();
if let Some(cod) = self.code_engines.get_mut(code_idx) {
node.set_backend(cod.get_backend());
}
}
if let Node::Scope { node } = &mut node {
if let Some(handle) = self.scopes.get(ni.instance()) {
node.set_scope_handle(handle.clone());
}
}
for i in 0..self.nodes.len() {
if let NodeId::Nop = self.nodes[i].0.to_id() {
index = Some(i);

View file

@ -8,7 +8,8 @@ use super::{
};
use crate::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE};
use crate::monitor::{MonitorBackend, MON_SIG_CNT};
use crate::util::{AtomicFloat, Smoother};
use crate::util::Smoother;
use synfx_dsp::AtomicFloat;
use crate::log;
use std::io::Write;

91
src/scope_handle.rs Normal file
View file

@ -0,0 +1,91 @@
// 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 crate::nodes::SCOPE_SAMPLES;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use synfx_dsp::{AtomicFloat, AtomicFloatPair};
#[derive(Debug)]
pub struct ScopeHandle {
bufs: [Vec<AtomicFloatPair>; 3],
active: [AtomicBool; 3],
offs_gain: [AtomicFloatPair; 3],
threshold: (AtomicBool, AtomicFloat),
}
impl ScopeHandle {
pub fn new_shared() -> Arc<Self> {
let mut v1 = vec![];
v1.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default());
let mut v2 = vec![];
v2.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default());
let mut v3 = vec![];
v3.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default());
Arc::new(Self {
bufs: [v1, v2, v3],
active: [AtomicBool::new(false), AtomicBool::new(false), AtomicBool::new(false)],
offs_gain: [
AtomicFloatPair::default(),
AtomicFloatPair::default(),
AtomicFloatPair::default(),
],
threshold: (AtomicBool::new(false), AtomicFloat::default()),
})
}
pub fn write_oversampled(&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, v));
}
}
pub fn set_offs_gain(&self, buf_idx: usize, offs: f32, gain: f32) {
self.offs_gain[buf_idx % 3].set((offs, gain));
}
pub fn get_offs_gain(&self, buf_idx: usize) -> (f32, f32) {
self.offs_gain[buf_idx % 3].get()
}
pub fn set_threshold(&self, thresh: Option<f32>) {
if let Some(t) = thresh {
self.threshold.1.set(t);
self.threshold.0.store(true, Ordering::Relaxed);
} else {
self.threshold.0.store(false, Ordering::Relaxed);
}
}
pub fn get_threshold(&self) -> Option<f32> {
if self.threshold.0.load(Ordering::Relaxed) {
Some(self.threshold.1.get())
} else {
None
}
}
pub fn write(&self, buf_idx: usize, idx: usize, v: (f32, f32)) {
self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v);
}
pub fn read(&self, buf_idx: usize, idx: usize) -> (f32, f32) {
self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].get()
}
pub fn set_active_from_mask(&self, mask: u64) {
self.active[0].store(mask & 0x1 > 0x0, Ordering::Relaxed);
self.active[1].store(mask & 0x2 > 0x0, Ordering::Relaxed);
self.active[2].store(mask & 0x4 > 0x0, Ordering::Relaxed);
}
pub fn is_active(&self, idx: usize) -> bool {
self.active[idx % 3].load(Ordering::Relaxed)
}
pub fn len(&self) -> usize {
SCOPE_SAMPLES
}
}

View file

@ -2,8 +2,6 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details.
use std::sync::atomic::{AtomicU32, Ordering};
const SMOOTHING_TIME_MS: f32 = 10.0;
pub struct Smoother {
@ -95,60 +93,3 @@ impl PerfTimer {
self.i = std::time::Instant::now();
}
}
// Implementation from vst-rs
// https://github.com/RustAudio/vst-rs/blob/master/src/util/atomic_float.rs
// Under MIT License
// Copyright (c) 2015 Marko Mijalkovic
pub struct AtomicFloat {
atomic: AtomicU32,
}
impl AtomicFloat {
/// New atomic float with initial value `value`.
pub fn new(value: f32) -> AtomicFloat {
AtomicFloat { atomic: AtomicU32::new(value.to_bits()) }
}
/// Get the current value of the atomic float.
#[inline]
pub fn get(&self) -> f32 {
f32::from_bits(self.atomic.load(Ordering::Relaxed))
}
/// Set the value of the atomic float to `value`.
#[inline]
pub fn set(&self, value: f32) {
self.atomic.store(value.to_bits(), Ordering::Relaxed)
}
}
impl Default for AtomicFloat {
fn default() -> Self {
AtomicFloat::new(0.0)
}
}
impl std::fmt::Debug for AtomicFloat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.get(), f)
}
}
impl std::fmt::Display for AtomicFloat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.get(), f)
}
}
impl From<f32> for AtomicFloat {
fn from(value: f32) -> Self {
AtomicFloat::new(value)
}
}
impl From<AtomicFloat> for f32 {
fn from(value: AtomicFloat) -> Self {
value.get()
}
}

776
src/wblockdsp/compiler.rs Normal file
View file

@ -0,0 +1,776 @@
// 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 std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use super::language::*;
#[cfg(feature = "synfx-dsp-jit")]
use synfx_dsp_jit::{ASTNode, JITCompileError};
#[derive(Debug)]
struct JASTNode {
id: usize,
typ: String,
lbl: String,
nodes: Vec<(String, String, ASTNodeRef)>,
}
/// The WBlockDSP Abstract Syntax Tree. It is generated by [BlockFun::generate_tree]
/// in [Block2JITCompiler::compile].
#[derive(Debug, Clone)]
pub struct ASTNodeRef(Rc<RefCell<JASTNode>>);
impl BlockASTNode for ASTNodeRef {
fn from(id: usize, typ: &str, lbl: &str) -> ASTNodeRef {
ASTNodeRef(Rc::new(RefCell::new(JASTNode {
id,
typ: typ.to_string(),
lbl: lbl.to_string(),
nodes: vec![],
})))
}
fn add_node(&self, in_port: String, out_port: String, node: ASTNodeRef) {
self.0.borrow_mut().nodes.push((in_port, out_port, node));
}
}
impl ASTNodeRef {
/// Returns the first child AST node.
pub fn first_child_ref(&self) -> Option<ASTNodeRef> {
self.0.borrow().nodes.get(0).map(|n| n.2.clone())
}
/// Returns the first child, including input/output info.
pub fn first_child(&self) -> Option<(String, String, ASTNodeRef)> {
self.0.borrow().nodes.get(0).cloned()
}
/// Returns the nth child, including input/output info.
pub fn nth_child(&self, i: usize) -> Option<(String, String, ASTNodeRef)> {
self.0.borrow().nodes.get(i).cloned()
}
/// Generates a recursive tree dump output.
///
///```ignore
/// println!("{}", node.walk_dump("", "", 0));
///```
pub fn walk_dump(&self, input: &str, output: &str, indent: usize) -> String {
let indent_str = " ".repeat(indent + 1);
let out_port = if output.len() > 0 { format!("(out: {})", output) } else { "".to_string() };
let in_port = if input.len() > 0 { format!("(in: {})", input) } else { "".to_string() };
let mut s = format!(
"{}{}#{}[{}] {}{}\n",
indent_str,
self.0.borrow().id,
self.0.borrow().typ,
self.0.borrow().lbl,
out_port,
in_port
);
for (inp, out, n) in &self.0.borrow().nodes {
s += &n.walk_dump(&inp, &out, indent + 1);
}
s
}
}
type BlkASTRef = Rc<BlkASTNode>;
#[derive(Debug, Clone)]
enum BlkASTNode {
Area {
childs: Vec<BlkASTRef>,
},
Set {
var: String,
expr: BlkASTRef,
},
Get {
id: usize,
var: String,
},
Node {
id: usize,
out: Option<String>,
typ: String,
lbl: String,
childs: Vec<(Option<String>, BlkASTRef)>,
},
Literal {
value: f64,
},
}
impl BlkASTNode {
pub fn dump(&self, indent: usize, inp: Option<&str>) -> String {
let mut indent_str = " ".repeat(indent + 1);
if let Some(inp) = inp {
indent_str += &format!("{}<= ", inp);
} else {
indent_str += "<= ";
}
match self {
BlkASTNode::Area { childs } => {
let mut s = format!("{}Area\n", indent_str);
for c in childs.iter() {
s += &c.dump(indent + 1, None);
}
s
}
BlkASTNode::Set { var, expr } => {
format!("{}set '{}'=\n", indent_str, var) + &expr.dump(indent + 1, None)
}
BlkASTNode::Get { id, var } => {
format!("{}get '{}' (id={})\n", indent_str, var, id)
}
BlkASTNode::Literal { value } => {
format!("{}{}\n", indent_str, value)
}
BlkASTNode::Node { id, out, typ, lbl, childs } => {
let lbl = if *typ == *lbl { "".to_string() } else { format!("[{}]", lbl) };
let mut s = if let Some(out) = out {
format!("{}{}{} (id={}/{})\n", indent_str, typ, lbl, id, out)
} else {
format!("{}{}{} (id={})\n", indent_str, typ, lbl, id)
};
for (inp, c) in childs.iter() {
s += &format!("{}", c.dump(indent + 1, inp.as_ref().map(|s| &s[..])));
}
s
}
}
}
pub fn new_area(childs: Vec<BlkASTRef>) -> BlkASTRef {
Rc::new(BlkASTNode::Area { childs })
}
pub fn new_set(var: &str, expr: BlkASTRef) -> BlkASTRef {
Rc::new(BlkASTNode::Set { var: var.to_string(), expr })
}
pub fn new_get(id: usize, var: &str) -> BlkASTRef {
Rc::new(BlkASTNode::Get { id, var: var.to_string() })
}
pub fn new_literal(val: &str) -> Result<BlkASTRef, BlkJITCompileError> {
if let Ok(value) = val.parse::<f64>() {
Ok(Rc::new(BlkASTNode::Literal { value }))
} else {
Err(BlkJITCompileError::BadLiteralNumber(val.to_string()))
}
}
pub fn new_node(
id: usize,
out: Option<String>,
typ: &str,
lbl: &str,
childs: Vec<(Option<String>, BlkASTRef)>,
) -> BlkASTRef {
Rc::new(BlkASTNode::Node { id, out, typ: typ.to_string(), lbl: lbl.to_string(), childs })
}
}
#[derive(Debug, Clone)]
pub enum BlkJITCompileError {
UnknownError,
NoSynfxDSPJit,
BadTree(ASTNodeRef),
NoOutputAtIdx(String, usize),
ASTMissingOutputLabel(usize),
NoTmpVarForOutput(usize, String),
BadLiteralNumber(String),
NodeWithoutID(String),
UnknownType(String),
TooManyInputs(String, usize),
WrongNumberOfChilds(String, usize, usize),
UnassignedInput(String, usize, String),
#[cfg(feature = "synfx-dsp-jit")]
JITCompileError(JITCompileError),
}
pub struct Block2JITCompiler {
idout_var_map: HashMap<String, String>,
lang: Rc<RefCell<BlockLanguage>>,
tmpvar_counter: usize,
}
#[cfg(not(feature = "synfx-dsp-jit"))]
pub enum ASTNode {
NoSynfxDSPJit
}
impl Block2JITCompiler {
pub fn new(lang: Rc<RefCell<BlockLanguage>>) -> Self {
Self { idout_var_map: HashMap::new(), lang, tmpvar_counter: 0 }
}
pub fn next_tmpvar_name(&mut self, extra: &str) -> String {
self.tmpvar_counter += 1;
format!("_tmp{}_{}_", self.tmpvar_counter, extra)
}
pub fn store_idout_var(&mut self, id: usize, out: &str, v: &str) {
self.idout_var_map.insert(format!("{}/{}", id, out), v.to_string());
}
pub fn get_var_for_idout(&self, id: usize, out: &str) -> Option<&str> {
self.idout_var_map.get(&format!("{}/{}", id, out)).map(|s| &s[..])
}
fn trans2bjit(
&mut self,
node: &ASTNodeRef,
my_out: Option<String>,
) -> Result<BlkASTRef, BlkJITCompileError> {
let id = node.0.borrow().id;
if let Some(out) = &my_out {
if let Some(tmpvar) = self.get_var_for_idout(id, out) {
return Ok(BlkASTNode::new_get(0, tmpvar));
}
} else {
if let Some(tmpvar) = self.get_var_for_idout(id, "") {
return Ok(BlkASTNode::new_get(0, tmpvar));
}
}
match &node.0.borrow().typ[..] {
"<a>" => {
let mut childs = vec![];
let mut i = 0;
while let Some((_in, out, child)) = node.nth_child(i) {
let out = if out.len() > 0 { Some(out) } else { None };
let child = self.trans2bjit(&child, out)?;
childs.push(child);
i += 1;
}
Ok(BlkASTNode::new_area(childs))
}
// TODO: handle results properly, like remembering the most recent result
// and append it to the end of the statements block. so that a temporary
// variable is created.
"<r>" => {
if let Some((_in, out, first)) = node.first_child() {
let out = if out.len() > 0 { Some(out) } else { None };
let childs =
vec![self.trans2bjit(&first, out)?, BlkASTNode::new_get(0, "_res_")];
Ok(BlkASTNode::new_area(childs))
} else {
Err(BlkJITCompileError::BadTree(node.clone()))
}
}
"->" => {
if let Some((_in, out, first)) = node.first_child() {
let out = if out.len() > 0 { Some(out) } else { None };
self.trans2bjit(&first, out)
} else {
Err(BlkJITCompileError::BadTree(node.clone()))
}
}
"value" => Ok(BlkASTNode::new_literal(&node.0.borrow().lbl)?),
"set" | "<res>" => {
if let Some((_in, out, first)) = node.first_child() {
let out = if out.len() > 0 { Some(out) } else { None };
let expr = self.trans2bjit(&first, out)?;
if &node.0.borrow().typ[..] == "<res>" {
Ok(BlkASTNode::new_set("_res_", expr))
} else {
Ok(BlkASTNode::new_set(&node.0.borrow().lbl, expr))
}
} else {
Err(BlkJITCompileError::BadTree(node.clone()))
}
}
"get" => Ok(BlkASTNode::new_get(id, &node.0.borrow().lbl)),
"->2" | "->3" => {
if let Some((_in, out, first)) = node.first_child() {
let out = if out.len() > 0 { Some(out) } else { None };
let mut area = vec![];
let tmp_var = self.next_tmpvar_name("");
let expr = self.trans2bjit(&first, out)?;
area.push(BlkASTNode::new_set(&tmp_var, expr));
area.push(BlkASTNode::new_get(0, &tmp_var));
self.store_idout_var(id, "", &tmp_var);
Ok(BlkASTNode::new_area(area))
} else {
Err(BlkJITCompileError::BadTree(node.clone()))
}
}
optype => {
let mut childs = vec![];
let mut i = 0;
while let Some((inp, out, child)) = node.nth_child(i) {
let out = if out.len() > 0 { Some(out) } else { None };
let child = self.trans2bjit(&child, out)?;
if inp.len() > 0 {
childs.push((Some(inp.to_string()), child));
} else {
childs.push((None, child));
}
i += 1;
}
// TODO: Reorder the childs/arguments according to the input
// order in the BlockLanguage
let cnt = self.lang.borrow().type_output_count(optype);
if cnt > 1 {
let mut area = vec![];
let oname = self.lang.borrow().get_output_name_at_index(optype, 0);
if let Some(oname) = oname {
let tmp_var = self.next_tmpvar_name(&oname);
area.push(BlkASTNode::new_set(
&tmp_var,
BlkASTNode::new_node(
id,
my_out.clone(),
&node.0.borrow().typ,
&node.0.borrow().lbl,
childs,
),
));
self.store_idout_var(id, &oname, &tmp_var);
} else {
return Err(BlkJITCompileError::NoOutputAtIdx(optype.to_string(), 0));
}
for i in 1..cnt {
let oname = self.lang.borrow().get_output_name_at_index(optype, i);
if let Some(oname) = oname {
let tmp_var = self.next_tmpvar_name(&oname);
area.push(BlkASTNode::new_set(
&tmp_var,
BlkASTNode::new_get(0, &format!("%{}", i)),
));
self.store_idout_var(id, &oname, &tmp_var);
} else {
return Err(BlkJITCompileError::NoOutputAtIdx(optype.to_string(), i));
}
}
if let Some(out) = &my_out {
if let Some(tmpvar) = self.get_var_for_idout(id, out) {
area.push(BlkASTNode::new_get(0, tmpvar));
} else {
return Err(BlkJITCompileError::NoTmpVarForOutput(id, out.to_string()));
}
} else {
return Err(BlkJITCompileError::ASTMissingOutputLabel(id));
}
Ok(BlkASTNode::new_area(area))
} else {
Ok(BlkASTNode::new_node(
id,
my_out,
&node.0.borrow().typ,
&node.0.borrow().lbl,
childs,
))
}
}
}
}
#[cfg(feature = "synfx-dsp-jit")]
fn bjit2jit(&mut self, ast: &BlkASTRef) -> Result<Box<ASTNode>, BlkJITCompileError> {
use synfx_dsp_jit::build::*;
match &**ast {
BlkASTNode::Area { childs } => {
let mut stmt = vec![];
for c in childs.iter() {
stmt.push(self.bjit2jit(&c)?);
}
Ok(stmts(&stmt[..]))
}
BlkASTNode::Set { var, expr } => {
let e = self.bjit2jit(&expr)?;
Ok(assign(var, e))
}
BlkASTNode::Get { var: varname, .. } => Ok(var(varname)),
BlkASTNode::Node { id, typ, childs, .. } => match &typ[..] {
"if" => Err(BlkJITCompileError::UnknownError),
"zero" => Ok(literal(0.0)),
_ => {
if *id == 0 {
return Err(BlkJITCompileError::NodeWithoutID(typ.to_string()));
}
let lang = self.lang.clone();
let mut args = vec![];
if let Some(inputs) = lang.borrow().get_type_inputs(typ) {
if childs.len() > inputs.len() {
return Err(BlkJITCompileError::TooManyInputs(typ.to_string(), *id));
}
if inputs.len() > 0 && inputs[0] == Some("".to_string()) {
if inputs.len() != childs.len() {
return Err(BlkJITCompileError::WrongNumberOfChilds(
typ.to_string(),
*id,
childs.len(),
));
}
// We assume all inputs are unnamed:
for (_inp, c) in childs.iter() {
args.push(self.bjit2jit(&c)?);
}
} else {
// We assume all inputs are named:
for input_name in inputs.iter() {
let mut found = false;
for (inp, c) in childs.iter() {
println!("FOFOFO '{:?}' = '{:?}'", inp, input_name);
if inp == input_name {
args.push(self.bjit2jit(&c)?);
found = true;
break;
}
}
if !found {
return Err(BlkJITCompileError::UnassignedInput(
typ.to_string(),
*id,
format!("{:?}", input_name),
));
}
}
}
} else {
return Err(BlkJITCompileError::UnknownType(typ.to_string()));
}
match &typ[..] {
"+" | "*" | "-" | "/" => {
if args.len() != 2 {
return Err(BlkJITCompileError::WrongNumberOfChilds(
typ.to_string(),
*id,
args.len(),
));
}
let a = args.remove(0);
let b = args.remove(0);
match &typ[..] {
"+" => Ok(op_add(a, b)),
"*" => Ok(op_mul(a, b)),
"-" => Ok(op_sub(a, b)),
"/" => Ok(op_div(a, b)),
_ => Err(BlkJITCompileError::UnknownType(typ.to_string())),
}
}
_ => Ok(call(typ, *id as u64, &args[..])),
}
}
},
BlkASTNode::Literal { value } => Ok(literal(*value)),
}
}
pub fn compile(&mut self, fun: &BlockFun) -> Result<Box<ASTNode>, BlkJITCompileError> {
#[cfg(feature = "synfx-dsp-jit")]
{
let tree = fun.generate_tree::<ASTNodeRef>("zero").unwrap();
println!("{}", tree.walk_dump("", "", 0));
let blkast = self.trans2bjit(&tree, None)?;
println!("R: {}", blkast.dump(0, None));
self.bjit2jit(&blkast)
}
#[cfg(not(feature = "synfx-dsp-jit"))]
{
Err(BlkJITCompileError::NoSynfxDSPJit)
}
}
}
#[cfg(feature = "synfx-dsp-jit")]
#[cfg(test)]
mod test {
use super::*;
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
)
}
};
}
fn put_n(bf: &mut BlockFun, a: usize, x: i64, y: i64, s: &str) {
bf.instanciate_at(a, x, y, s, None).expect("no put error");
}
fn put_v(bf: &mut BlockFun, a: usize, x: i64, y: i64, s: &str, v: &str) {
bf.instanciate_at(a, x, y, s, Some(v.to_string())).expect("no put error");
}
use synfx_dsp_jit::{get_standard_library, ASTFun, DSPFunction, DSPNodeContext, JIT};
fn new_jit_fun<F: FnMut(&mut BlockFun)>(
mut f: F,
) -> (Rc<RefCell<DSPNodeContext>>, Box<DSPFunction>) {
let lib = get_standard_library();
let lang = crate::wblockdsp::setup_hxdsp_block_language(lib.clone());
let mut bf = BlockFun::new(lang.clone());
f(&mut bf);
let mut compiler = Block2JITCompiler::new(bf.block_language());
let ast = compiler.compile(&bf).expect("blk2jit compiles");
let ctx = DSPNodeContext::new_ref();
let jit = JIT::new(lib, ctx.clone());
let mut fun = jit.compile(ASTFun::new(ast)).expect("jit compiles");
fun.init(44100.0, None);
(ctx, fun)
}
#[test]
fn check_blocklang_sig1() {
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 1, 1, "set", "&sig1");
put_v(bf, 0, 0, 2, "value", "-0.3");
put_v(bf, 0, 1, 2, "set", "&sig2");
put_v(bf, 0, 0, 3, "value", "-1.3");
});
let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(s1, 0.3);
assert_float_eq!(s2, -0.3);
assert_float_eq!(ret, -1.3);
ctx.borrow_mut().free();
}
#[test]
fn check_blocklang_accum_shift() {
let (ctx, mut fun) = new_jit_fun(|bf| {
put_n(bf, 0, 1, 1, "accum");
bf.shift_port(0, 1, 1, 1, false);
put_v(bf, 0, 0, 2, "value", "0.01");
put_v(bf, 0, 0, 1, "get", "*reset");
});
fun.exec_2in_2out(0.0, 0.0);
fun.exec_2in_2out(0.0, 0.0);
fun.exec_2in_2out(0.0, 0.0);
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.04);
let reset_idx = ctx.borrow().get_persistent_variable_index_by_name("*reset").unwrap();
fun.access_persistent_var(reset_idx).map(|reset| *reset = 1.0);
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.0);
fun.access_persistent_var(reset_idx).map(|reset| *reset = 0.0);
fun.exec_2in_2out(0.0, 0.0);
fun.exec_2in_2out(0.0, 0.0);
fun.exec_2in_2out(0.0, 0.0);
fun.exec_2in_2out(0.0, 0.0);
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.05);
ctx.borrow_mut().free();
}
#[test]
fn check_blocklang_arithmetics() {
// Check + and *
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.50");
put_v(bf, 0, 0, 2, "value", "0.01");
put_n(bf, 0, 1, 1, "+");
bf.shift_port(0, 1, 1, 1, true);
put_v(bf, 0, 1, 3, "value", "2.0");
put_n(bf, 0, 2, 2, "*");
});
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 1.02);
ctx.borrow_mut().free();
// Check - and /
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.50");
put_v(bf, 0, 0, 2, "value", "0.01");
put_n(bf, 0, 1, 1, "-");
bf.shift_port(0, 1, 1, 1, true);
put_v(bf, 0, 1, 3, "value", "2.0");
put_n(bf, 0, 2, 2, "/");
});
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, (0.5 - 0.01) / 2.0);
ctx.borrow_mut().free();
// Check swapping inputs of "-"
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.50");
put_v(bf, 0, 0, 2, "value", "0.01");
put_n(bf, 0, 1, 1, "-");
bf.shift_port(0, 1, 1, 1, false);
});
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.01 - 0.5);
ctx.borrow_mut().free();
// Check swapping inputs of "/"
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.50");
put_v(bf, 0, 0, 2, "value", "0.01");
put_n(bf, 0, 1, 1, "/");
bf.shift_port(0, 1, 1, 1, false);
});
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.01 / 0.5);
ctx.borrow_mut().free();
// Check division of 0.0
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.50");
put_v(bf, 0, 0, 2, "value", "0.0");
put_n(bf, 0, 1, 1, "/");
});
let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.5 / 0.0);
ctx.borrow_mut().free();
}
#[test]
fn check_blocklang_divrem() {
// &sig1 on second output:
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 0, 2, "value", "-0.4");
put_n(bf, 0, 1, 1, "/%");
put_v(bf, 0, 2, 2, "set", "&sig1");
});
let (s1, _, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(s1, 0.3);
assert_float_eq!(ret, -0.75);
ctx.borrow_mut().free();
// &sig1 on first output:
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 0, 2, "value", "-0.4");
put_n(bf, 0, 1, 1, "/%");
put_v(bf, 0, 2, 1, "set", "&sig1");
});
let (s1, _, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.3);
assert_float_eq!(s1, -0.75);
ctx.borrow_mut().free();
// &sig1 on second output, but swapped outputs:
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 0, 2, "value", "-0.4");
put_n(bf, 0, 1, 1, "/%");
put_v(bf, 0, 2, 2, "set", "&sig1");
bf.shift_port(0, 1, 1, 0, true);
});
let (s1, _, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, 0.3);
assert_float_eq!(s1, -0.75);
ctx.borrow_mut().free();
// &sig1 on first output, but swapped outputs:
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 0, 2, "value", "-0.4");
put_n(bf, 0, 1, 1, "/%");
put_v(bf, 0, 2, 1, "set", "&sig1");
bf.shift_port(0, 1, 1, 0, true);
});
let (s1, _, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(s1, 0.3);
assert_float_eq!(ret, -0.75);
ctx.borrow_mut().free();
// &sig1 on first output, but swapped inputs:
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 0, 2, "value", "-0.4");
put_n(bf, 0, 1, 1, "/%");
put_v(bf, 0, 2, 1, "set", "&sig1");
bf.shift_port(0, 1, 1, 0, false);
});
let (s1, _, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(s1, -1.33333);
assert_float_eq!(ret, -0.1);
ctx.borrow_mut().free();
// &sig1 on first output, but swapped inputs and outputs:
let (ctx, mut fun) = new_jit_fun(|bf| {
put_v(bf, 0, 0, 1, "value", "0.3");
put_v(bf, 0, 0, 2, "value", "-0.4");
put_n(bf, 0, 1, 1, "/%");
put_v(bf, 0, 2, 1, "set", "&sig1");
bf.shift_port(0, 1, 1, 0, false);
bf.shift_port(0, 1, 1, 0, true);
});
let (s1, _, ret) = fun.exec_2in_2out(0.0, 0.0);
assert_float_eq!(ret, -1.33333);
assert_float_eq!(s1, -0.1);
ctx.borrow_mut().free();
}
}

257
src/wblockdsp/definition.rs Normal file
View file

@ -0,0 +1,257 @@
// 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 crate::wblockdsp::{BlockLanguage, BlockType, BlockUserInput};
use std::cell::RefCell;
use std::rc::Rc;
#[cfg(feature = "synfx-dsp-jit")]
use synfx_dsp_jit::DSPNodeTypeLibrary;
/** WBlockDSP language definition and standard library of nodes.
Most of the nodes are taken from the [synfx_dsp_jit] crate standard library.
*/
#[cfg(feature = "synfx-dsp-jit")]
pub fn setup_hxdsp_block_language(
dsp_lib: Rc<RefCell<DSPNodeTypeLibrary>>,
) -> Rc<RefCell<BlockLanguage>> {
let mut lang = BlockLanguage::new();
lang.define(BlockType {
category: "literals".to_string(),
name: "zero".to_string(),
rows: 1,
inputs: vec![],
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "The 0.0 value".to_string(),
color: 1,
});
lang.define(BlockType {
category: "literals".to_string(),
name: "π".to_string(),
rows: 1,
inputs: vec![],
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "The PI number".to_string(),
color: 1,
});
lang.define(BlockType {
category: "literals".to_string(),
name: "".to_string(),
rows: 1,
inputs: vec![],
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "2 * PI == TAU".to_string(),
color: 1,
});
lang.define(BlockType {
category: "literals".to_string(),
name: "value".to_string(),
rows: 1,
inputs: vec![],
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::Float,
description: "A literal value, typed in by the user.".to_string(),
color: 1,
});
lang.define(BlockType {
category: "routing".to_string(),
name: "->".to_string(),
rows: 1,
inputs: vec![Some("".to_string())],
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "Forwards the value one block".to_string(),
color: 6,
});
lang.define(BlockType {
category: "routing".to_string(),
name: "->2".to_string(),
rows: 2,
inputs: vec![Some("".to_string())],
outputs: vec![Some("".to_string()), Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "Forwards the value one block and sends it to multiple destinations"
.to_string(),
color: 6,
});
lang.define(BlockType {
category: "routing".to_string(),
name: "->3".to_string(),
rows: 3,
inputs: vec![Some("".to_string())],
outputs: vec![Some("".to_string()), Some("".to_string()), Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "Forwards the value one block and sends it to multiple destinations"
.to_string(),
color: 6,
});
lang.define(BlockType {
category: "variables".to_string(),
name: "set".to_string(),
rows: 1,
inputs: vec![Some("".to_string())],
outputs: vec![],
area_count: 0,
user_input: BlockUserInput::Identifier,
description: "Stores into a variable".to_string(),
color: 2,
});
lang.define(BlockType {
category: "variables".to_string(),
name: "get".to_string(),
rows: 1,
inputs: vec![],
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::Identifier,
description: "Loads a variable".to_string(),
color: 12,
});
lang.define(BlockType {
category: "variables".to_string(),
name: "if".to_string(),
rows: 1,
inputs: vec![Some("".to_string())],
outputs: vec![Some("".to_string())],
area_count: 2,
user_input: BlockUserInput::None,
description: "Divides the controlflow based on a true (>= 0.5) \
or false (< 0.5) input value."
.to_string(),
color: 0,
});
// lang.define(BlockType {
// category: "nodes".to_string(),
// name: "1pole".to_string(),
// rows: 2,
// inputs: vec![Some("in".to_string()), Some("f".to_string())],
// outputs: vec![Some("lp".to_string()), Some("hp".to_string())],
// area_count: 0,
// user_input: BlockUserInput::None,
// description: "Runs a simple one pole filter on the input".to_string(),
// color: 8,
// });
//
// lang.define(BlockType {
// category: "nodes".to_string(),
// name: "svf".to_string(),
// rows: 3,
// inputs: vec![Some("in".to_string()), Some("f".to_string()), Some("r".to_string())],
// outputs: vec![Some("lp".to_string()), Some("bp".to_string()), Some("hp".to_string())],
// area_count: 0,
// user_input: BlockUserInput::None,
// description: "Runs a state variable filter on the input".to_string(),
// color: 8,
// });
//
// lang.define(BlockType {
// category: "functions".to_string(),
// name: "sin".to_string(),
// rows: 1,
// inputs: vec![Some("".to_string())],
// outputs: vec![Some("".to_string())],
// area_count: 0,
// user_input: BlockUserInput::None,
// description: "Calculates the sine of the input".to_string(),
// color: 16,
// });
//
// lang.define(BlockType {
// category: "nodes".to_string(),
// name: "delay".to_string(),
// rows: 2,
// inputs: vec![Some("in".to_string()), Some("t".to_string())],
// outputs: vec![Some("".to_string())],
// area_count: 0,
// user_input: BlockUserInput::None,
// description: "Runs a linearly interpolated delay on the input".to_string(),
// color: 8,
// });
for fun_name in &["+", "-", "*", "/"] {
lang.define(BlockType {
category: "arithmetics".to_string(),
name: fun_name.to_string(),
rows: 2,
inputs: if fun_name == &"-" || fun_name == &"/" {
vec![Some("a".to_string()), Some("b".to_string())]
} else {
vec![Some("".to_string()), Some("".to_string())]
},
outputs: vec![Some("".to_string())],
area_count: 0,
user_input: BlockUserInput::None,
description: "A binary arithmetics operation".to_string(),
color: 4,
});
}
dsp_lib.borrow().for_each(|node_type| -> Result<(), ()> {
let max_ports = node_type.input_count().max(node_type.output_count());
let is_stateful = node_type.is_stateful();
let mut inputs = vec![];
let mut outputs = vec![];
let mut i = 0;
while let Some(name) = node_type.input_names(i) {
inputs.push(Some(name[0..(name.len().min(2))].to_string()));
i += 1;
}
let mut i = 0;
while let Some(name) = node_type.output_names(i) {
outputs.push(Some(name[0..(name.len().min(2))].to_string()));
i += 1;
}
lang.define(BlockType {
category: if is_stateful { "nodes".to_string() } else { "functions".to_string() },
name: node_type.name().to_string(),
rows: max_ports,
area_count: 0,
user_input: BlockUserInput::None,
description: node_type.documentation().to_string(),
color: if is_stateful { 8 } else { 16 },
inputs,
outputs,
});
Ok(())
}).expect("seriously no error here");
lang.define_identifier("in1");
lang.define_identifier("in2");
lang.define_identifier("israte");
lang.define_identifier("srate");
lang.define_identifier("alpha");
lang.define_identifier("beta");
lang.define_identifier("delta");
lang.define_identifier("gamma");
lang.define_identifier("&sig1");
lang.define_identifier("&sig2");
Rc::new(RefCell::new(lang))
}

1855
src/wblockdsp/language.rs Normal file

File diff suppressed because it is too large Load diff

15
src/wblockdsp/mod.rs Normal file
View file

@ -0,0 +1,15 @@
// 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.
/*! Contains the implementation of the visual DSP programming language named WBlockDSP.
*/
mod definition;
mod language;
mod compiler;
pub use definition::*;
pub use language::*;
pub use compiler::*;

View file

@ -5,6 +5,7 @@
pub use hexodsp::dsp::*;
pub use hexodsp::matrix::*;
pub use hexodsp::nodes::new_node_engine;
pub use hexodsp::MatrixCellChain;
pub use hexodsp::NodeExecutor;
use hound;
@ -392,21 +393,50 @@ macro_rules! assert_minmax_of_rms {
};
}
#[allow(unused)]
pub fn wait_params_smooth(ne: &mut NodeExecutor) {
run_for_ms(ne, 15.0);
}
#[allow(unused)]
pub fn node_pset_s(matrix: &mut Matrix, node: &str, instance: usize, parm: &str, set: i64) {
let nid = NodeId::from_str(node).to_instance(instance);
assert!(nid != NodeId::Nop);
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::setting(set));
}
#[allow(unused)]
pub fn pset_s(matrix: &mut Matrix, nid: NodeId, parm: &str, set: i64) {
let p = nid.inp_param(parm).unwrap();
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::setting(set));
}
#[allow(unused)]
pub fn pset_n(matrix: &mut Matrix, nid: NodeId, parm: &str, v_norm: f32) {
let p = nid.inp_param(parm).unwrap();
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::param(v_norm));
}
#[allow(unused)]
pub fn node_pset_n(matrix: &mut Matrix, node: &str, instance: usize, parm: &str, v_norm: f32) {
let nid = NodeId::from_str(node).to_instance(instance);
assert!(nid != NodeId::Nop);
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::param(v_norm));
}
#[allow(unused)]
pub fn pset_d(matrix: &mut Matrix, nid: NodeId, parm: &str, v_denorm: f32) {
let p = nid.inp_param(parm).unwrap();
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::param(p.norm(v_denorm)));
}
#[allow(unused)]
pub fn node_pset_d(matrix: &mut Matrix, node: &str, instance: usize, parm: &str, v_denorm: f32) {
let nid = NodeId::from_str(node).to_instance(instance);
assert!(nid != NodeId::Nop);
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::param(p.norm(v_denorm)));
}
@ -418,9 +448,9 @@ pub fn pset_n_wait(
parm: &str,
v_norm: f32,
) {
let p = nid.inp_param(parm).unwrap();
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::param(v_norm));
run_for_ms(ne, 15.0);
wait_params_smooth(ne);
}
#[allow(unused)]
@ -431,14 +461,14 @@ pub fn pset_d_wait(
parm: &str,
v_denorm: f32,
) {
let p = nid.inp_param(parm).unwrap();
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param(p, SAtom::param(p.norm(v_denorm)));
run_for_ms(ne, 15.0);
wait_params_smooth(ne);
}
#[allow(unused)]
pub fn pset_mod(matrix: &mut Matrix, nid: NodeId, parm: &str, modamt: f32) {
let p = nid.inp_param(parm).unwrap();
let p = nid.inp_param(parm).expect("param exists");
matrix.set_param_modamt(p, Some(modamt));
}
@ -452,7 +482,7 @@ pub fn pset_mod_wait(
) {
let p = nid.inp_param(parm).unwrap();
matrix.set_param_modamt(p, Some(modamt));
run_for_ms(ne, 15.0);
wait_params_smooth(ne);
}
#[allow(dead_code)]

View file

@ -1,4 +1,4 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// Copyright (c) 2021-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.
@ -10,12 +10,11 @@ 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));
let mut chain = MatrixCellChain::new(CellDir::B);
chain.node_out("ad", "sig").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap();
matrix.sync().unwrap();
let ad = NodeId::Ad(0);
let trig_p = ad.inp_param("trig").unwrap();
matrix.set_param(trig_p, SAtom::param(1.0));

View file

@ -38,7 +38,7 @@ fn check_node_allp() {
// starts with original signal * -0.7
let mut v = vec![0.7; (2.0 * 44.1_f32).ceil() as usize];
// silence for 1ms, which is the internal delay of the allpass
v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 2]);
v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 1]);
// allpass feedback of the original signal for 2ms:
// XXX: the smearing before and after the allpass is due to the
@ -47,7 +47,7 @@ fn check_node_allp() {
v.append(&mut vec![0.51; (2.0 * 44.1_f32).ceil() as usize - 3]);
// 1ms allpass silence like before:
v.append(&mut vec![0.54748523, 0.13158606, -0.016065884]);
v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 5]);
v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 4]);
// 2ms the previous 1.0 * 0.7 fed back into the filter,
// including even more smearing due to cubic interpolation:

101
tests/node_code.rs Normal file
View file

@ -0,0 +1,101 @@
// 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::wblockdsp::BlockFun;
fn setup() -> (Matrix, NodeExecutor) {
let (node_conf, node_exec) = new_node_engine();
let mut matrix = Matrix::new(node_conf, 3, 3);
let mut chain = MatrixCellChain::new(CellDir::B);
chain
.node_out("code", "sig1")
.set_denorm("in1", 0.5)
.set_denorm("in2", -0.6)
.node_inp("out", "ch1")
.place(&mut matrix, 0, 0)
.unwrap();
matrix.sync().unwrap();
(matrix, node_exec)
}
fn put_n(bf: &mut BlockFun, a: usize, x: i64, y: i64, s: &str) {
bf.instanciate_at(a, x, y, s, None).expect("no put error");
}
fn put_v(bf: &mut BlockFun, a: usize, x: i64, y: i64, s: &str, v: &str) {
bf.instanciate_at(a, x, y, s, Some(v.to_string())).expect("no put error");
}
#[test]
fn check_node_code_1() {
let (mut matrix, mut node_exec) = setup();
let block_fun = matrix.get_block_function(0).expect("block fun exists");
{
let mut block_fun = block_fun.lock().expect("matrix lock");
put_v(&mut block_fun, 0, 0, 1, "value", "0.3");
put_v(&mut block_fun, 0, 1, 1, "set", "&sig1");
}
matrix.check_block_function(0).expect("no compile error");
let res = run_for_ms(&mut node_exec, 25.0);
assert_decimated_feq!(res.0, 50, vec![0.3; 10]);
}
#[test]
fn check_node_code_state() {
let (mut matrix, mut node_exec) = setup();
let block_fun = matrix.get_block_function(0).expect("block fun exists");
{
let mut block_fun = block_fun.lock().expect("matrix lock");
put_v(&mut block_fun, 0, 0, 2, "value", "220.0");
put_n(&mut block_fun, 0, 1, 2, "phase");
put_v(&mut block_fun, 0, 1, 3, "value", "2.0");
put_n(&mut block_fun, 0, 2, 2, "*");
put_n(&mut block_fun, 0, 3, 1, "-");
put_v(&mut block_fun, 0, 2, 1, "value", "1.0");
put_v(&mut block_fun, 0, 4, 1, "set", "&sig1");
}
matrix.check_block_function(0).expect("no compile error");
let fft = run_and_get_fft4096_now(&mut node_exec, 50);
// Aliasing sawtooth I expect:
assert_eq!(
fft,
vec![
(205, 133),
(215, 576),
(226, 527),
(237, 90),
(431, 195),
(441, 322),
(452, 131),
(646, 61),
(657, 204),
(668, 157),
(872, 113),
(883, 155),
(894, 51),
(1098, 127),
(1109, 82),
(1314, 85),
(1324, 98),
(1540, 93),
(1755, 70),
(1766, 67),
(1981, 72),
(2196, 60),
(2422, 57),
(2638, 52)
]
);
}

View file

@ -34,46 +34,28 @@ fn check_node_comb_1() {
fft,
vec![
(0, 216),
(11, 221),
(22, 216),
(3370, 206),
(3381, 248),
(3391, 191),
(6740, 185),
(6751, 207),
(6761, 195),
(10131, 215),
(10142, 210),
(10153, 213),
(10164, 201),
(20338, 187),
(20349, 184)
(11, 219),
(22, 210),
(3122, 189),
(3133, 190),
(6266, 181),
(9421, 210),
(9432, 193),
(12565, 224),
(12575, 234)
]
);
pset_n_wait(&mut matrix, &mut node_exec, comb_1, "time", 0.030);
let fft = run_and_get_avg_fft4096_now(&mut node_exec, 180);
assert_eq!(
fft,
vec![(1001, 206), (2993, 196), (3004, 219), (3994, 197), (6998, 211), (8000, 201)]
);
assert_eq!(fft, vec![(980, 219), (3908, 225), (5868, 203), (6848, 195)]);
pset_n_wait(&mut matrix, &mut node_exec, comb_1, "g", 0.999);
let fft = run_and_get_avg_fft4096_now(&mut node_exec, 1000);
assert_eq!(
fft,
vec![
(0, 2003),
(11, 1015),
(991, 1078),
(1001, 1837),
(2003, 1059),
(2993, 1420),
(3004, 1775),
(3994, 1297),
(4005, 1485)
]
vec![(0, 1979), (11, 1002), (980, 1245), (1960, 1144), (2929, 1569), (2939, 1545)]
);
}

View file

@ -68,16 +68,16 @@ fn check_node_delay_1() {
0.0,
0.0,
// delayed burst of sine for 100ms:
0.047408286,
-0.17181452,
0.2669317,
-0.22377986,
0.000059626997,
0.24652793,
-0.30384338,
0.2087649,
-0.070256576,
0.000003647874,
0.05125899,
-0.17475566,
0.2607654,
-0.20392825,
-0.03003881,
0.26745066,
-0.30965388,
0.20431,
-0.064184606,
-0.0012322,
// silence afterwards:
0.0,
0.0,
@ -119,9 +119,20 @@ fn check_node_delay_2() {
vec![
// 10ms smoothing time for "inp"
0.001133, // 30ms delaytime just mixing the 0.5:
0.5, 0.5, 0.5, // the delayed smoothing ramp (10ms):
0.951113, // the delay + input signal:
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
0.5,
0.5,
0.5, // the delayed smoothing ramp (10ms):
0.950001113, // the delay + input signal:
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0
]
);
}
@ -172,9 +183,9 @@ fn check_node_delay_time_mod() {
let fft = run_and_get_fft4096_now(&mut node_exec, 110);
// Expect a sine sweep over a
// range of low frequencies:
assert_eq!(fft[0], (86, 111));
assert_eq!(fft[5], (237, 114));
assert_eq!(fft[10], (517, 110));
assert_eq!(fft[0], (97, 113));
assert_eq!(fft[5], (312, 114));
assert_eq!(fft[10], (635, 110));
// Sweep upwards:
run_for_ms(&mut node_exec, 300.0);
@ -241,7 +252,7 @@ fn check_node_delay_trig() {
}
// We expect the signal to be delayed by 20ms:
assert_eq!(idx_first_non_zero, (44100 * 20) / 1000);
assert_eq!(idx_first_non_zero, (44100 * 20) / 1000 + 1);
}
#[test]
@ -274,7 +285,7 @@ fn check_node_delay_fb() {
let idxs_big = collect_signal_changes(&res.0[..], 50);
// We expect the signal to be delayed by 20ms:
assert_eq!(idxs_big, vec![(221, 106), (442, 53)]);
assert_eq!(idxs_big, vec![(222, 106), (444, 53)]);
}
#[test]
@ -306,7 +317,7 @@ fn check_node_delay_fb_neg() {
let idxs_big = collect_signal_changes(&res.0[..], 70);
assert_eq!(idxs_big, vec![(441, 100), (882, -100), (1323, 100)]);
assert_eq!(idxs_big, vec![(442, 100), (884, -100), (1326, 100)]);
}
#[test]
@ -341,15 +352,15 @@ fn check_node_delay_fb_pos() {
assert_eq!(
idxs_big,
vec![
(441, 100),
(441 + 1 * 441, 100),
(441 + 2 * 441, 100),
(441 + 3 * 441, 100),
(441 + 4 * 441, 100),
(441 + 5 * 441, 100),
(441 + 6 * 441, 100),
(441 + 7 * 441, 100),
(441 + 8 * 441, 100),
(442, 100),
(442 + 1 * 442, 100),
(442 + 2 * 442, 100),
(442 + 3 * 442, 100),
(442 + 4 * 442, 100),
(442 + 5 * 442, 100),
(442 + 6 * 442, 100),
(442 + 7 * 442, 100),
(442 + 8 * 442, 100),
]
);
}

View file

@ -125,10 +125,10 @@ fn check_node_pverb_dcy_1() {
// 19 []
// Now we see a very much longer tail:
assert_eq!(spec[0], vec![(388, 19), (431, 74), (474, 61), (517, 10)]);
assert_eq!(spec[5], vec![(388, 9), (431, 37), (474, 26)]);
assert_eq!(spec[9], vec![(388, 18), (431, 50), (474, 37), (517, 5)]);
assert_eq!(spec[19], vec![(388, 7), (431, 15), (474, 8)]);
assert_eq!(spec[0], vec![(388, 21), (431, 79), (474, 65), (517, 10)]);
assert_eq!(spec[5], vec![(388, 8), (431, 35), (474, 27), (517, 5)]);
assert_eq!(spec[9], vec![(388, 19), (431, 50), (474, 37), (517, 5)]);
assert_eq!(spec[19], vec![(388, 8), (431, 19), (474, 10)]);
}
#[test]
@ -149,7 +149,7 @@ fn check_node_pverb_dcy_2() {
assert_vec_feq!(
rms_spec.iter().map(|rms| rms.0).collect::<Vec<f32>>(),
// Decay over 500 ms:
vec![0.2108, 0.5744, 0.0881, 0.0021, 0.0006]
vec![0.23928945, 0.5664783, 0.07564733, 0.0016927856, 0.0006737139]
);
}
@ -172,7 +172,7 @@ fn check_node_pverb_dcy_3() {
assert_vec_feq!(
rms_spec.iter().map(|rms| rms.0).collect::<Vec<f32>>(),
// Decay over 5000 ms:
vec![0.6254, 0.2868, 0.0633, 0.0385, 0.0186,]
vec![0.6168, 0.2924, 0.0640, 0.0385, 0.0191]
);
}
@ -194,7 +194,7 @@ fn check_node_pverb_dcy_4() {
assert_vec_feq!(
rms_spec.iter().map(|rms| rms.0).collect::<Vec<f32>>(),
// Decay over 10000 ms:
vec![0.1313, 0.0995, 0.0932, 0.0507, 0.0456,]
vec![0.1319, 0.1046, 0.0942, 0.0517, 0.0435,]
);
}
@ -241,9 +241,9 @@ fn check_node_pverb_dif_on() {
// 17 []
// We expect a diffuse but defined response:
assert_eq!(spec[0], vec![(388, 8), (431, 35), (474, 35), (517, 7), (560, 5)]);
assert_eq!(spec[7], vec![(431, 18), (474, 21), (517, 6)]);
assert_eq!(spec[13], vec![(388, 6), (431, 6)]);
assert_eq!(spec[0], vec![(388, 9), (431, 38), (474, 36), (517, 7), (560, 6)]);
assert_eq!(spec[7], vec![(431, 15), (474, 19), (517, 6)]);
assert_eq!(spec[13], vec![(388, 5), (431, 4)]);
assert_eq!(spec[17], vec![]);
}
@ -295,10 +295,10 @@ fn check_node_pverb_dif_off() {
assert_eq!(spec[0], vec![]);
assert_eq!(
spec[1],
vec![(301, 4), (345, 6), (388, 84), (431, 206), (474, 152), (517, 23), (560, 7)]
vec![(301, 4), (345, 6), (388, 85), (431, 208), (474, 152), (517, 23), (560, 7)]
);
assert_eq!(spec[2], vec![]);
assert_eq!(spec[3], vec![(345, 7), (388, 79), (431, 198), (474, 134), (517, 15), (560, 4)]);
assert_eq!(spec[3], vec![(345, 7), (388, 79), (431, 198), (474, 134), (517, 16), (560, 5)]);
assert_eq!(spec[7], vec![]);
assert_eq!(spec[8], vec![(388, 6), (431, 17), (474, 11)]);
assert_eq!(spec[9], vec![(388, 7), (431, 20), (474, 13)]);
@ -331,7 +331,7 @@ fn check_node_pverb_dif_off_predly() {
trig_env(matrix, node_exec);
let spec = run_fft_spectrum_each_47ms(node_exec, 4, 20);
dump_table!(spec);
//d// dump_table!(spec);
// 0 []
// 1 []
@ -354,12 +354,12 @@ fn check_node_pverb_dif_off_predly() {
vec![
(215, 5),
(301, 11),
(345, 15),
(345, 14),
(388, 46),
(431, 105),
(431, 104),
(474, 86),
(517, 18),
(560, 14),
(517, 17),
(560, 15),
(603, 5)
]
);

View file

@ -153,7 +153,7 @@ fn check_node_sampl_reverse() {
matrix.set_param(dir_p, SAtom::setting(1));
let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 50.0);
assert_rmsmima!((rms, min, max), (0.50059, -0.9997, 0.9997));
assert_rmsmima!((rms, min, max), (0.5003373, -0.9997, 0.9997));
let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0);
assert_eq!(fft[0], (441, 1023));
@ -164,11 +164,11 @@ fn check_node_sampl_reverse() {
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], (215, 880));
assert_eq!(fft[0], (215, 881));
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, 986));
assert_eq!(fft[0], (108, 987));
matrix.set_param(freq_p, SAtom::param(-0.4));
let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0);
@ -176,7 +176,7 @@ fn check_node_sampl_reverse() {
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, 999));
assert_eq!(fft[0], (11, 1000));
matrix.set_param(freq_p, SAtom::param(0.2));
let fft = run_and_get_fft4096(&mut node_exec, 800, 20.0);
@ -617,8 +617,8 @@ fn check_node_sampl_rev_2() {
res.0,
5000,
vec![
0.9999773, 0.886596, 0.77321476, 0.6598335, 0.5464522, 0.43307102, 0.31968975,
0.20630851, 0.09292727
0.0, 0.88664144, 0.7732602, 0.6598789, 0.54649764, 0.4331164, 0.31973514, 0.20635389,
0.09297263
]
);

278
tests/node_scope.rs Normal file
View file

@ -0,0 +1,278 @@
// 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;
fn read_scope_buf(matrix: &Matrix, sig_idx: usize) -> (Vec<f32>, Vec<f32>, f32, f32) {
let handle = matrix.get_scope_handle(0).unwrap();
let mut min = vec![];
let mut max = vec![];
let mut total_min: f32 = 99999.9;
let mut total_max: f32 = -99999.9;
for i in 0..SCOPE_SAMPLES {
let (ma, mi) = handle.read(sig_idx, i);
min.push(mi);
max.push(ma);
total_min = total_min.min(mi);
total_max = total_max.max(ma);
}
(max, min, total_max, total_min)
}
#[test]
fn check_node_scope_inputs() {
for (sig_idx, inp_name) in ["in1", "in2", "in3"].iter().enumerate() {
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_out("amp", "sig")
.node_inp("scope", inp_name)
.set_denorm("time", (1000.0 / 44100.0) * (SCOPE_SAMPLES as f32))
.place(&mut matrix, 0, 0)
.unwrap();
matrix.sync().unwrap();
node_pset_d(&mut matrix, "amp", 0, "inp", 1.0);
let _res = run_for_ms(&mut node_exec, 11.0);
let (minv, maxv, max, min) = read_scope_buf(&matrix, sig_idx);
// This tests the smoothing ramp that is applied to setting the "inp" of the Amp(0) node:
assert_decimated_feq!(minv, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]);
assert_decimated_feq!(maxv, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]);
assert_float_eq!(min, 0.0);
assert_float_eq!(max, 1.0);
}
}
#[test]
fn check_node_scope_offs_gain_thrsh() {
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_out("amp", "sig").node_inp("scope", "in1").place(&mut matrix, 0, 0).unwrap();
matrix.sync().unwrap();
node_pset_d(&mut matrix, "scope", 0, "off1", 0.1);
node_pset_d(&mut matrix, "scope", 0, "off2", 0.2);
node_pset_d(&mut matrix, "scope", 0, "off3", 0.3);
node_pset_d(&mut matrix, "scope", 0, "gain1", 2.0);
node_pset_d(&mut matrix, "scope", 0, "gain2", 3.0);
node_pset_d(&mut matrix, "scope", 0, "gain3", 4.0);
node_pset_d(&mut matrix, "scope", 0, "thrsh", 0.95);
node_pset_s(&mut matrix, "scope", 0, "tsrc", 1);
wait_params_smooth(&mut node_exec);
let handle = matrix.get_scope_handle(0).unwrap();
let _res = run_for_ms(&mut node_exec, 11.0);
let thres = handle.get_threshold().unwrap();
assert_float_eq!(thres, 0.95);
let (off, gain) = handle.get_offs_gain(0);
assert_float_eq!(off, 0.1);
assert_float_eq!(gain, 2.0);
let (off, gain) = handle.get_offs_gain(1);
assert_float_eq!(off, 0.2);
assert_float_eq!(gain, 3.0);
let (off, gain) = handle.get_offs_gain(2);
assert_float_eq!(off, 0.3);
assert_float_eq!(gain, 4.0);
}
#[test]
fn check_node_scope_sine_2hz() {
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_out("sin", "sig")
.set_denorm("freq", 2.0)
.node_io("amp", "inp", "sig")
.node_inp("scope", "in1")
.place(&mut matrix, 0, 0)
.unwrap();
matrix.sync().unwrap();
wait_params_smooth(&mut node_exec);
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, minv, max, min) = read_scope_buf(&matrix, 0);
// 2 Hz is exactly 2 sine peaks in 1000ms. 1000ms is the default time of the Scope.
assert_decimated_feq!(
maxv,
64,
vec![0.0264, 1.0, -0.0004, -0.99968, 0.02546, 1.0, -0.0011, -0.9996]
);
assert_decimated_feq!(
minv,
64,
vec![0.0016, 0.9996, -0.0249, -1.0, 0.0009, 0.9996, -0.0256, -0.9999]
);
assert_float_eq!(max, 1.0);
assert_float_eq!(min, -1.0);
// Now change timing to 500ms, so we expect one peak:
node_pset_d(&mut matrix, "scope", 0, "time", 500.0);
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, minv, max, min) = read_scope_buf(&matrix, 0);
// 2 Hz is exactly 1 sine peaks in 500ms.
assert_decimated_feq!(maxv, 128, vec![0.1494, 0.9905, -0.1371, -0.9887]);
assert_decimated_feq!(minv, 128, vec![0.1376, 0.9888, -0.1489, -0.9904]);
assert_float_eq!(max, 1.0);
assert_float_eq!(min, -1.0);
}
#[test]
fn check_node_scope_sine_oversampled() {
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_out("sin", "sig")
.set_denorm("freq", 440.0)
.node_io("amp", "inp", "sig")
.node_inp("scope", "in1")
.set_denorm("time", 1.0)
.place(&mut matrix, 0, 0)
.unwrap();
matrix.sync().unwrap();
wait_params_smooth(&mut node_exec);
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(
maxv[0..25],
5,
// We expect multiple copies of the same sample at the
// time resolution of 1 millisecond.
vec![
0.4506, 0.4506, 0.4506, 0.3938, 0.3938, 0.3354, 0.3354, 0.2150, 0.2150, 0.1534, 0.1534,
0.1534,
]
);
// Full wave does not fit into the buffer at 1ms for 512 samples
assert_float_eq!(max, 0.9996);
assert_float_eq!(min, -0.5103);
}
#[test]
fn check_node_scope_sine_threshold() {
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_out("sin", "sig")
.set_denorm("freq", 10.0)
.node_io("amp", "inp", "sig")
.set_denorm("att", 0.9)
.node_inp("scope", "in1")
.set_denorm("time", 100.0)
.set_atom("tsrc", SAtom::setting(1))
.set_denorm("thrsh", 1.0)
.place(&mut matrix, 0, 0)
.unwrap();
matrix.sync().unwrap();
wait_params_smooth(&mut node_exec);
// Expect a sine that starts at the beginning, because the
// at the beginning of the Scope state it is basically "triggered"
// by default. That means it will record one full buffer at startup:
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(maxv[0..35], 5, vec![0.0115, 0.06666, 0.1214, 0.1758]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
// Expect getting a waveform that starts at the top:
node_pset_d(&mut matrix, "scope", 0, "thrsh", 0.9 - 0.0002);
wait_params_smooth(&mut node_exec);
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
// Confirm we are starting at the threshold top:
assert_decimated_feq!(maxv[0..35], 5, vec![0.8999, 0.8988, 0.8942, 0.8864]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
// Expect frozen waveform:
node_pset_d(&mut matrix, "scope", 0, "thrsh", 1.0);
wait_params_smooth(&mut node_exec);
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(maxv[0..35], 5, vec![0.8999, 0.8988, 0.8942, 0.8864]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
}
#[test]
fn check_node_scope_sine_ext_trig() {
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_out("sin", "sig")
.set_denorm("freq", 10.0)
.node_io("amp", "inp", "sig")
.set_denorm("att", 0.9)
.node_inp("scope", "in1")
.set_denorm("time", 100.0)
.set_atom("tsrc", SAtom::setting(2))
.set_denorm("thrsh", 0.0)
.place(&mut matrix, 0, 0)
.unwrap();
matrix.sync().unwrap();
wait_params_smooth(&mut node_exec);
// Expect a sine that starts at the beginning, because the
// at the beginning of the Scope state it is basically "triggered"
// by default. That means it will record one full buffer at startup:
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(maxv[0..35], 5, vec![0.0115, 0.06666, 0.1214, 0.1758]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
// Expect the buffer to not change:
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(maxv[0..35], 5, vec![0.0115, 0.06666, 0.1214, 0.1758]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
// Apply external trigger and expect the buffer to change:
node_pset_d(&mut matrix, "scope", 0, "trig", 1.0);
wait_params_smooth(&mut node_exec);
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(maxv[0..35], 5, vec![0.7325241, 0.7631615, 0.7909356, 0.8157421]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
// Expect the buffer to not change, because the trigger has not reset/changed:
let _res = run_for_ms(&mut node_exec, 1000.0);
let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0);
assert_decimated_feq!(maxv[0..35], 5, vec![0.7325241, 0.7631615, 0.7909356, 0.8157421]);
assert_float_eq!(max, 0.8999);
assert_float_eq!(min, -0.8999);
}

View file

@ -3,10 +3,10 @@
// See README.md and COPYING for details.
mod common;
use common::*;
//use common::*;
use hexodsp::d_pit;
use hexodsp::dsp::helpers::Quantizer;
use synfx_dsp::Quantizer;
#[test]
fn check_quant_pos_neg_exact() {