diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3c58b22 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.toml b/Cargo.toml index 9746475..56bc1c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index ee182e9..2366c6b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/examples/jack_demo_node_api.rs b/examples/jack_demo_node_api.rs index a41a78c..6c146e4 100644 --- a/examples/jack_demo_node_api.rs +++ b/examples/jack_demo_node_api.rs @@ -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 diff --git a/src/chain_builder.rs b/src/chain_builder.rs new file mode 100644 index 0000000..4ad0d72 --- /dev/null +++ b/src/chain_builder.rs @@ -0,0 +1,321 @@ +// Copyright (c) 2021-2022 Weird Constructor +// 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, + error: Option, + 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 { + 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)" + ); + } +} diff --git a/src/dsp/biquad.rs b/src/dsp/biquad.rs deleted file mode 100644 index fdc3738..0000000 --- a/src/dsp/biquad.rs +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright (c) 2021 Weird Constructor -// 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 { - filters: [Biquad; 4], - buffer: [f32; N], -} - -impl Oversampling { - 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 - } -} diff --git a/src/dsp/dattorro.rs b/src/dsp/dattorro.rs deleted file mode 100644 index 8dd70ed..0000000 --- a/src/dsp/dattorro.rs +++ /dev/null @@ -1,443 +0,0 @@ -// Copyright (c) 2021 Weird Constructor -// 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; 2], - out_dc_block: [DCBlockFilter; 2], - - lfos: [TriSawLFO; 4], - - input_hpf: OnePoleHPF, - input_lpf: OnePoleLPF, - - pre_delay: DelayBuffer, - input_apfs: [(AllPass, f64, f64); 4], - - apf1: [(AllPass, f64, f64); 2], - hpf: [OnePoleHPF; 2], - lpf: [OnePoleLPF; 2], - apf2: [(AllPass, f64, f64); 2], - delay1: [(DelayBuffer, f64); 2], - delay2: [(DelayBuffer, 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) - } -} diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs deleted file mode 100644 index 91bd482..0000000 --- a/src/dsp/helpers.rs +++ /dev/null @@ -1,2498 +0,0 @@ -// Copyright (c) 2021 Weird Constructor -// This file is a part of HexoDSP. Released under GPL-3.0-or-later. -// See README.md and COPYING for details. - -use num_traits::{cast::FromPrimitive, cast::ToPrimitive, Float, FloatConst}; -use std::cell::RefCell; - -macro_rules! trait_alias { - ($name:ident = $base1:ident + $($base2:ident +)+) => { - pub trait $name: $base1 $(+ $base2)+ { } - impl $name for T { } - }; -} - -trait_alias!(Flt = Float + FloatConst + ToPrimitive + FromPrimitive +); - -/// Logarithmic table size of the table in [fast_cos] / [fast_sin]. -static FAST_COS_TAB_LOG2_SIZE: usize = 9; -/// Table size of the table in [fast_cos] / [fast_sin]. -static FAST_COS_TAB_SIZE: usize = 1 << FAST_COS_TAB_LOG2_SIZE; // =512 -/// The wave table of [fast_cos] / [fast_sin]. -static mut FAST_COS_TAB: [f32; 513] = [0.0; 513]; - -/// Initializes the cosine wave table for [fast_cos] and [fast_sin]. -pub fn init_cos_tab() { - for i in 0..(FAST_COS_TAB_SIZE + 1) { - let phase: f32 = (i as f32) * ((std::f32::consts::TAU) / (FAST_COS_TAB_SIZE as f32)); - unsafe { - // XXX: note: mutable statics can be mutated by multiple - // threads: aliasing violations or data races - // will cause undefined behavior - FAST_COS_TAB[i] = phase.cos(); - } - } -} - -/// Internal phase increment/scaling for [fast_cos]. -const PHASE_SCALE: f32 = 1.0_f32 / (std::f32::consts::TAU); - -/// A faster implementation of cosine. It's not that much faster than -/// Rust's built in cosine function. But YMMV. -/// -/// Don't forget to call [init_cos_tab] before using this! -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// init_cos_tab(); // Once on process initialization. -/// -/// // ... -/// assert!((fast_cos(std::f32::consts::PI) - -1.0).abs() < 0.001); -///``` -pub fn fast_cos(mut x: f32) -> f32 { - x = x.abs(); // cosine is symmetrical around 0, let's get rid of negative values - - // normalize range from 0..2PI to 1..2 - let phase = x * PHASE_SCALE; - - let index = FAST_COS_TAB_SIZE as f32 * phase; - - let fract = index.fract(); - let index = index.floor() as usize; - - unsafe { - // XXX: note: mutable statics can be mutated by multiple - // threads: aliasing violations or data races - // will cause undefined behavior - let left = FAST_COS_TAB[index as usize]; - let right = FAST_COS_TAB[index as usize + 1]; - - return left + (right - left) * fract; - } -} - -/// A faster implementation of sine. It's not that much faster than -/// Rust's built in sine function. But YMMV. -/// -/// Don't forget to call [init_cos_tab] before using this! -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// init_cos_tab(); // Once on process initialization. -/// -/// // ... -/// assert!((fast_sin(0.5 * std::f32::consts::PI) - 1.0).abs() < 0.001); -///``` -pub fn fast_sin(x: f32) -> f32 { - fast_cos(x - (std::f32::consts::PI / 2.0)) -} - -/// A wavetable filled entirely with white noise. -/// Don't forget to call [init_white_noise_tab] before using it. -static mut WHITE_NOISE_TAB: [f64; 1024] = [0.0; 1024]; - -#[allow(rustdoc::private_intra_doc_links)] -/// Initializes [WHITE_NOISE_TAB]. -pub fn init_white_noise_tab() { - let mut rng = RandGen::new(); - unsafe { - for i in 0..WHITE_NOISE_TAB.len() { - WHITE_NOISE_TAB[i as usize] = rng.next_open01(); - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq)] -/// Random number generator based on xoroshiro128. -/// Requires two internal state variables. You may prefer [SplitMix64] or [Rng]. -pub struct RandGen { - r: [u64; 2], -} - -// Taken from xoroshiro128 crate under MIT License -// Implemented by Matthew Scharley (Copyright 2016) -// https://github.com/mscharley/rust-xoroshiro128 -/// Given the mutable `state` generates the next pseudo random number. -pub fn next_xoroshiro128(state: &mut [u64; 2]) -> u64 { - let s0: u64 = state[0]; - let mut s1: u64 = state[1]; - let result: u64 = s0.wrapping_add(s1); - - s1 ^= s0; - state[0] = s0.rotate_left(55) ^ s1 ^ (s1 << 14); // a, b - state[1] = s1.rotate_left(36); // c - - result -} - -// Taken from rand::distributions -// Licensed under the Apache License, Version 2.0 -// Copyright 2018 Developers of the Rand project. -/// Maps any `u64` to a `f64` in the open interval `[0.0, 1.0)`. -pub fn u64_to_open01(u: u64) -> f64 { - use core::f64::EPSILON; - let float_size = std::mem::size_of::() as u32 * 8; - let fraction = u >> (float_size - 52); - let exponent_bits: u64 = (1023 as u64) << 52; - f64::from_bits(fraction | exponent_bits) - (1.0 - EPSILON / 2.0) -} - -impl RandGen { - pub fn new() -> Self { - RandGen { r: [0x193a6754a8a7d469, 0x97830e05113ba7bb] } - } - - /// Next random unsigned 64bit integer. - pub fn next(&mut self) -> u64 { - next_xoroshiro128(&mut self.r) - } - - /// Next random float between `[0.0, 1.0)`. - pub fn next_open01(&mut self) -> f64 { - u64_to_open01(self.next()) - } -} - -#[derive(Debug, Copy, Clone)] -/// Random number generator based on [SplitMix64]. -/// Requires two internal state variables. You may prefer [SplitMix64] or [Rng]. -pub struct Rng { - sm: SplitMix64, -} - -impl Rng { - pub fn new() -> Self { - Self { sm: SplitMix64::new(0x193a67f4a8a6d769) } - } - - pub fn seed(&mut self, seed: u64) { - self.sm = SplitMix64::new(seed); - } - - #[inline] - pub fn next(&mut self) -> f32 { - self.sm.next_open01() as f32 - } - - #[inline] - pub fn next_u64(&mut self) -> u64 { - self.sm.next_u64() - } -} - -thread_local! { - static GLOBAL_RNG: RefCell = RefCell::new(Rng::new()); -} - -#[inline] -pub fn rand_01() -> f32 { - GLOBAL_RNG.with(|r| r.borrow_mut().next()) -} - -#[inline] -pub fn rand_u64() -> u64 { - GLOBAL_RNG.with(|r| r.borrow_mut().next_u64()) -} - -// Copyright 2018 Developers of the Rand project. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. -//- splitmix64 (http://xoroshiro.di.unimi.it/splitmix64.c) -// -/// A splitmix64 random number generator. -/// -/// The splitmix algorithm is not suitable for cryptographic purposes, but is -/// very fast and has a 64 bit state. -/// -/// The algorithm used here is translated from [the `splitmix64.c` -/// reference source code](http://xoshiro.di.unimi.it/splitmix64.c) by -/// Sebastiano Vigna. For `next_u32`, a more efficient mixing function taken -/// from [`dsiutils`](http://dsiutils.di.unimi.it/) is used. -#[derive(Debug, Copy, Clone)] -pub struct SplitMix64(pub u64); - -/// Internal random constant for [SplitMix64]. -const PHI: u64 = 0x9e3779b97f4a7c15; - -impl SplitMix64 { - pub fn new(seed: u64) -> Self { - Self(seed) - } - pub fn new_from_i64(seed: i64) -> Self { - Self::new(u64::from_be_bytes(seed.to_be_bytes())) - } - - pub fn new_time_seed() -> Self { - use std::time::SystemTime; - - match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(n) => Self::new(n.as_secs() as u64), - Err(_) => Self::new(123456789), - } - } - - #[inline] - pub fn next_u64(&mut self) -> u64 { - self.0 = self.0.wrapping_add(PHI); - let mut z = self.0; - z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); - z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); - z ^ (z >> 31) - } - - #[inline] - pub fn next_i64(&mut self) -> i64 { - i64::from_be_bytes(self.next_u64().to_be_bytes()) - } - - #[inline] - pub fn next_open01(&mut self) -> f64 { - u64_to_open01(self.next_u64()) - } -} - -/// Linear crossfade. -/// -/// * `v1` - signal 1, range -1.0 to 1.0 -/// * `v2` - signal 2, range -1.0 to 1.0 -/// * `mix` - mix position, range 0.0 to 1.0, mid is at 0.5 -#[inline] -pub fn crossfade(v1: F, v2: F, mix: F) -> F { - v1 * (f::(1.0) - mix) + v2 * mix -} - -/// Linear crossfade with clipping the `v2` result. -/// -/// This crossfade actually does clip the `v2` signal to the -1.0 to 1.0 -/// range. This is useful for Dry/Wet of plugins that might go beyond the -/// normal signal range. -/// -/// * `v1` - signal 1, range -1.0 to 1.0 -/// * `v2` - signal 2, range -1.0 to 1.0 -/// * `mix` - mix position, range 0.0 to 1.0, mid is at 0.5 -#[inline] -pub fn crossfade_clip(v1: F, v2: F, mix: F) -> F { - v1 * (f::(1.0) - mix) + (v2 * mix).min(f::(1.0)).max(f::(-1.0)) -} - -/// Linear (f32) crossfade with driving the `v2` result through a tanh(). -/// -/// * `v1` - signal 1, range -1.0 to 1.0 -/// * `v2` - signal 2, range -1.0 to 1.0 -/// * `mix` - mix position, range 0.0 to 1.0, mid is at 0.5 -#[inline] -pub fn crossfade_drive_tanh(v1: f32, v2: f32, mix: f32) -> f32 { - v1 * (1.0 - mix) + tanh_approx_drive(v2 * mix * 0.111, 0.95) * 0.9999 -} - -/// Constant power crossfade. -/// -/// * `v1` - signal 1, range -1.0 to 1.0 -/// * `v2` - signal 2, range -1.0 to 1.0 -/// * `mix` - mix position, range 0.0 to 1.0, mid is at 0.5 -#[inline] -pub fn crossfade_cpow(v1: f32, v2: f32, mix: f32) -> f32 { - let s1 = (mix * std::f32::consts::FRAC_PI_2).sin(); - let s2 = ((1.0 - mix) * std::f32::consts::FRAC_PI_2).sin(); - v1 * s2 + v2 * s1 -} - -const CROSS_LOG_MIN: f32 = -13.815510557964274; // (0.000001_f32).ln(); -const CROSS_LOG_MAX: f32 = 0.0; // (1.0_f32).ln(); - -/// Logarithmic crossfade. -/// -/// * `v1` - signal 1, range -1.0 to 1.0 -/// * `v2` - signal 2, range -1.0 to 1.0 -/// * `mix` - mix position, range 0.0 to 1.0, mid is at 0.5 -#[inline] -pub fn crossfade_log(v1: f32, v2: f32, mix: f32) -> f32 { - let x = (mix * (CROSS_LOG_MAX - CROSS_LOG_MIN) + CROSS_LOG_MIN).exp(); - crossfade(v1, v2, x) -} - -/// Exponential crossfade. -/// -/// * `v1` - signal 1, range -1.0 to 1.0 -/// * `v2` - signal 2, range -1.0 to 1.0 -/// * `mix` - mix position, range 0.0 to 1.0, mid is at 0.5 -#[inline] -pub fn crossfade_exp(v1: f32, v2: f32, mix: f32) -> f32 { - crossfade(v1, v2, mix * mix) -} - -#[inline] -pub fn clamp(f: f32, min: f32, max: f32) -> f32 { - if f < min { - min - } else if f > max { - max - } else { - f - } -} - -pub fn square_135(phase: f32) -> f32 { - fast_sin(phase) + fast_sin(phase * 3.0) / 3.0 + fast_sin(phase * 5.0) / 5.0 -} - -pub fn square_35(phase: f32) -> f32 { - fast_sin(phase * 3.0) / 3.0 + fast_sin(phase * 5.0) / 5.0 -} - -// note: MIDI note value? -pub fn note_to_freq(note: f32) -> f32 { - 440.0 * (2.0_f32).powf((note - 69.0) / 12.0) -} - -// Ported from LMMS under GPLv2 -// * DspEffectLibrary.h - library with template-based inline-effects -// * Copyright (c) 2006-2014 Tobias Doerffel -// -// Original source seems to be musicdsp.org, Author: Bram de Jong -// see also: https://www.musicdsp.org/en/latest/Effects/41-waveshaper.html -// Notes: -// where x (in [-1..1] will be distorted and a is a distortion parameter -// that goes from 1 to infinity. The equation is valid for positive and -// negativ values. If a is 1, it results in a slight distortion and with -// bigger a's the signal get's more funky. -// A good thing about the shaper is that feeding it with bigger-than-one -// values, doesn't create strange fx. The maximum this function will reach -// is 1.2 for a=1. -// -// f(x,a) = x*(abs(x) + a)/(x^2 + (a-1)*abs(x) + 1) -/// Signal distortion by Bram de Jong. -/// ```text -/// gain: 0.1 - 5.0 default = 1.0 -/// threshold: 0.0 - 100.0 default = 0.8 -/// i: signal -/// ``` -#[inline] -pub fn f_distort(gain: f32, threshold: f32, i: f32) -> f32 { - gain * (i * (i.abs() + threshold) / (i * i + (threshold - 1.0) * i.abs() + 1.0)) -} - -// Ported from LMMS under GPLv2 -// * DspEffectLibrary.h - library with template-based inline-effects -// * Copyright (c) 2006-2014 Tobias Doerffel -// -/// Foldback Signal distortion -/// ```text -/// gain: 0.1 - 5.0 default = 1.0 -/// threshold: 0.0 - 100.0 default = 0.8 -/// i: signal -/// ``` -#[inline] -pub fn f_fold_distort(gain: f32, threshold: f32, i: f32) -> f32 { - if i >= threshold || i < -threshold { - gain * ((((i - threshold) % threshold * 4.0).abs() - threshold * 2.0).abs() - threshold) - } else { - gain * i - } -} - -pub fn lerp(x: f32, a: f32, b: f32) -> f32 { - (a * (1.0 - x)) + (b * x) -} - -pub fn lerp64(x: f64, a: f64, b: f64) -> f64 { - (a * (1.0 - x)) + (b * x) -} - -pub fn p2range(x: f32, a: f32, b: f32) -> f32 { - lerp(x, a, b) -} - -pub fn p2range_exp(x: f32, a: f32, b: f32) -> f32 { - let x = x * x; - (a * (1.0 - x)) + (b * x) -} - -pub fn p2range_exp4(x: f32, a: f32, b: f32) -> f32 { - let x = x * x * x * x; - (a * (1.0 - x)) + (b * x) -} - -pub fn range2p(v: f32, a: f32, b: f32) -> f32 { - ((v - a) / (b - a)).abs() -} - -pub fn range2p_exp(v: f32, a: f32, b: f32) -> f32 { - (((v - a) / (b - a)).abs()).sqrt() -} - -pub fn range2p_exp4(v: f32, a: f32, b: f32) -> f32 { - (((v - a) / (b - a)).abs()).sqrt().sqrt() -} - -/// ```text -/// gain: 24.0 - -90.0 default = 0.0 -/// ``` -pub fn gain2coef(gain: f32) -> f32 { - if gain > -90.0 { - 10.0_f32.powf(gain * 0.05) - } else { - 0.0 - } -} - -// quickerTanh / quickerTanh64 credits to mopo synthesis library: -// Under GPLv3 or any later. -// Little IO -// Matt Tytel -pub fn quicker_tanh64(v: f64) -> f64 { - let square = v * v; - v / (1.0 + square / (3.0 + square / 5.0)) -} - -#[inline] -pub fn quicker_tanh(v: f32) -> f32 { - let square = v * v; - v / (1.0 + square / (3.0 + square / 5.0)) -} - -// quickTanh / quickTanh64 credits to mopo synthesis library: -// Under GPLv3 or any later. -// Little IO -// Matt Tytel -pub fn quick_tanh64(v: f64) -> f64 { - let abs_v = v.abs(); - let square = v * v; - let num = v - * (2.45550750702956 - + 2.45550750702956 * abs_v - + square * (0.893229853513558 + 0.821226666969744 * abs_v)); - let den = - 2.44506634652299 + (2.44506634652299 + square) * (v + 0.814642734961073 * v * abs_v).abs(); - - num / den -} - -pub fn quick_tanh(v: f32) -> f32 { - let abs_v = v.abs(); - let square = v * v; - let num = v - * (2.45550750702956 - + 2.45550750702956 * abs_v - + square * (0.893229853513558 + 0.821226666969744 * abs_v)); - let den = - 2.44506634652299 + (2.44506634652299 + square) * (v + 0.814642734961073 * v * abs_v).abs(); - - num / den -} - -// Taken from ValleyAudio -// Copyright Dale Johnson -// https://github.dev/ValleyAudio/ValleyRackFree/tree/v2.0 -// Under GPLv3 license -pub fn tanh_approx_drive(v: f32, drive: f32) -> f32 { - let x = v * drive; - - if x < -1.25 { - -1.0 - } else if x < -0.75 { - 1.0 - (x * (-2.5 - x) - 0.5625) - 1.0 - } else if x > 1.25 { - 1.0 - } else if x > 0.75 { - x * (2.5 - x) - 0.5625 - } else { - x - } -} - -/// A helper function for exponential envelopes. -/// It's a bit faster than calling the `pow` function of Rust. -/// -/// * `x` the input value -/// * `v' the shape value. -/// Which is linear at `0.5`, the forth root of `x` at `1.0` and x to the power -/// of 4 at `0.0`. You can vary `v` as you like. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// assert!(((sqrt4_to_pow4(0.25, 0.0) - 0.25_f32 * 0.25 * 0.25 * 0.25) -/// .abs() - 1.0) -/// < 0.0001); -/// -/// assert!(((sqrt4_to_pow4(0.25, 1.0) - (0.25_f32).sqrt().sqrt()) -/// .abs() - 1.0) -/// < 0.0001); -/// -/// assert!(((sqrt4_to_pow4(0.25, 0.5) - 0.25_f32).abs() - 1.0) < 0.0001); -///``` -#[inline] -pub fn sqrt4_to_pow4(x: f32, v: f32) -> f32 { - if v > 0.75 { - let xsq1 = x.sqrt(); - let xsq = xsq1.sqrt(); - let v = (v - 0.75) * 4.0; - xsq1 * (1.0 - v) + xsq * v - } else if v > 0.5 { - let xsq = x.sqrt(); - let v = (v - 0.5) * 4.0; - x * (1.0 - v) + xsq * v - } else if v > 0.25 { - let xx = x * x; - let v = (v - 0.25) * 4.0; - x * v + xx * (1.0 - v) - } else { - let xx = x * x; - let xxxx = xx * xx; - let v = v * 4.0; - xx * v + xxxx * (1.0 - v) - } -} - -/// A-100 Eurorack states, that a trigger is usually 2-10 milliseconds. -pub const TRIG_SIGNAL_LENGTH_MS: f32 = 2.0; - -/// The lower threshold for the schmidt trigger to reset. -pub const TRIG_LOW_THRES: f32 = 0.25; -/// The threshold, once reached, will cause a trigger event and signals -/// a logical '1'. Anything below this is a logical '0'. -pub const TRIG_HIGH_THRES: f32 = 0.5; - -#[derive(Debug, Clone, Copy)] -pub struct TrigSignal { - length: u32, - scount: u32, -} - -impl TrigSignal { - pub fn new() -> Self { - Self { length: ((44100.0 * TRIG_SIGNAL_LENGTH_MS) / 1000.0).ceil() as u32, scount: 0 } - } - - pub fn reset(&mut self) { - self.scount = 0; - } - - pub fn set_sample_rate(&mut self, srate: f32) { - self.length = ((srate * TRIG_SIGNAL_LENGTH_MS) / 1000.0).ceil() as u32; - self.scount = 0; - } - - #[inline] - pub fn trigger(&mut self) { - self.scount = self.length; - } - - #[inline] - pub fn next(&mut self) -> f32 { - if self.scount > 0 { - self.scount -= 1; - 1.0 - } else { - 0.0 - } - } -} - -impl Default for TrigSignal { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ChangeTrig { - ts: TrigSignal, - last: f32, -} - -impl ChangeTrig { - pub fn new() -> Self { - Self { - ts: TrigSignal::new(), - last: -100.0, // some random value :-) - } - } - - pub fn reset(&mut self) { - self.ts.reset(); - self.last = -100.0; - } - - pub fn set_sample_rate(&mut self, srate: f32) { - self.ts.set_sample_rate(srate); - } - - #[inline] - pub fn next(&mut self, inp: f32) -> f32 { - if (inp - self.last).abs() > std::f32::EPSILON { - self.ts.trigger(); - self.last = inp; - } - - self.ts.next() - } -} - -impl Default for ChangeTrig { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, Clone, Copy)] -pub struct Trigger { - triggered: bool, -} - -impl Trigger { - pub fn new() -> Self { - Self { triggered: false } - } - - #[inline] - pub fn reset(&mut self) { - self.triggered = false; - } - - #[inline] - pub fn check_trigger(&mut self, input: f32) -> bool { - if self.triggered { - if input <= TRIG_LOW_THRES { - self.triggered = false; - } - - false - } else if input > TRIG_HIGH_THRES { - self.triggered = true; - true - } else { - false - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TriggerPhaseClock { - clock_phase: f64, - clock_inc: f64, - prev_trigger: bool, - clock_samples: u32, -} - -impl TriggerPhaseClock { - pub fn new() -> Self { - Self { clock_phase: 0.0, clock_inc: 0.0, prev_trigger: true, clock_samples: 0 } - } - - #[inline] - pub fn reset(&mut self) { - self.clock_samples = 0; - self.clock_inc = 0.0; - self.prev_trigger = true; - self.clock_samples = 0; - } - - #[inline] - pub fn sync(&mut self) { - self.clock_phase = 0.0; - } - - #[inline] - pub fn next_phase(&mut self, clock_limit: f64, trigger_in: f32) -> f64 { - if self.prev_trigger { - if trigger_in <= TRIG_LOW_THRES { - self.prev_trigger = false; - } - } else if trigger_in > TRIG_HIGH_THRES { - self.prev_trigger = true; - - if self.clock_samples > 0 { - self.clock_inc = 1.0 / (self.clock_samples as f64); - } - - self.clock_samples = 0; - } - - self.clock_samples += 1; - - self.clock_phase += self.clock_inc; - self.clock_phase = self.clock_phase % clock_limit; - - self.clock_phase - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TriggerSampleClock { - prev_trigger: bool, - clock_samples: u32, - counter: u32, -} - -impl TriggerSampleClock { - pub fn new() -> Self { - Self { prev_trigger: true, clock_samples: 0, counter: 0 } - } - - #[inline] - pub fn reset(&mut self) { - self.clock_samples = 0; - self.counter = 0; - } - - #[inline] - pub fn next(&mut self, trigger_in: f32) -> u32 { - if self.prev_trigger { - if trigger_in <= TRIG_LOW_THRES { - self.prev_trigger = false; - } - } else if trigger_in > TRIG_HIGH_THRES { - self.prev_trigger = true; - self.clock_samples = self.counter; - self.counter = 0; - } - - self.counter += 1; - - self.clock_samples - } -} - -/// A slew rate limiter, with a configurable time per 1.0 increase. -#[derive(Debug, Clone, Copy)] -pub struct SlewValue { - current: F, - slew_per_ms: F, -} - -impl SlewValue { - pub fn new() -> Self { - Self { current: f(0.0), slew_per_ms: f(1000.0 / 44100.0) } - } - - pub fn reset(&mut self) { - self.current = f(0.0); - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.slew_per_ms = f::(1000.0) / srate; - } - - #[inline] - pub fn value(&self) -> F { - self.current - } - - /// * `slew_ms_per_1` - The time (in milliseconds) it should take - /// to get to 1.0 from 0.0. - #[inline] - pub fn next(&mut self, target: F, slew_ms_per_1: F) -> F { - // at 0.11ms, there are barely enough samples for proper slew. - if slew_ms_per_1 < f(0.11) { - self.current = target; - } else { - let max_delta = self.slew_per_ms / slew_ms_per_1; - self.current = target.min(self.current + max_delta).max(self.current - max_delta); - } - - self.current - } -} - -/// A ramped value changer, with a configurable time to reach the target value. -#[derive(Debug, Clone, Copy)] -pub struct RampValue { - slew_count: u64, - current: F, - target: F, - inc: F, - sr_ms: F, -} - -impl RampValue { - pub fn new() -> Self { - Self { - slew_count: 0, - current: f(0.0), - target: f(0.0), - inc: f(0.0), - sr_ms: f(44100.0 / 1000.0), - } - } - - pub fn reset(&mut self) { - self.slew_count = 0; - self.current = f(0.0); - self.target = f(0.0); - self.inc = f(0.0); - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.sr_ms = srate / f(1000.0); - } - - #[inline] - pub fn set_target(&mut self, target: F, slew_time_ms: F) { - self.target = target; - - // 0.02ms, thats a fraction of a sample at 44.1kHz - if slew_time_ms < f(0.02) { - self.current = self.target; - self.slew_count = 0; - } else { - let slew_samples = slew_time_ms * self.sr_ms; - self.slew_count = slew_samples.to_u64().unwrap_or(0); - self.inc = (self.target - self.current) / slew_samples; - } - } - - #[inline] - pub fn value(&self) -> F { - self.current - } - - #[inline] - pub fn next(&mut self) -> F { - if self.slew_count > 0 { - self.current = self.current + self.inc; - self.slew_count -= 1; - } else { - self.current = self.target; - } - - self.current - } -} - -/// Default size of the delay buffer: 5 seconds at 8 times 48kHz -const DEFAULT_DELAY_BUFFER_SAMPLES: usize = 8 * 48000 * 5; - -macro_rules! fc { - ($F: ident, $e: expr) => { - F::from_f64($e).unwrap() - }; -} - -#[allow(dead_code)] -#[inline] -fn f(x: f64) -> F { - F::from_f64(x).unwrap() -} - -#[allow(dead_code)] -#[inline] -fn fclamp(x: F, mi: F, mx: F) -> F { - x.max(mi).min(mx) -} - -#[allow(dead_code)] -#[inline] -fn fclampc(x: F, mi: f64, mx: f64) -> F { - x.max(f(mi)).min(f(mx)) -} - -#[derive(Debug, Clone, Default)] -pub struct DelayBuffer { - data: Vec, - wr: usize, - srate: F, -} - -impl DelayBuffer { - pub fn new() -> Self { - Self { data: vec![f(0.0); DEFAULT_DELAY_BUFFER_SAMPLES], wr: 0, srate: f(44100.0) } - } - - pub fn new_with_size(size: usize) -> Self { - Self { data: vec![f(0.0); size], wr: 0, srate: f(44100.0) } - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.srate = srate; - } - - pub fn reset(&mut self) { - self.data.fill(f(0.0)); - self.wr = 0; - } - - /// Feed one sample into the delay line and increment the write pointer. - /// Please note: For sample accurate feedback you need to retrieve the - /// output of the delay line before feeding in a new signal. - #[inline] - pub fn feed(&mut self, input: F) { - self.data[self.wr] = input; - self.wr = (self.wr + 1) % self.data.len(); - } - - /// Combines [DelayBuffer::cubic_interpolate_at] and [DelayBuffer::feed] - /// into one convenient function. - #[inline] - pub fn next_cubic(&mut self, delay_time_ms: F, input: F) -> F { - let res = self.cubic_interpolate_at(delay_time_ms); - self.feed(input); - res - } - - /// Combines [DelayBuffer::linear_interpolate_at] and [DelayBuffer::feed] - /// into one convenient function. - #[inline] - pub fn next_linear(&mut self, delay_time_ms: F, input: F) -> F { - let res = self.linear_interpolate_at(delay_time_ms); - self.feed(input); - res - } - - /// Combines [DelayBuffer::nearest_at] and [DelayBuffer::feed] - /// into one convenient function. - #[inline] - pub fn next_nearest(&mut self, delay_time_ms: F, input: F) -> F { - let res = self.nearest_at(delay_time_ms); - self.feed(input); - res - } - - /// Shorthand for [DelayBuffer::cubic_interpolate_at]. - #[inline] - pub fn tap_c(&self, delay_time_ms: F) -> F { - self.cubic_interpolate_at(delay_time_ms) - } - - /// Shorthand for [DelayBuffer::cubic_interpolate_at]. - #[inline] - pub fn tap_n(&self, delay_time_ms: F) -> F { - self.nearest_at(delay_time_ms) - } - - /// Shorthand for [DelayBuffer::cubic_interpolate_at]. - #[inline] - pub fn tap_l(&self, delay_time_ms: F) -> F { - self.linear_interpolate_at(delay_time_ms) - } - - /// Fetch a sample from the delay buffer at the given time. - /// - /// * `delay_time_ms` - Delay time in milliseconds. - #[inline] - pub fn linear_interpolate_at(&self, delay_time_ms: F) -> F { - self.linear_interpolate_at_s((delay_time_ms * self.srate) / f(1000.0)) - } - - /// Fetch a sample from the delay buffer at the given offset. - /// - /// * `s_offs` - Sample offset in samples. - #[inline] - pub fn linear_interpolate_at_s(&self, s_offs: F) -> F { - let data = &self.data[..]; - let len = data.len(); - let offs = s_offs.floor().to_usize().unwrap_or(0) % len; - let fract = s_offs.fract(); - - let i = (self.wr + len) - offs; - let x0 = data[i % len]; - let x1 = data[(i - 1) % len]; - - x0 + fract * (x1 - x0) - } - - /// Fetch a sample from the delay buffer at the given time. - /// - /// * `delay_time_ms` - Delay time in milliseconds. - #[inline] - pub fn cubic_interpolate_at(&self, delay_time_ms: F) -> F { - self.cubic_interpolate_at_s((delay_time_ms * self.srate) / f(1000.0)) - } - - /// Fetch a sample from the delay buffer at the given offset. - /// - /// * `s_offs` - Sample offset in samples. - #[inline] - pub fn cubic_interpolate_at_s(&self, s_offs: F) -> F { - let data = &self.data[..]; - let len = data.len(); - let offs = s_offs.floor().to_usize().unwrap_or(0) % len; - let fract = s_offs.fract(); - - let i = (self.wr + len) - offs; - - // 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 = data[(i + 1) % len]; - let x0 = data[i % len]; - let x1 = data[(i - 1) % len]; - let x2 = data[(i - 2) % len]; - - let c = (x1 - xm1) * f(0.5); - let v = x0 - x1; - let w = c + v; - let a = w + v + (x2 - x0) * f(0.5); - let b_neg = w + a; - - let fract = fract as F; - (((a * fract) - b_neg) * fract + c) * fract + x0 - } - - #[inline] - pub fn nearest_at(&self, delay_time_ms: F) -> F { - let len = self.data.len(); - let offs = ((delay_time_ms * self.srate) / f(1000.0)).floor().to_usize().unwrap_or(0) % len; - let idx = ((self.wr + len) - offs) % len; - self.data[idx] - } - - #[inline] - pub fn at(&self, delay_sample_count: usize) -> F { - let len = self.data.len(); - let idx = ((self.wr + len) - delay_sample_count) % len; - self.data[idx] - } -} - -/// Default size of the delay buffer: 1 seconds at 8 times 48kHz -const DEFAULT_ALLPASS_COMB_SAMPLES: usize = 8 * 48000; - -#[derive(Debug, Clone, Default)] -pub struct AllPass { - delay: DelayBuffer, -} - -impl AllPass { - pub fn new() -> Self { - Self { delay: DelayBuffer::new_with_size(DEFAULT_ALLPASS_COMB_SAMPLES) } - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.delay.set_sample_rate(srate); - } - - pub fn reset(&mut self) { - self.delay.reset(); - } - - #[inline] - pub fn delay_tap_n(&self, time_ms: F) -> F { - self.delay.tap_n(time_ms) - } - - #[inline] - pub fn next(&mut self, time_ms: F, g: F, v: F) -> F { - let s = self.delay.cubic_interpolate_at(time_ms); - let input = v + -g * s; - self.delay.feed(input); - input * g + s - } -} - -#[derive(Debug, Clone)] -pub struct Comb { - delay: DelayBuffer, -} - -impl Comb { - pub fn new() -> Self { - Self { delay: DelayBuffer::new_with_size(DEFAULT_ALLPASS_COMB_SAMPLES) } - } - - pub fn set_sample_rate(&mut self, srate: f32) { - self.delay.set_sample_rate(srate); - } - - pub fn reset(&mut self) { - self.delay.reset(); - } - - #[inline] - pub fn delay_tap_c(&self, time_ms: f32) -> f32 { - self.delay.tap_c(time_ms) - } - - #[inline] - pub fn delay_tap_n(&self, time_ms: f32) -> f32 { - self.delay.tap_n(time_ms) - } - - #[inline] - pub fn next_feedback(&mut self, time: f32, g: f32, v: f32) -> f32 { - let s = self.delay.cubic_interpolate_at(time); - let v = v + s * g; - self.delay.feed(v); - v - } - - #[inline] - pub fn next_feedforward(&mut self, time: f32, g: f32, v: f32) -> f32 { - let s = self.delay.next_cubic(time, v); - v + s * g - } -} - -// one pole lp from valley rack free: -// https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Common/DSP/OnePoleFilters.cpp -#[inline] -/// Process a very simple one pole 6dB low pass filter. -/// Useful in various applications, from usage in a synthesizer to -/// damping stuff in a reverb/delay. -/// -/// * `input` - Input sample -/// * `freq` - Frequency between 1.0 and 22000.0Hz -/// * `israte` - 1.0 / samplerate -/// * `z` - The internal one sample buffer of the filter. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut z = 0.0; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let s_out = -/// process_1pole_lowpass(*s, freq, 1.0 / 44100.0, &mut z); -/// // ... do something with the result here. -/// } -///``` -pub fn process_1pole_lowpass(input: f32, freq: f32, israte: f32, z: &mut f32) -> f32 { - let b = (-std::f32::consts::TAU * freq * israte).exp(); - let a = 1.0 - b; - *z = a * input + *z * b; - *z -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct OnePoleLPF { - israte: F, - a: F, - b: F, - freq: F, - z: F, -} - -impl OnePoleLPF { - pub fn new() -> Self { - Self { - israte: f::(1.0) / f(44100.0), - a: f::(0.0), - b: f::(0.0), - freq: f::(1000.0), - z: f::(0.0), - } - } - - pub fn reset(&mut self) { - self.z = f(0.0); - } - - #[inline] - fn recalc(&mut self) { - self.b = (f::(-1.0) * F::TAU() * self.freq * self.israte).exp(); - self.a = f::(1.0) - self.b; - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.israte = f::(1.0) / srate; - self.recalc(); - } - - #[inline] - pub fn set_freq(&mut self, freq: F) { - if freq != self.freq { - self.freq = freq; - self.recalc(); - } - } - - #[inline] - pub fn process(&mut self, input: F) -> F { - self.z = self.a * input + self.z * self.b; - self.z - } -} - -// Fixed one pole with setable pole and gain. -// Implementation taken from tubonitaub / alec-deason -// from https://github.com/alec-deason/virtual_modular/blob/4025f1ef343c2eb9cd74eac07b5350c1e7ec9c09/src/simd_graph.rs#L4292 -// under MIT License -#[derive(Debug, Copy, Clone, Default)] -pub struct FixedOnePole { - b0: f32, - a1: f32, - y1: f32, - gain: f32, -} - -impl FixedOnePole { - pub fn new(pole: f32, gain: f32) -> Self { - let b0 = if pole > 0.0 { 1.0 - pole } else { 1.0 + pole }; - - Self { b0, a1: -pole, y1: 0.0, gain } - } - - pub fn reset(&mut self) { - self.y1 = 0.0; - } - - pub fn set_gain(&mut self, gain: f32) { - self.gain = gain; - } - - pub fn process(&mut self, input: f32) -> f32 { - let output = self.b0 * self.gain * input - self.a1 * self.y1; - self.y1 = output; - output - } -} - -// one pole hp from valley rack free: -// https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Common/DSP/OnePoleFilters.cpp -#[inline] -/// Process a very simple one pole 6dB high pass filter. -/// Useful in various applications. -/// -/// * `input` - Input sample -/// * `freq` - Frequency between 1.0 and 22000.0Hz -/// * `israte` - 1.0 / samplerate -/// * `z` - The first internal buffer of the filter. -/// * `y` - The second internal buffer of the filter. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut z = 0.0; -/// let mut y = 0.0; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let s_out = -/// process_1pole_highpass(*s, freq, 1.0 / 44100.0, &mut z, &mut y); -/// // ... do something with the result here. -/// } -///``` -pub fn process_1pole_highpass(input: f32, freq: f32, israte: f32, z: &mut f32, y: &mut f32) -> f32 { - let b = (-std::f32::consts::TAU * freq * israte).exp(); - let a = (1.0 + b) / 2.0; - - let v = a * input - a * *z + b * *y; - *y = v; - *z = input; - v -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct OnePoleHPF { - israte: F, - a: F, - b: F, - freq: F, - z: F, - y: F, -} - -impl OnePoleHPF { - pub fn new() -> Self { - Self { - israte: f(1.0 / 44100.0), - a: f(0.0), - b: f(0.0), - freq: f(1000.0), - z: f(0.0), - y: f(0.0), - } - } - - pub fn reset(&mut self) { - self.z = f(0.0); - self.y = f(0.0); - } - - #[inline] - fn recalc(&mut self) { - self.b = (f::(-1.0) * F::TAU() * self.freq * self.israte).exp(); - self.a = (f::(1.0) + self.b) / f(2.0); - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.israte = f::(1.0) / srate; - self.recalc(); - } - - #[inline] - pub fn set_freq(&mut self, freq: F) { - if freq != self.freq { - self.freq = freq; - self.recalc(); - } - } - - #[inline] - pub fn process(&mut self, input: F) -> F { - let v = self.a * input - self.a * self.z + self.b * self.y; - - self.y = v; - self.z = input; - - v - } -} - -// one pole from: -// http://www.willpirkle.com/Downloads/AN-4VirtualAnalogFilters.pdf -// (page 5) -#[inline] -/// Process a very simple one pole 6dB low pass filter in TPT form. -/// Useful in various applications, from usage in a synthesizer to -/// damping stuff in a reverb/delay. -/// -/// * `input` - Input sample -/// * `freq` - Frequency between 1.0 and 22000.0Hz -/// * `israte` - 1.0 / samplerate -/// * `z` - The internal one sample buffer of the filter. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut z = 0.0; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let s_out = -/// process_1pole_tpt_highpass(*s, freq, 1.0 / 44100.0, &mut z); -/// // ... do something with the result here. -/// } -///``` -pub fn process_1pole_tpt_lowpass(input: f32, freq: f32, israte: f32, z: &mut f32) -> f32 { - let g = (std::f32::consts::PI * freq * israte).tan(); - let a = g / (1.0 + g); - - let v1 = a * (input - *z); - let v2 = v1 + *z; - *z = v2 + v1; - - // let (m0, m1) = (0.0, 1.0); - // (m0 * input + m1 * v2) as f32); - v2 -} - -// one pole from: -// http://www.willpirkle.com/Downloads/AN-4VirtualAnalogFilters.pdf -// (page 5) -#[inline] -/// Process a very simple one pole 6dB high pass filter in TPT form. -/// Useful in various applications. -/// -/// * `input` - Input sample -/// * `freq` - Frequency between 1.0 and 22000.0Hz -/// * `israte` - 1.0 / samplerate -/// * `z` - The internal one sample buffer of the filter. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut z = 0.0; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let s_out = -/// process_1pole_tpt_lowpass(*s, freq, 1.0 / 44100.0, &mut z); -/// // ... do something with the result here. -/// } -///``` -pub fn process_1pole_tpt_highpass(input: f32, freq: f32, israte: f32, z: &mut f32) -> f32 { - let g = (std::f32::consts::PI * freq * israte).tan(); - let a1 = g / (1.0 + g); - - let v1 = a1 * (input - *z); - let v2 = v1 + *z; - *z = v2 + v1; - - input - v2 -} - -/// The internal oversampling factor of [process_hal_chamberlin_svf]. -const FILTER_OVERSAMPLE_HAL_CHAMBERLIN: usize = 2; -// Hal Chamberlin's State Variable (12dB/oct) filter -// https://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/ -// Inspired by SynthV1 by Rui Nuno Capela, under the terms of -// GPLv2 or any later: -/// Process a HAL Chamberlin filter with two delays/state variables that is 12dB. -/// The filter does internal oversampling with very simple decimation to -/// rise the stability for cutoff frequency up to 16kHz. -/// -/// * `input` - Input sample. -/// * `freq` - Frequency in Hz. Please keep it inside 0.0 to 16000.0 Hz! -/// otherwise the filter becomes unstable. -/// * `res` - Resonance from 0.0 to 0.99. Resonance of 1.0 is not recommended, -/// as the filter will then oscillate itself out of control. -/// * `israte` - 1.0 divided by the sampling rate (eg. 1.0 / 44100.0). -/// * `band` - First state variable, containing the band pass result -/// after processing. -/// * `low` - Second state variable, containing the low pass result -/// after processing. -/// -/// Returned are the results of the high and notch filter. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut band = 0.0; -/// let mut low = 0.0; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let (high, notch) = -/// process_hal_chamberlin_svf( -/// *s, freq, 0.5, 1.0 / 44100.0, &mut band, &mut low); -/// // ... do something with the result here. -/// } -///``` -#[inline] -pub fn process_hal_chamberlin_svf( - input: f32, - freq: f32, - res: f32, - israte: f32, - band: &mut f32, - low: &mut f32, -) -> (f32, f32) { - let q = 1.0 - res; - let cutoff = 2.0 * (std::f32::consts::PI * freq * 0.5 * israte).sin(); - - let mut high = 0.0; - let mut notch = 0.0; - - for _ in 0..FILTER_OVERSAMPLE_HAL_CHAMBERLIN { - *low += cutoff * *band; - high = input - *low - q * *band; - *band += cutoff * high; - notch = high + *low; - } - - //d// println!("q={:4.2} cut={:8.3} freq={:8.1} LP={:8.3} HP={:8.3} BP={:8.3} N={:8.3}", - //d// q, cutoff, freq, *low, high, *band, notch); - - (high, notch) -} - -/// This function processes a Simper SVF with 12dB. It's a much newer algorithm -/// for filtering and provides easy to calculate multiple outputs. -/// -/// * `input` - Input sample. -/// * `freq` - Frequency in Hz. -/// otherwise the filter becomes unstable. -/// * `res` - Resonance from 0.0 to 0.99. Resonance of 1.0 is not recommended, -/// as the filter will then oscillate itself out of control. -/// * `israte` - 1.0 divided by the sampling rate (eg. 1.0 / 44100.0). -/// * `band` - First state variable, containing the band pass result -/// after processing. -/// * `low` - Second state variable, containing the low pass result -/// after processing. -/// -/// This function returns the low pass, band pass and high pass signal. -/// For a notch or peak filter signal, please consult the following example: -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut ic1eq = 0.0; -/// let mut ic2eq = 0.0; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let (low, band, high) = -/// process_simper_svf( -/// *s, freq, 0.5, 1.0 / 44100.0, &mut ic1eq, &mut ic2eq); -/// -/// // You can easily calculate the notch and peak results too: -/// let notch = low + high; -/// let peak = low - high; -/// // ... do something with the result here. -/// } -///``` -// Simper SVF implemented from -// https://cytomic.com/files/dsp/SvfLinearTrapezoidalSin.pdf -// Big thanks go to Andrew Simper @ Cytomic for developing and publishing -// the paper. -#[inline] -pub fn process_simper_svf( - input: f32, - freq: f32, - res: f32, - israte: f32, - ic1eq: &mut f32, - ic2eq: &mut f32, -) -> (f32, f32, f32) { - // XXX: the 1.989 were tuned by hand, so the resonance is more audible. - let k = 2f32 - (1.989f32 * res); - let w = std::f32::consts::PI * freq * israte; - - let s1 = w.sin(); - let s2 = (2.0 * w).sin(); - let nrm = 1.0 / (2.0 + k * s2); - - let g0 = s2 * nrm; - let g1 = (-2.0 * s1 * s1 - k * s2) * nrm; - let g2 = (2.0 * s1 * s1) * nrm; - - let t0 = input - *ic2eq; - let t1 = g0 * t0 + g1 * *ic1eq; - let t2 = g2 * t0 + g0 * *ic1eq; - - let v1 = t1 + *ic1eq; - let v2 = t2 + *ic2eq; - - *ic1eq += 2.0 * t1; - *ic2eq += 2.0 * t2; - - // low = v2 - // band = v1 - // high = input - k * v1 - v2 - // notch = low + high = input - k * v1 - // peak = low - high = 2 * v2 - input + k * v1 - // all = low + high - k * band = input - 2 * k * v1 - - (v2, v1, input - k * v1 - v2) -} - -/// This function implements a simple Stilson/Moog low pass filter with 24dB. -/// It provides only a low pass output. -/// -/// * `input` - Input sample. -/// * `freq` - Frequency in Hz. -/// otherwise the filter becomes unstable. -/// * `res` - Resonance from 0.0 to 0.99. Resonance of 1.0 is not recommended, -/// as the filter will then oscillate itself out of control. -/// * `israte` - 1.0 divided by the sampling rate (`1.0 / 44100.0`). -/// * `b0` to `b3` - Internal values used for filtering. -/// * `delay` - A buffer holding other delayed samples. -/// -///``` -/// use hexodsp::dsp::helpers::*; -/// -/// let samples = vec![0.0; 44100]; -/// let mut b0 = 0.0; -/// let mut b1 = 0.0; -/// let mut b2 = 0.0; -/// let mut b3 = 0.0; -/// let mut delay = [0.0; 4]; -/// let mut freq = 1000.0; -/// -/// for s in samples.iter() { -/// let low = -/// process_stilson_moog( -/// *s, freq, 0.5, 1.0 / 44100.0, -/// &mut b0, &mut b1, &mut b2, &mut b3, -/// &mut delay); -/// -/// // ... do something with the result here. -/// } -///``` -// Stilson/Moog implementation partly translated from here: -// https://github.com/ddiakopoulos/MoogLadders/blob/master/src/MusicDSPModel.h -// without any copyright as found on musicdsp.org -// (http://www.musicdsp.org/showone.php?id=24). -// -// It's also found on MusicDSP and has probably no proper license anyways. -// See also: https://github.com/ddiakopoulos/MoogLadders -// and https://github.com/rncbc/synthv1/blob/master/src/synthv1_filter.h#L103 -// and https://github.com/ddiakopoulos/MoogLadders/blob/master/src/MusicDSPModel.h -#[inline] -pub fn process_stilson_moog( - input: f32, - freq: f32, - res: f32, - israte: f32, - b0: &mut f32, - b1: &mut f32, - b2: &mut f32, - b3: &mut f32, - delay: &mut [f32; 4], -) -> f32 { - let cutoff = 2.0 * freq * israte; - - let p = cutoff * (1.8 - 0.8 * cutoff); - let k = 2.0 * (cutoff * std::f32::consts::PI * 0.5).sin() - 1.0; - - let t1 = (1.0 - p) * 1.386249; - let t2 = 12.0 + t1 * t1; - - let res = res * (t2 + 6.0 * t1) / (t2 - 6.0 * t1); - - let x = input - res * *b3; - - // Four cascaded one-pole filters (bilinear transform) - *b0 = x * p + delay[0] * p - k * *b0; - *b1 = *b0 * p + delay[1] * p - k * *b1; - *b2 = *b1 * p + delay[2] * p - k * *b2; - *b3 = *b2 * p + delay[3] * p - k * *b3; - - // Clipping band-limited sigmoid - *b3 -= (*b3 * *b3 * *b3) * 0.166667; - - delay[0] = x; - delay[1] = *b0; - delay[2] = *b1; - delay[3] = *b2; - - *b3 -} - -// translated from Odin 2 Synthesizer Plugin -// Copyright (C) 2020 TheWaveWarden -// under GPLv3 or any later -#[derive(Debug, Clone, Copy)] -pub struct DCBlockFilter { - xm1: F, - ym1: F, - r: F, -} - -impl DCBlockFilter { - pub fn new() -> Self { - Self { xm1: f(0.0), ym1: f(0.0), r: f(0.995) } - } - - pub fn reset(&mut self) { - self.xm1 = f(0.0); - self.ym1 = f(0.0); - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.r = f(0.995); - if srate > f(90000.0) { - self.r = f(0.9965); - } else if srate > f(120000.0) { - self.r = f(0.997); - } - } - - pub fn next(&mut self, input: F) -> F { - let y = input - self.xm1 + self.r * self.ym1; - self.xm1 = input; - self.ym1 = y; - y as F - } -} - -// PolyBLEP by Tale -// (slightly modified) -// http://www.kvraudio.com/forum/viewtopic.php?t=375517 -// from http://www.martin-finke.de/blog/articles/audio-plugins-018-polyblep-oscillator/ -// -// default for `pw' should be 1.0, it's the pulse width -// for the square wave. -#[allow(dead_code)] -fn poly_blep_64(t: f64, dt: f64) -> f64 { - if t < dt { - let t = t / dt; - 2. * t - (t * t) - 1. - } else if t > (1.0 - dt) { - let t = (t - 1.0) / dt; - (t * t) + 2. * t + 1. - } else { - 0. - } -} - -fn poly_blep(t: f32, dt: f32) -> f32 { - if t < dt { - let t = t / dt; - 2. * t - (t * t) - 1. - } else if t > (1.0 - dt) { - let t = (t - 1.0) / dt; - (t * t) + 2. * t + 1. - } else { - 0. - } -} - -/// This is a band-limited oscillator based on the PolyBlep technique. -/// Here is a quick example on how to use it: -/// -///``` -/// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; -/// -/// // Randomize the initial phase to make cancellation on summing less -/// // likely: -/// let mut osc = -/// PolyBlepOscillator::new(rand_01() * 0.25); -/// -/// -/// let freq = 440.0; // Hz -/// let israte = 1.0 / 44100.0; // Seconds per Sample -/// let pw = 0.2; // Pulse-Width for the next_pulse() -/// let waveform = 0; // 0 being pulse in this example, 1 being sawtooth -/// -/// let mut block_of_samples = [0.0; 128]; -/// // in your process function: -/// for output_sample in block_of_samples.iter_mut() { -/// *output_sample = -/// if waveform == 1 { -/// osc.next_saw(freq, israte) -/// } else { -/// osc.next_pulse(freq, israte, pw) -/// } -/// } -///``` -#[derive(Debug, Clone)] -pub struct PolyBlepOscillator { - phase: f32, - init_phase: f32, - last_output: f32, -} - -impl PolyBlepOscillator { - /// Create a new instance of [PolyBlepOscillator]. - /// - /// * `init_phase` - Initial phase of the oscillator. - /// Range of this parameter is from 0.0 to 1.0. Passing a random - /// value is advised for preventing phase cancellation when summing multiple - /// oscillators. - /// - ///``` - /// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; - /// - /// let mut osc = PolyBlepOscillator::new(rand_01() * 0.25); - ///``` - pub fn new(init_phase: f32) -> Self { - Self { phase: 0.0, last_output: 0.0, init_phase } - } - - /// Reset the internal state of the oscillator as if you just called - /// [PolyBlepOscillator::new]. - #[inline] - pub fn reset(&mut self) { - self.phase = self.init_phase; - self.last_output = 0.0; - } - - /// Creates the next sample of a sine wave. - /// - /// * `freq` - The frequency in Hz. - /// * `israte` - The inverse sampling rate, or seconds per sample as in eg. `1.0 / 44100.0`. - ///``` - /// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; - /// - /// let mut osc = PolyBlepOscillator::new(rand_01() * 0.25); - /// - /// let freq = 440.0; // Hz - /// let israte = 1.0 / 44100.0; // Seconds per Sample - /// - /// // ... - /// let sample = osc.next_sin(freq, israte); - /// // ... - ///``` - #[inline] - pub fn next_sin(&mut self, freq: f32, israte: f32) -> f32 { - let phase_inc = freq * israte; - - let s = fast_sin(self.phase * 2.0 * std::f32::consts::PI); - - self.phase += phase_inc; - self.phase = self.phase.fract(); - - s as f32 - } - - /// Creates the next sample of a triangle wave. Please note that the - /// bandlimited waveform needs a few initial samples to swing in. - /// - /// * `freq` - The frequency in Hz. - /// * `israte` - The inverse sampling rate, or seconds per sample as in eg. `1.0 / 44100.0`. - ///``` - /// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; - /// - /// let mut osc = PolyBlepOscillator::new(rand_01() * 0.25); - /// - /// let freq = 440.0; // Hz - /// let israte = 1.0 / 44100.0; // Seconds per Sample - /// - /// // ... - /// let sample = osc.next_tri(freq, israte); - /// // ... - ///``` - #[inline] - pub fn next_tri(&mut self, freq: f32, israte: f32) -> f32 { - let phase_inc = freq * israte; - - let mut s = if self.phase < 0.5 { 1.0 } else { -1.0 }; - - s += poly_blep(self.phase, phase_inc); - s -= poly_blep((self.phase + 0.5).fract(), phase_inc); - - // leaky integrator: y[n] = A * x[n] + (1 - A) * y[n-1] - s = phase_inc * s + (1.0 - phase_inc) * self.last_output; - self.last_output = s; - - self.phase += phase_inc; - self.phase = self.phase.fract(); - - // the signal is a bit too weak, we need to amplify it - // or else the volume diff between the different waveforms - // is too big: - s * 4.0 - } - - /// Creates the next sample of a sawtooth wave. - /// - /// * `freq` - The frequency in Hz. - /// * `israte` - The inverse sampling rate, or seconds per sample as in eg. `1.0 / 44100.0`. - ///``` - /// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; - /// - /// let mut osc = PolyBlepOscillator::new(rand_01() * 0.25); - /// - /// let freq = 440.0; // Hz - /// let israte = 1.0 / 44100.0; // Seconds per Sample - /// - /// // ... - /// let sample = osc.next_saw(freq, israte); - /// // ... - ///``` - #[inline] - pub fn next_saw(&mut self, freq: f32, israte: f32) -> f32 { - let phase_inc = freq * israte; - - let mut s = (2.0 * self.phase) - 1.0; - s -= poly_blep(self.phase, phase_inc); - - self.phase += phase_inc; - self.phase = self.phase.fract(); - - s - } - - /// Creates the next sample of a pulse wave. - /// In comparison to [PolyBlepOscillator::next_pulse_no_dc] this - /// version is DC compensated, so that you may add multiple different - /// pulse oscillators for a unison effect without too big DC issues. - /// - /// * `freq` - The frequency in Hz. - /// * `israte` - The inverse sampling rate, or seconds per sample as in eg. `1.0 / 44100.0`. - /// * `pw` - The pulse width. Use the value 0.0 for a square wave. - ///``` - /// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; - /// - /// let mut osc = PolyBlepOscillator::new(rand_01() * 0.25); - /// - /// let freq = 440.0; // Hz - /// let israte = 1.0 / 44100.0; // Seconds per Sample - /// let pw = 0.0; // 0.0 is a square wave. - /// - /// // ... - /// let sample = osc.next_pulse(freq, israte, pw); - /// // ... - ///``` - #[inline] - pub fn next_pulse(&mut self, freq: f32, israte: f32, pw: f32) -> f32 { - let phase_inc = freq * israte; - - let pw = (0.1 * pw) + ((1.0 - pw) * 0.5); // some scaling - let dc_compensation = (0.5 - pw) * 2.0; - - let mut s = if self.phase < pw { 1.0 } else { -1.0 }; - - s += poly_blep(self.phase, phase_inc); - s -= poly_blep((self.phase + (1.0 - pw)).fract(), phase_inc); - - s += dc_compensation; - - self.phase += phase_inc; - self.phase = self.phase.fract(); - - s - } - - /// Creates the next sample of a pulse wave. - /// In comparison to [PolyBlepOscillator::next_pulse] this - /// version is not DC compensated. So be careful when adding multiple - /// of this or generally using it in an audio context. - /// - /// * `freq` - The frequency in Hz. - /// * `israte` - The inverse sampling rate, or seconds per sample as in eg. `1.0 / 44100.0`. - /// * `pw` - The pulse width. Use the value 0.0 for a square wave. - ///``` - /// use hexodsp::dsp::helpers::{PolyBlepOscillator, rand_01}; - /// - /// let mut osc = PolyBlepOscillator::new(rand_01() * 0.25); - /// - /// let freq = 440.0; // Hz - /// let israte = 1.0 / 44100.0; // Seconds per Sample - /// let pw = 0.0; // 0.0 is a square wave. - /// - /// // ... - /// let sample = osc.next_pulse_no_dc(freq, israte, pw); - /// // ... - ///``` - #[inline] - pub fn next_pulse_no_dc(&mut self, freq: f32, israte: f32, pw: f32) -> f32 { - let phase_inc = freq * israte; - - let pw = (0.1 * pw) + ((1.0 - pw) * 0.5); // some scaling - - let mut s = if self.phase < pw { 1.0 } else { -1.0 }; - - s += poly_blep(self.phase, phase_inc); - s -= poly_blep((self.phase + (1.0 - pw)).fract(), phase_inc); - - self.phase += phase_inc; - self.phase = self.phase.fract(); - - s - } -} - -// This oscillator is based on the work "VECTOR PHASESHAPING SYNTHESIS" -// by: Jari Kleimola*, Victor Lazzarini†, Joseph Timoney†, Vesa Välimäki* -// *Aalto University School of Electrical Engineering Espoo, Finland; -// †National University of Ireland, Maynooth Ireland -// -// See also this PDF: http://recherche.ircam.fr/pub/dafx11/Papers/55_e.pdf -/// Vector Phase Shaping Oscillator. -/// The parameters `d` and `v` control the shape of the sinus -/// wave. This leads to interesting modulation properties of those -/// control values. -/// -///``` -/// use hexodsp::dsp::helpers::{VPSOscillator, rand_01}; -/// -/// // Randomize the initial phase to make cancellation on summing less -/// // likely: -/// let mut osc = -/// VPSOscillator::new(rand_01() * 0.25); -/// -/// -/// let freq = 440.0; // Hz -/// let israte = 1.0 / 44100.0; // Seconds per Sample -/// let d = 0.5; // Range: 0.0 to 1.0 -/// let v = 0.75; // Range: 0.0 to 1.0 -/// -/// let mut block_of_samples = [0.0; 128]; -/// // in your process function: -/// for output_sample in block_of_samples.iter_mut() { -/// // It is advised to limit the `v` value, because with certain -/// // `d` values the combination creates just a DC offset. -/// let v = VPSOscillator::limit_v(d, v); -/// *output_sample = osc.next(freq, israte, d, v); -/// } -///``` -/// -/// It can be beneficial to apply distortion and oversampling. -/// Especially oversampling can be important for some `d` and `v` -/// combinations, even without distortion. -/// -///``` -/// use hexodsp::dsp::helpers::{VPSOscillator, rand_01, apply_distortion}; -/// use hexodsp::dsp::biquad::Oversampling; -/// -/// let mut osc = VPSOscillator::new(rand_01() * 0.25); -/// let mut ovr : Oversampling<4> = Oversampling::new(); -/// -/// let freq = 440.0; // Hz -/// let israte = 1.0 / 44100.0; // Seconds per Sample -/// let d = 0.5; // Range: 0.0 to 1.0 -/// let v = 0.75; // Range: 0.0 to 1.0 -/// -/// let mut block_of_samples = [0.0; 128]; -/// // in your process function: -/// for output_sample in block_of_samples.iter_mut() { -/// // It is advised to limit the `v` value, because with certain -/// // `d` values the combination creates just a DC offset. -/// let v = VPSOscillator::limit_v(d, v); -/// -/// let overbuf = ovr.resample_buffer(); -/// for b in overbuf { -/// *b = apply_distortion(osc.next(freq, israte, d, v), 0.9, 1); -/// } -/// *output_sample = ovr.downsample(); -/// } -///``` -#[derive(Debug, Clone)] -pub struct VPSOscillator { - phase: f32, - init_phase: f32, -} - -impl VPSOscillator { - /// Create a new instance of [VPSOscillator]. - /// - /// * `init_phase` - The initial phase of the oscillator. - pub fn new(init_phase: f32) -> Self { - Self { phase: 0.0, init_phase } - } - - /// Reset the phase of the oscillator to the initial phase. - #[inline] - pub fn reset(&mut self) { - self.phase = self.init_phase; - } - - #[inline] - fn s(p: f32) -> f32 { - -(std::f32::consts::TAU * p).cos() - } - - #[inline] - fn phi_vps(x: f32, v: f32, d: f32) -> f32 { - if x < d { - (v * x) / d - } else { - v + ((1.0 - v) * (x - d)) / (1.0 - d) - } - } - - /// This rather complicated function blends out some - /// combinations of 'd' and 'v' that just lead to a constant DC - /// offset. Which is not very useful in an audio oscillator - /// context. - /// - /// Call this before passing `v` to [VPSOscillator::next]. - #[inline] - pub fn limit_v(d: f32, v: f32) -> f32 { - let delta = 0.5 - (d - 0.5).abs(); - if delta < 0.05 { - let x = (0.05 - delta) * 19.99; - if d < 0.5 { - let mm = x * 0.5; - let max = 1.0 - mm; - if v > max && v < 1.0 { - max - } else if v >= 1.0 && v < (1.0 + mm) { - 1.0 + mm - } else { - v - } - } else { - if v < 1.0 { - v.clamp(x * 0.5, 1.0) - } else { - v - } - } - } else { - v - } - } - - /// Creates the next sample of this oscillator. - /// - /// * `freq` - The frequency in Hz. - /// * `israte` - The inverse sampling rate, or seconds per sample as in eg. `1.0 / 44100.0`. - /// * `d` - The phase distortion parameter `d` which must be in the range `0.0` to `1.0`. - /// * `v` - The phase distortion parameter `v` which must be in the range `0.0` to `1.0`. - /// - /// It is advised to limit the `v` using the [VPSOscillator::limit_v] function - /// before calling this function. To prevent DC offsets when modulating the parameters. - pub fn next(&mut self, freq: f32, israte: f32, d: f32, v: f32) -> f32 { - let s = Self::s(Self::phi_vps(self.phase, v, d)); - - self.phase += freq * israte; - self.phase = self.phase.fract(); - - s - } -} - -// Adapted from https://github.com/ValleyAudio/ValleyRackFree/blob/v1.0/src/Common/DSP/LFO.hpp -// -// ValleyRackFree Copyright (C) 2020, Valley Audio Soft, Dale Johnson -// Adapted under the GPL-3.0-or-later License. -/// An LFO with a variable reverse point, which can go from reverse Saw, to Tri -/// and to Saw, depending on the reverse point. -#[derive(Debug, Clone, Copy)] -pub struct TriSawLFO { - /// The (inverse) sample rate. Eg. 1.0 / 44100.0. - israte: F, - /// The current oscillator phase. - phase: F, - /// The point from where the falling edge will be used. - rev: F, - /// The frequency. - freq: F, - /// Precomputed rise/fall rate of the LFO. - rise_r: F, - fall_r: F, - /// Initial phase offset. - init_phase: F, -} - -impl TriSawLFO { - pub fn new() -> Self { - let mut this = Self { - israte: f(1.0 / 44100.0), - phase: f(0.0), - rev: f(0.5), - freq: f(1.0), - fall_r: f(0.0), - rise_r: f(0.0), - init_phase: f(0.0), - }; - this.recalc(); - this - } - - pub fn set_phase_offs(&mut self, phase: F) { - self.init_phase = phase; - self.phase = phase; - } - - #[inline] - fn recalc(&mut self) { - self.rev = fclampc(self.rev, 0.0001, 0.999); - self.rise_r = f::(1.0) / self.rev; - self.fall_r = f::(-1.0) / (f::(1.0) - self.rev); - } - - pub fn set_sample_rate(&mut self, srate: F) { - self.israte = f::(1.0) / (srate as F); - self.recalc(); - } - - pub fn reset(&mut self) { - self.phase = self.init_phase; - self.rev = f(0.5); - } - - #[inline] - pub fn set(&mut self, freq: F, rev: F) { - self.freq = freq as F; - self.rev = rev as F; - self.recalc(); - } - - #[inline] - pub fn next_unipolar(&mut self) -> F { - if self.phase >= f(1.0) { - self.phase = self.phase - f(1.0); - } - - let s = if self.phase < self.rev { - self.phase * self.rise_r - } else { - self.phase * self.fall_r - self.fall_r - }; - - if s.abs() > f(1.0) { - println!( - "RECALC TRISAW: rev={}, rise={}, fall={}, phase={}", - self.rev.to_f64().unwrap_or(0.0), - self.rise_r.to_f64().unwrap_or(0.0) as f32, - self.fall_r.to_f64().unwrap_or(0.0) as f32, - self.phase.to_f64().unwrap_or(0.0) as f32 - ); - } - - self.phase = self.phase + self.freq * self.israte; - - s - } - - #[inline] - pub fn next_bipolar(&mut self) -> F { - (self.next_unipolar() * f(2.0)) - f(1.0) - } -} - -#[derive(Debug, Clone)] -pub struct Quantizer { - old_mask: i64, - lkup_tbl: [(f32, f32); 24], - last_key: f32, -} - -impl Quantizer { - pub fn new() -> Self { - Self { old_mask: 0xFFFF_FFFF, lkup_tbl: [(0.0, 0.0); 24], last_key: 0.0 } - } - - #[inline] - pub fn set_keys(&mut self, keys_mask: i64) { - if keys_mask == self.old_mask { - return; - } - self.old_mask = keys_mask; - - self.setup_lookup_table(); - } - - #[inline] - fn setup_lookup_table(&mut self) { - let mask = self.old_mask; - let any_enabled = mask > 0x0; - - for i in 0..24 { - let mut min_d_note_idx = 0; - let mut min_dist = 1000000000; - - for note in -12..=24 { - let dist = ((i + 1_i64) / 2 - note).abs(); - let note_idx = note.rem_euclid(12); - - // XXX: We add 9 here for the mask lookup, - // to shift the keyboard, which starts at C! - // And first bit in the mask is the C note. 10th is the A note. - if any_enabled && (mask & (0x1 << ((note_idx + 9) % 12))) == 0x0 { - continue; - } - - //d// println!("I={:3} NOTE={:3} (IDX={:3} => bitset {}) DIST={:3}", - //d// i, note, note_idx, - //d// if (mask & (0x1 << ((note_idx + 9) % 12))) > 0x0 { 1 } else { 0 }, - //d// dist); - - if dist < min_dist { - min_d_note_idx = note; - min_dist = dist; - } else { - break; - } - } - - self.lkup_tbl[i as usize] = ( - (min_d_note_idx + 9).rem_euclid(12) as f32 * (0.1 / 12.0), - min_d_note_idx.rem_euclid(12) as f32 * (0.1 / 12.0) - + (if min_d_note_idx < 0 { - -0.1 - } else if min_d_note_idx > 11 { - 0.1 - } else { - 0.0 - }), - ); - } - //d// println!("TBL: {:?}", self.lkup_tbl); - } - - #[inline] - pub fn last_key_pitch(&self) -> f32 { - self.last_key - } - - #[inline] - pub fn process(&mut self, inp: f32) -> f32 { - let note_num = (inp * 240.0).round() as i64; - let octave = note_num.div_euclid(24); - let note_idx = note_num - octave * 24; - - // println!( - // "INP {:7.4} => octave={:3}, note_idx={:3} note_num={:3} inp={:9.6}", - // inp, octave, note_idx, note_num, inp * 240.0); - //d// println!("TBL: {:?}", self.lkup_tbl); - - let (ui_key_pitch, note_pitch) = self.lkup_tbl[note_idx as usize % 24]; - self.last_key = ui_key_pitch; - note_pitch + octave as f32 * 0.1 - } -} - -#[derive(Debug, Clone)] -pub struct CtrlPitchQuantizer { - /// All keys, containing the min/max octave! - keys: Vec, - /// Only the used keys with their pitches from the UI - used_keys: [f32; 12], - /// A value combination of the arguments to `update_keys`. - input_params: u64, - /// The number of used keys from the mask. - mask_key_count: u16, - /// The last key for the pitch that was returned by `process`. - last_key: u8, -} - -const QUANT_TUNE_TO_A4: f32 = (9.0 / 12.0) * 0.1; - -impl CtrlPitchQuantizer { - pub fn new() -> Self { - Self { - keys: vec![0.0; 12 * 10], - used_keys: [0.0; 12], - mask_key_count: 0, - input_params: 0xFFFFFFFFFF, - last_key: 0, - } - } - - #[inline] - pub fn last_key_pitch(&self) -> f32 { - self.used_keys[self.last_key as usize % (self.mask_key_count as usize)] + QUANT_TUNE_TO_A4 - } - - #[inline] - pub fn update_keys(&mut self, mut mask: i64, min_oct: i64, max_oct: i64) { - let inp_params = (mask as u64) | ((min_oct as u64) << 12) | ((max_oct as u64) << 20); - - if self.input_params == inp_params { - return; - } - - self.input_params = inp_params; - - let mut mask_count = 0; - - // set all keys, if none are set! - if mask == 0x0 { - mask = 0xFFFF; - } - - for i in 0..12 { - if mask & (0x1 << i) > 0 { - self.used_keys[mask_count] = (i as f32 / 12.0) * 0.1 - QUANT_TUNE_TO_A4; - mask_count += 1; - } - } - - self.keys.clear(); - - let min_oct = min_oct as usize; - for o in 0..min_oct { - let o = min_oct - o; - - for i in 0..mask_count { - self.keys.push(self.used_keys[i] - (o as f32) * 0.1); - } - } - - for i in 0..mask_count { - self.keys.push(self.used_keys[i]); - } - - let max_oct = max_oct as usize; - for o in 1..=max_oct { - for i in 0..mask_count { - self.keys.push(self.used_keys[i] + (o as f32) * 0.1); - } - } - - self.mask_key_count = mask_count as u16; - } - - #[inline] - pub fn signal_to_pitch(&mut self, inp: f32) -> f32 { - let len = self.keys.len(); - let key = (inp.clamp(0.0, 0.9999) * (len as f32)).floor(); - let key = key as usize % len; - self.last_key = key as u8; - self.keys[key] - } -} - -#[macro_export] -macro_rules! fa_distort { - ($formatter: expr, $v: expr, $denorm_v: expr) => {{ - let s = match ($v.round() as usize) { - 0 => "Off", - 1 => "TanH", - 2 => "B.D.Jong", - 3 => "Fold", - _ => "?", - }; - write!($formatter, "{}", s) - }}; -} - -#[inline] -pub fn apply_distortion(s: f32, damt: f32, dist_type: u8) -> f32 { - match dist_type { - 1 => (damt.clamp(0.01, 1.0) * 100.0 * s).tanh(), - 2 => f_distort(1.0, damt * damt * damt * 1000.0, s), - 3 => { - let damt = damt.clamp(0.0, 0.99); - let damt = 1.0 - damt * damt; - f_fold_distort(1.0, damt, s) * (1.0 / damt) - } - _ => s, - } -} - -//pub struct UnisonBlep { -// oscs: Vec, -//// dc_block: crate::filter::DCBlockFilter, -//} -// -//impl UnisonBlep { -// pub fn new(max_unison: usize) -> Self { -// let mut oscs = vec![]; -// let mut rng = RandGen::new(); -// -// let dis_init_phase = 0.05; -// for i in 0..(max_unison + 1) { -// // randomize phases so we fatten the unison, get -// // less DC and not an amplified signal until the -// // detune desyncs the waves. -// // But no random phase for first, so we reduce the click -// let init_phase = -// if i == 0 { 0.0 } else { rng.next_open01() }; -// oscs.push(PolyBlepOscillator::new(init_phase)); -// } -// -// Self { -// oscs, -//// dc_block: crate::filter::DCBlockFilter::new(), -// } -// } -// -// pub fn set_sample_rate(&mut self, srate: f32) { -//// self.dc_block.set_sample_rate(srate); -// for o in self.oscs.iter_mut() { -// o.set_sample_rate(srate); -// } -// } -// -// pub fn reset(&mut self) { -//// self.dc_block.reset(); -// for o in self.oscs.iter_mut() { -// o.reset(); -// } -// } -// -// pub fn next(&mut self, params: &P) -> f32 { -// let unison = -// (params.unison().floor() as usize) -// .min(self.oscs.len() - 1); -// let detune = params.detune() as f64; -// -// let mix = (1.0 / ((unison + 1) as f32)).sqrt(); -// -// let mut s = mix * self.oscs[0].next(params, 0.0); -// -// for u in 0..unison { -// let detune_factor = -// detune * (((u / 2) + 1) as f64 -// * if (u % 2) == 0 { 1.0 } else { -1.0 }); -// s += mix * self.oscs[u + 1].next(params, detune_factor * 0.01); -// } -// -//// self.dc_block.next(s) -// s -// } -//} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn check_range2p_exp() { - let a = p2range_exp(0.5, 1.0, 100.0); - let x = range2p_exp(a, 1.0, 100.0); - - assert!((x - 0.5).abs() < std::f32::EPSILON); - } - - #[test] - fn check_range2p() { - let a = p2range(0.5, 1.0, 100.0); - let x = range2p(a, 1.0, 100.0); - - assert!((x - 0.5).abs() < std::f32::EPSILON); - } -} diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 91df89e..936e189 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1,7 +1,489 @@ -// Copyright (c) 2021 Weird Constructor +// Copyright (c) 2021-2022 Weird Constructor // 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 + " \n + A short description what this paramter means/does and relates to others.\n + 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>, + trig: Trigger, + } + + // ... + impl DspNode for TsLFO { + // ... + #[inline] + fn process(&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( + &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 { + 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]; @@ -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 { - 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 } } diff --git a/src/dsp/node_ad.rs b/src/dsp/node_ad.rs index 55c26f5..ef69e5a 100644 --- a/src/dsp/node_ad.rs +++ b/src/dsp/node_ad.rs @@ -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, }; diff --git a/src/dsp/node_allp.rs b/src/dsp/node_allp.rs index 901293d..adf3cbd 100644 --- a/src/dsp/node_allp.rs +++ b/src/dsp/node_allp.rs @@ -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}; diff --git a/src/dsp/node_biqfilt.rs b/src/dsp/node_biqfilt.rs index 43a9d4a..8d3c373 100644 --- a/src/dsp/node_biqfilt.rs +++ b/src/dsp/node_biqfilt.rs @@ -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}; diff --git a/src/dsp/node_bosc.rs b/src/dsp/node_bosc.rs index b965a41..e4eb603 100644 --- a/src/dsp/node_bosc.rs +++ b/src/dsp/node_bosc.rs @@ -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, }; diff --git a/src/dsp/node_bowstri.rs b/src/dsp/node_bowstri.rs index a806118..f16ea01 100644 --- a/src/dsp/node_bowstri.rs +++ b/src/dsp/node_bowstri.rs @@ -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, }; diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs new file mode 100644 index 0000000..4a23385 --- /dev/null +++ b/src/dsp/node_code.rs @@ -0,0 +1,142 @@ +// Copyright (c) 2021 Weird Constructor +// 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>, + 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( + &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); + } + } +} diff --git a/src/dsp/node_comb.rs b/src/dsp/node_comb.rs index 36db6a4..285368b 100644 --- a/src/dsp/node_comb.rs +++ b/src/dsp/node_comb.rs @@ -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, + comb: Box, } 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)"; diff --git a/src/dsp/node_cqnt.rs b/src/dsp/node_cqnt.rs index 8d607a4..3f36f3a 100644 --- a/src/dsp/node_cqnt.rs +++ b/src/dsp/node_cqnt.rs @@ -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}; diff --git a/src/dsp/node_delay.rs b/src/dsp/node_delay.rs index cd8c821..2e2e826 100644 --- a/src/dsp/node_delay.rs +++ b/src/dsp/node_delay.rs @@ -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}; diff --git a/src/dsp/node_mux9.rs b/src/dsp/node_mux9.rs index c637f64..ff8aa58 100644 --- a/src/dsp/node_mux9.rs +++ b/src/dsp/node_mux9.rs @@ -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}; diff --git a/src/dsp/node_noise.rs b/src/dsp/node_noise.rs index 8f4b548..ce3d5ca 100644 --- a/src/dsp/node_noise.rs +++ b/src/dsp/node_noise.rs @@ -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}; diff --git a/src/dsp/node_pverb.rs b/src/dsp/node_pverb.rs index cdb8529..20853c1 100644 --- a/src/dsp/node_pverb.rs +++ b/src/dsp/node_pverb.rs @@ -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}; diff --git a/src/dsp/node_quant.rs b/src/dsp/node_quant.rs index d18ae51..6955fd6 100644 --- a/src/dsp/node_quant.rs +++ b/src/dsp/node_quant.rs @@ -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}; diff --git a/src/dsp/node_rndwk.rs b/src/dsp/node_rndwk.rs index 748cc42..3996944 100644 --- a/src/dsp/node_rndwk.rs +++ b/src/dsp/node_rndwk.rs @@ -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}; diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index 176d25c..0e7e6ae 100644 --- a/src/dsp/node_sampl.rs +++ b/src/dsp/node_sampl.rs @@ -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; diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs new file mode 100644 index 0000000..76a6f46 --- /dev/null +++ b/src/dsp/node_scope.rs @@ -0,0 +1,230 @@ +// Copyright (c) 2022 Weird Constructor +// 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, + 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) { + 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( + &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), + ); + } +} diff --git a/src/dsp/node_sfilter.rs b/src/dsp/node_sfilter.rs index 20c3f08..ad6f519 100644 --- a/src/dsp/node_sfilter.rs +++ b/src/dsp/node_sfilter.rs @@ -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, diff --git a/src/dsp/node_sin.rs b/src/dsp/node_sin.rs index 9fe362a..05afe05 100644 --- a/src/dsp/node_sin.rs +++ b/src/dsp/node_sin.rs @@ -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, }; diff --git a/src/dsp/node_test.rs b/src/dsp/node_test.rs index 5b36563..3eb0b32 100644 --- a/src/dsp/node_test.rs +++ b/src/dsp/node_test.rs @@ -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, }; diff --git a/src/dsp/node_tseq.rs b/src/dsp/node_tseq.rs index 215cee1..3397568 100644 --- a/src/dsp/node_tseq.rs +++ b/src/dsp/node_tseq.rs @@ -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}; diff --git a/src/dsp/node_tslfo.rs b/src/dsp/node_tslfo.rs index eff09d5..00355d6 100644 --- a/src/dsp/node_tslfo.rs +++ b/src/dsp/node_tslfo.rs @@ -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, }; diff --git a/src/dsp/node_vosc.rs b/src/dsp/node_vosc.rs index 1f04bdc..a35a371 100644 --- a/src/dsp/node_vosc.rs +++ b/src/dsp/node_vosc.rs @@ -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, }; diff --git a/src/dsp/tracker/sequencer.rs b/src/dsp/tracker/sequencer.rs index 194ffa1..6ea6a7e 100644 --- a/src/dsp/tracker/sequencer.rs +++ b/src/dsp/tracker/sequencer.rs @@ -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, diff --git a/src/lib.rs b/src/lib.rs index 5ea9202..f944009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,15 @@ -// Copyright (c) 2021 Weird Constructor +// Copyright (c) 2021-2022 Weird Constructor // 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, diff --git a/src/matrix.rs b/src/matrix.rs index cea1c89..ee2fb50 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Weird Constructor +// Copyright (c) 2021-2022 Weird Constructor // 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, i2: Option, i3: Option) -> 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)> { 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 { + 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>> { 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> { + 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>> { + 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> = 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> { + 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(&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] diff --git a/src/matrix_repr.rs b/src/matrix_repr.rs index 93866d5..3d34aa3 100644 --- a/src/matrix_repr.rs +++ b/src/matrix_repr.rs @@ -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>, pub properties: Vec<(String, SAtom)>, + pub block_funs: Vec>, 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() } } diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index cd450c5..9888f83 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -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) } diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index ba9f313..422e8be 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -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, /// Holding the tracker sequencers pub(crate) trackers: Vec, + /// Holding the scope buffers: + pub(crate) scopes: Vec>, + /// Holding the WBlockDSP code engine backends: + #[cfg(feature = "synfx-dsp-jit")] + pub(crate) code_engines: Vec, + /// 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>)>, /// 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> { + 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>> { 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>> { + #[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); diff --git a/src/nodes/node_exec.rs b/src/nodes/node_exec.rs index 699d026..db374c9 100644 --- a/src/nodes/node_exec.rs +++ b/src/nodes/node_exec.rs @@ -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; diff --git a/src/scope_handle.rs b/src/scope_handle.rs new file mode 100644 index 0000000..5af06c8 --- /dev/null +++ b/src/scope_handle.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2022 Weird Constructor +// 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; 3], + active: [AtomicBool; 3], + offs_gain: [AtomicFloatPair; 3], + threshold: (AtomicBool, AtomicFloat), +} + +impl ScopeHandle { + pub fn new_shared() -> Arc { + 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) { + 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 { + 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 + } +} diff --git a/src/util.rs b/src/util.rs index 4ae966d..64594ce 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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 for AtomicFloat { - fn from(value: f32) -> Self { - AtomicFloat::new(value) - } -} - -impl From for f32 { - fn from(value: AtomicFloat) -> Self { - value.get() - } -} diff --git a/src/wblockdsp/compiler.rs b/src/wblockdsp/compiler.rs new file mode 100644 index 0000000..2492d60 --- /dev/null +++ b/src/wblockdsp/compiler.rs @@ -0,0 +1,776 @@ +// Copyright (c) 2022 Weird Constructor +// 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>); + +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 { + 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; + +#[derive(Debug, Clone)] +enum BlkASTNode { + Area { + childs: Vec, + }, + Set { + var: String, + expr: BlkASTRef, + }, + Get { + id: usize, + var: String, + }, + Node { + id: usize, + out: Option, + typ: String, + lbl: String, + childs: Vec<(Option, 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 { + 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 { + if let Ok(value) = val.parse::() { + Ok(Rc::new(BlkASTNode::Literal { value })) + } else { + Err(BlkJITCompileError::BadLiteralNumber(val.to_string())) + } + } + + pub fn new_node( + id: usize, + out: Option, + typ: &str, + lbl: &str, + childs: Vec<(Option, 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, + lang: Rc>, + tmpvar_counter: usize, +} + +#[cfg(not(feature = "synfx-dsp-jit"))] +pub enum ASTNode { + NoSynfxDSPJit +} + +impl Block2JITCompiler { + pub fn new(lang: Rc>) -> 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, + ) -> Result { + 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[..] { + "" => { + 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. + "" => { + 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" | "" => { + 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[..] == "" { + 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, 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, BlkJITCompileError> { + #[cfg(feature = "synfx-dsp-jit")] + { + let tree = fun.generate_tree::("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( + mut f: F, + ) -> (Rc>, Box) { + 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(); + } +} diff --git a/src/wblockdsp/definition.rs b/src/wblockdsp/definition.rs new file mode 100644 index 0000000..f73a00a --- /dev/null +++ b/src/wblockdsp/definition.rs @@ -0,0 +1,257 @@ +// Copyright (c) 2022 Weird Constructor +// 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>, +) -> Rc> { + 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: "2π".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)) +} diff --git a/src/wblockdsp/language.rs b/src/wblockdsp/language.rs new file mode 100644 index 0000000..17dc369 --- /dev/null +++ b/src/wblockdsp/language.rs @@ -0,0 +1,1855 @@ +// Copyright (c) 2022 Weird Constructor +// 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::rc::Rc; + +use std::collections::HashMap; +use std::collections::HashSet; +use std::collections::VecDeque; + +use serde_json::{json, Value}; + +pub trait BlockView { + fn rows(&self) -> usize; + fn contains(&self, idx: usize) -> Option; + fn expanded(&self) -> bool; + fn label(&self, buf: &mut [u8]) -> usize; + fn has_input(&self, idx: usize) -> bool; + fn has_output(&self, idx: usize) -> bool; + fn input_label(&self, idx: usize, buf: &mut [u8]) -> usize; + fn output_label(&self, idx: usize, buf: &mut [u8]) -> usize; + fn custom_color(&self) -> Option; +} + +pub trait BlockCodeView { + fn area_header(&self, id: usize) -> Option<&str>; + fn area_size(&self, id: usize) -> (usize, usize); + fn block_at(&self, id: usize, x: i64, y: i64) -> Option<&dyn BlockView>; + fn origin_at(&self, id: usize, x: i64, y: i64) -> Option<(i64, i64)>; + fn generation(&self) -> u64; +} + +#[derive(Debug, Clone)] +pub struct BlockIDGenerator { + counter: Rc>, +} + +impl BlockIDGenerator { + pub fn new() -> Self { + Self { counter: Rc::new(RefCell::new(0)) } + } + + pub fn new_with_id(id: usize) -> Self { + Self { counter: Rc::new(RefCell::new(id)) } + } + + pub fn current(&self) -> usize { + *self.counter.borrow_mut() + } + + pub fn next(&self) -> usize { + let mut c = self.counter.borrow_mut(); + *c += 1; + *c + } +} + +/// This structure represents a block inside the [BlockArea] of a [BlockFun]. +/// It stores everything required for calculating a node of the AST. +/// +/// A [BlockType::instanciate_block] is used to create a new instance of this +/// structure. +/// +/// You usually don't use this structure directly, but you use the +/// position of it inside the [BlockFun]. The position of a block +/// is specified by the `area_id`, and the `x` and `y` coordinates. +#[derive(Debug, Clone)] +pub struct Block { + /// An ID to track this block. + id: usize, + /// How many rows this block spans. A [Block] can only be 1 cell wide. + rows: usize, + /// Up to two sub [BlockArea] can be specified here by their ID. + contains: (Option, Option), + /// Whether the sub areas are visible/drawn. + expanded: bool, + /// The type of this block. It's just a string set by the [BlockType] + /// and it should be everything that determines what this block is + /// going to end up as in the AST. + typ: String, + /// The label of the block. + lbl: String, + /// The input ports, the index into the [Vec] is the row. The [String] + /// is the label of the input port. + inputs: Vec>, + /// The output ports, the index into the [Vec] is the row. The [String] + /// is the label of the output port. + outputs: Vec>, + /// The color index of this block. + color: usize, +} + +impl Block { + pub fn clone_with_new_id(&self, new_id: usize) -> Self { + Self { + id: new_id, + rows: self.rows, + contains: self.contains.clone(), + expanded: self.expanded, + typ: self.typ.clone(), + lbl: self.lbl.clone(), + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + color: self.color, + } + } + + /// Takes the (input) port at row `idx` and pushed it one row further + /// down, wrapping around at the end. If `output` is true, the + /// output port at `idx` is shifted. + pub fn shift_port(&mut self, idx: usize, output: bool) { + if self.rows <= 1 { + return; + } + + let v = if output { &mut self.outputs } else { &mut self.inputs }; + + if v.len() < self.rows { + v.resize(self.rows, None); + } + + let idx_from = idx; + let idx_to = (idx + 1) % v.len(); + let elem = v.remove(idx_from); + v.insert(idx_to, elem); + } + + /// Calls `f` for every output port that is available. + /// `f` gets passed the row index. + pub fn for_output_ports(&self, mut f: F) { + for i in 0..self.rows { + if let Some(p) = self.outputs.get(i) { + if let Some(p) = p { + f(i, p); + } + } + } + } + + /// Returns the number of output ports of this [Block]. + pub fn count_outputs(&self) -> usize { + let mut count = 0; + + for i in 0..self.rows { + if let Some(o) = self.outputs.get(i) { + if o.is_some() { + count += 1; + } + } + } + + count + } + + /// Calls `f` for every input port that is available. + /// `f` gets passed the row index. + pub fn for_input_ports(&self, mut f: F) { + for i in 0..self.rows { + if let Some(p) = self.inputs.get(i) { + if let Some(p) = p { + f(i, p); + } + } + } + } + + /// Calls `f` for every input port that is available. + /// `f` gets passed the row index. + pub fn for_input_ports_reverse(&self, mut f: F) { + for i in 1..=self.rows { + let i = self.rows - i; + if let Some(p) = self.inputs.get(i) { + if let Some(p) = p { + f(i, p); + } + } + } + } + + /// Serializes this [Block] into a [Value]. Called by [BlockArea::serialize]. + pub fn serialize(&self) -> Value { + let mut inputs = json!([]); + let mut outputs = json!([]); + + if let Value::Array(inputs) = &mut inputs { + for p in self.inputs.iter() { + inputs.push(json!(p)); + } + } + + if let Value::Array(outputs) = &mut outputs { + for p in self.outputs.iter() { + outputs.push(json!(p)); + } + } + + let c0 = if let Some(c) = self.contains.0 { c.into() } else { Value::Null }; + let c1 = if let Some(c) = self.contains.1 { c.into() } else { Value::Null }; + json!({ + "id": self.id as i64, + "rows": self.rows as i64, + "contains": json!([c0, c1]), + "expanded": self.expanded, + "typ": self.typ, + "lbl": self.lbl, + "color": self.color, + "inputs": inputs, + "outputs": outputs, + + }) + } + + /// Deserializes this [Block] from a [Value]. Called by [BlockArea::deserialize]. + pub fn deserialize(v: &Value) -> Result, serde_json::Error> { + let mut inputs = vec![]; + let mut outputs = vec![]; + + let inps = &v["inputs"]; + if let Value::Array(inps) = inps { + for v in inps.iter() { + inputs.push(if v.is_string() { + Some(v.as_str().unwrap_or("").to_string()) + } else { + None + }) + } + } + + let outs = &v["outputs"]; + if let Value::Array(outs) = outs { + for v in outs.iter() { + outputs.push(if v.is_string() { + Some(v.as_str().unwrap_or("").to_string()) + } else { + None + }) + } + } + + Ok(Box::new(Block { + id: v["id"].as_i64().unwrap_or(0) as usize, + rows: v["rows"].as_i64().unwrap_or(0) as usize, + contains: ( + if v["contains"][0].is_i64() { + Some(v["contains"][0].as_i64().unwrap_or(0) as usize) + } else { + None + }, + if v["contains"][1].is_i64() { + Some(v["contains"][1].as_i64().unwrap_or(0) as usize) + } else { + None + }, + ), + expanded: v["expanded"].as_bool().unwrap_or(true), + typ: v["typ"].as_str().unwrap_or("?").to_string(), + lbl: v["lbl"].as_str().unwrap_or("?").to_string(), + inputs, + outputs, + color: v["color"].as_i64().unwrap_or(0) as usize, + })) + } +} + +impl BlockView for Block { + fn rows(&self) -> usize { + self.rows + } + fn contains(&self, idx: usize) -> Option { + if idx == 0 { + self.contains.0 + } else { + self.contains.1 + } + } + fn expanded(&self) -> bool { + true + } + fn label(&self, buf: &mut [u8]) -> usize { + use std::io::Write; + let mut bw = std::io::BufWriter::new(buf); + match write!(bw, "{}", self.lbl) { + Ok(_) => bw.buffer().len(), + _ => 0, + } + } + fn has_input(&self, idx: usize) -> bool { + self.inputs.get(idx).map(|s| s.is_some()).unwrap_or(false) + } + fn has_output(&self, idx: usize) -> bool { + self.outputs.get(idx).map(|s| s.is_some()).unwrap_or(false) + } + fn input_label(&self, idx: usize, buf: &mut [u8]) -> usize { + use std::io::Write; + if let Some(lbl_opt) = self.inputs.get(idx) { + if let Some(lbl) = lbl_opt { + let mut bw = std::io::BufWriter::new(buf); + match write!(bw, "{}", lbl) { + Ok(_) => bw.buffer().len(), + _ => 0, + } + } else { + 0 + } + } else { + 0 + } + } + fn output_label(&self, idx: usize, buf: &mut [u8]) -> usize { + use std::io::Write; + if let Some(lbl_opt) = self.outputs.get(idx) { + if let Some(lbl) = lbl_opt { + let mut bw = std::io::BufWriter::new(buf); + match write!(bw, "{}", lbl) { + Ok(_) => bw.buffer().len(), + _ => 0, + } + } else { + 0 + } + } else { + 0 + } + } + fn custom_color(&self) -> Option { + Some(self.color) + } +} + +/// Represents a connected collection of blocks. Is created by +/// [BlockFun::retrieve_block_chain_at] or [BlockArea::chain_at]. +/// +/// After creating a [BlockChain] structure you can decide to +/// clone the blocks from the [BlockArea] with [BlockChain::clone_load] +/// or remove the blocks from the [BlockArea] and store them +/// inside this [BlockChain] via [BlockChain::remove_load]. +/// +/// The original positions of the _loaded_ blocks is stored too. +/// If you want to move the whole chain in the coordinate system +/// to the upper left most corner, you can use [BlockChain::normalize_load_pos]. +#[derive(Debug)] +pub struct BlockChain { + /// The area ID this BlockChain was created from. + #[allow(dead_code)] + area_id: usize, + /// Stores the positions of the blocks of the chain inside the [BlockArea]. + blocks: HashSet<(i64, i64)>, + /// Stores the positions of blocks that only have output ports. + #[allow(dead_code)] + sources: HashSet<(i64, i64)>, + /// Stores the positions of blocks that only have input ports. + #[allow(dead_code)] + sinks: HashSet<(i64, i64)>, + /// This field stores _loaded_ blocks from the [BlockArea] + /// into this [BlockChain] for inserting or analyzing them. + /// + /// Stores the blocks themself, with their position in the [BlockArea], + /// which can be normalized (moved to the upper left) with + /// [BlockChain::normalize_load_pos]. + /// + /// The blocks in this [Vec] are stored in sorted order. + /// They are stored in ascending order of their `x` coordinate, + /// and for the same `x` coordinate in + /// ascending order of their `y` coordinate. + load: Vec<(Box, i64, i64)>, +} + +impl BlockChain { + pub fn move_by_offs(&mut self, xo: i64, yo: i64) { + for (_, x, y) in &mut self.load { + *x += xo; + *y += yo; + //d// println!("MOVE_BY_OFFS TO x={:3} y={:3}", *x, *y); + } + } + + /// Normalizes the position of all loaded blocks and returns + /// the original top left most position of the chain. + pub fn normalize_load_pos(&mut self) -> (i64, i64) { + let mut min_x = 100000000; + let mut min_y = 100000000; + + for (_, xo, yo) in &self.load { + min_x = min_x.min(*xo); + min_y = min_y.min(*yo); + } + + for (_, xo, yo) in &mut self.load { + *xo -= min_x; + *yo -= min_y; + } + + self.sort_load_pos(); + + (min_x, min_y) + } + + fn sort_load_pos(&mut self) { + self.load.sort_by(|&(_, x0, y0), &(_, x1, y1)| x0.cmp(&x1).then(y0.cmp(&y1))); + } + + pub fn get_connected_inputs_from_load_at_x(&self, x_split: i64) -> Vec<(i64, i64)> { + let mut output_points = vec![]; + for (block, x, y) in &self.load { + if *x == x_split { + block.for_output_ports(|row, _| { + output_points.push(y + (row as i64)); + }); + } + } + + let mut connection_pos = vec![]; + + for (block, x, y) in &self.load { + if *x == (x_split + 1) { + block.for_input_ports(|row, _| { + if output_points.iter().find(|&&out_y| out_y == (y + (row as i64))).is_some() { + connection_pos.push((*x, y + (row as i64))); + } + }); + } + } + + connection_pos + } + + // pub fn join_load_after_x(&mut self, x_join: i64, y_split: i64) -> bool { + // let filler_pos : Vec<(i64, i64)> = + // self.get_connected_inputs_from_load_at_x(x_split); + // if filler_pos.len() > 1 + // || (filler_pos.len() == 1 && filler_pos[0] != (x_join, y_split) + // } + + pub fn split_load_after_x( + &mut self, + x_split: i64, + y_split: i64, + filler: Option<&BlockType>, + id_gen: BlockIDGenerator, + ) { + let filler_pos: Vec<(i64, i64)> = self.get_connected_inputs_from_load_at_x(x_split); + + for (_block, x, _y) in &mut self.load { + if *x > x_split { + *x += 1; + } + } + + if let Some(filler) = filler { + for (x, y) in filler_pos { + if y == y_split { + continue; + } + let filler_block = filler.instanciate_block(None, id_gen.clone()); + + self.load.push((filler_block, x, y)); + } + } + + self.sort_load_pos(); + } + + pub fn clone_load(&mut self, area: &mut BlockArea, id_gen: BlockIDGenerator) { + self.load.clear(); + + for b in &self.blocks { + if let Some((block, xo, yo)) = area.ref_at_origin(b.0, b.1) { + self.load.push((Box::new(block.clone_with_new_id(id_gen.next())), xo, yo)); + } + } + + self.sort_load_pos(); + } + + pub fn remove_load(&mut self, area: &mut BlockArea) { + self.load.clear(); + + for b in &self.blocks { + if let Some((block, xo, yo)) = area.remove_at(b.0, b.1) { + self.load.push((block, xo, yo)); + } + } + + self.sort_load_pos(); + } + + pub fn place_load(&mut self, area: &mut BlockArea) { + let load = std::mem::replace(&mut self.load, vec![]); + area.set_blocks_from(load); + } + + pub fn try_fit_load_into_space(&mut self, area: &mut BlockArea) -> bool { + for (xo, yo) in &[ + (0, 0), // where it currently is + (0, -1), + (0, -2), + (0, -3), + (-1, 0), + (-1, -1), + (-1, -2), + (-1, -3), + (1, 0), + (1, -1), + (1, -2), + (1, -3), + (0, 1), + (0, 2), + (0, 3), + (-1, 1), + (-1, 2), + (-1, 3), + (1, 1), + (1, 2), + (1, 3), + ] { + println!("TRY {},{}", *xo, *yo); + if self.area_has_space_for_load(area, *xo, *yo) { + self.move_by_offs(*xo, *yo); + return true; + } + + //d// println!("RETRY xo={}, yo={}", *xo, *yo); + } + + return false; + } + + pub fn area_has_space_for_load( + &mut self, + area: &mut BlockArea, + xoffs: i64, + yoffs: i64, + ) -> bool { + for (block, x, y) in self.load.iter() { + if !area.check_space_at(*x + xoffs, *y + yoffs, block.rows) { + return false; + } + } + + true + } + + pub fn area_is_subarea_of_loaded(&mut self, area: usize, fun: &mut BlockFun) -> bool { + let mut areas = vec![]; + + for (block, _, _) in self.load.iter() { + fun.all_sub_areas_of(block.as_ref(), &mut areas); + } + + for a_id in areas.iter() { + if *a_id == area { + return true; + } + } + + return false; + } +} + +#[derive(Debug, Clone)] +pub struct BlockArea { + blocks: HashMap<(i64, i64), Box>, + origin_map: HashMap<(i64, i64), (i64, i64)>, + size: (usize, usize), + auto_shrink: bool, + header: String, +} + +impl BlockArea { + fn new(w: usize, h: usize) -> Self { + Self { + blocks: HashMap::new(), + origin_map: HashMap::new(), + size: (w, h), + auto_shrink: false, + header: "".to_string(), + } + } + + pub fn set_header(&mut self, header: String) { + self.header = header; + } + + pub fn set_auto_shrink(&mut self, shrink: bool) { + self.auto_shrink = shrink; + } + + pub fn auto_shrink(&self) -> bool { + self.auto_shrink + } + + pub fn chain_at(&self, x: i64, y: i64) -> Option> { + let (_block, xo, yo) = self.ref_at_origin(x, y)?; + + let mut dq: VecDeque<(i64, i64)> = VecDeque::new(); + dq.push_back((xo, yo)); + + let mut blocks: HashSet<(i64, i64)> = HashSet::new(); + let mut sources: HashSet<(i64, i64)> = HashSet::new(); + let mut sinks: HashSet<(i64, i64)> = HashSet::new(); + + let mut check_port_conns = vec![]; + + while let Some((x, y)) = dq.pop_front() { + check_port_conns.clear(); + + // First we find all adjacent output/input port positions + // and collect them in `check_port_conns`. + // + // While are at it, we also record which blocks are only + // sinks and which are only sources. Might be important for + // other algorithms that do things with this. + if let Some((block, xo, yo)) = self.ref_at_origin(x, y) { + if blocks.contains(&(xo, yo)) { + continue; + } + + blocks.insert((xo, yo)); + + let mut has_input = false; + let mut has_output = false; + + block.for_input_ports(|idx, _| { + check_port_conns.push((xo - 1, yo + (idx as i64), true)); + has_input = true; + }); + + block.for_output_ports(|idx, _| { + check_port_conns.push((xo + 1, yo + (idx as i64), false)); + has_output = true; + }); + + if !has_input { + sources.insert((xo, yo)); + } + + if !has_output { + sinks.insert((xo, yo)); + } + } + + // Then we look if there is a block at that position, with + // a corresponding input or output port at the right + // row inside the block. + for (x, y, is_output) in &check_port_conns { + if let Some((_block, xo, yo, _row)) = self.find_port_at(*x, *y, *is_output) { + dq.push_back((xo, yo)); + } + } + } + + Some(Box::new(BlockChain { area_id: 0, blocks, sources, sinks, load: vec![] })) + } + + pub fn find_last_unconnected_output(&self) -> Option<(i64, i64, String)> { + let mut max_x = 0; + let mut max_y = 0; + let mut port: Option<(i64, i64, String)> = None; + + for ((x, y), block) in &self.blocks { + let (x, y) = (*x, *y); + + block.for_output_ports(|row, _| { + let y = y + (row as i64); + + if self.find_port_at(x + 1, y, false).is_none() { + if y > max_y { + max_y = y; + max_x = x; + + port = Some(( + max_x, + max_y, + block + .outputs + .get(row) + .cloned() + .flatten() + .unwrap_or_else(|| "".to_string()), + )); + } else if y == max_y && x > max_x { + max_x = x; + + port = Some(( + max_x, + max_y, + block + .outputs + .get(row) + .cloned() + .flatten() + .unwrap_or_else(|| "".to_string()), + )); + } + } + }) + } + + port + } + + /// Collects the sinks in this area. + /// It returns a list of [Block] positions inside the + /// area. For unconnected outputs, which are also evaluated + /// and returned as possible last value of an [BlockArea], + /// the output row is also given. + /// + /// The result is sorted so, that the bottom right most element + /// is the first one in the result list. + pub fn collect_sinks(&self) -> Vec<(i64, i64, Option)> { + let mut sinks_out = vec![]; + + for ((x, y), block) in &self.blocks { + if block.count_outputs() == 0 { + sinks_out.push((*x, *y, None)); + } else { + block.for_output_ports(|row, _| { + if self.find_port_at(*x + 1, *y + (row as i64), false).is_none() { + sinks_out.push((*x, *y + (row as i64), Some(row))); + } + }); + } + } + + sinks_out.sort_by(|&(x0, y0, _), &(x1, y1, _)| y1.cmp(&y0).then(x1.cmp(&x0))); + + sinks_out + } + + fn ref_at(&self, x: i64, y: i64) -> Option<&Block> { + let (xo, yo) = self.origin_map.get(&(x, y))?; + self.blocks.get(&(*xo, *yo)).map(|b| b.as_ref()) + } + + fn ref_at_origin(&self, x: i64, y: i64) -> Option<(&Block, i64, i64)> { + let (xo, yo) = self.origin_map.get(&(x, y))?; + let (xo, yo) = (*xo, *yo); + self.blocks.get(&(xo, yo)).map(|b| (b.as_ref(), xo, yo)) + } + + fn ref_mut_at(&mut self, x: i64, y: i64) -> Option<&mut Block> { + let (xo, yo) = self.origin_map.get(&(x, y))?; + self.blocks.get_mut(&(*xo, *yo)).map(|b| b.as_mut()) + } + + fn ref_mut_at_origin(&mut self, x: i64, y: i64) -> Option<(&mut Block, i64, i64)> { + let (xo, yo) = self.origin_map.get(&(x, y))?; + let (xo, yo) = (*xo, *yo); + self.blocks.get_mut(&(xo, yo)).map(|b| (b.as_mut(), xo, yo)) + } + + fn find_port_at( + &self, + x: i64, + y: i64, + expect_output: bool, + ) -> Option<(&Block, i64, i64, usize)> { + let (block, xo, yo) = self.ref_at_origin(x, y)?; + + let port_y = (y - yo).max(0) as usize; + + if expect_output { + if let Some(o) = block.outputs.get(port_y) { + if o.is_some() { + return Some((block, xo, yo, port_y)); + } + } + } else { + if let Some(i) = block.inputs.get(port_y) { + if i.is_some() { + return Some((block, xo, yo, port_y)); + } + } + } + + None + } + + fn set_blocks_from(&mut self, list: Vec<(Box, i64, i64)>) { + for (block, x, y) in list.into_iter() { + self.blocks.insert((x, y), block); + } + + self.update_origin_map(); + } + + fn set_block_at(&mut self, x: i64, y: i64, block: Box) { + self.blocks.insert((x, y), block); + self.update_origin_map(); + } + + fn remove_at(&mut self, x: i64, y: i64) -> Option<(Box, i64, i64)> { + let (xo, yo) = self.origin_map.get(&(x, y))?; + if let Some(block) = self.blocks.remove(&(*xo, *yo)) { + let (xo, yo) = (*xo, *yo); + self.update_origin_map(); + Some((block, xo, yo)) + } else { + None + } + } + + fn set_size(&mut self, w: usize, h: usize) { + self.size = (w, h); + } + + fn get_direct_sub_areas(&self, out: &mut Vec) { + for ((_x, _y), block) in &self.blocks { + if let Some(sub_area) = block.contains.0 { + out.push(sub_area); + } + + if let Some(sub_area) = block.contains.1 { + out.push(sub_area); + } + } + } + + /// Calculates only the size of the area in the +x/+y quadrant. + /// The negative areas are not counted in. + fn resolve_size (usize, usize)>(&self, resolve_sub_areas: F) -> (usize, usize) { + let mut min_w = 1; + let mut min_h = 1; + + for ((ox, oy), _) in &self.origin_map { + let (ox, oy) = ((*ox).max(0) as usize, (*oy).max(0) as usize); + + if min_w < (ox + 1) { + min_w = ox + 1; + } + if min_h < (oy + 1) { + min_h = oy + 1; + } + } + + for ((x, y), block) in &self.blocks { + let (x, y) = ((*x).max(0) as usize, (*y).max(0) as usize); + + let mut prev_h = 1; // one for the top block + + if let Some(sub_area) = block.contains.0 { + let (sub_w, mut sub_h) = resolve_sub_areas(sub_area); + sub_h += prev_h; + prev_h += sub_h; + if min_w < (x + sub_w + 1) { + min_w = x + sub_w + 1; + } + if min_h < (y + sub_h + 1) { + min_h = y + sub_h + 1; + } + } + + if let Some(sub_area) = block.contains.1 { + let (sub_w, mut sub_h) = resolve_sub_areas(sub_area); + sub_h += prev_h; + if min_w < (x + sub_w + 1) { + min_w = x + sub_w + 1; + } + if min_h < (y + sub_h + 1) { + min_h = y + sub_h + 1; + } + } + } + + if self.auto_shrink { + (min_w, min_h) + } else { + ( + if self.size.0 < min_w { min_w } else { self.size.0 }, + if self.size.1 < min_h { min_h } else { self.size.1 }, + ) + } + } + + fn update_origin_map(&mut self) { + self.origin_map.clear(); + + for ((ox, oy), block) in &self.blocks { + for r in 0..block.rows { + self.origin_map.insert((*ox, *oy + (r as i64)), (*ox, *oy)); + } + } + } + + fn check_space_at(&self, x: i64, y: i64, rows: usize) -> bool { + for i in 0..rows { + let yo = y + (i as i64); + + if self.origin_map.get(&(x, yo)).is_some() { + return false; + } + } + + true + } + + /// Serializes this [BlockArea] to a JSON [Value]. + /// Usually called by [BlockFunSnapshot::serialize]. + pub fn serialize(&self) -> Value { + let mut v = json!({ + "size": [self.size.0 as i64, self.size.1 as i64], + "header": self.header, + "auto_shrink": self.auto_shrink, + }); + + let mut blks = json!([]); + if let Value::Array(blks) = &mut blks { + for ((x, y), b) in self.blocks.iter() { + blks.push(json!({ + "x": x, + "y": y, + "block": b.serialize(), + })); + } + } + + v["blocks"] = blks; + + v + } + + /// Deserializes a from a JSON [Value]. + /// Usually called by [BlockFunSnapshot::deserialize]. + pub fn deserialize(v: &Value) -> Result, serde_json::Error> { + let mut blocks = HashMap::new(); + + let blks = &v["blocks"]; + if let Value::Array(blks) = blks { + for b in blks.iter() { + let x = b["x"].as_i64().unwrap_or(0); + let y = b["y"].as_i64().unwrap_or(0); + blocks.insert((x, y), Block::deserialize(&b["block"])?); + } + } + + let size = ( + v["size"][0].as_i64().unwrap_or(0) as usize, + v["size"][1].as_i64().unwrap_or(0) as usize, + ); + let auto_shrink = v["auto_shrink"].as_bool().unwrap_or(true); + let header = v["header"].as_str().unwrap_or("").to_string(); + + let mut ba = + Box::new(BlockArea { blocks, origin_map: HashMap::new(), size, auto_shrink, header }); + + ba.update_origin_map(); + + Ok(ba) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BlockUserInput { + None, + Float, + Integer, + Identifier, + ClientDecision, +} + +impl Default for BlockUserInput { + fn default() -> Self { + Self::None + } +} + +impl BlockUserInput { + pub fn needs_input(&self) -> bool { + *self != BlockUserInput::None + } +} + +#[derive(Debug, Clone, Default)] +pub struct BlockType { + pub category: String, + pub name: String, + pub rows: usize, + pub inputs: Vec>, + pub outputs: Vec>, + pub area_count: usize, + pub user_input: BlockUserInput, + pub description: String, + pub color: usize, +} + +impl BlockType { + fn touch_contains(&self, block: &mut Block) { + block.contains = match self.area_count { + 0 => (None, None), + 1 => (Some(1), None), + 2 => (Some(1), Some(1)), + _ => (None, None), + }; + } + + pub fn instanciate_block( + &self, + user_input: Option, + id_gen: BlockIDGenerator, + ) -> Box { + let mut block = Box::new(Block { + id: id_gen.next(), + rows: self.rows, + contains: (None, None), + expanded: true, + typ: self.name.clone(), + lbl: if let Some(inp) = user_input { inp } else { self.name.clone() }, + inputs: self.inputs.clone(), + outputs: self.outputs.clone(), + color: self.color, + }); + self.touch_contains(&mut *block); + block + } +} + +#[derive(Debug, Clone)] +pub struct BlockLanguage { + types: HashMap, + identifiers: HashMap, +} + +impl BlockLanguage { + pub fn new() -> Self { + Self { types: HashMap::new(), identifiers: HashMap::new() } + } + + pub fn define_identifier(&mut self, id: &str) { + let v = id.to_string(); + self.identifiers.insert(id.to_string(), v); + } + + pub fn define(&mut self, typ: BlockType) { + self.types.insert(typ.name.clone(), typ); + } + + pub fn is_identifier(&self, id: &str) -> bool { + self.identifiers.get(id).is_some() + } + + pub fn list_identifiers(&self) -> Vec { + let mut identifiers: Vec = self.identifiers.keys().cloned().collect(); + identifiers.sort(); + identifiers + } + + pub fn get_type_outputs(&self, typ: &str) -> Option<&[Option]> { + let typ = self.types.get(typ)?; + Some(&typ.outputs) + } + + pub fn get_type_inputs(&self, typ: &str) -> Option<&[Option]> { + let typ = self.types.get(typ)?; + Some(&typ.inputs) + } + + pub fn get_output_name_at_index(&self, typ: &str, idx: usize) -> Option { + if let Some(outs) = self.get_type_outputs(typ) { + let mut i = 0; + for o in outs.iter() { + if let Some(outname) = o { + if i == idx { + return Some(outname.to_string()); + } + i += 1; + } + } + } + + None + } + + pub fn type_output_count(&self, typ: &str) -> usize { + let mut cnt = 0; + + if let Some(outs) = self.get_type_outputs(typ) { + for o in outs.iter() { + if o.is_some() { + cnt += 1; + } + } + } + + cnt + } + + pub fn get_type_list(&self) -> Vec<(String, String, BlockUserInput)> { + let mut out = vec![]; + for (_, typ) in &self.types { + out.push((typ.category.clone(), typ.name.clone(), typ.user_input)); + } + out + } +} + +pub trait BlockASTNode: std::fmt::Debug + Clone { + fn from(id: usize, typ: &str, lbl: &str) -> Self; + fn add_node(&self, in_port: String, out_port: String, node: Self); + fn add_structural_node(&self, node: Self) { + self.add_node("".to_string(), "".to_string(), node); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BlockDSPError { + UnknownArea(usize), + UnknownLanguageType(String), + NoBlockAt(usize, i64, i64), + CircularAction(usize, usize), + NoSpaceAvailable(usize, i64, i64, usize), +} + +#[derive(Debug, Clone)] +pub struct BlockFunSnapshot { + areas: Vec>, + cur_id: usize, +} + +impl BlockFunSnapshot { + pub fn serialize(&self) -> Value { + let mut v = json!({ + "VERSION": 1, + }); + + v["current_block_id_counter"] = self.cur_id.into(); + + let mut areas = json!([]); + if let Value::Array(areas) = &mut areas { + for area in self.areas.iter() { + areas.push(area.serialize()); + } + } + + v["areas"] = areas; + + v + } + + pub fn deserialize(v: &Value) -> Result { + let mut a = vec![]; + + let areas = &v["areas"]; + if let Value::Array(areas) = areas { + for v in areas.iter() { + a.push(BlockArea::deserialize(v)?); + } + } + + Ok(BlockFunSnapshot { + areas: a, + cur_id: v["current_block_id_counter"].as_i64().unwrap_or(0) as usize, + }) + } +} + +#[derive(Debug, Clone)] +pub struct BlockFun { + language: Rc>, + areas: Vec>, + size_work_dq: VecDeque, + area_work_dq: VecDeque, + id_gen: BlockIDGenerator, + generation: u64, +} + +#[derive(Debug)] +enum GenTreeJob { + Node { node: N, out: N }, + Output { area_id: usize, x: i64, y: i64, in_port: String, out: N }, + Sink { area_id: usize, x: i64, y: i64, out: N }, + Area { area_id: usize, out: N }, +} + +impl BlockFun { + pub fn new(lang: Rc>) -> Self { + Self { + language: lang, + areas: vec![Box::new(BlockArea::new(16, 16))], + size_work_dq: VecDeque::new(), + area_work_dq: VecDeque::new(), + id_gen: BlockIDGenerator::new(), + generation: 0, + } + } + + pub fn is_unset(&self) -> bool { + self.generation == 0 + } + + pub fn block_language(&self) -> Rc> { + self.language.clone() + } + + pub fn block_ref(&self, id: usize, x: i64, y: i64) -> Option<&Block> { + let area = self.areas.get(id)?; + area.ref_at(x, y) + } + + pub fn block_ref_mut(&mut self, id: usize, x: i64, y: i64) -> Option<&mut Block> { + let area = self.areas.get_mut(id)?; + area.ref_mut_at(x, y) + } + + pub fn shift_port(&mut self, id: usize, x: i64, y: i64, row: usize, output: bool) { + if let Some(block) = self.block_ref_mut(id, x, y) { + block.shift_port(row, output); + self.generation += 1; + } + } + + pub fn save_snapshot(&self) -> BlockFunSnapshot { + BlockFunSnapshot { + areas: self.areas.iter().cloned().collect(), + cur_id: self.id_gen.current(), + } + } + + pub fn load_snapshot(&mut self, repr: &BlockFunSnapshot) { + self.areas = repr.areas.iter().cloned().collect(); + self.id_gen = BlockIDGenerator::new_with_id(repr.cur_id); + self.recalculate_area_sizes(); + self.generation += 1; + } + + pub fn generate_tree(&self, null_typ: &str) -> Result { + // This is a type for filling in unfilled outputs: + let lang = self.language.borrow(); + let null_typ = lang + .types + .get(null_typ) + .ok_or(BlockDSPError::UnknownLanguageType(null_typ.to_string()))? + .name + .to_string(); + + // Next we build the root AST node set: + let mut tree_builder: Vec> = vec![]; + + let main_node = Node::from(0, "", ""); + + tree_builder.push(GenTreeJob::::Area { area_id: 0, out: main_node.clone() }); + + // A HashMap to store those blocks, that have multiple outputs. + // Their AST nodes need to be shared to multiple parent nodes. + let mut multi_outs: HashMap<(usize, i64, i64), Node> = HashMap::new(); + + // We do a depth first search here: + while let Some(job) = tree_builder.pop() { + match job { + GenTreeJob::::Area { area_id, out } => { + let area = + self.areas.get(area_id).ok_or(BlockDSPError::UnknownArea(area_id))?; + + let sinks = area.collect_sinks(); + + let area_node = Node::from(0, "", ""); + out.add_structural_node(area_node.clone()); + + for (x, y, uncon_out_row) in sinks { + if let Some(_row) = uncon_out_row { + let result_node = Node::from(0, "", ""); + + tree_builder.push(GenTreeJob::::Output { + area_id, + x, + y, + in_port: "".to_string(), + out: result_node.clone(), + }); + + tree_builder.push(GenTreeJob::::Node { + node: result_node, + out: area_node.clone(), + }); + } else { + tree_builder.push(GenTreeJob::::Sink { + area_id, + x, + y, + out: area_node.clone(), + }); + } + } + } + GenTreeJob::::Node { node, out } => { + out.add_structural_node(node); + } + GenTreeJob::::Sink { area_id, x, y, out } => { + let area = + self.areas.get(area_id).ok_or(BlockDSPError::UnknownArea(area_id))?; + + if let Some((block, xo, yo)) = area.ref_at_origin(x, y) { + let (node, needs_init) = + if let Some(node) = multi_outs.get(&(area_id, xo, yo)) { + (node.clone(), false) + } else { + (Node::from(block.id, &block.typ, &block.lbl), true) + }; + + out.add_structural_node(node.clone()); + + if needs_init { + multi_outs.insert((area_id, xo, yo), node.clone()); + + if let Some(cont_area_id) = block.contains.1 { + tree_builder.push(GenTreeJob::::Area { + area_id: cont_area_id, + out: node.clone(), + }); + } + + if let Some(cont_area_id) = block.contains.0 { + tree_builder.push(GenTreeJob::::Area { + area_id: cont_area_id, + out: node.clone(), + }); + } + + block.for_input_ports_reverse(|row, port_name| { + tree_builder.push(GenTreeJob::::Output { + area_id, + x: xo - 1, + y: yo + (row as i64), + in_port: port_name.to_string(), + out: node.clone(), + }); + }); + } + } + } + GenTreeJob::::Output { area_id, x, y, in_port, out } => { + let area = + self.areas.get(area_id).ok_or(BlockDSPError::UnknownArea(area_id))?; + + if let Some((block, xo, yo)) = area.ref_at_origin(x, y) { + let row = y - yo; + + let (node, needs_init) = + if let Some(node) = multi_outs.get(&(area_id, xo, yo)) { + (node.clone(), false) + } else { + (Node::from(block.id, &block.typ, &block.lbl), true) + }; + + if let Some(out_name) = block.outputs.get(row as usize).cloned().flatten() { + out.add_node(in_port, out_name, node.clone()); + } else { + let node = Node::from(0, &null_typ, ""); + out.add_node(in_port, "".to_string(), node.clone()); + } + + if needs_init { + multi_outs.insert((area_id, xo, yo), node.clone()); + + if let Some(cont_area_id) = block.contains.1 { + tree_builder.push(GenTreeJob::::Area { + area_id: cont_area_id, + out: node.clone(), + }); + } + + if let Some(cont_area_id) = block.contains.0 { + tree_builder.push(GenTreeJob::::Area { + area_id: cont_area_id, + out: node.clone(), + }); + } + + block.for_input_ports_reverse(|row, port_name| { + tree_builder.push(GenTreeJob::::Output { + area_id, + x: xo - 1, + y: yo + (row as i64), + in_port: port_name.to_string(), + out: node.clone(), + }); + }); + } + } else { + let node = Node::from(0, &null_typ, ""); + out.add_node(in_port, "".to_string(), node.clone()); + } + } + } + } + + Ok(main_node) + } + + pub fn recalculate_area_sizes(&mut self) { + let mut parents = vec![0; self.areas.len()]; + let mut sizes = vec![(0, 0); self.areas.len()]; + + // First we dive downwards, to record all the parents + // and get the sizes of the (leafs). + + self.area_work_dq.clear(); + self.size_work_dq.clear(); + + let parents_work_list = &mut self.area_work_dq; + let size_work_list = &mut self.size_work_dq; + + // Push the root area: + parents_work_list.push_back(0); + + let mut cur_sub = vec![]; + while let Some(area_idx) = parents_work_list.pop_back() { + cur_sub.clear(); + + self.areas[area_idx].get_direct_sub_areas(&mut cur_sub); + + // XXX: The resolver gets (0, 0), thats wrong for the + // areas with sub areas. But it resolves the leaf area + // sizes already correctly! + let (w, h) = self.areas[area_idx].resolve_size(|_id| (0, 0)); + sizes[area_idx] = (w, h); + + if cur_sub.len() == 0 { + size_work_list.push_front(area_idx); + } else { + for sub_idx in &cur_sub { + // XXX: Record the parent: + parents[*sub_idx] = area_idx; + parents_work_list.push_back(*sub_idx); + } + } + } + + // XXX: Invariant now is: + // - `parents` contains all the parent area IDs. + // - `size_work_list` contains all the leaf area IDs. + // - `sizes` contains correct sizes for the leafs + // (but wrong for the non leafs). + + // Next we need to work through the size_work_list upwards. + // That means, for each leaf in front of the Deque, + // we push the parent to the back. + while let Some(area_idx) = size_work_list.pop_front() { + // XXX: The invariant as we walk upwards is, that once we + // encounter a parent area ID in the size_work_list, + // we know that all sub areas already have been computed. + let (w, h) = self.areas[area_idx].resolve_size(|id| sizes[id]); + sizes[area_idx] = (w, h); + self.areas[area_idx].set_size(w, h); + + // XXX: area_idx == 0 is the root area, so skip that + // when pushing further parents! + if area_idx > 0 { + size_work_list.push_back(parents[area_idx]); + } + } + } + + pub fn area_is_subarea_of(&mut self, area_id: usize, a_id: usize, x: i64, y: i64) -> bool { + let mut areas = vec![]; + + let block = if let Some(block) = self.block_ref(a_id, x, y) { + block.clone() + } else { + return false; + }; + + self.all_sub_areas_of(&block, &mut areas); + + for a_id in &areas { + if area_id == *a_id { + return true; + } + } + + return false; + } + + pub fn all_sub_areas_of(&mut self, block: &Block, areas: &mut Vec) { + let contains = block.contains.clone(); + + let area_work_list = &mut self.area_work_dq; + area_work_list.clear(); + + if let Some(area_id) = contains.0 { + area_work_list.push_back(area_id); + } + if let Some(area_id) = contains.1 { + area_work_list.push_back(area_id); + } + + if area_work_list.len() <= 0 { + return; + } + + let mut cur_sub = vec![]; + while let Some(area_idx) = area_work_list.pop_front() { + areas.push(area_idx); + + cur_sub.clear(); + self.areas[area_idx].get_direct_sub_areas(&mut cur_sub); + + for sub_idx in &cur_sub { + area_work_list.push_back(*sub_idx); + } + } + } + + pub fn retrieve_block_chain_at( + &mut self, + id: usize, + x: i64, + y: i64, + remove_blocks: bool, + ) -> Option> { + let area = self.areas.get_mut(id)?; + let mut chain = area.chain_at(x, y)?; + + if remove_blocks { + chain.remove_load(area); + } else { + chain.clone_load(area, self.id_gen.clone()); + } + + Some(chain) + } + + pub fn clone_block_from_to( + &mut self, + id: usize, + x: i64, + y: i64, + id2: usize, + x2: i64, + mut y2: i64, + ) -> Result<(), BlockDSPError> { + let lang = self.language.clone(); + + let (mut block, _xo, yo) = if let Some(area) = self.areas.get_mut(id) { + let (block, xo, yo) = + area.ref_mut_at_origin(x, y).ok_or(BlockDSPError::NoBlockAt(id, x, y))?; + + let mut new_block = Box::new(block.clone_with_new_id(self.id_gen.next())); + if let Some(typ) = lang.borrow().types.get(&new_block.typ) { + typ.touch_contains(new_block.as_mut()); + } + + (new_block, xo, yo) + } else { + return Err(BlockDSPError::UnknownArea(id)); + }; + + self.create_areas_for_block(block.as_mut()); + + // check if the user grabbed at a different row than the top row: + if y > yo { + // if so, adjust the destination: + let offs = y - yo; + y2 = (y2 - offs).max(0); + } + + let area2 = self.areas.get_mut(id2).ok_or(BlockDSPError::UnknownArea(id2))?; + let rows = block.rows; + + if area2.check_space_at(x2, y2, block.rows) { + area2.set_block_at(x2, y2, block); + self.generation += 1; + Ok(()) + } else { + Err(BlockDSPError::NoSpaceAvailable(id2, x2, y2, rows)) + } + } + + pub fn split_block_chain_after( + &mut self, + id: usize, + x: i64, + y: i64, + filler_type: Option<&str>, + ) -> Result<(), BlockDSPError> { + let mut area_clone = self.areas.get(id).ok_or(BlockDSPError::UnknownArea(id))?.clone(); + + let mut chain = area_clone.chain_at(x, y).ok_or(BlockDSPError::NoBlockAt(id, x, y))?; + + chain.remove_load(area_clone.as_mut()); + + let lang = self.language.borrow(); + let typ: Option<&BlockType> = if let Some(filler_type) = filler_type { + Some( + lang.types + .get(filler_type) + .ok_or(BlockDSPError::UnknownLanguageType(filler_type.to_string()))?, + ) + } else { + None + }; + + chain.split_load_after_x(x, y, typ, self.id_gen.clone()); + + if !chain.area_has_space_for_load(&mut area_clone, 0, 0) { + return Err(BlockDSPError::NoSpaceAvailable(id, x, y, 0)); + } + + chain.place_load(&mut area_clone); + self.generation += 1; + + self.areas[id] = area_clone; + + Ok(()) + } + + pub fn move_block_chain_from_to( + &mut self, + id: usize, + x: i64, + y: i64, + id2: usize, + x2: i64, + y2: i64, + ) -> Result<(), BlockDSPError> { + let mut area_clone = self.areas.get(id).ok_or(BlockDSPError::UnknownArea(id))?.clone(); + + let mut chain = area_clone.chain_at(x, y).ok_or(BlockDSPError::NoBlockAt(id, x, y))?; + + chain.remove_load(area_clone.as_mut()); + self.generation += 1; + + if id2 == id { + let move_x_offs = x2 - x; + let move_y_offs = y2 - y; + chain.move_by_offs(move_x_offs, move_y_offs); + + if !chain.try_fit_load_into_space(&mut area_clone) { + return Err(BlockDSPError::NoSpaceAvailable(id, x2, y2, 0)); + } + + chain.place_load(&mut area_clone); + self.areas[id] = area_clone; + } else { + // id2 != id + if chain.area_is_subarea_of_loaded(id2, self) { + return Err(BlockDSPError::CircularAction(id, id2)); + } + + let (xo, yo) = chain.normalize_load_pos(); + let (grab_x_offs, grab_y_offs) = (xo - x, yo - y); + + // println!("xo={}, yo={}, grab_x={}, grab_y={}, x2={}, y2={}", + // xo, yo, grab_x_offs, grab_y_offs, x2, y2); + + // XXX: .max(0) prevents us from moving the + // chain outside the subarea accendentally! + chain.move_by_offs((grab_x_offs + x2).max(0), (grab_y_offs + y2).max(0)); + + let mut area2_clone = + self.areas.get(id2).ok_or(BlockDSPError::UnknownArea(id))?.clone(); + + if !chain.try_fit_load_into_space(&mut area2_clone) { + return Err(BlockDSPError::NoSpaceAvailable(id, x2, y2, 1)); + } + + chain.place_load(&mut area2_clone); + + self.areas[id] = area_clone; + self.areas[id2] = area2_clone; + } + + self.generation += 1; + + // let mut chain = + // self.retrieve_block_chain_at(id, x, y, true) + // .ok_or(BlockDSPError::NoBlockAt(id, x, y))?; + // + // chain.normalize_load_pos(); + + Ok(()) + } + + pub fn move_block_from_to( + &mut self, + id: usize, + x: i64, + y: i64, + id2: usize, + x2: i64, + mut y2: i64, + ) -> Result<(), BlockDSPError> { + if self.area_is_subarea_of(id2, id, x, y) { + return Err(BlockDSPError::CircularAction(id, id2)); + } + + let (block, xo, yo) = if let Some(area) = self.areas.get_mut(id) { + area.remove_at(x, y).ok_or(BlockDSPError::NoBlockAt(id, x, y))? + } else { + return Err(BlockDSPError::UnknownArea(id)); + }; + + // check if the user grabbed at a different row than the top row: + if y > yo { + // if so, adjust the destination: + let offs = y - yo; + y2 = (y2 - offs).max(0); + } + + let area2 = self.areas.get_mut(id2).ok_or(BlockDSPError::UnknownArea(id2))?; + let rows = block.rows; + + self.generation += 1; + + if area2.check_space_at(x2, y2, block.rows) { + area2.set_block_at(x2, y2, block); + Ok(()) + } else { + if let Some(area) = self.areas.get_mut(id) { + area.set_block_at(xo, yo, block); + } + Err(BlockDSPError::NoSpaceAvailable(id2, x2, y2, rows)) + } + } + + fn create_areas_for_block(&mut self, block: &mut Block) { + if let Some(area_id) = &mut block.contains.0 { + let mut area = Box::new(BlockArea::new(1, 1)); + area.set_auto_shrink(true); + self.areas.push(area); + *area_id = self.areas.len() - 1; + } + + if let Some(area_id) = &mut block.contains.1 { + let mut area = Box::new(BlockArea::new(1, 1)); + area.set_auto_shrink(true); + self.areas.push(area); + *area_id = self.areas.len() - 1; + } + } + + pub fn instanciate_at( + &mut self, + id: usize, + x: i64, + y: i64, + typ: &str, + user_input: Option, + ) -> Result<(), BlockDSPError> { + let mut block = { + let lang = self.language.borrow(); + + if let Some(area) = self.areas.get_mut(id) { + if let Some(typ) = lang.types.get(typ) { + if !area.check_space_at(x, y, typ.rows) { + return Err(BlockDSPError::NoSpaceAvailable(id, x, y, typ.rows)); + } + } + } else { + return Err(BlockDSPError::UnknownArea(id)); + } + + let typ = + lang.types.get(typ).ok_or(BlockDSPError::UnknownLanguageType(typ.to_string()))?; + + typ.instanciate_block(user_input, self.id_gen.clone()) + }; + + self.create_areas_for_block(block.as_mut()); + + self.generation += 1; + + if let Some(area) = self.areas.get_mut(id) { + area.set_block_at(x, y, block); + } + + Ok(()) + } + + pub fn remove_at(&mut self, id: usize, x: i64, y: i64) -> Result<(), BlockDSPError> { + let area = self.areas.get_mut(id).ok_or(BlockDSPError::UnknownArea(id))?; + area.remove_at(x, y).ok_or(BlockDSPError::NoBlockAt(id, x, y))?; + self.generation += 1; + Ok(()) + } + + pub fn area_size(&self, id: usize) -> (usize, usize) { + self.areas.get(id).map(|a| a.size).unwrap_or((0, 0)) + } + + pub fn block_at(&self, id: usize, x: i64, y: i64) -> Option<&dyn BlockView> { + let area = self.areas.get(id)?; + Some(area.blocks.get(&(x, y))?.as_ref()) + } + + pub fn origin_at(&self, id: usize, x: i64, y: i64) -> Option<(i64, i64)> { + self.areas.get(id).map(|a| a.origin_map.get(&(x, y)).copied()).flatten() + } +} + +impl BlockCodeView for BlockFun { + fn area_header(&self, id: usize) -> Option<&str> { + self.areas.get(id).map(|a| &a.header[..]) + } + + fn area_size(&self, id: usize) -> (usize, usize) { + self.area_size(id) + } + + fn block_at(&self, id: usize, x: i64, y: i64) -> Option<&dyn BlockView> { + self.block_at(id, x, y) + } + + fn origin_at(&self, id: usize, x: i64, y: i64) -> Option<(i64, i64)> { + self.origin_at(id, x, y) + } + + fn generation(&self) -> u64 { + self.generation + } +} + +#[cfg(feature = "synfx-dsp-jit")] +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn check_blockfun_serialize_empty() { + let dsp_lib = synfx_dsp_jit::get_standard_library(); + let lang = crate::wblockdsp::setup_hxdsp_block_language(dsp_lib); + let mut bf = BlockFun::new(lang.clone()); + + let sn = bf.save_snapshot(); + let serialized = sn.serialize().to_string(); + assert_eq!(serialized, "{\"VERSION\":1,\"areas\":[{\"auto_shrink\":false,\"blocks\":[],\"header\":\"\",\"size\":[16,16]}],\"current_block_id_counter\":0}"); + + let v: Value = serde_json::from_str(&serialized).unwrap(); + let sn = BlockFunSnapshot::deserialize(&v).expect("No deserialization error"); + let mut bf2 = BlockFun::new(lang); + let bf2 = bf2.load_snapshot(&sn); + } + + #[test] + fn check_blockfun_serialize_1() { + let dsp_lib = synfx_dsp_jit::get_standard_library(); + let lang = crate::wblockdsp::setup_hxdsp_block_language(dsp_lib); + let mut bf = BlockFun::new(lang.clone()); + + bf.instanciate_at(0, 0, 0, "+", None).unwrap(); + + let sn = bf.save_snapshot(); + let serialized = sn.serialize().to_string(); + assert_eq!(serialized, + "{\"VERSION\":1,\"areas\":[{\"auto_shrink\":false,\"blocks\":[{\"block\":{\"color\":4,\"contains\":[null,null],\"expanded\":true,\"id\":1,\"inputs\":[\"\",\"\"],\"lbl\":\"+\",\"outputs\":[\"\"],\"rows\":2,\"typ\":\"+\"},\"x\":0,\"y\":0}],\"header\":\"\",\"size\":[16,16]}],\"current_block_id_counter\":1}"); + + let v: Value = serde_json::from_str(&serialized).unwrap(); + let sn = BlockFunSnapshot::deserialize(&v).expect("No deserialization error"); + let mut bf2 = BlockFun::new(lang); + bf2.load_snapshot(&sn); + + let bv = bf2.block_at(0, 0, 0).unwrap(); + assert!(bv.has_input(0)); + } +} diff --git a/src/wblockdsp/mod.rs b/src/wblockdsp/mod.rs new file mode 100644 index 0000000..fc210df --- /dev/null +++ b/src/wblockdsp/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Weird Constructor +// 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::*; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0b1a3af..942dc90 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -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)] diff --git a/tests/node_ad.rs b/tests/node_ad.rs index 53fc11d..c5d5c6f 100644 --- a/tests/node_ad.rs +++ b/tests/node_ad.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Weird Constructor +// Copyright (c) 2021-2022 Weird Constructor // 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)); diff --git a/tests/node_allp.rs b/tests/node_allp.rs index 0680bce..042c9f3 100644 --- a/tests/node_allp.rs +++ b/tests/node_allp.rs @@ -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: diff --git a/tests/node_code.rs b/tests/node_code.rs new file mode 100644 index 0000000..45ec533 --- /dev/null +++ b/tests/node_code.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2022 Weird Constructor +// 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) + ] + ); +} diff --git a/tests/node_comb.rs b/tests/node_comb.rs index b855609..2d137cc 100644 --- a/tests/node_comb.rs +++ b/tests/node_comb.rs @@ -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)] ); } diff --git a/tests/node_delay.rs b/tests/node_delay.rs index d137a60..23a16e5 100644 --- a/tests/node_delay.rs +++ b/tests/node_delay.rs @@ -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), ] ); } diff --git a/tests/node_pverb.rs b/tests/node_pverb.rs index 529582b..9ecfe25 100644 --- a/tests/node_pverb.rs +++ b/tests/node_pverb.rs @@ -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::>(), // 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::>(), // 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::>(), // 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) ] ); diff --git a/tests/node_sampl.rs b/tests/node_sampl.rs index 02ae956..66391aa 100644 --- a/tests/node_sampl.rs +++ b/tests/node_sampl.rs @@ -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 ] ); diff --git a/tests/node_scope.rs b/tests/node_scope.rs new file mode 100644 index 0000000..ceedf24 --- /dev/null +++ b/tests/node_scope.rs @@ -0,0 +1,278 @@ +// Copyright (c) 2022 Weird Constructor +// 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, Vec, 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); +} diff --git a/tests/quant.rs b/tests/quant.rs index 0f6c510..a5ee47c 100644 --- a/tests/quant.rs +++ b/tests/quant.rs @@ -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() {