From d67529be1b6a326692c2750348a2aebdf5173757 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 05:54:26 +0200 Subject: [PATCH 01/88] added documentation for writing nodes for HexoDSP --- src/dsp/helpers.rs | 10 -- src/dsp/mod.rs | 325 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 324 insertions(+), 11 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 5020fc7..3fa7c27 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -2181,16 +2181,6 @@ impl TriSawLFO { 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 diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index e726073..c5b33c6 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1,7 +1,330 @@ -// 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 +- [ ] Document boilerplate +- [ ] DSP implementation +- [ ] Parameter fine tuning +- [ ] DSP tests for all params +- [ ] Ensure Documentation is properly formatted for the GUI +- [ ] 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 + +Here are some hints to get you started: + +### 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. + +### Node Documentation + +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. + +Look at `node_tslfo.rs` for instance. It wires up the `TriSawLFO` from `dsp/helpers.rs` +to the HexoDSP node interface. + +```ignore + // node_tslfo.rs + use super::helpers::{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 `dsp/helpers.rs` is then independent and reusable else where. + +### 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 + +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 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)); + matrix.sync().unwrap(); + + // ... + } +``` + +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 you usually want to define short variable names for the [NodeId] that refer to the DSP +node instances: + +```ignore + let ad = NodeId::Ad(0); + let out = NodeId::Out(0); +``` + +You can have multiple instances for a node. The number in the parenthesis are +the instance index of that node. + +Next you want to layout the nodes adjacent to each other on the hexagonal grid. +This is a bit more tricky than with a rectangular grid. + +```ignore + 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)); + matrix.sync().unwrap(); +``` + +The `sync` is necessary to update the DSP graph. +When doing this, keep the following grid layout in mind: + +```text + _____ _____ + / \ / \ + / 0,0 \_____/ 2,0 \_____ + \ / \ / \ + \_____/ 1,0 \_____/ 3,0 \ + / \ / \ / + / 0,1 \_____/ 2,1 \_____/ + \ / \ / \ + \_____/ 1,1 \_____/ 3,1 \ + / \ / \ / + / 0,2 \_____/ 2,2 \_____/ + \ / \ / + \_____/ 1,2 \_____/ + \ / + \_____/ +``` + +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 + + // 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)] From e0f51d04a2601aa58a60334bb1858fbdc4849a37 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 05:55:10 +0200 Subject: [PATCH 02/88] fix formatting --- src/nodes/node_conf.rs | 11 +++-------- src/nodes/node_exec.rs | 4 ++-- tests/node_delay.rs | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index f067199..ba9f313 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -3,8 +3,8 @@ // 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_TRACKERS, + MAX_INPUTS, UNUSED_MONITOR_IDX, }; use crate::dsp::tracker::{PatternData, Tracker}; use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom}; @@ -245,12 +245,7 @@ impl SharedNodeConf { } ( - Self { - node_ctx_values, - graph_update_prod: rb_graph_prod, - monitor, - drop_thread, - }, + Self { node_ctx_values, graph_update_prod: rb_graph_prod, monitor, drop_thread }, SharedNodeExec { node_ctx_values: exec_node_ctx_vals, graph_update_con: rb_graph_con, diff --git a/src/nodes/node_exec.rs b/src/nodes/node_exec.rs index 069ffb2..699d026 100644 --- a/src/nodes/node_exec.rs +++ b/src/nodes/node_exec.rs @@ -3,8 +3,8 @@ // See README.md and COPYING for details. use super::{ - DropMsg, GraphMessage, NodeProg, FB_DELAY_TIME_US, MAX_ALLOCATED_NODES, - MAX_FB_DELAY_SIZE, MAX_SMOOTHERS, UNUSED_MONITOR_IDX, + DropMsg, GraphMessage, NodeProg, FB_DELAY_TIME_US, MAX_ALLOCATED_NODES, MAX_FB_DELAY_SIZE, + MAX_SMOOTHERS, UNUSED_MONITOR_IDX, }; use crate::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE}; use crate::monitor::{MonitorBackend, MON_SIG_CNT}; diff --git a/tests/node_delay.rs b/tests/node_delay.rs index 9bbe833..d137a60 100644 --- a/tests/node_delay.rs +++ b/tests/node_delay.rs @@ -119,7 +119,7 @@ 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.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 ] From 5b1aa9a9e37e7b48ca9ae5c232f25743e66ff630 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 06:00:40 +0200 Subject: [PATCH 03/88] paragraph about node inputs --- src/dsp/mod.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index c5b33c6..2ff87c9 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -167,6 +167,81 @@ to the HexoDSP node interface. The code for `TriSawLFO` in `dsp/helpers.rs` 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]. From f8a0ebb447bc135f9628a78389103e1c3628afb5 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 06:02:13 +0200 Subject: [PATCH 04/88] more documentation --- src/dsp/mod.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 2ff87c9..42b9b8e 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -10,17 +10,24 @@ When adding a new node to HexoDSP, I recommend working through the following checklist: -- [ ] Implement boilerplate -- [ ] Document boilerplate +- [ ] 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 params +- [ ] DSP tests for all (relevant) params - [ ] Ensure Documentation is properly formatted for the GUI - [ ] 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. + Here are some hints to get you started: ### Boilerplate @@ -61,6 +68,9 @@ can not be automated. ### 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 From ad4ea837b9148c33a54ca5b77023e9134b0e4b32 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 06:04:30 +0200 Subject: [PATCH 05/88] more details about testing --- src/dsp/mod.rs | 27 +++++++++++++++++++++++++++ src/matrix.rs | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 42b9b8e..a84d7ed 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -376,9 +376,36 @@ When doing this, keep the following grid layout in mind: \_____/ ``` +Defining the outputs of a cell is done like this: + +```ignore + Cell::empty(ad).out(None, None, ad.out("sig")) +``` + +[crate::Cell::empty] takes a [NodeId] as first argument. The [crate::Cell] +structure then allows you to specify the output ports using the [crate::Cell::out] +function. The 3 arguments of that function are for the 3 edges of that hex tile: + +```ignore + // TopRight BottomRight Bottom + Cell::empty(ad).out(None, None, ad.out("sig")) +``` + +[crate::Cell::input] works the same way, but the 3 arguments refer to the 3 input +edges of a hex tile: + +```ignore + // Top TopLeft BottomLeft + Cell::empty(out).input(out.inp("ch1"), None, None) +``` + +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")`. + 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() { diff --git a/src/matrix.rs b/src/matrix.rs index cea1c89..57c1291 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -388,7 +388,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); From 239b9b574a20f99b1a62de8ae362c33ad8e21db0 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 06:07:14 +0200 Subject: [PATCH 06/88] More documentation --- CHANGELOG.md | 7 +++++++ README.md | 29 ++++++++++++++++++++++++++++- src/dsp/mod.rs | 21 ++++++++++++++++++++- src/lib.rs | 32 +++++++++++++++++++++++++++++--- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8d29614 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +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. diff --git a/README.md b/README.md index ee182e9..1e91765 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 @@ -25,6 +24,34 @@ Here a short list of features: * Extensible framework for quickly developing new nodes at compile time * A comprehensive automated test suite +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 | +| 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 | + ### API Examples #### Documentation diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index a84d7ed..005f3c7 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -28,7 +28,26 @@ 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. -Here are some hints to get you started: +**Be aware that new DSP nodes need to meet these quality guidelines to be included:** + +- Clean Rust code that I can understand and maintain. +- 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. +- 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 diff --git a/src/lib.rs b/src/lib.rs index 5ea9202..aed0d45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,7 @@ // 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). @@ -28,6 +26,34 @@ Here a short list of features: * Extensible framework for quickly developing new nodes at compile time * A comprehensive automated test suite +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 | +| 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 | + ## API Examples ### Documentation From 649ce74aafd6523dc85890e2b5c906928b61b1a1 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 06:10:24 +0200 Subject: [PATCH 07/88] more notes about the quality guideline --- src/dsp/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 005f3c7..a4f996a 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -17,6 +17,7 @@ When adding a new node to HexoDSP, I recommend working through the following che - [ ] 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 @@ -30,13 +31,17 @@ until you get the hang of all the things involved to make it compile in the firs **Be aware that new DSP nodes need to meet these quality guidelines to be included:** -- Clean Rust code that I can understand and maintain. +- 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. +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. From 1fbc6741cb7f89f5e1c7e79603009e1f7216dcc3 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 19 Jul 2022 20:47:46 +0200 Subject: [PATCH 08/88] fixing interpolation --- src/dsp/helpers.rs | 66 +++++++++++++++++++++++++++++-------------- src/dsp/node_sampl.rs | 44 ++--------------------------- tests/node_allp.rs | 32 ++++++++++----------- tests/node_comb.rs | 43 +++++++++------------------- tests/node_delay.rs | 36 +++++++++++------------ tests/node_sampl.rs | 12 ++++---- 6 files changed, 101 insertions(+), 132 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 3fa7c27..4491216 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -892,6 +892,48 @@ fn fclampc(x: F, mi: f64, mx: f64) -> F { x.max(f(mi)).min(f(mx)) } + +/// Hermite / Cubic interpolation of a buffer full of samples at the given _index_. +/// _len_ is the buffer length to consider and wrap the index into. And _fract_ is the +/// fractional part of the index. +/// +/// Commonly used like this: +/// +///``` +/// use hexodsp::dsp::helpers::cubic_interpolate; +/// +/// let buf [f32; 9] = [1.0, 0.2, 0.4, 0.5, 0.7, 0.9, 1.0, 0.3, 0.3]; +/// let pos = 3.3_f32; +/// +/// let i = pos.floor(); +/// let f = pos.fract(); +/// +/// let res = cubic_interpolate(&buf[..], buf.len(), i, f); +/// assert_eq!((res - 0.4).abs() < 0.2); +///``` +#[inline] +pub fn cubic_interpolate(data: &[F], len: usize, index: usize, fract: F) -> F { + // 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[(index - 1) % len]; + let x0 = data[index % len]; + let x1 = data[(index + 1) % len]; + let x2 = data[(index + 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; + + (((a * fract) - b_neg) * fract + c) * fract + x0 +} + #[derive(Debug, Clone, Default)] pub struct DelayBuffer { data: Vec, @@ -1006,7 +1048,8 @@ impl DelayBuffer { /// Fetch a sample from the delay buffer at the given offset. /// - /// * `s_offs` - Sample offset in samples. + /// * `s_offs` - Sample offset in samples into the past of the [DelayBuffer] + /// from the current write (or the "now") position. #[inline] pub fn cubic_interpolate_at_s(&self, s_offs: F) -> F { let data = &self.data[..]; @@ -1016,26 +1059,7 @@ impl DelayBuffer { 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 + cubic_interpolate(data, len, i, f::(1.0) - fract) } #[inline] diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index 176d25c..e5d5708 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 super::helpers::{Trigger, cubic_interpolate}; 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}; @@ -154,26 +154,7 @@ impl Sampl { let f = self.phase.fract(); self.phase = j 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 + cubic_interpolate(&sample_data[..], sd_len, i, (1.0 - f) as f32) } #[allow(clippy::many_single_char_names)] @@ -188,26 +169,7 @@ impl Sampl { 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 + cubic_interpolate(&sample_data[..], sd_len, i, f as f32) } #[allow(clippy::float_cmp)] diff --git a/tests/node_allp.rs b/tests/node_allp.rs index 0680bce..a516ff2 100644 --- a/tests/node_allp.rs +++ b/tests/node_allp.rs @@ -38,35 +38,35 @@ 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 - 3]); // allpass feedback of the original signal for 2ms: // XXX: the smearing before and after the allpass is due to the // cubic interpolation! - v.append(&mut vec![-0.03748519, 0.37841395, 0.5260659]); + v.append(&mut vec![-0.01606, 0.13158, 0.54748]); 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.5260659, 0.37841395, -0.03748519]); + v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 6]); // 2ms the previous 1.0 * 0.7 fed back into the filter, // including even more smearing due to cubic interpolation: v.append(&mut vec![ - -0.0019286226, - 0.04086761, - -0.1813516, - -0.35157663, - -0.36315754, - -0.35664573, + -0.00035427228, + 0.006157537, + -0.005423375, + -0.1756484, + -0.39786762, + -0.3550714, ]); v.append(&mut vec![-0.357; (2.0 * 44.1_f32).floor() as usize - 5]); v.append(&mut vec![ - -0.3550714, - -0.39786762, - -0.1756484, - -0.005423375, - 0.006157537, - -0.00035427228, + -0.35664573, + -0.36315754, + -0.35157663, + -0.1813516, + 0.04086761, + -0.0019286226, ]); v.append(&mut vec![0.0; 10]); diff --git a/tests/node_comb.rs b/tests/node_comb.rs index b855609..f86a550 100644 --- a/tests/node_comb.rs +++ b/tests/node_comb.rs @@ -36,45 +36,28 @@ fn check_node_comb_1() { (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) + (3381, 184), + (3391, 189), + (3402, 195), + (3413, 213), + (3424, 198), + (3435, 203), + (6815, 188), + (13587, 196), + (13598, 210), + (13609, 207), + (13620, 193) ] ); 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![(1001, 186), (3015, 186), (7031, 191)]); 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) - ] - ); + assert_eq!(fft, vec![(0, 2008), (11, 1017)]); } #[test] diff --git a/tests/node_delay.rs b/tests/node_delay.rs index d137a60..8e5977c 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.039102618, + -0.16390327, + 0.27611724, + -0.2608055, + 0.060164057, + 0.20197779, + -0.28871512, + 0.21515398, + -0.081471935, + 0.0023831273, // silence afterwards: 0.0, 0.0, @@ -119,8 +119,8 @@ 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: + 0.5, 0.5, 0.5, // the delayed smoothing ramp (10ms): + 0.9513626, // 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,15 +172,15 @@ 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], (86, 112)); + assert_eq!(fft[5], (237, 112)); + assert_eq!(fft[10], (517, 111)); // Sweep upwards: run_for_ms(&mut node_exec, 300.0); let fft = run_and_get_fft4096_now(&mut node_exec, 122); - assert_eq!(fft[0], (2498, 122)); - assert_eq!(fft[7], (2681, 122)); + assert_eq!(fft[0], (2509, 123)); + assert_eq!(fft[7], (2821, 123)); // Sweep at mostly highest point: run_for_ms(&mut node_exec, 700.0); @@ -274,7 +274,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![(220, 106), (440, 53)]); } #[test] 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 ] ); From 1ee4af6cd10d73e8584d6fd2cea9a616587af60e Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 02:01:47 +0200 Subject: [PATCH 09/88] working on finally fixed interpolation functions in the delay line and the sampler --- src/dsp/helpers.rs | 36 +++++++-- src/dsp/node_sampl.rs | 6 +- tests/delay_buffer.rs | 182 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 tests/delay_buffer.rs diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 4491216..b8526fd 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -892,7 +892,6 @@ fn fclampc(x: F, mi: f64, mx: f64) -> F { x.max(f(mi)).min(f(mx)) } - /// Hermite / Cubic interpolation of a buffer full of samples at the given _index_. /// _len_ is the buffer length to consider and wrap the index into. And _fract_ is the /// fractional part of the index. @@ -913,6 +912,7 @@ fn fclampc(x: F, mi: f64, mx: f64) -> F { ///``` #[inline] pub fn cubic_interpolate(data: &[F], len: usize, index: usize, fract: F) -> F { + let index = index + len; // Hermite interpolation, take from // https://github.com/eric-wood/delay/blob/main/src/delay.rs#L52 // @@ -1031,11 +1031,21 @@ impl DelayBuffer { let offs = s_offs.floor().to_usize().unwrap_or(0) % len; let fract = s_offs.fract(); - let i = (self.wr + len) - offs; + // one extra offset, because feed() advances self.wr to the next writing position! + let i = (self.wr + len) - (offs + 1); let x0 = data[i % len]; let x1 = data[(i - 1) % len]; - x0 + fract * (x1 - x0) + let res = x0 + fract * (x1 - x0); + //d// eprintln!( + //d// "INTERP: {:6.4} x0={:6.4} x1={:6.4} fract={:6.4} => {:6.4}", + //d// s_offs.to_f64().unwrap_or(0.0), + //d// x0.to_f64().unwrap(), + //d// x1.to_f64().unwrap(), + //d// fract.to_f64().unwrap(), + //d// res.to_f64().unwrap(), + //d// ); + res } /// Fetch a sample from the delay buffer at the given time. @@ -1057,23 +1067,33 @@ impl DelayBuffer { let offs = s_offs.floor().to_usize().unwrap_or(0) % len; let fract = s_offs.fract(); - let i = (self.wr + len) - offs; - - cubic_interpolate(data, len, i, f::(1.0) - fract) + let i = (self.wr + len) - (offs + 2); + let res = cubic_interpolate(data, len, i, f::(1.0) - fract); +// eprintln!( +// "cubic at={} ({:6.4}) res={:6.4}", +// i % len, +// s_offs.to_f64().unwrap(), +// res.to_f64().unwrap() +// ); + res } #[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; + // (offs + 1) one extra offset, because feed() advances + // self.wr to the next writing position! + let idx = ((self.wr + len) - (offs + 1)) % 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; + // (delay_sample_count + 1) one extra offset, because feed() advances self.wr to + // the next writing position! + let idx = ((self.wr + len) - (delay_sample_count + 1)) % len; self.data[idx] } } diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index e5d5708..7964a26 100644 --- a/src/dsp/node_sampl.rs +++ b/src/dsp/node_sampl.rs @@ -148,8 +148,8 @@ impl Sampl { return 0.0; } - let j = self.phase.floor() as usize % sd_len; - let i = ((sd_len - 1) - j) + sd_len; + let j = self.phase.floor() as usize; + let i = ((sd_len - 1) - j); let f = self.phase.fract(); self.phase = j as f64 + f + sr_factor * speed; @@ -165,7 +165,7 @@ impl Sampl { return 0.0; } - let i = self.phase.floor() as usize + sd_len; + let i = self.phase.floor() as usize; let f = self.phase.fract(); self.phase = (i % sd_len) as f64 + f + sr_factor * speed; diff --git a/tests/delay_buffer.rs b/tests/delay_buffer.rs new file mode 100644 index 0000000..c305ada --- /dev/null +++ b/tests/delay_buffer.rs @@ -0,0 +1,182 @@ +// 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. + +mod common; +use common::*; + +#[test] +fn check_delaybuffer_linear_interpolation() { + let mut buf = crate::helpers::DelayBuffer::new(); + + buf.feed(0.0); + buf.feed(0.1); + buf.feed(0.2); + buf.feed(0.3); + buf.feed(0.4); + buf.feed(0.5); + buf.feed(0.6); + buf.feed(0.7); + buf.feed(0.8); + buf.feed(0.9); + buf.feed(1.0); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.5; + for _ in 0..20 { + samples_out.push(buf.linear_interpolate_at_s(pos)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35000002, 0.3, + 0.25, 0.2, 0.15, 0.1, 0.05 + ] + ); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.2; + for _ in 0..30 { + samples_out.push(buf.linear_interpolate_at_s(pos)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, 0.98, 0.96, 0.94, 0.91999996, 0.9, 0.88, 0.85999995, 0.84, 0.82, 0.8, 0.78, 0.76, + 0.73999995, 0.71999997, 0.6999999, 0.67999995, 0.65999997, 0.6399999, 0.61999995, + 0.59999996, 0.58, 0.56, 0.54, 0.52000004, 0.50000006, 0.48000008, 0.4600001, + 0.44000012, 0.42000014 + ] + ); +} + +#[test] +fn check_delaybuffer_nearest() { + let mut buf = crate::helpers::DelayBuffer::new(); + + buf.feed(0.0); + buf.feed(0.1); + buf.feed(0.2); + buf.feed(0.3); + buf.feed(0.4); + buf.feed(0.5); + buf.feed(0.6); + buf.feed(0.7); + buf.feed(0.8); + buf.feed(0.9); + buf.feed(1.0); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.5; + for _ in 0..20 { + samples_out.push(buf.at(pos as usize)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, 1.0, 0.9, 0.9, 0.8, 0.8, 0.7, 0.7, 0.6, 0.6, 0.5, 0.5, 0.4, 0.4, 0.3, 0.3, 0.2, + 0.2, 0.1, 0.1 + ] + ); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.2; + for _ in 0..30 { + samples_out.push(buf.at(pos as usize)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.8, 0.8, 0.8, 0.8, 0.7, 0.7, + 0.7, 0.7, 0.7, 0.6, 0.6, 0.6, 0.6, 0.6, 0.5, 0.5, 0.5, 0.5, 0.5 + ] + ); +} + +#[test] +fn check_cubic_interpolate() { + use crate::helpers::cubic_interpolate; + let data = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]; + + let mut samples_out = vec![]; + let mut pos = 0.0_f32; + let pos_inc = 0.1_f32; + for _ in 0..30 { + let i = pos.floor() as usize; + let f = pos.fract(); + samples_out.push(cubic_interpolate(&data[..], data.len(), i, f)); + pos += pos_inc; + } + assert_vec_feq!( + samples_out, + vec![ + 1.0, 1.03455, 1.0504, 1.05085, 1.0392, 1.01875, 0.99279994, 0.9646499, 0.9375999, + 0.91494995, 0.9, 0.89, 0.87999994, 0.86999995, 0.85999995, 0.84999996, 0.84, 0.83, + 0.82, 0.80999994, 0.8, 0.79, 0.78000003, 0.77000004, 0.76, 0.75, 0.74, 0.73, 0.72, + 0.71000004 + ] + ); +} + +#[test] +fn check_delaybuffer_cubic_interpolation() { + let mut buf = crate::helpers::DelayBuffer::new(); + + buf.feed(0.0); + buf.feed(0.1); + buf.feed(0.2); + buf.feed(0.3); + buf.feed(0.4); + buf.feed(0.5); + buf.feed(0.6); + buf.feed(0.7); + buf.feed(0.8); + buf.feed(0.9); + buf.feed(1.0); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.5; + for _ in 0..20 { + samples_out.push(buf.cubic_interpolate_at_s(pos)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35000002, 0.3, + 0.25, 0.2, 0.15, 0.1, 0.05 + ] + ); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.2; + for _ in 0..30 { + samples_out.push(buf.cubic_interpolate_at_s(pos)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, 0.98, 0.96, 0.94, 0.91999996, 0.9, 0.88, 0.85999995, 0.84, 0.82, 0.8, 0.78, 0.76, + 0.73999995, 0.71999997, 0.6999999, 0.67999995, 0.65999997, 0.6399999, 0.61999995, + 0.59999996, 0.58, 0.56, 0.54, 0.52000004, 0.50000006, 0.48000008, 0.4600001, + 0.44000012, 0.42000014 + ] + ); +} From 5a4dc0e2aab21d49baf3cf7aa4d119a7d957ec31 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 03:57:33 +0200 Subject: [PATCH 10/88] Tested the cubic interpolation now properly, also against alternative maths --- src/dsp/helpers.rs | 48 ++++++++++++++---- tests/delay_buffer.rs | 115 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 132 insertions(+), 31 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index b8526fd..5df3b93 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -901,14 +901,14 @@ fn fclampc(x: F, mi: f64, mx: f64) -> F { ///``` /// use hexodsp::dsp::helpers::cubic_interpolate; /// -/// let buf [f32; 9] = [1.0, 0.2, 0.4, 0.5, 0.7, 0.9, 1.0, 0.3, 0.3]; +/// let buf : [f32; 9] = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2]; /// let pos = 3.3_f32; /// -/// let i = pos.floor(); +/// let i = pos.floor() as usize; /// let f = pos.fract(); /// /// let res = cubic_interpolate(&buf[..], buf.len(), i, f); -/// assert_eq!((res - 0.4).abs() < 0.2); +/// assert!((res - 0.67).abs() < 0.2_f32); ///``` #[inline] pub fn cubic_interpolate(data: &[F], len: usize, index: usize, fract: F) -> F { @@ -931,7 +931,31 @@ pub fn cubic_interpolate(data: &[F], len: usize, index: usize, fract: F) let a = w + v + (x2 - x0) * f(0.5); let b_neg = w + a; - (((a * fract) - b_neg) * fract + c) * fract + x0 + let res = (((a * fract) - b_neg) * fract + c) * fract + x0; + + // let rr2 = + // x0 + f::(0.5) * fract * ( + // x1 - xm1 + fract * ( + // f::(4.0) * x1 + // + f::(2.0) * xm1 + // - f::(5.0) * x0 + // - x2 + // + fract * (f::(3.0) * (x0 - x1) - xm1 + x2))); + + // eprintln!( + // "index={} fract={:6.4} xm1={:6.4} x0={:6.4} x1={:6.4} x2={:6.4} = {:6.4} <> {:6.4}", + // index, fract.to_f64().unwrap(), xm1.to_f64().unwrap(), x0.to_f64().unwrap(), x1.to_f64().unwrap(), x2.to_f64().unwrap(), + // res.to_f64().unwrap(), + // rr2.to_f64().unwrap() + // ); + + // eprintln!( + // "index={} fract={:6.4} xm1={:6.4} x0={:6.4} x1={:6.4} x2={:6.4} = {:6.4}", + // index, fract.to_f64().unwrap(), xm1.to_f64().unwrap(), x0.to_f64().unwrap(), x1.to_f64().unwrap(), x2.to_f64().unwrap(), + // res.to_f64().unwrap(), + // ); + + res } #[derive(Debug, Clone, Default)] @@ -1067,14 +1091,18 @@ impl DelayBuffer { let offs = s_offs.floor().to_usize().unwrap_or(0) % len; let fract = s_offs.fract(); + // (offs + 1) offset for compensating that self.wr points to the next + // unwritten position. + // Additional (offs + 1 + 1) offset for cubic_interpolate, which + // interpolates into the past through the delay buffer. let i = (self.wr + len) - (offs + 2); let res = cubic_interpolate(data, len, i, f::(1.0) - fract); -// eprintln!( -// "cubic at={} ({:6.4}) res={:6.4}", -// i % len, -// s_offs.to_f64().unwrap(), -// res.to_f64().unwrap() -// ); + // eprintln!( + // "cubic at={} ({:6.4}) res={:6.4}", + // i % len, + // s_offs.to_f64().unwrap(), + // res.to_f64().unwrap() + // ); res } diff --git a/tests/delay_buffer.rs b/tests/delay_buffer.rs index c305ada..d27a1d5 100644 --- a/tests/delay_buffer.rs +++ b/tests/delay_buffer.rs @@ -110,6 +110,51 @@ fn check_cubic_interpolate() { use crate::helpers::cubic_interpolate; let data = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]; + let mut samples_out = vec![]; + let mut pos = 0.0_f32; + let pos_inc = 0.5_f32; + for _ in 0..30 { + let i = pos.floor() as usize; + let f = pos.fract(); + samples_out.push(cubic_interpolate(&data[..], data.len(), i, f)); + pos += pos_inc; + } + assert_vec_feq!( + samples_out, + vec![ + 1.0, + 1.01875, + 0.9, + 0.85, + 0.8, + 0.75, + 0.7, + 0.65, + 0.6, + 0.55, + 0.5, + 0.45, + 0.4, + 0.35000002, + 0.3, + 0.25, + 0.2, + 0.15, + 0.1, + -0.018750004, + 0.0, + 0.49999997, + 1.0, + 1.01875, + 0.9, + 0.85, + 0.8, + 0.75, + 0.7, + 0.65 + ] + ); + let mut samples_out = vec![]; let mut pos = 0.0_f32; let pos_inc = 0.1_f32; @@ -148,23 +193,7 @@ fn check_delaybuffer_cubic_interpolation() { let mut samples_out = vec![]; let mut pos = 0.0; - let pos_inc = 0.5; - for _ in 0..20 { - samples_out.push(buf.cubic_interpolate_at_s(pos)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35000002, 0.3, - 0.25, 0.2, 0.15, 0.1, 0.05 - ] - ); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.2; + let pos_inc = 0.1; for _ in 0..30 { samples_out.push(buf.cubic_interpolate_at_s(pos)); pos += pos_inc; @@ -173,10 +202,54 @@ fn check_delaybuffer_cubic_interpolation() { assert_vec_feq!( samples_out, vec![ - 1.0, 0.98, 0.96, 0.94, 0.91999996, 0.9, 0.88, 0.85999995, 0.84, 0.82, 0.8, 0.78, 0.76, - 0.73999995, 0.71999997, 0.6999999, 0.67999995, 0.65999997, 0.6399999, 0.61999995, - 0.59999996, 0.58, 0.56, 0.54, 0.52000004, 0.50000006, 0.48000008, 0.4600001, - 0.44000012, 0.42000014 + 1.0, 1.03455, 1.0504, 1.05085, 1.0392, 1.01875, 0.99279994, 0.9646499, 0.9375999, + 0.91494995, 0.9, 0.89, 0.87999994, 0.86999995, 0.85999995, 0.84999996, 0.84, 0.83, + 0.82, 0.80999994, 0.8, 0.79, 0.78000003, 0.77000004, 0.76, 0.75, 0.74, 0.73, 0.72, + 0.71000004 + ] + ); + + let mut samples_out = vec![]; + let mut pos = 0.0; + let pos_inc = 0.5; + for _ in 0..30 { + samples_out.push(buf.cubic_interpolate_at_s(pos)); + pos += pos_inc; + } + + assert_vec_feq!( + samples_out, + vec![ + 1.0, + 1.01875, + 0.9, + 0.85, + 0.8, + 0.75, + 0.7, + 0.65, + 0.6, + 0.55, + 0.5, + 0.45, + 0.4, + 0.35000002, + 0.3, + 0.25, + 0.2, + 0.15, + 0.1, + 0.043750003, + 0.0, + -0.00625, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ] ); } From c558e8226e8cf9b11a85682ea300bed4511236ba Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 05:16:11 +0200 Subject: [PATCH 11/88] fix allp test --- tests/node_allp.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/node_allp.rs b/tests/node_allp.rs index a516ff2..042c9f3 100644 --- a/tests/node_allp.rs +++ b/tests/node_allp.rs @@ -38,35 +38,35 @@ 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 - 3]); + 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 // cubic interpolation! - v.append(&mut vec![-0.01606, 0.13158, 0.54748]); + v.append(&mut vec![-0.03748519, 0.37841395, 0.5260659]); 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.5260659, 0.37841395, -0.03748519]); - v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 6]); + v.append(&mut vec![0.54748523, 0.13158606, -0.016065884]); + 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: v.append(&mut vec![ - -0.00035427228, - 0.006157537, - -0.005423375, - -0.1756484, - -0.39786762, - -0.3550714, + -0.0019286226, + 0.04086761, + -0.1813516, + -0.35157663, + -0.36315754, + -0.35664573, ]); v.append(&mut vec![-0.357; (2.0 * 44.1_f32).floor() as usize - 5]); v.append(&mut vec![ - -0.35664573, - -0.36315754, - -0.35157663, - -0.1813516, - 0.04086761, - -0.0019286226, + -0.3550714, + -0.39786762, + -0.1756484, + -0.005423375, + 0.006157537, + -0.00035427228, ]); v.append(&mut vec![0.0; 10]); From bad3e2043e101ac33d289609e73d9a3d967c81aa Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 05:21:56 +0200 Subject: [PATCH 12/88] fixed broken tests over delaybuffer fix --- tests/node_comb.rs | 29 +++++++++++------------ tests/node_delay.rs | 58 ++++++++++++++++++++++----------------------- tests/node_pverb.rs | 34 +++++++++++++------------- 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/tests/node_comb.rs b/tests/node_comb.rs index f86a550..2d137cc 100644 --- a/tests/node_comb.rs +++ b/tests/node_comb.rs @@ -34,30 +34,29 @@ fn check_node_comb_1() { fft, vec![ (0, 216), - (11, 221), - (22, 216), - (3381, 184), - (3391, 189), - (3402, 195), - (3413, 213), - (3424, 198), - (3435, 203), - (6815, 188), - (13587, 196), - (13598, 210), - (13609, 207), - (13620, 193) + (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, 186), (3015, 186), (7031, 191)]); + 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, 2008), (11, 1017)]); + assert_eq!( + fft, + vec![(0, 1979), (11, 1002), (980, 1245), (1960, 1144), (2929, 1569), (2939, 1545)] + ); } #[test] diff --git a/tests/node_delay.rs b/tests/node_delay.rs index 8e5977c..1bf5a7d 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.039102618, - -0.16390327, - 0.27611724, - -0.2608055, - 0.060164057, - 0.20197779, - -0.28871512, - 0.21515398, - -0.081471935, - 0.0023831273, + 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,8 +119,8 @@ 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.9513626, // the delay + input signal: + 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,15 +172,15 @@ 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, 112)); - assert_eq!(fft[5], (237, 112)); - assert_eq!(fft[10], (517, 111)); + 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); let fft = run_and_get_fft4096_now(&mut node_exec, 122); - assert_eq!(fft[0], (2509, 123)); - assert_eq!(fft[7], (2821, 123)); + assert_eq!(fft[0], (2498, 122)); + assert_eq!(fft[7], (2681, 122)); // Sweep at mostly highest point: run_for_ms(&mut node_exec, 700.0); @@ -241,7 +241,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 +274,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![(220, 106), (440, 53)]); + assert_eq!(idxs_big, vec![(222, 106), (444, 53)]); } #[test] @@ -306,7 +306,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 +341,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) ] ); From 7c26d1971febfd1b3b5d0dbd48e6b8294333ddad Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 05:23:20 +0200 Subject: [PATCH 13/88] Fixing sample player reverse --- CHANGELOG.md | 3 +++ src/dsp/node_sampl.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d29614..b3b1149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,6 @@ 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. diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index 7964a26..0fc9ca1 100644 --- a/src/dsp/node_sampl.rs +++ b/src/dsp/node_sampl.rs @@ -148,7 +148,7 @@ impl Sampl { return 0.0; } - let j = self.phase.floor() as usize; + let j = self.phase.floor() as usize % sd_len; let i = ((sd_len - 1) - j); let f = self.phase.fract(); From 2d1b989880d0cf008de9a1ffa9677b037a8cd242 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 05:25:09 +0200 Subject: [PATCH 14/88] Refactored Sampl --- src/dsp/node_sampl.rs | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/src/dsp/node_sampl.rs b/src/dsp/node_sampl.rs index 0fc9ca1..ce51506 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, cubic_interpolate}; +use super::helpers::{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,33 +142,23 @@ 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); - + let i = self.phase.floor() as usize % sd_len; let f = self.phase.fract(); - self.phase = j as f64 + f + sr_factor * speed; - - cubic_interpolate(&sample_data[..], sd_len, i, (1.0 - f) as f32) - } - - #[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; - let f = self.phase.fract(); - self.phase = (i % sd_len) as f64 + f + sr_factor * speed; + self.phase = i as f64 + f + sr_factor * speed; + 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) } @@ -256,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; From 2febe4a7e86bc5a8fd2214e103cc8918cd406a38 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 05:40:41 +0200 Subject: [PATCH 15/88] Improved documentation of the helpers --- src/dsp/helpers.rs | 83 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 5df3b93..f24782c 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -395,10 +395,21 @@ pub fn f_fold_distort(gain: f32, threshold: f32, i: f32) -> f32 { } } +/// Apply linear interpolation between the value a and b. +/// +/// * `a` - value at x=0.0 +/// * `b` - value at x=1.0 +/// * `x` - value between 0.0 and 1.0 to blend between `a` and `b`. +#[inline] pub fn lerp(x: f32, a: f32, b: f32) -> f32 { (a * (1.0 - x)) + (b * x) } +/// Apply 64bit linear interpolation between the value a and b. +/// +/// * `a` - value at x=0.0 +/// * `b` - value at x=1.0 +/// * `x` - value between 0.0 and 1.0 to blend between `a` and `b`. pub fn lerp64(x: f64, a: f64, b: f64) -> f64 { (a * (1.0 - x)) + (b * x) } @@ -558,6 +569,10 @@ pub const TRIG_LOW_THRES: f32 = 0.25; /// a logical '1'. Anything below this is a logical '0'. pub const TRIG_HIGH_THRES: f32 = 0.5; +/// Trigger signal generator for HexoDSP nodes. +/// +/// A trigger in HexoSynth and HexoDSP is commonly 2.0 milliseconds. +/// This generator generates a trigger signal when [TrigSignal::trigger] is called. #[derive(Debug, Clone, Copy)] pub struct TrigSignal { length: u32, @@ -565,24 +580,29 @@ pub struct TrigSignal { } impl TrigSignal { + /// Create a new trigger generator pub fn new() -> Self { Self { length: ((44100.0 * TRIG_SIGNAL_LENGTH_MS) / 1000.0).ceil() as u32, scount: 0 } } + /// Reset the trigger generator. pub fn reset(&mut self) { self.scount = 0; } + /// Set the sample rate to calculate the amount of samples for the trigger signal. pub fn set_sample_rate(&mut self, srate: f32) { self.length = ((srate * TRIG_SIGNAL_LENGTH_MS) / 1000.0).ceil() as u32; self.scount = 0; } + /// Enable sending a trigger impulse the next time [TrigSignal::next] is called. #[inline] pub fn trigger(&mut self) { self.scount = self.length; } + /// Trigger signal output. #[inline] pub fn next(&mut self) -> f32 { if self.scount > 0 { @@ -600,6 +620,9 @@ impl Default for TrigSignal { } } +/// Signal change detector that emits a trigger when the input signal changed. +/// +/// This is commonly used for control signals. It has not much use for audio signals. #[derive(Debug, Clone, Copy)] pub struct ChangeTrig { ts: TrigSignal, @@ -607,6 +630,7 @@ pub struct ChangeTrig { } impl ChangeTrig { + /// Create a new change detector pub fn new() -> Self { Self { ts: TrigSignal::new(), @@ -614,15 +638,20 @@ impl ChangeTrig { } } + /// Reset internal state. pub fn reset(&mut self) { self.ts.reset(); self.last = -100.0; } + /// Set the sample rate for the trigger signal generator pub fn set_sample_rate(&mut self, srate: f32) { self.ts.set_sample_rate(srate); } + /// Feed a new input signal sample. + /// + /// The return value is the trigger signal. #[inline] pub fn next(&mut self, inp: f32) -> f32 { if (inp - self.last).abs() > std::f32::EPSILON { @@ -640,21 +669,30 @@ impl Default for ChangeTrig { } } +/// Trigger signal detector for HexoDSP. +/// +/// Whenever you need to detect a trigger on an input you can use this component. +/// A trigger in HexoDSP is any signal over [TRIG_HIGH_THRES]. The internal state is +/// resetted when the signal drops below [TRIG_LOW_THRES]. #[derive(Debug, Clone, Copy)] pub struct Trigger { triggered: bool, } impl Trigger { + /// Create a new trigger detector. pub fn new() -> Self { Self { triggered: false } } + /// Reset the internal state of the trigger detector. #[inline] pub fn reset(&mut self) { self.triggered = false; } + /// Checks the input signal for a trigger and returns true when the signal + /// surpassed [TRIG_HIGH_THRES] and has not fallen below [TRIG_LOW_THRES] yet. #[inline] pub fn check_trigger(&mut self, input: f32) -> bool { if self.triggered { @@ -672,6 +710,10 @@ impl Trigger { } } +/// Generates a phase signal from a trigger/gate input signal. +/// +/// This helper allows you to measure the distance between trigger or gate pulses +/// and generates a phase signal for you that increases from 0.0 to 1.0. #[derive(Debug, Clone, Copy)] pub struct TriggerPhaseClock { clock_phase: f64, @@ -681,10 +723,12 @@ pub struct TriggerPhaseClock { } impl TriggerPhaseClock { + /// Create a new phase clock. pub fn new() -> Self { Self { clock_phase: 0.0, clock_inc: 0.0, prev_trigger: true, clock_samples: 0 } } + /// Reset the phase clock. #[inline] pub fn reset(&mut self) { self.clock_samples = 0; @@ -693,11 +737,16 @@ impl TriggerPhaseClock { self.clock_samples = 0; } + /// Restart the phase clock. It will count up from 0.0 again on [TriggerPhaseClock::next_phase]. #[inline] pub fn sync(&mut self) { self.clock_phase = 0.0; } + /// Generate the phase signal of this clock. + /// + /// * `clock_limit` - The maximum number of samples to detect two trigger signals in. + /// * `trigger_in` - Trigger signal input. #[inline] pub fn next_phase(&mut self, clock_limit: f64, trigger_in: f32) -> f64 { if self.prev_trigger { @@ -896,6 +945,8 @@ fn fclampc(x: F, mi: f64, mx: f64) -> F { /// _len_ is the buffer length to consider and wrap the index into. And _fract_ is the /// fractional part of the index. /// +/// This function is generic over f32 and f64. That means you can use your preferred float size. +/// /// Commonly used like this: /// ///``` @@ -958,6 +1009,11 @@ pub fn cubic_interpolate(data: &[F], len: usize, index: usize, fract: F) res } +/// This is a delay buffer/line with linear and cubic interpolation. +/// +/// It's the basic building block underneath the all-pass filter, comb filters and delay effects. +/// You can use linear and cubic and no interpolation to access samples in the past. Either +/// by sample offset or time (millisecond) based. #[derive(Debug, Clone, Default)] pub struct DelayBuffer { data: Vec, @@ -966,18 +1022,22 @@ pub struct DelayBuffer { } impl DelayBuffer { + /// Creates a delay buffer with about 5 seconds of capacity at 8*48000Hz sample rate. pub fn new() -> Self { Self { data: vec![f(0.0); DEFAULT_DELAY_BUFFER_SAMPLES], wr: 0, srate: f(44100.0) } } + /// Creates a delay buffer with the given amount of samples capacity. pub fn new_with_size(size: usize) -> Self { Self { data: vec![f(0.0); size], wr: 0, srate: f(44100.0) } } + /// Sets the sample rate that is used for milliseconds => sample conversion. pub fn set_sample_rate(&mut self, srate: F) { self.srate = srate; } + /// Reset the delay buffer contents and write position. pub fn reset(&mut self) { self.data.fill(f(0.0)); self.wr = 0; @@ -1037,7 +1097,7 @@ impl DelayBuffer { self.linear_interpolate_at(delay_time_ms) } - /// Fetch a sample from the delay buffer at the given time. + /// Fetch a sample from the delay buffer at the given tim with linear interpolation. /// /// * `delay_time_ms` - Delay time in milliseconds. #[inline] @@ -1045,7 +1105,7 @@ impl DelayBuffer { self.linear_interpolate_at_s((delay_time_ms * self.srate) / f(1000.0)) } - /// Fetch a sample from the delay buffer at the given offset. + /// Fetch a sample from the delay buffer at the given offset with linear interpolation. /// /// * `s_offs` - Sample offset in samples. #[inline] @@ -1072,7 +1132,7 @@ impl DelayBuffer { res } - /// Fetch a sample from the delay buffer at the given time. + /// Fetch a sample from the delay buffer at the given time with cubic interpolation. /// /// * `delay_time_ms` - Delay time in milliseconds. #[inline] @@ -1080,7 +1140,7 @@ impl DelayBuffer { self.cubic_interpolate_at_s((delay_time_ms * self.srate) / f(1000.0)) } - /// Fetch a sample from the delay buffer at the given offset. + /// Fetch a sample from the delay buffer at the given offset with cubic interpolation. /// /// * `s_offs` - Sample offset in samples into the past of the [DelayBuffer] /// from the current write (or the "now") position. @@ -1106,6 +1166,9 @@ impl DelayBuffer { res } + /// Fetch a sample from the delay buffer at the given time without any interpolation. + /// + /// * `delay_time_ms` - Delay time in milliseconds. #[inline] pub fn nearest_at(&self, delay_time_ms: F) -> F { let len = self.data.len(); @@ -1116,6 +1179,7 @@ impl DelayBuffer { self.data[idx] } + /// Fetch a sample from the delay buffer at the given number of samples in the past. #[inline] pub fn at(&self, delay_sample_count: usize) -> F { let len = self.data.len(); @@ -1129,29 +1193,39 @@ impl DelayBuffer { /// Default size of the delay buffer: 1 seconds at 8 times 48kHz const DEFAULT_ALLPASS_COMB_SAMPLES: usize = 8 * 48000; +/// An all-pass filter based on a delay line. #[derive(Debug, Clone, Default)] pub struct AllPass { delay: DelayBuffer, } impl AllPass { + /// Creates a new all-pass filter with about 1 seconds space for samples. pub fn new() -> Self { Self { delay: DelayBuffer::new_with_size(DEFAULT_ALLPASS_COMB_SAMPLES) } } + /// Set the sample rate for millisecond based access. pub fn set_sample_rate(&mut self, srate: F) { self.delay.set_sample_rate(srate); } + /// Reset the internal delay buffer. pub fn reset(&mut self) { self.delay.reset(); } + /// Access the internal delay at the given amount of milliseconds in the past. #[inline] pub fn delay_tap_n(&self, time_ms: F) -> F { self.delay.tap_n(time_ms) } + /// Retrieve the next sample from the all-pass filter while feeding in the next. + /// + /// * `time_ms` - Delay time in milliseconds. + /// * `g` - Feedback factor (usually something around 0.7 is common) + /// * `v` - The new input sample to feed the filter. #[inline] pub fn next(&mut self, time_ms: F, g: F, v: F) -> F { let s = self.delay.cubic_interpolate_at(time_ms); @@ -1266,6 +1340,7 @@ impl OnePoleLPF { self.a = f::(1.0) - self.b; } + #[inline] pub fn set_sample_rate(&mut self, srate: F) { self.israte = f::(1.0) / srate; self.recalc(); From d6f5ef70891ca43da6f3f401e072efceda759f69 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 05:42:05 +0200 Subject: [PATCH 16/88] . --- README.md | 3 +++ src/lib.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 1e91765..076aaf5 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,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/src/lib.rs b/src/lib.rs index aed0d45..7840467 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,6 +188,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. From fab61d330af25b8c810e111ea4c5b21d31d42acd Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 06:26:28 +0200 Subject: [PATCH 17/88] wrote a paragraph about node names --- src/dsp/mod.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index a4f996a..8423711 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -90,6 +90,25 @@ Input parameters can be connected to outputs of other DSP nodes. In contrast to 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. + ### Node Documentation **Attention: Defining the documentation for your DSP node is not optional. It's required to make From e6fad9086b2600d88a7964d152fe00f8cea2a0ff Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 20 Jul 2022 06:29:56 +0200 Subject: [PATCH 18/88] described signal ranges a bit more --- src/dsp/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 8423711..f0a671b 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -109,6 +109,21 @@ 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 From fa6af749de3ea9cba9ee3d0dfeaeae30b817c59f Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Thu, 21 Jul 2022 02:03:16 +0200 Subject: [PATCH 19/88] added AllPass::next_linear --- src/dsp/helpers.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index f24782c..4931e85 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -1221,7 +1221,8 @@ impl AllPass { self.delay.tap_n(time_ms) } - /// Retrieve the next sample from the all-pass filter while feeding in the next. + /// Retrieve the next (cubic interpolated) sample from the all-pass + /// filter while feeding in the next. /// /// * `time_ms` - Delay time in milliseconds. /// * `g` - Feedback factor (usually something around 0.7 is common) @@ -1233,6 +1234,20 @@ impl AllPass { self.delay.feed(input); input * g + s } + + /// Retrieve the next (linear interpolated) sample from the all-pass + /// filter while feeding in the next. + /// + /// * `time_ms` - Delay time in milliseconds. + /// * `g` - Feedback factor (usually something around 0.7 is common) + /// * `v` - The new input sample to feed the filter. + #[inline] + pub fn next_linear(&mut self, time_ms: F, g: F, v: F) -> F { + let s = self.delay.linear_interpolate_at(time_ms); + let input = v + -g * s; + self.delay.feed(input); + input * g + s + } } #[derive(Debug, Clone)] From 557b0b7121fb669913d6dca3afdadd345c7371af Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 23 Jul 2022 09:54:16 +0200 Subject: [PATCH 20/88] Added Matrix::get_connections. --- CHANGELOG.md | 2 ++ src/matrix.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++- tests/quant.rs | 2 +- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b1149..6e9b0c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,3 +8,5 @@ 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. diff --git a/src/matrix.rs b/src/matrix.rs index 57c1291..4dc1974 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -978,6 +978,39 @@ impl Matrix { } } + 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(( + (dir, node_io_idx), + (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 +1380,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_dir, src_io_idx) = res[0].0; + let (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_dir, src_io_idx) = res[1].0; + let (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 +1613,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/tests/quant.rs b/tests/quant.rs index 0f6c510..2761e57 100644 --- a/tests/quant.rs +++ b/tests/quant.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. mod common; -use common::*; +//use common::*; use hexodsp::d_pit; use hexodsp::dsp::helpers::Quantizer; From b2f869fe2b2261fc01d3263daebb39556fe04619 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 23 Jul 2022 11:50:01 +0200 Subject: [PATCH 21/88] comment the function --- src/matrix.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/matrix.rs b/src/matrix.rs index 4dc1974..84fa965 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -978,6 +978,14 @@ impl Matrix { } } + /// Retrieves the immediate connections to adjacent cells and returns a list. + /// + /// Returns a vector with pairs of this content: + /// + /// ( + /// (this_cell_connection_dir, this_cell_node_io_index), + /// (other_cell_connection_dir, other_cell_node_io_index, (other_cell_x, other_cell_y)) + /// ) pub fn get_connections( &self, x: usize, From 879bb46cc292579f6a564d5e3d10952fd8c57c75 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 23 Jul 2022 13:39:52 +0200 Subject: [PATCH 22/88] Changed get_connections API --- src/matrix.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/matrix.rs b/src/matrix.rs index 84fa965..d5a2280 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -979,18 +979,24 @@ 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: /// /// ( - /// (this_cell_connection_dir, this_cell_node_io_index), - /// (other_cell_connection_dir, other_cell_node_io_index, (other_cell_x, other_cell_y)) + /// (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> { + ) -> Option> { let this_cell = self.get(x, y)?; let mut ret = vec![]; @@ -1007,8 +1013,8 @@ impl Matrix { 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(( - (dir, node_io_idx), - (dir.flip(), other_node_io_idx, (nx, ny)), + (*this_cell, dir, node_io_idx), + (*other_cell, dir.flip(), other_node_io_idx, (nx, ny)), )); } } @@ -1407,8 +1413,8 @@ mod tests { let res = matrix.get_connections(1, 0); let res = res.expect("Found connected cells"); - let (src_dir, src_io_idx) = res[0].0; - let (dst_dir, dst_io_idx, (nx, ny)) = res[0].1; + 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"); @@ -1416,8 +1422,8 @@ mod tests { assert_eq!(dst_io_idx, 0, "Correct output port"); assert_eq!((nx, ny), (1, 1), "Correct other position"); - let (src_dir, src_io_idx) = res[1].0; - let (dst_dir, dst_io_idx, (nx, ny)) = res[1].1; + 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"); From 1a4be5421d737a34296ae1c72ffb2deec7dd79ee Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 23 Jul 2022 16:56:59 +0200 Subject: [PATCH 23/88] remove unnecessary &mut --- src/matrix.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix.rs b/src/matrix.rs index d5a2280..2591165 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -275,13 +275,13 @@ impl Cell { self } - /// Finds the first free input (one without an adjacent cell). If any free input + /// 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. /// 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 +320,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![]; @@ -345,7 +345,7 @@ impl Cell { } /// 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) { From 10bb0f96f49fdbd421a730b27cc7a4b114c32afd Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 06:58:09 +0200 Subject: [PATCH 24/88] Added MatrixCellChain abstraction --- src/lib.rs | 1 + src/matrix.rs | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 7840467..ed6096d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,6 +284,7 @@ pub mod matrix_repr; pub mod monitor; pub mod nodes; pub mod sample_lib; +pub mod chain_builder; mod util; pub use cell_dir::CellDir; diff --git a/src/matrix.rs b/src/matrix.rs index 2591165..2e5925f 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. @@ -149,6 +149,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 +265,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; From eff41e7ad5a2aab9fa68804aed4b9ab6213804f2 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 07:08:18 +0200 Subject: [PATCH 25/88] Forgot chain_builder.rs and added a real world test --- src/chain_builder.rs | 283 +++++++++++++++++++++++++++++++++++++++++++ tests/node_ad.rs | 7 +- 2 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 src/chain_builder.rs diff --git a/src/chain_builder.rs b/src/chain_builder.rs new file mode 100644 index 0000000..88b6e0c --- /dev/null +++ b/src/chain_builder.rs @@ -0,0 +1,283 @@ +// 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. +use crate::{Cell, CellDir, Matrix, NodeId, ParamId, SAtom}; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +struct MatrixChainLink { + cell: Cell, + x: i32, + y: i32, + params: Vec<(ParamId, SAtom)>, +} + +/// A DSP chain builder for the [hexodsp::Matrix]. +/// +/// This is an extremely easy API to create and place new DSP chains into the [hexodsp::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") +/// .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(Clone)] +pub struct MatrixCellChain { + chain: Vec, + error: Option, + dir: CellDir, + pos: (i32, i32), + param_idx: usize, +} + +#[derive(Debug, Clone)] +pub enum ChainError { + 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, + pos: (0, 0), + 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 [hexodsp::Cell] for this chain. + pub fn spawn_cell_from_node_id_name(&mut self, node_id: &str) -> Cell { + let node_id = NodeId::from_str(node_id); + + Cell::empty(node_id) + } + + /// Utility function to add a pre-built [hexodsp::Cell] as next link. + /// + /// This also sets the current parameter cell. + pub fn add_link(&mut self, cell: Cell) { + self.chain.push(MatrixChainLink { x: self.pos.0, y: self.pos.1, cell, params: vec![] }); + + let offs = self.dir.as_offs(self.pos.0 as usize); + self.pos.0 += offs.0; + self.pos.1 += offs.1; + + self.param_idx = self.chain.len() - 1; + } + + /// 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 { + let 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); + + 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 { + let 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); + + 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 { + let 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); + + 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(); + + for link in self.chain.iter() { + let (x, y) = (link.x, link.y); + let x = (x + (at_x as i32)) as usize; + let y = (y + (at_y as i32)) as usize; + + 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); + + println!("PLACE: ({},{}) {:?}", x, y, cell); + + matrix.place(x, y, cell); + } + + 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/tests/node_ad.rs b/tests/node_ad.rs index 53fc11d..0c5cf68 100644 --- a/tests/node_ad.rs +++ b/tests/node_ad.rs @@ -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 = hexodsp::chain_builder::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)); From ed1ac581cef710902ccb19b5579720f7207946e3 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 07:27:18 +0200 Subject: [PATCH 26/88] Improved documentation of MatrixCellChain --- src/chain_builder.rs | 33 +++++++++++++--- src/dsp/mod.rs | 94 +++++++++++++++----------------------------- src/lib.rs | 1 + tests/common/mod.rs | 1 + tests/node_ad.rs | 6 ++- 5 files changed, 66 insertions(+), 69 deletions(-) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index 88b6e0c..f1c10c5 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -1,6 +1,28 @@ // 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; @@ -12,16 +34,16 @@ struct MatrixChainLink { params: Vec<(ParamId, SAtom)>, } -/// A DSP chain builder for the [hexodsp::Matrix]. +/// A DSP chain builder for the [crate::Matrix]. /// -/// This is an extremely easy API to create and place new DSP chains into the [hexodsp::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") +/// chain.node_out("sin", "sig") /// .set_denorm("freq", 220.0) /// .node_io("amp", "inp", "sig") /// .set_denorm("att", 0.5) @@ -42,6 +64,7 @@ pub struct MatrixCellChain { param_idx: usize, } +/// Error type for the [crate::MatrixCellChain]. #[derive(Debug, Clone)] pub enum ChainError { UnknownOutput(NodeId, String), @@ -122,14 +145,14 @@ impl MatrixCellChain { self } - /// Utility function for creating [hexodsp::Cell] for this chain. + /// Utility function for creating [crate::Cell] for this chain. pub fn spawn_cell_from_node_id_name(&mut self, node_id: &str) -> Cell { let node_id = NodeId::from_str(node_id); Cell::empty(node_id) } - /// Utility function to add a pre-built [hexodsp::Cell] as next link. + /// 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) { diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index f0a671b..f079960 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -366,12 +366,13 @@ The start of your `tests/node_*.rs` file usually should look like this: 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); // ... } ``` @@ -393,81 +394,50 @@ The two parameters to _new_ are the width and height of the hex grid. 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); - let out = NodeId::Out(0); -``` - -You can have multiple instances for a node. The number in the parenthesis are -the instance index of that node. - -Next you want to layout the nodes adjacent to each other on the hexagonal grid. -This is a bit more tricky than with a rectangular grid. - -```ignore - 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)); - matrix.sync().unwrap(); -``` - -The `sync` is necessary to update the DSP graph. -When doing this, keep the following grid layout in mind: - -```text - _____ _____ - / \ / \ - / 0,0 \_____/ 2,0 \_____ - \ / \ / \ - \_____/ 1,0 \_____/ 3,0 \ - / \ / \ / - / 0,1 \_____/ 2,1 \_____/ - \ / \ / \ - \_____/ 1,1 \_____/ 3,1 \ - / \ / \ / - / 0,2 \_____/ 2,2 \_____/ - \ / \ / - \_____/ 1,2 \_____/ - \ / - \_____/ -``` - -Defining the outputs of a cell is done like this: - -```ignore - Cell::empty(ad).out(None, None, ad.out("sig")) -``` - -[crate::Cell::empty] takes a [NodeId] as first argument. The [crate::Cell] -structure then allows you to specify the output ports using the [crate::Cell::out] -function. The 3 arguments of that function are for the 3 edges of that hex tile: - -```ignore - // TopRight BottomRight Bottom - Cell::empty(ad).out(None, None, ad.out("sig")) -``` - -[crate::Cell::input] works the same way, but the 3 arguments refer to the 3 input -edges of a hex tile: - -```ignore - // Top TopLeft BottomLeft - Cell::empty(out).input(out.inp("ch1"), None, None) ``` 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(); diff --git a/src/lib.rs b/src/lib.rs index ed6096d..7704886 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -295,6 +295,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 chain_builder::MatrixCellChain; pub struct Context<'a, 'b, 'c, 'd> { pub nframes: usize, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0b1a3af..d6ff59b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -6,6 +6,7 @@ pub use hexodsp::dsp::*; pub use hexodsp::matrix::*; pub use hexodsp::nodes::new_node_engine; pub use hexodsp::NodeExecutor; +pub use hexodsp::MatrixCellChain; use hound; diff --git a/tests/node_ad.rs b/tests/node_ad.rs index 0c5cf68..ba1e3d8 100644 --- a/tests/node_ad.rs +++ b/tests/node_ad.rs @@ -10,8 +10,10 @@ 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 = hexodsp::chain_builder::MatrixCellChain::new(CellDir::B); - chain.node_out("ad", "sig").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap(); + 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); From ff202ea8910b23ed6c3e79d8ab73a3e081995dd4 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 07:34:45 +0200 Subject: [PATCH 27/88] Improved top level documentation --- CHANGELOG.md | 2 ++ README.md | 29 +++++++++++++++++++++++++++-- src/lib.rs | 29 +++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e9b0c2..085e13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,5 @@ 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. diff --git a/README.md b/README.md index 076aaf5..832e0ef 100644 --- a/README.md +++ b/README.md @@ -111,13 +111,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(); @@ -128,6 +131,28 @@ let (out_l, out_r) = node_exec.test_run(0.11, true); // samples now. ``` +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 diff --git a/src/lib.rs b/src/lib.rs index 7704886..f68aa3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,13 +113,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(); @@ -130,6 +133,28 @@ let (out_l, out_r) = node_exec.test_run(0.11, true); // samples now. ``` +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 From 3bdb52f2384bb7e0925888b5516282006cfe7950 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 07:46:37 +0200 Subject: [PATCH 28/88] Added MatrixCellChain::node() --- src/chain_builder.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index f1c10c5..dbfb08b 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -165,6 +165,14 @@ impl MatrixCellChain { 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 { + let cell = self.spawn_cell_from_node_id_name(node_id); + self.add_link(cell); + 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 { let mut cell = self.spawn_cell_from_node_id_name(node_id); From a559d689ebd5dff3b1a4eaea701ad6b747e86d48 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 07:52:53 +0200 Subject: [PATCH 29/88] Derive Debug on MatrixCellChain --- src/chain_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index dbfb08b..47819d0 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -55,7 +55,7 @@ struct MatrixChainLink { /// /// chain.place(&mut matrix, 2, 2).expect("no error in this case"); ///``` -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct MatrixCellChain { chain: Vec, error: Option, From b98f978ab5fd7590bb9c56edb8708e1923949af7 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 08:12:59 +0200 Subject: [PATCH 30/88] Fixed a bug in MatrixCellChain placement --- src/chain_builder.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index 47819d0..1d541a1 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -29,8 +29,7 @@ use std::collections::HashMap; #[derive(Debug, Clone)] struct MatrixChainLink { cell: Cell, - x: i32, - y: i32, + dir: CellDir, params: Vec<(ParamId, SAtom)>, } @@ -60,7 +59,6 @@ pub struct MatrixCellChain { chain: Vec, error: Option, dir: CellDir, - pos: (i32, i32), param_idx: usize, } @@ -80,7 +78,6 @@ impl MatrixCellChain { dir, chain: vec![], error: None, - pos: (0, 0), param_idx: 0, } } @@ -156,12 +153,7 @@ impl MatrixCellChain { /// /// This also sets the current parameter cell. pub fn add_link(&mut self, cell: Cell) { - self.chain.push(MatrixChainLink { x: self.pos.0, y: self.pos.1, cell, params: vec![] }); - - let offs = self.dir.as_offs(self.pos.0 as usize); - self.pos.0 += offs.0; - self.pos.1 += offs.1; - + self.chain.push(MatrixChainLink { dir: self.dir, cell, params: vec![] }); self.param_idx = self.chain.len() - 1; } @@ -227,10 +219,10 @@ impl MatrixCellChain { let mut last_unused = HashMap::new(); + let mut pos = (at_x, at_y); + for link in self.chain.iter() { - let (x, y) = (link.x, link.y); - let x = (x + (at_x as i32)) as usize; - let y = (y + (at_y as i32)) as usize; + let (x, y) = pos; let mut cell = link.cell.clone(); @@ -251,6 +243,10 @@ impl MatrixCellChain { println!("PLACE: ({},{}) {:?}", x, y, cell); 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() { From ee8bbd36a16fa3ea60de8a3354981ff2314cf322 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 08:14:25 +0200 Subject: [PATCH 31/88] remove debug print --- src/chain_builder.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index 1d541a1..c608210 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -240,8 +240,6 @@ impl MatrixCellChain { cell.set_node_id_keep_ios(node_id); - println!("PLACE: ({},{}) {:?}", x, y, cell); - matrix.place(x, y, cell); let offs = link.dir.as_offs(pos.0); From 480aa8d9c5c00beca8a9e52526250ffa6c8ec041 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 09:06:58 +0200 Subject: [PATCH 32/88] Extended error reporting on MatrixCellChain --- src/chain_builder.rs | 62 +++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index c608210..6d47d6b 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -65,6 +65,7 @@ pub struct MatrixCellChain { /// Error type for the [crate::MatrixCellChain]. #[derive(Debug, Clone)] pub enum ChainError { + UnknownNodeId(String), UnknownOutput(NodeId, String), UnknownInput(NodeId, String), } @@ -143,10 +144,13 @@ impl MatrixCellChain { } /// Utility function for creating [crate::Cell] for this chain. - pub fn spawn_cell_from_node_id_name(&mut self, node_id: &str) -> Cell { - let node_id = NodeId::from_str(node_id); + 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; + } - Cell::empty(node_id) + Some(Cell::empty(node_id)) } /// Utility function to add a pre-built [crate::Cell] as next link. @@ -160,51 +164,61 @@ impl MatrixCellChain { /// 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 { - let cell = self.spawn_cell_from_node_id_name(node_id); - self.add_link(cell); + 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 { - let mut cell = self.spawn_cell_from_node_id_name(node_id); + 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())); + } - 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.add_link(cell); - 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 { - let mut cell = self.spawn_cell_from_node_id_name(node_id); + 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_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.add_link(cell); - 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 { - let mut cell = self.spawn_cell_from_node_id_name(node_id); + 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_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())); } - 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); - self } From 51453bae16d69881d1dcc7cd8737957e6c512854 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 12:19:49 +0200 Subject: [PATCH 33/88] reformatting --- src/chain_builder.rs | 15 +++++++-------- src/lib.rs | 4 ++-- tests/common/mod.rs | 2 +- tests/node_ad.rs | 4 +--- tests/node_delay.rs | 15 +++++++++++++-- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/chain_builder.rs b/src/chain_builder.rs index 6d47d6b..4ad0d72 100644 --- a/src/chain_builder.rs +++ b/src/chain_builder.rs @@ -22,7 +22,6 @@ The [crate::MatrixCellChain] abstractions allows very easy placement of DSP sign ``` */ - use crate::{Cell, CellDir, Matrix, NodeId, ParamId, SAtom}; use std::collections::HashMap; @@ -75,12 +74,7 @@ impl MatrixCellChain { /// /// 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, - } + Self { dir, chain: vec![], error: None, param_idx: 0 } } fn output_dir(&self) -> CellDir { @@ -226,7 +220,12 @@ impl MatrixCellChain { /// /// 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> { + 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); } diff --git a/src/lib.rs b/src/lib.rs index f68aa3a..034e480 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -301,6 +301,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; @@ -309,10 +310,10 @@ pub mod matrix_repr; pub mod monitor; pub mod nodes; pub mod sample_lib; -pub mod chain_builder; 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}; @@ -320,7 +321,6 @@ 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 chain_builder::MatrixCellChain; pub struct Context<'a, 'b, 'c, 'd> { pub nframes: usize, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d6ff59b..80785d7 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,8 +5,8 @@ pub use hexodsp::dsp::*; pub use hexodsp::matrix::*; pub use hexodsp::nodes::new_node_engine; -pub use hexodsp::NodeExecutor; pub use hexodsp::MatrixCellChain; +pub use hexodsp::NodeExecutor; use hound; diff --git a/tests/node_ad.rs b/tests/node_ad.rs index ba1e3d8..59dfccf 100644 --- a/tests/node_ad.rs +++ b/tests/node_ad.rs @@ -11,9 +11,7 @@ fn check_node_ad_1() { 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(); + chain.node_out("ad", "sig").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap(); matrix.sync().unwrap(); let ad = NodeId::Ad(0); diff --git a/tests/node_delay.rs b/tests/node_delay.rs index 1bf5a7d..23a16e5 100644 --- a/tests/node_delay.rs +++ b/tests/node_delay.rs @@ -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.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 + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 ] ); } From f1e3dc8aaec69196a54bedb777b0a567fdcfb7e8 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 13:03:02 +0200 Subject: [PATCH 34/88] added more information to the automated test section in dsp/mod.rs --- src/dsp/mod.rs | 18 ++++++++++++++++++ src/matrix.rs | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index f079960..c5b0975 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -355,6 +355,24 @@ 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 diff --git a/src/matrix.rs b/src/matrix.rs index 2e5925f..5519680 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -314,7 +314,7 @@ impl Cell { } /// 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. + /// 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( From 7bcca553fc73fea28ae8d4cd76614d51b0289d9d Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 19:04:08 +0200 Subject: [PATCH 35/88] Added find_unconnected_ports --- src/matrix.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/matrix.rs b/src/matrix.rs index 5519680..6dd169e 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -379,7 +379,34 @@ impl Cell { } } - free_ports.to_vec() + free_ports + } + + /// Finds all dangling ports in the specified direction. + /// With `dir` you can specify input with `CellDir::T`, output with `CellDir::B` + /// and any with `CellDir::C`. + pub fn find_unconnected_ports( + &self, + m: &Matrix, + dir: CellDir, + ) -> Vec<(CellDir, (usize, usize))> { + 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 let Some(pos) = self.is_port_dir_connected(m, *dir) { + unused_ports.push((*dir, pos)); + } + } + + unused_ports } /// If the port is connected, it will return the position of the other cell. From a336ae94203f1e462b70517fa6d54216fe6c2534 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 19:11:02 +0200 Subject: [PATCH 36/88] Fix find_unconnected_ports --- src/matrix.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/matrix.rs b/src/matrix.rs index 6dd169e..21653b7 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -401,8 +401,10 @@ impl Cell { }; for dir in options { - if let Some(pos) = self.is_port_dir_connected(m, *dir) { - unused_ports.push((*dir, pos)); + if self.is_port_dir_connected(m, *dir).is_none() { + if let Some(pos) = dir.offs_pos((self.x as usize, self.y as usize)) { + unused_ports.push((*dir, pos)); + } } } From 5ec75c3062f0b8eeb214df88db9ecae4a5591498 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 24 Jul 2022 19:12:43 +0200 Subject: [PATCH 37/88] Fix find_unconnected_ports --- src/matrix.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/matrix.rs b/src/matrix.rs index 21653b7..3585bad 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -385,11 +385,7 @@ impl Cell { /// Finds all dangling ports in the specified direction. /// With `dir` you can specify input with `CellDir::T`, output with `CellDir::B` /// and any with `CellDir::C`. - pub fn find_unconnected_ports( - &self, - m: &Matrix, - dir: CellDir, - ) -> Vec<(CellDir, (usize, usize))> { + pub fn find_unconnected_ports(&self, m: &Matrix, dir: CellDir) -> Vec { let mut unused_ports = vec![]; let options: &[CellDir] = if dir == CellDir::C { @@ -402,9 +398,7 @@ impl Cell { for dir in options { if self.is_port_dir_connected(m, *dir).is_none() { - if let Some(pos) = dir.offs_pos((self.x as usize, self.y as usize)) { - unused_ports.push((*dir, pos)); - } + unused_ports.push(*dir); } } From e7507b2dd0a027b020d97d3ea50e163380d7b934 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 25 Jul 2022 05:09:06 +0200 Subject: [PATCH 38/88] Slight documentation improvement --- README.md | 8 +++++--- src/lib.rs | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 832e0ef..f25ae7b 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,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::*; @@ -131,6 +131,8 @@ 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: diff --git a/src/lib.rs b/src/lib.rs index 034e480..6bf0243 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,9 +103,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::*; @@ -133,6 +133,8 @@ 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: From c73199185b9c64e0510d76d7ec885c66cb92fefb Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 25 Jul 2022 05:49:00 +0200 Subject: [PATCH 39/88] Hide the inner workings of the UnsyncFloatBuf more --- src/lib.rs | 2 + src/unsync_float_buf.rs | 129 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/unsync_float_buf.rs diff --git a/src/lib.rs b/src/lib.rs index 6bf0243..fb81ad0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -312,6 +312,7 @@ pub mod matrix_repr; pub mod monitor; pub mod nodes; pub mod sample_lib; +pub mod unsync_float_buf; mod util; pub use cell_dir::CellDir; @@ -323,6 +324,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 unsync_float_buf::UnsyncFloatBuf; pub struct Context<'a, 'b, 'c, 'd> { pub nframes: usize, diff --git a/src/unsync_float_buf.rs b/src/unsync_float_buf.rs new file mode 100644 index 0000000..5103c57 --- /dev/null +++ b/src/unsync_float_buf.rs @@ -0,0 +1,129 @@ +// 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::util::AtomicFloat; +use std::sync::Arc; + +/// A float buffer that can be written to and read from in an unsynchronized manner. +/// +/// One use case is writing samples to this buffer in the audio thread while +/// a GUI thread reads from this buffer. Mostly useful for an oscilloscope. +/// +///``` +/// use hexodsp::UnsyncFloatBuf; +/// +/// let handle1 = UnsyncFloatBuf::new_with_len(10); +/// let handle2 = handle1.clone(); +/// +/// std::thread::spawn(move || { +/// handle1.write(9, 2032.0); +/// }).join().unwrap(); +/// +/// std::thread::spawn(move || { +/// assert_eq!(handle2.read(9), 2032.0); +/// assert_eq!(handle2.read(20), 0.0); // out of range! +/// }).join().unwrap(); +///``` +#[derive(Debug, Clone)] +pub struct UnsyncFloatBuf(Arc); + +impl UnsyncFloatBuf { + /// Creates a new unsynchronized float buffer with the given length. + pub fn new_with_len(len: usize) -> Self { + Self(UnsyncFloatBufImpl::new_shared(len)) + } + + /// Write float to the given index. + /// + /// If index is out of range, nothing will be written. + pub fn write(&self, idx: usize, v: f32) { + self.0.write(idx, v) + } + + /// Reads a float from the given index. + /// + /// If index is out of range, 0.0 will be returned. + pub fn read(&self, idx: usize) -> f32 { + self.0.read(idx) + } +} + +#[derive(Debug)] +struct UnsyncFloatBufImpl { + data_store: Vec, + len: usize, + ptr: *mut AtomicFloat, +} + +unsafe impl Sync for UnsyncFloatBuf {} +unsafe impl Send for UnsyncFloatBuf {} + +impl UnsyncFloatBufImpl { + fn new_shared(len: usize) -> Arc { + let mut rc = Arc::new(Self { data_store: Vec::new(), len, ptr: std::ptr::null_mut() }); + + let mut unsync_buf = Arc::get_mut(&mut rc).expect("No other reference to this Arc"); + unsync_buf.data_store.resize_with(len, || AtomicFloat::new(0.0)); + // Taking the pointer to the Vec data buffer is fine, + // because it will not be moved when inside the Arc. + unsync_buf.ptr = unsync_buf.data_store.as_mut_ptr(); + + rc + } + + fn write(&self, idx: usize, v: f32) { + if idx < self.len { + unsafe { + (*self.ptr.add(idx)).set(v); + } + } + } + + fn read(&self, idx: usize) -> f32 { + if idx < self.len { + unsafe { (*self.ptr.add(idx)).get() } + } else { + 0.0 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_unsync_float_buf_working() { + let handle1 = UnsyncFloatBuf::new_with_len(512); + for i in 0..512 { + handle1.write(i, i as f32); + } + let handle2 = handle1.clone(); + for i in 0..512 { + assert_eq!(handle2.read(i), i as f32); + } + } + + #[test] + fn check_unsync_float_buf_thread() { + let handle1 = UnsyncFloatBuf::new_with_len(512); + let handle2 = handle1.clone(); + + std::thread::spawn(move || { + for i in 0..512 { + handle1.write(i, i as f32); + } + }) + .join() + .unwrap(); + + std::thread::spawn(move || { + for i in 0..512 { + assert_eq!(handle2.read(i), i as f32); + } + }) + .join() + .unwrap(); + } +} From 64ece501ad7eaa612f4599893cefd724f79b4276 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 25 Jul 2022 05:55:54 +0200 Subject: [PATCH 40/88] Added a scope probe node, to provide direct signal views from the frontend --- src/dsp/mod.rs | 6 ++++ src/dsp/node_scope.rs | 77 +++++++++++++++++++++++++++++++++++++++++ src/matrix.rs | 5 +++ src/nodes/mod.rs | 2 ++ src/nodes/node_conf.rs | 18 ++++++++-- src/unsync_float_buf.rs | 9 +++++ tests/node_scope.rs | 32 +++++++++++++++++ 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/dsp/node_scope.rs create mode 100644 tests/node_scope.rs diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index c5b0975..7a2270c 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -535,6 +535,8 @@ mod node_tseq; mod node_tslfo; #[allow(non_upper_case_globals)] mod node_vosc; +#[allow(non_upper_case_globals)] +mod node_scope; pub mod biquad; pub mod dattorro; @@ -605,6 +607,7 @@ use node_test::Test; use node_tseq::TSeq; use node_tslfo::TsLFO; use node_vosc::VOsc; +use node_scope::Scope; pub const MIDI_MAX_FREQ: f32 = 13289.75; @@ -1403,6 +1406,9 @@ 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 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + [0 sig], ad => Ad UIType::Generic UICategory::Mod (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) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs new file mode 100644 index 0000000..d6d5268 --- /dev/null +++ b/src/dsp/node_scope.rs @@ -0,0 +1,77 @@ +// 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 super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger}; +use crate::dsp::{ + DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom, +}; +use crate::nodes::{NodeAudioContext, NodeExecContext}; +use crate::UnsyncFloatBuf; + +/// A simple signal scope +#[derive(Debug, Clone)] +pub struct Scope { + buf: UnsyncFloatBuf, + idx: usize, +} + +impl Scope { + pub fn new(_nid: &NodeId) -> Self { + Self { buf: UnsyncFloatBuf::new_with_len(1), idx: 0 } + } + pub const inp: &'static str = "Scope inp\nSignal input.\nRange: (-1..1)\n"; + pub const sig: &'static str = + "Scope sig\nSignal output. The exact same signal that was received on 'inp'!\n"; + pub const DESC: &'static str = r#"Signal Oscilloscope Probe + +This is a signal oscilloscope probe node. You can have up to 16 of these, +which will be displayed in the GUI. +"#; + pub const HELP: &'static str = r#"Scope - Signal Oscilloscope Probe + +You can have up to 16 of these probes in your patch. The received signal will be +forwarded to the GUI and you can inspect the waveform there. +"#; + + pub fn set_scope_buffer(&mut self, buf: UnsyncFloatBuf) { + self.buf = buf; + } +} + +impl DspNode for Scope { + fn outputs() -> usize { + 1 + } + + fn set_sample_rate(&mut self, _srate: f32) {} + + fn reset(&mut self) {} + + #[inline] + fn process( + &mut self, + ctx: &mut T, + _ectx: &mut NodeExecContext, + _nctx: &NodeContext, + _atoms: &[SAtom], + inputs: &[ProcBuf], + outputs: &mut [ProcBuf], + ctx_vals: LedPhaseVals, + ) { + use crate::dsp::{inp, out}; + + let inp = inp::Ad::inp(inputs); + let out = out::Scope::sig(outputs); + + for frame in 0..ctx.nframes() { + let in_val = inp.read(frame); + self.buf.write(self.idx, in_val); + self.idx = (self.idx + 1) % self.buf.len(); + out.write(frame, in_val); + } + + let last_frame = ctx.nframes() - 1; + ctx_vals[0].set(out.read(last_frame)); + } +} diff --git a/src/matrix.rs b/src/matrix.rs index 3585bad..f6a3d80 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -9,6 +9,7 @@ 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::UnsyncFloatBuf; use std::collections::{HashMap, HashSet}; @@ -581,6 +582,10 @@ impl Matrix { self.config.get_pattern_data(tracker_id) } + pub fn get_scope_buffer(&self, scope: usize) -> Option { + self.config.get_scope_buffer(scope) + } + /// Checks if pattern data updates need to be sent to the /// DSP thread. pub fn check_pattern_data(&mut self, tracker_id: usize) { diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index cd450c5..bdbf419 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -3,6 +3,8 @@ // See README.md and COPYING for details. pub const MAX_ALLOCATED_NODES: usize = 256; +pub const MAX_SCOPES: usize = 16; +pub const SCOPE_SAMPLES: usize = 512; pub const MAX_INPUTS: usize = 32; pub const MAX_SMOOTHERS: usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs pub const MAX_AVAIL_TRACKERS: usize = 128; diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index ba9f313..7b091fa 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -3,14 +3,15 @@ // 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_TRACKERS, MAX_INPUTS, UNUSED_MONITOR_IDX, MAX_SCOPES, SCOPE_SAMPLES }; 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; +use crate::unsync_float_buf::UnsyncFloatBuf; use crate::SampleLibrary; use ringbuf::{Producer, RingBuffer}; @@ -177,6 +178,8 @@ pub struct NodeConfigurator { pub(crate) node2idx: HashMap, /// Holding the tracker sequencers pub(crate) trackers: Vec, + /// Holding the scope buffers: + pub(crate) scopes: Vec, /// The shared parts of the [NodeConfigurator] /// and the [crate::nodes::NodeExecutor]. pub(crate) shared: SharedNodeConf, @@ -279,6 +282,7 @@ impl NodeConfigurator { atom_values: std::collections::HashMap::new(), node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], + scopes: vec![UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES); MAX_SCOPES], }, shared_exec, ) @@ -638,6 +642,10 @@ impl NodeConfigurator { } } + pub fn get_scope_buffer(&self, scope: usize) -> Option { + self.scopes.get(scope).cloned() + } + pub fn get_pattern_data(&self, tracker_id: usize) -> Option>> { if tracker_id >= self.trackers.len() { return None; @@ -677,6 +685,12 @@ impl NodeConfigurator { } } + if let Node::Scope { node } = &mut node { + if let Some(buf) = self.scopes.get(ni.instance()) { + node.set_scope_buffer(buf.clone()); + } + } + for i in 0..self.nodes.len() { if let NodeId::Nop = self.nodes[i].0.to_id() { index = Some(i); diff --git a/src/unsync_float_buf.rs b/src/unsync_float_buf.rs index 5103c57..d9f7f6f 100644 --- a/src/unsync_float_buf.rs +++ b/src/unsync_float_buf.rs @@ -47,6 +47,11 @@ impl UnsyncFloatBuf { pub fn read(&self, idx: usize) -> f32 { self.0.read(idx) } + + /// Length of the buffer. + pub fn len(&self) -> usize { + self.0.len() + } } #[derive(Debug)] @@ -87,6 +92,10 @@ impl UnsyncFloatBufImpl { 0.0 } } + + fn len(&self) -> usize { + self.len + } } #[cfg(test)] diff --git a/tests/node_scope.rs b/tests/node_scope.rs new file mode 100644 index 0000000..36bfa4a --- /dev/null +++ b/tests/node_scope.rs @@ -0,0 +1,32 @@ +// 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; + +#[test] +fn check_node_scope_1() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let mut chain = MatrixCellChain::new(CellDir::B); + chain.node_inp("scope", "inp").place(&mut matrix, 0, 0).unwrap(); + matrix.sync().unwrap(); + + let scope = NodeId::Scope(0); + let inp_p = scope.inp_param("inp").unwrap(); + + matrix.set_param(inp_p, SAtom::param(1.0)); + let _res = run_for_ms(&mut node_exec, 11.0); + + let scope = matrix.get_scope_buffer(0).unwrap(); + let mut v = vec![]; + for x in 0..SCOPE_SAMPLES { + v.push(scope.read(x)); + } + + assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); +} From 8d0dbe797ca550dea3ce3d5bbe3cee8045c89eb1 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 25 Jul 2022 06:15:48 +0200 Subject: [PATCH 41/88] capture 3 signals at once --- src/dsp/mod.rs | 5 +++-- src/dsp/node_scope.rs | 50 ++++++++++++++++++++++++----------------- src/matrix.rs | 4 ++-- src/nodes/mod.rs | 2 +- src/nodes/node_conf.rs | 21 +++++++++++------ src/unsync_float_buf.rs | 13 ++++++++++- tests/node_scope.rs | 25 ++++++++++++++++----- 7 files changed, 82 insertions(+), 38 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 7a2270c..579030a 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1407,8 +1407,9 @@ macro_rules! node_list { (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 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) - [0 sig], + (0 in1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (0 in2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) + (0 in3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0), 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) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index d6d5268..0a31119 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -3,38 +3,42 @@ // See README.md and COPYING for details. //use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger}; -use crate::dsp::{ - DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom, -}; +use crate::nodes::SCOPE_SAMPLES; +use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::{NodeAudioContext, NodeExecContext}; use crate::UnsyncFloatBuf; /// A simple signal scope #[derive(Debug, Clone)] pub struct Scope { - buf: UnsyncFloatBuf, + buf: [UnsyncFloatBuf; 3], idx: usize, } impl Scope { pub fn new(_nid: &NodeId) -> Self { - Self { buf: UnsyncFloatBuf::new_with_len(1), idx: 0 } + let buf = [ + UnsyncFloatBuf::new_with_len(1), + UnsyncFloatBuf::new_with_len(1), + UnsyncFloatBuf::new_with_len(1), + ]; + Self { buf, idx: 0 } } - pub const inp: &'static str = "Scope inp\nSignal input.\nRange: (-1..1)\n"; - pub const sig: &'static str = - "Scope sig\nSignal output. The exact same signal that was received on 'inp'!\n"; + pub const 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 DESC: &'static str = r#"Signal Oscilloscope Probe -This is a signal oscilloscope probe node. You can have up to 16 of these, -which will be displayed in the GUI. +This is a signal oscilloscope probe node, you can capture up to 3 signals. "#; pub const HELP: &'static str = r#"Scope - Signal Oscilloscope Probe -You can have up to 16 of these probes in your patch. The received signal will be -forwarded to the GUI and you can inspect the waveform there. +You can have up to 8 different scopes in your patch. That means you can in theory +record up to 24 signals. The received signal will be forwarded to the GUI and +you can inspect the waveform there. "#; - pub fn set_scope_buffer(&mut self, buf: UnsyncFloatBuf) { + pub fn set_scope_buffers(&mut self, buf: [UnsyncFloatBuf; 3]) { self.buf = buf; } } @@ -61,17 +65,23 @@ impl DspNode for Scope { ) { use crate::dsp::{inp, out}; - let inp = inp::Ad::inp(inputs); - let out = out::Scope::sig(outputs); + let in1 = inp::Scope::in1(inputs); + let in2 = inp::Scope::in2(inputs); + let in3 = inp::Scope::in3(inputs); + let inputs = [in1, in2, in3]; for frame in 0..ctx.nframes() { - let in_val = inp.read(frame); - self.buf.write(self.idx, in_val); - self.idx = (self.idx + 1) % self.buf.len(); - out.write(frame, in_val); + for (i, input) in inputs.iter().enumerate() { + let in_val = input.read(frame); + self.buf[i].write(self.idx, in_val); + } + + self.idx = (self.idx + 1) % SCOPE_SAMPLES; } let last_frame = ctx.nframes() - 1; - ctx_vals[0].set(out.read(last_frame)); + 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/matrix.rs b/src/matrix.rs index f6a3d80..6e30476 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -582,8 +582,8 @@ impl Matrix { self.config.get_pattern_data(tracker_id) } - pub fn get_scope_buffer(&self, scope: usize) -> Option { - self.config.get_scope_buffer(scope) + pub fn get_scope_buffers(&self, scope: usize) -> Option<[UnsyncFloatBuf; 3]> { + self.config.get_scope_buffers(scope) } /// Checks if pattern data updates need to be sent to the diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index bdbf419..06e6e55 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. pub const MAX_ALLOCATED_NODES: usize = 256; -pub const MAX_SCOPES: usize = 16; +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 diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 7b091fa..25bfff6 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -3,15 +3,15 @@ // See README.md and COPYING for details. use super::{ - FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, - MAX_AVAIL_TRACKERS, MAX_INPUTS, UNUSED_MONITOR_IDX, MAX_SCOPES, SCOPE_SAMPLES + FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS, + MAX_INPUTS, MAX_SCOPES, SCOPE_SAMPLES, UNUSED_MONITOR_IDX, }; 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; use crate::unsync_float_buf::UnsyncFloatBuf; +use crate::util::AtomicFloat; use crate::SampleLibrary; use ringbuf::{Producer, RingBuffer}; @@ -179,7 +179,7 @@ pub struct NodeConfigurator { /// Holding the tracker sequencers pub(crate) trackers: Vec, /// Holding the scope buffers: - pub(crate) scopes: Vec, + pub(crate) scopes: Vec<[UnsyncFloatBuf; 3]>, /// The shared parts of the [NodeConfigurator] /// and the [crate::nodes::NodeExecutor]. pub(crate) shared: SharedNodeConf, @@ -282,7 +282,14 @@ impl NodeConfigurator { atom_values: std::collections::HashMap::new(), node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], - scopes: vec![UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES); MAX_SCOPES], + scopes: vec![ + [ + UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES), + UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES), + UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES) + ]; + MAX_SCOPES + ], }, shared_exec, ) @@ -642,7 +649,7 @@ impl NodeConfigurator { } } - pub fn get_scope_buffer(&self, scope: usize) -> Option { + pub fn get_scope_buffers(&self, scope: usize) -> Option<[UnsyncFloatBuf; 3]> { self.scopes.get(scope).cloned() } @@ -687,7 +694,7 @@ impl NodeConfigurator { if let Node::Scope { node } = &mut node { if let Some(buf) = self.scopes.get(ni.instance()) { - node.set_scope_buffer(buf.clone()); + node.set_scope_buffers(buf.clone()); } } diff --git a/src/unsync_float_buf.rs b/src/unsync_float_buf.rs index d9f7f6f..8449921 100644 --- a/src/unsync_float_buf.rs +++ b/src/unsync_float_buf.rs @@ -54,6 +54,10 @@ impl UnsyncFloatBuf { } } +/// Private implementation detail for [UnsyncFloatBuf]. +/// +/// This mostly allows [UnsyncFloatBuf] to wrap UnsyncFloatBufImpl into an [std::sync::Arc], +/// to make sure the `data_store` Vector is not moved accidentally. #[derive(Debug)] struct UnsyncFloatBufImpl { data_store: Vec, @@ -65,18 +69,23 @@ unsafe impl Sync for UnsyncFloatBuf {} unsafe impl Send for UnsyncFloatBuf {} impl UnsyncFloatBufImpl { + /// Create a new shared reference of this. You must not create + /// an UnsyncFloatBufImpl that can move! Otherwise the internal pointer + /// would be invalidated. fn new_shared(len: usize) -> Arc { let mut rc = Arc::new(Self { data_store: Vec::new(), len, ptr: std::ptr::null_mut() }); let mut unsync_buf = Arc::get_mut(&mut rc).expect("No other reference to this Arc"); unsync_buf.data_store.resize_with(len, || AtomicFloat::new(0.0)); - // Taking the pointer to the Vec data buffer is fine, + + // XXX: Taking the pointer to the Vec data buffer is fine, // because it will not be moved when inside the Arc. unsync_buf.ptr = unsync_buf.data_store.as_mut_ptr(); rc } + /// Write a sample. fn write(&self, idx: usize, v: f32) { if idx < self.len { unsafe { @@ -85,6 +94,7 @@ impl UnsyncFloatBufImpl { } } + /// Read a sample. fn read(&self, idx: usize) -> f32 { if idx < self.len { unsafe { (*self.ptr.add(idx)).get() } @@ -93,6 +103,7 @@ impl UnsyncFloatBufImpl { } } + /// Return the length of this buffer. fn len(&self) -> usize { self.len } diff --git a/tests/node_scope.rs b/tests/node_scope.rs index 36bfa4a..6523050 100644 --- a/tests/node_scope.rs +++ b/tests/node_scope.rs @@ -13,20 +13,35 @@ fn check_node_scope_1() { let mut matrix = Matrix::new(node_conf, 3, 3); let mut chain = MatrixCellChain::new(CellDir::B); - chain.node_inp("scope", "inp").place(&mut matrix, 0, 0).unwrap(); + chain.node_inp("scope", "in1").place(&mut matrix, 0, 0).unwrap(); matrix.sync().unwrap(); let scope = NodeId::Scope(0); - let inp_p = scope.inp_param("inp").unwrap(); + let in1_p = scope.inp_param("in1").unwrap(); + let in2_p = scope.inp_param("in2").unwrap(); + let in3_p = scope.inp_param("in3").unwrap(); - matrix.set_param(inp_p, SAtom::param(1.0)); + matrix.set_param(in1_p, SAtom::param(1.0)); + matrix.set_param(in2_p, SAtom::param(1.0)); + matrix.set_param(in3_p, SAtom::param(1.0)); let _res = run_for_ms(&mut node_exec, 11.0); - let scope = matrix.get_scope_buffer(0).unwrap(); + let scope = matrix.get_scope_buffers(0).unwrap(); let mut v = vec![]; for x in 0..SCOPE_SAMPLES { - v.push(scope.read(x)); + v.push(scope[0].read(x)); } + assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); + let mut v = vec![]; + for x in 0..SCOPE_SAMPLES { + v.push(scope[1].read(x)); + } + assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); + + let mut v = vec![]; + for x in 0..SCOPE_SAMPLES { + v.push(scope[2].read(x)); + } assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); } From 544c0084ffc22a794b7a36e52c85fab8fa1e18b8 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 25 Jul 2022 06:36:07 +0200 Subject: [PATCH 42/88] Get rid of the weird thingie... --- src/dsp/node_scope.rs | 28 ++++---- src/lib.rs | 4 +- src/matrix.rs | 6 +- src/nodes/node_conf.rs | 24 +++---- src/scope_handle.rs | 51 ++++++++++++++ src/unsync_float_buf.rs | 149 ---------------------------------------- tests/node_scope.rs | 8 +-- 7 files changed, 83 insertions(+), 187 deletions(-) create mode 100644 src/scope_handle.rs delete mode 100644 src/unsync_float_buf.rs diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 0a31119..5c31070 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -3,26 +3,22 @@ // See README.md and COPYING for details. //use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger}; -use crate::nodes::SCOPE_SAMPLES; use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; +use crate::nodes::SCOPE_SAMPLES; use crate::nodes::{NodeAudioContext, NodeExecContext}; -use crate::UnsyncFloatBuf; +use crate::ScopeHandle; +use std::sync::Arc; /// A simple signal scope #[derive(Debug, Clone)] pub struct Scope { - buf: [UnsyncFloatBuf; 3], + handle: Arc, idx: usize, } impl Scope { pub fn new(_nid: &NodeId) -> Self { - let buf = [ - UnsyncFloatBuf::new_with_len(1), - UnsyncFloatBuf::new_with_len(1), - UnsyncFloatBuf::new_with_len(1), - ]; - Self { buf, idx: 0 } + Self { handle: ScopeHandle::new_shared(), idx: 0 } } 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"; @@ -38,8 +34,8 @@ record up to 24 signals. The received signal will be forwarded to the GUI and you can inspect the waveform there. "#; - pub fn set_scope_buffers(&mut self, buf: [UnsyncFloatBuf; 3]) { - self.buf = buf; + pub fn set_scope_handle(&mut self, handle: Arc) { + self.handle = handle; } } @@ -57,23 +53,25 @@ impl DspNode for Scope { &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, - _nctx: &NodeContext, + nctx: &NodeContext, _atoms: &[SAtom], inputs: &[ProcBuf], - outputs: &mut [ProcBuf], + _outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals, ) { - use crate::dsp::{inp, out}; + use crate::dsp::inp; let in1 = inp::Scope::in1(inputs); let in2 = inp::Scope::in2(inputs); let in3 = inp::Scope::in3(inputs); let inputs = [in1, in2, in3]; + self.handle.set_active_from_mask(nctx.in_connected); + for frame in 0..ctx.nframes() { for (i, input) in inputs.iter().enumerate() { let in_val = input.read(frame); - self.buf[i].write(self.idx, in_val); + self.handle.write(i, self.idx, in_val); } self.idx = (self.idx + 1) % SCOPE_SAMPLES; diff --git a/src/lib.rs b/src/lib.rs index fb81ad0..dd03d86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -312,7 +312,7 @@ pub mod matrix_repr; pub mod monitor; pub mod nodes; pub mod sample_lib; -pub mod unsync_float_buf; +pub mod scope_handle; mod util; pub use cell_dir::CellDir; @@ -324,7 +324,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 unsync_float_buf::UnsyncFloatBuf; +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 6e30476..1e1a408 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -9,7 +9,7 @@ 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::UnsyncFloatBuf; +use crate::ScopeHandle; use std::collections::{HashMap, HashSet}; @@ -582,8 +582,8 @@ impl Matrix { self.config.get_pattern_data(tracker_id) } - pub fn get_scope_buffers(&self, scope: usize) -> Option<[UnsyncFloatBuf; 3]> { - self.config.get_scope_buffers(scope) + pub fn get_scope_handle(&self, scope: usize) -> Option> { + self.config.get_scope_handle(scope) } /// Checks if pattern data updates need to be sent to the diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 25bfff6..b9c8dad 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -4,15 +4,15 @@ use super::{ FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS, - MAX_INPUTS, MAX_SCOPES, SCOPE_SAMPLES, UNUSED_MONITOR_IDX, + MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, }; 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::unsync_float_buf::UnsyncFloatBuf; use crate::util::AtomicFloat; use crate::SampleLibrary; +use crate::ScopeHandle; use ringbuf::{Producer, RingBuffer}; use std::collections::HashMap; @@ -179,7 +179,7 @@ pub struct NodeConfigurator { /// Holding the tracker sequencers pub(crate) trackers: Vec, /// Holding the scope buffers: - pub(crate) scopes: Vec<[UnsyncFloatBuf; 3]>, + pub(crate) scopes: Vec>, /// The shared parts of the [NodeConfigurator] /// and the [crate::nodes::NodeExecutor]. pub(crate) shared: SharedNodeConf, @@ -266,6 +266,9 @@ impl NodeConfigurator { let (shared, shared_exec) = SharedNodeConf::new(); + let mut scopes = vec![]; + scopes.resize_with(MAX_SCOPES, || ScopeHandle::new_shared()); + ( NodeConfigurator { nodes, @@ -282,14 +285,7 @@ impl NodeConfigurator { atom_values: std::collections::HashMap::new(), node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], - scopes: vec![ - [ - UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES), - UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES), - UnsyncFloatBuf::new_with_len(SCOPE_SAMPLES) - ]; - MAX_SCOPES - ], + scopes, }, shared_exec, ) @@ -649,7 +645,7 @@ impl NodeConfigurator { } } - pub fn get_scope_buffers(&self, scope: usize) -> Option<[UnsyncFloatBuf; 3]> { + pub fn get_scope_handle(&self, scope: usize) -> Option> { self.scopes.get(scope).cloned() } @@ -693,8 +689,8 @@ impl NodeConfigurator { } if let Node::Scope { node } = &mut node { - if let Some(buf) = self.scopes.get(ni.instance()) { - node.set_scope_buffers(buf.clone()); + if let Some(handle) = self.scopes.get(ni.instance()) { + node.set_scope_handle(handle.clone()); } } diff --git a/src/scope_handle.rs b/src/scope_handle.rs new file mode 100644 index 0000000..fbc322b --- /dev/null +++ b/src/scope_handle.rs @@ -0,0 +1,51 @@ +// 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 crate::util::AtomicFloat; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +#[derive(Debug)] +pub struct ScopeHandle { + bufs: [Vec; 3], + active: [AtomicBool; 3], +} + +impl ScopeHandle { + pub fn new_shared() -> Arc { + let mut v1 = vec![]; + v1.resize_with(SCOPE_SAMPLES, || AtomicFloat::new(0.0)); + let mut v2 = vec![]; + v2.resize_with(SCOPE_SAMPLES, || AtomicFloat::new(0.0)); + let mut v3 = vec![]; + v3.resize_with(SCOPE_SAMPLES, || AtomicFloat::new(0.0)); + Arc::new(Self { + bufs: [v1, v2, v3], + active: [AtomicBool::new(false), AtomicBool::new(false), AtomicBool::new(false)], + }) + } + + pub fn write(&self, buf_idx: usize, idx: usize, v: f32) { + self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v); + } + + pub fn read(&self, buf_idx: usize, idx: usize) -> 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/unsync_float_buf.rs b/src/unsync_float_buf.rs deleted file mode 100644 index 8449921..0000000 --- a/src/unsync_float_buf.rs +++ /dev/null @@ -1,149 +0,0 @@ -// 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::util::AtomicFloat; -use std::sync::Arc; - -/// A float buffer that can be written to and read from in an unsynchronized manner. -/// -/// One use case is writing samples to this buffer in the audio thread while -/// a GUI thread reads from this buffer. Mostly useful for an oscilloscope. -/// -///``` -/// use hexodsp::UnsyncFloatBuf; -/// -/// let handle1 = UnsyncFloatBuf::new_with_len(10); -/// let handle2 = handle1.clone(); -/// -/// std::thread::spawn(move || { -/// handle1.write(9, 2032.0); -/// }).join().unwrap(); -/// -/// std::thread::spawn(move || { -/// assert_eq!(handle2.read(9), 2032.0); -/// assert_eq!(handle2.read(20), 0.0); // out of range! -/// }).join().unwrap(); -///``` -#[derive(Debug, Clone)] -pub struct UnsyncFloatBuf(Arc); - -impl UnsyncFloatBuf { - /// Creates a new unsynchronized float buffer with the given length. - pub fn new_with_len(len: usize) -> Self { - Self(UnsyncFloatBufImpl::new_shared(len)) - } - - /// Write float to the given index. - /// - /// If index is out of range, nothing will be written. - pub fn write(&self, idx: usize, v: f32) { - self.0.write(idx, v) - } - - /// Reads a float from the given index. - /// - /// If index is out of range, 0.0 will be returned. - pub fn read(&self, idx: usize) -> f32 { - self.0.read(idx) - } - - /// Length of the buffer. - pub fn len(&self) -> usize { - self.0.len() - } -} - -/// Private implementation detail for [UnsyncFloatBuf]. -/// -/// This mostly allows [UnsyncFloatBuf] to wrap UnsyncFloatBufImpl into an [std::sync::Arc], -/// to make sure the `data_store` Vector is not moved accidentally. -#[derive(Debug)] -struct UnsyncFloatBufImpl { - data_store: Vec, - len: usize, - ptr: *mut AtomicFloat, -} - -unsafe impl Sync for UnsyncFloatBuf {} -unsafe impl Send for UnsyncFloatBuf {} - -impl UnsyncFloatBufImpl { - /// Create a new shared reference of this. You must not create - /// an UnsyncFloatBufImpl that can move! Otherwise the internal pointer - /// would be invalidated. - fn new_shared(len: usize) -> Arc { - let mut rc = Arc::new(Self { data_store: Vec::new(), len, ptr: std::ptr::null_mut() }); - - let mut unsync_buf = Arc::get_mut(&mut rc).expect("No other reference to this Arc"); - unsync_buf.data_store.resize_with(len, || AtomicFloat::new(0.0)); - - // XXX: Taking the pointer to the Vec data buffer is fine, - // because it will not be moved when inside the Arc. - unsync_buf.ptr = unsync_buf.data_store.as_mut_ptr(); - - rc - } - - /// Write a sample. - fn write(&self, idx: usize, v: f32) { - if idx < self.len { - unsafe { - (*self.ptr.add(idx)).set(v); - } - } - } - - /// Read a sample. - fn read(&self, idx: usize) -> f32 { - if idx < self.len { - unsafe { (*self.ptr.add(idx)).get() } - } else { - 0.0 - } - } - - /// Return the length of this buffer. - fn len(&self) -> usize { - self.len - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn check_unsync_float_buf_working() { - let handle1 = UnsyncFloatBuf::new_with_len(512); - for i in 0..512 { - handle1.write(i, i as f32); - } - let handle2 = handle1.clone(); - for i in 0..512 { - assert_eq!(handle2.read(i), i as f32); - } - } - - #[test] - fn check_unsync_float_buf_thread() { - let handle1 = UnsyncFloatBuf::new_with_len(512); - let handle2 = handle1.clone(); - - std::thread::spawn(move || { - for i in 0..512 { - handle1.write(i, i as f32); - } - }) - .join() - .unwrap(); - - std::thread::spawn(move || { - for i in 0..512 { - assert_eq!(handle2.read(i), i as f32); - } - }) - .join() - .unwrap(); - } -} diff --git a/tests/node_scope.rs b/tests/node_scope.rs index 6523050..b2a1769 100644 --- a/tests/node_scope.rs +++ b/tests/node_scope.rs @@ -26,22 +26,22 @@ fn check_node_scope_1() { matrix.set_param(in3_p, SAtom::param(1.0)); let _res = run_for_ms(&mut node_exec, 11.0); - let scope = matrix.get_scope_buffers(0).unwrap(); + let scope = matrix.get_scope_handle(0).unwrap(); let mut v = vec![]; for x in 0..SCOPE_SAMPLES { - v.push(scope[0].read(x)); + v.push(scope.read(0, x)); } assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); let mut v = vec![]; for x in 0..SCOPE_SAMPLES { - v.push(scope[1].read(x)); + v.push(scope.read(1, x)); } assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); let mut v = vec![]; for x in 0..SCOPE_SAMPLES { - v.push(scope[2].read(x)); + v.push(scope.read(2, x)); } assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); } From f3437c895d705f76be954400938f570f608aab69 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 25 Jul 2022 06:47:57 +0200 Subject: [PATCH 43/88] fix offset in scope --- src/dsp/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 579030a..912d6de 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1408,8 +1408,8 @@ macro_rules! node_list { [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) - (0 in2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) - (0 in3 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), 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) From 2f307a3c2e68d82bce17b82b9013071cdee08146 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 26 Jul 2022 06:14:49 +0200 Subject: [PATCH 44/88] added more input parameters to the scope --- src/dsp/mod.rs | 13 ++++++++++++- src/dsp/node_scope.rs | 44 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 912d6de..23f433b 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -579,6 +579,7 @@ use crate::fa_smap_mode; use crate::fa_test_s; use crate::fa_tseq_cmode; use crate::fa_vosc_ovrsmpl; +use crate::fa_scope_tsrc; use node_ad::Ad; use node_allp::AllP; @@ -1409,7 +1410,17 @@ macro_rules! node_list { 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), + (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_lfot 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_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) + (10 gain2 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) + (11 gain3 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) + {12 0 tsrc setting(0) mode fa_scope_tsrc 0 1}, 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) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 5c31070..521d3ce 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -9,6 +9,19 @@ 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 { @@ -23,15 +36,38 @@ impl Scope { 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.\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. +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 theory -record up to 24 signals. The received signal will be forwarded to the GUI and -you can inspect the waveform there. +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'. The 'thrsh' parameter +is the trigger detection parameter. That means, if your signal passes that +trigger from negative to positive, 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'. + +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) { From a025c7fbe227f741e48760627088a22a1cb2cc55 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 26 Jul 2022 06:51:41 +0200 Subject: [PATCH 45/88] Reworked scope DSP algorithm --- src/dsp/helpers.rs | 47 ++++++++++++++++++++++ src/dsp/mod.rs | 7 ++-- src/dsp/node_scope.rs | 92 ++++++++++++++++++++++++++++++++++++------- src/scope_handle.rs | 7 ++++ 4 files changed, 136 insertions(+), 17 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 4931e85..9725a04 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -710,6 +710,53 @@ impl Trigger { } } +/// Trigger signal detector with custom range. +/// +/// Whenever you need to detect a trigger with a custom threshold. +#[derive(Debug, Clone, Copy)] +pub struct CustomTrigger { + triggered: bool, + low_thres: f32, + high_thres: f32, +} + +impl CustomTrigger { + /// Create a new trigger detector. + pub fn new(low_thres: f32, high_thres: f32) -> Self { + Self { triggered: false, low_thres, high_thres } + } + + pub fn set_threshold(&mut self, low_thres: f32, high_thres: f32) { + self.low_thres = low_thres; + self.high_thres = high_thres; + } + + /// Reset the internal state of the trigger detector. + #[inline] + pub fn reset(&mut self) { + self.triggered = false; + } + + /// Checks the input signal for a trigger and returns true when the signal + /// surpassed the high threshold and has not fallen below low threshold yet. + #[inline] + pub fn check_trigger(&mut self, input: f32) -> bool { +// println!("TRIG CHECK: {} <> {}", input, self.high_thres); + if self.triggered { + if input <= self.low_thres { + self.triggered = false; + } + + false + } else if input > self.high_thres { + self.triggered = true; + true + } else { + false + } + } +} + /// Generates a phase signal from a trigger/gate input signal. /// /// This helper allows you to measure the distance between trigger or gate pulses diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 23f433b..05dd957 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1232,6 +1232,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} @@ -1417,9 +1418,9 @@ macro_rules! node_list { (6 off1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (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_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) - (10 gain2 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) - (11 gain3 n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) + (9 gain1 n_xgin d_xgin r_id f_def stp_d 0.0, 1.0, 1.0) + (10 gain2 n_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 1}, ad => Ad UIType::Generic UICategory::Mod (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 521d3ce..3c97f75 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -1,8 +1,14 @@ // 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 crate::dsp::helpers::CustomTrigger; use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::SCOPE_SAMPLES; use crate::nodes::{NodeAudioContext, NodeExecContext}; @@ -10,7 +16,7 @@ use crate::ScopeHandle; use std::sync::Arc; #[macro_export] -macro_rules! fa_scope_tsrc { +macro_rules! fa_scope_tsrc { ($formatter: expr, $v: expr, $denorm_v: expr) => {{ let s = match ($v.round() as usize) { 0 => "Off", @@ -27,24 +33,37 @@ macro_rules! fa_scope_tsrc { pub struct Scope { handle: Arc, idx: usize, + frame_count: usize, + srate_ms: f32, + trig: CustomTrigger, } impl Scope { pub fn new(_nid: &NodeId) -> Self { - Self { handle: ScopeHandle::new_shared(), idx: 0 } + Self { + handle: ScopeHandle::new_shared(), + idx: 0, + srate_ms: 44.1, + frame_count: 0, + trig: CustomTrigger::new(0.0, 0.0001), + } } pub const in1: &'static str = "Scope in1\nSignal input 1.\nRange: (-1..1)\n"; pub const 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 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 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.\nRange: (-1..1)\n"; pub const DESC: &'static str = r#"Signal Oscilloscope Probe @@ -80,7 +99,9 @@ impl DspNode for Scope { 1 } - fn set_sample_rate(&mut self, _srate: f32) {} + fn set_sample_rate(&mut self, srate: f32) { + self.srate_ms = srate / 1000.0; + } fn reset(&mut self) {} @@ -95,22 +116,65 @@ impl DspNode for Scope { _outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals, ) { - use crate::dsp::inp; + use crate::dsp::{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 inputs = [in1, in2, in3]; self.handle.set_active_from_mask(nctx.in_connected); - for frame in 0..ctx.nframes() { - for (i, input) in inputs.iter().enumerate() { - let in_val = input.read(frame); - self.handle.write(i, self.idx, in_val); - } + let time = denorm::Scope::time(time, 0); + let samples_per_block = (time * self.srate_ms) / SCOPE_SAMPLES as f32; + let threshold = denorm::Scope::thrsh(thrsh, 0); + self.trig.set_threshold(threshold, threshold + 0.001); - self.idx = (self.idx + 1) % SCOPE_SAMPLES; + let trigger_input = in1; + + if samples_per_block < 1.0 { + let copy_count = ((1.0 / samples_per_block) as usize).min(SCOPE_SAMPLES); + + for frame in 0..ctx.nframes() { + if self.idx < SCOPE_SAMPLES { + for (i, input) in inputs.iter().enumerate() { + let in_val = input.read(frame); + self.handle.write_copies(i, self.idx, copy_count, in_val); + } + + self.idx = self.idx.saturating_add(copy_count); + } + + if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) { + self.frame_count = 0; + self.idx = 0; + } + } + } else { + let samples_per_block = samples_per_block as usize; + + for frame in 0..ctx.nframes() { + if self.idx < SCOPE_SAMPLES { + if self.frame_count >= samples_per_block { + for (i, input) in inputs.iter().enumerate() { + let in_val = input.read(frame); + self.handle.write(i, self.idx, in_val); + } + + self.idx = self.idx.saturating_add(1); + self.frame_count = 0; + } + + self.frame_count += 1; + } + + if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) { + self.frame_count = 0; + self.idx = 0; + } + } } let last_frame = ctx.nframes() - 1; diff --git a/src/scope_handle.rs b/src/scope_handle.rs index fbc322b..3f8cb75 100644 --- a/src/scope_handle.rs +++ b/src/scope_handle.rs @@ -27,6 +27,13 @@ impl ScopeHandle { }) } + pub fn write_copies(&self, buf_idx: usize, idx: usize, copies: usize, v: f32) { + let end = (idx + copies).min(SCOPE_SAMPLES); + for i in idx..end { + self.bufs[buf_idx % 3][i % SCOPE_SAMPLES].set(v); + } + } + pub fn write(&self, buf_idx: usize, idx: usize, v: f32) { self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v); } From 30284e27dc7c1dd461b2fdc31b935f22ae84f1f7 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 26 Jul 2022 06:58:30 +0200 Subject: [PATCH 46/88] smoother time parameter for the scope --- src/dsp/mod.rs | 18 +++++++++++++++++- src/dsp/node_scope.rs | 19 +++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 05dd957..f1c4fb4 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1215,6 +1215,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 }; @@ -1412,7 +1428,7 @@ macro_rules! node_list { (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_lfot stp_f 0.0, 1.0, 1000.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) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 3c97f75..3dc0788 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -33,7 +33,7 @@ macro_rules! fa_scope_tsrc { pub struct Scope { handle: Arc, idx: usize, - frame_count: usize, + frame_time: f32, srate_ms: f32, trig: CustomTrigger, } @@ -44,7 +44,7 @@ impl Scope { handle: ScopeHandle::new_shared(), idx: 0, srate_ms: 44.1, - frame_count: 0, + frame_time: 0.0, trig: CustomTrigger::new(0.0, 0.0001), } } @@ -129,11 +129,14 @@ impl DspNode for Scope { let time = denorm::Scope::time(time, 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.001); let trigger_input = in1; + //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); @@ -148,30 +151,30 @@ impl DspNode for Scope { } if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) { - self.frame_count = 0; + self.frame_time = 0.0; self.idx = 0; } } } else { - let samples_per_block = samples_per_block as usize; +// let samples_per_block = samples_per_block as usize; for frame in 0..ctx.nframes() { if self.idx < SCOPE_SAMPLES { - if self.frame_count >= samples_per_block { + if self.frame_time >= time_per_block { for (i, input) in inputs.iter().enumerate() { let in_val = input.read(frame); self.handle.write(i, self.idx, in_val); } self.idx = self.idx.saturating_add(1); - self.frame_count = 0; + self.frame_time -= time_per_block; } - self.frame_count += 1; + self.frame_time += sample_time; } if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) { - self.frame_count = 0; + self.frame_time = 0.0; self.idx = 0; } } From db906e7c2be184fbb3106db2fc4d70ccd6c8782c Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 02:02:54 +0200 Subject: [PATCH 47/88] converted scope_handle to store min/max values for a proper scope --- src/dsp/mod.rs | 2 +- src/dsp/node_scope.rs | 71 +++++++++++++++++++++++++++++-------------- src/scope_handle.rs | 18 +++++------ src/util.rs | 64 +++++++++++++++++++++++++++++++++++++- 4 files changed, 121 insertions(+), 34 deletions(-) diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index f1c4fb4..6972f2a 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -1437,7 +1437,7 @@ macro_rules! node_list { (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 1}, + {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) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 3dc0788..ea6bce1 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -35,6 +35,7 @@ pub struct Scope { idx: usize, frame_time: f32, srate_ms: f32, + cur_mm: Box<[(f32, f32); 3]>, trig: CustomTrigger, } @@ -45,6 +46,7 @@ impl Scope { 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), } } @@ -64,7 +66,7 @@ impl Scope { "Scope gain2\nVisual amplification/attenuation of the signal input 2.\nRange: (0..1)\n"; pub const gain3: &'static str = "Scope gain3\nVisual amplification/attenuation of the signal input 3.\nRange: (0..1)\n"; - pub const tsrc: &'static str = "Scope tsrc\nTriggering allows you to capture fast signals or pinning fast waveforms into the scope view for better inspection.\nRange: (-1..1)\n"; + pub const tsrc: &'static str = "Scope tsrc\nTriggering allows you to capture fast signals or pinning fast waveforms into the scope view for better inspection. 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. @@ -76,13 +78,15 @@ 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'. The 'thrsh' parameter -is the trigger detection parameter. That means, if your signal passes that -trigger from negative to positive, the signal recording will be -reset to that point. +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'. +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 @@ -111,30 +115,34 @@ impl DspNode for Scope { ctx: &mut T, _ectx: &mut NodeExecContext, nctx: &NodeContext, - _atoms: &[SAtom], + atoms: &[SAtom], inputs: &[ProcBuf], _outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals, ) { - use crate::dsp::{denorm, inp}; + 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 inputs = [in1, in2, in3]; self.handle.set_active_from_mask(nctx.in_connected); - let time = denorm::Scope::time(time, 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.001); - let trigger_input = in1; + let trigger_input = if tsrc.i() == 2 { trig } else { in1 }; + let trigger_disabled = tsrc.i() == 0; //d// println!("TIME time={}; st={}; tpb={}; frame_time={}", time, sample_time, time_per_block, self.frame_time); if samples_per_block < 1.0 { @@ -144,28 +152,39 @@ impl DspNode for Scope { if self.idx < SCOPE_SAMPLES { for (i, input) in inputs.iter().enumerate() { let in_val = input.read(frame); - self.handle.write_copies(i, self.idx, copy_count, in_val); + self.handle.write_oversampled(i, self.idx, copy_count, in_val); } self.idx = self.idx.saturating_add(copy_count); } - if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) { - self.frame_time = 0.0; - self.idx = 0; + 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 samples_per_block = samples_per_block as usize; + 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 { - if self.frame_time >= time_per_block { - for (i, input) in inputs.iter().enumerate() { - let in_val = input.read(frame); - self.handle.write(i, self.idx, in_val); - } + for (i, input) in inputs.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..inputs.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; } @@ -173,9 +192,15 @@ impl DspNode for Scope { self.frame_time += sample_time; } - if self.idx >= SCOPE_SAMPLES && self.trig.check_trigger(trigger_input.read(frame)) { - self.frame_time = 0.0; - self.idx = 0; + 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; + } } } } diff --git a/src/scope_handle.rs b/src/scope_handle.rs index 3f8cb75..6c1e99a 100644 --- a/src/scope_handle.rs +++ b/src/scope_handle.rs @@ -3,42 +3,42 @@ // See README.md and COPYING for details. use crate::nodes::SCOPE_SAMPLES; -use crate::util::AtomicFloat; +use crate::util::AtomicFloatPair; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; #[derive(Debug)] pub struct ScopeHandle { - bufs: [Vec; 3], + bufs: [Vec; 3], active: [AtomicBool; 3], } impl ScopeHandle { pub fn new_shared() -> Arc { let mut v1 = vec![]; - v1.resize_with(SCOPE_SAMPLES, || AtomicFloat::new(0.0)); + v1.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default()); let mut v2 = vec![]; - v2.resize_with(SCOPE_SAMPLES, || AtomicFloat::new(0.0)); + v2.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default()); let mut v3 = vec![]; - v3.resize_with(SCOPE_SAMPLES, || AtomicFloat::new(0.0)); + v3.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default()); Arc::new(Self { bufs: [v1, v2, v3], active: [AtomicBool::new(false), AtomicBool::new(false), AtomicBool::new(false)], }) } - pub fn write_copies(&self, buf_idx: usize, idx: usize, copies: usize, v: f32) { + 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); + self.bufs[buf_idx % 3][i % SCOPE_SAMPLES].set((v, v)); } } - pub fn write(&self, buf_idx: usize, idx: usize, v: f32) { + 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 { + pub fn read(&self, buf_idx: usize, idx: usize) -> (f32, f32) { self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].get() } diff --git a/src/util.rs b/src/util.rs index 4ae966d..6ccb4fc 100644 --- a/src/util.rs +++ b/src/util.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 std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; const SMOOTHING_TIME_MS: f32 = 10.0; @@ -152,3 +152,65 @@ impl From for f32 { value.get() } } + +/// The AtomicFloatPair can store two `f32` numbers atomically. +/// +/// This is useful for storing eg. min and max values of a sampled signal. +pub struct AtomicFloatPair { + atomic: AtomicU64, +} + +impl AtomicFloatPair { + /// New atomic float with initial value `value`. + pub fn new(v: (f32, f32)) -> AtomicFloatPair { + AtomicFloatPair { + atomic: AtomicU64::new(((v.0.to_bits() as u64) << 32) | (v.1.to_bits() as u64)), + } + } + + /// Get the current value of the atomic float. + #[inline] + pub fn get(&self) -> (f32, f32) { + let v = self.atomic.load(Ordering::Relaxed); + (f32::from_bits((v >> 32 & 0xFFFFFFFF) as u32), f32::from_bits((v & 0xFFFFFFFF) as u32)) + } + + /// Set the value of the atomic float to `value`. + #[inline] + pub fn set(&self, v: (f32, f32)) { + let v = ((v.0.to_bits() as u64) << 32) | (v.1.to_bits()) as u64; + self.atomic.store(v, Ordering::Relaxed) + } +} + +impl Default for AtomicFloatPair { + fn default() -> Self { + AtomicFloatPair::new((0.0, 0.0)) + } +} + +impl std::fmt::Debug for AtomicFloatPair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let v = self.get(); + write!(f, "({}, {})", v.0, v.1) + } +} + +impl std::fmt::Display for AtomicFloatPair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let v = self.get(); + write!(f, "({}, {})", v.0, v.1) + } +} + +impl From<(f32, f32)> for AtomicFloatPair { + fn from(value: (f32, f32)) -> Self { + AtomicFloatPair::new((value.0, value.1)) + } +} + +impl From for (f32, f32) { + fn from(value: AtomicFloatPair) -> Self { + value.get() + } +} From 78446e659e6c8af5ad76afd463b062137efa2901 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 05:52:54 +0200 Subject: [PATCH 48/88] Pass through offs and gain for the scope --- src/dsp/node_scope.rs | 20 ++++++++++++++++---- src/scope_handle.rs | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index ea6bce1..8d30b1c 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -129,9 +129,21 @@ impl DspNode for Scope { let thrsh = inp::Scope::thrsh(inputs); let trig = inp::Scope::trig(inputs); let tsrc = at::Scope::tsrc(atoms); - let inputs = [in1, in2, in3]; + 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; @@ -150,7 +162,7 @@ impl DspNode for Scope { for frame in 0..ctx.nframes() { if self.idx < SCOPE_SAMPLES { - for (i, input) in inputs.iter().enumerate() { + 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); } @@ -174,14 +186,14 @@ impl DspNode for Scope { for frame in 0..ctx.nframes() { if self.idx < SCOPE_SAMPLES { - for (i, input) in inputs.iter().enumerate() { + 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..inputs.len() { + for i in 0..input_bufs.len() { self.handle.write(i, self.idx, cur_mm[i]); } *cur_mm = [(-99999.0, 99999.0); 3]; diff --git a/src/scope_handle.rs b/src/scope_handle.rs index 6c1e99a..41ee1f2 100644 --- a/src/scope_handle.rs +++ b/src/scope_handle.rs @@ -11,6 +11,7 @@ use std::sync::Arc; pub struct ScopeHandle { bufs: [Vec; 3], active: [AtomicBool; 3], + offs_gain: [AtomicFloatPair; 3], } impl ScopeHandle { @@ -24,6 +25,11 @@ impl ScopeHandle { 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(), + ], }) } @@ -34,6 +40,14 @@ impl ScopeHandle { } } + 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 write(&self, buf_idx: usize, idx: usize, v: (f32, f32)) { self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v); } From c1f09e33f750125c095784930b4a4dc1c4d614bc Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 05:55:07 +0200 Subject: [PATCH 49/88] Noted the Scope feature in CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 085e13b..52b1bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,3 +12,5 @@ the cubic interpolation and tested it seperately now. 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. From 5bfe9e8c97e28c58023bf0eb40a89c58625a76d7 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 06:02:06 +0200 Subject: [PATCH 50/88] Added threshold to ScopeHandle --- src/dsp/node_scope.rs | 11 ++++++++--- src/scope_handle.rs | 21 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 8d30b1c..ea1298d 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -135,15 +135,18 @@ impl DspNode for Scope { self.handle.set_offs_gain( 0, denorm::Scope::off1(inp::Scope::off1(inputs), 0), - denorm::Scope::gain1(inp::Scope::gain1(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)); + 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)); + 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; @@ -156,6 +159,8 @@ impl DspNode for Scope { 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); diff --git a/src/scope_handle.rs b/src/scope_handle.rs index 41ee1f2..ec5337e 100644 --- a/src/scope_handle.rs +++ b/src/scope_handle.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. use crate::nodes::SCOPE_SAMPLES; -use crate::util::AtomicFloatPair; +use crate::util::{AtomicFloatPair, AtomicFloat}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -12,6 +12,7 @@ pub struct ScopeHandle { bufs: [Vec; 3], active: [AtomicBool; 3], offs_gain: [AtomicFloatPair; 3], + threshold: (AtomicBool, AtomicFloat), } impl ScopeHandle { @@ -30,6 +31,7 @@ impl ScopeHandle { AtomicFloatPair::default(), AtomicFloatPair::default(), ], + threshold: (AtomicBool::new(false), AtomicFloat::default()), }) } @@ -48,6 +50,23 @@ impl ScopeHandle { 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); } From 90da8a1842456326a3f2dd725802376d1a827407 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 06:08:54 +0200 Subject: [PATCH 51/88] format --- src/dsp/helpers.rs | 2 +- src/dsp/mod.rs | 8 ++++---- src/scope_handle.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 9725a04..6ce44b5 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -741,7 +741,7 @@ impl CustomTrigger { /// surpassed the high threshold and has not fallen below low threshold yet. #[inline] pub fn check_trigger(&mut self, input: f32) -> bool { -// println!("TRIG CHECK: {} <> {}", input, self.high_thres); + // println!("TRIG CHECK: {} <> {}", input, self.high_thres); if self.triggered { if input <= self.low_thres { self.triggered = false; diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 6972f2a..219c707 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -522,6 +522,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; @@ -535,8 +537,6 @@ mod node_tseq; mod node_tslfo; #[allow(non_upper_case_globals)] mod node_vosc; -#[allow(non_upper_case_globals)] -mod node_scope; pub mod biquad; pub mod dattorro; @@ -573,13 +573,13 @@ 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 crate::fa_scope_tsrc; use node_ad::Ad; use node_allp::AllP; @@ -601,6 +601,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; @@ -608,7 +609,6 @@ use node_test::Test; use node_tseq::TSeq; use node_tslfo::TsLFO; use node_vosc::VOsc; -use node_scope::Scope; pub const MIDI_MAX_FREQ: f32 = 13289.75; diff --git a/src/scope_handle.rs b/src/scope_handle.rs index ec5337e..23895b4 100644 --- a/src/scope_handle.rs +++ b/src/scope_handle.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. use crate::nodes::SCOPE_SAMPLES; -use crate::util::{AtomicFloatPair, AtomicFloat}; +use crate::util::{AtomicFloat, AtomicFloatPair}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; From cacd417c6d8da2886c0955b5d2899e9c58b9e566 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 06:09:42 +0200 Subject: [PATCH 52/88] Noted the Scope node --- README.md | 1 + src/lib.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index f25ae7b..b8e00fc 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ And following DSP nodes: | 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 diff --git a/src/lib.rs b/src/lib.rs index dd03d86..5ba821d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,7 @@ And following DSP nodes: | 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 From 366903bee50f40615f6288e1b98cdf28d677a848 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 06:59:58 +0200 Subject: [PATCH 53/88] Repaired first scope test --- tests/common/mod.rs | 16 ++++++++++ tests/node_scope.rs | 72 ++++++++++++++++++++++++--------------------- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 80785d7..ab57382 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -405,12 +405,28 @@ pub fn pset_n(matrix: &mut Matrix, nid: NodeId, parm: &str, v_norm: f32) { 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).unwrap(); + 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(); 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).unwrap(); + matrix.set_param(p, SAtom::param(p.norm(v_denorm))); +} + #[allow(unused)] pub fn pset_n_wait( matrix: &mut Matrix, diff --git a/tests/node_scope.rs b/tests/node_scope.rs index b2a1769..e3bf294 100644 --- a/tests/node_scope.rs +++ b/tests/node_scope.rs @@ -7,41 +7,47 @@ use common::*; use hexodsp::nodes::SCOPE_SAMPLES; -#[test] -fn check_node_scope_1() { - let (node_conf, mut node_exec) = new_node_engine(); - let mut matrix = Matrix::new(node_conf, 3, 3); +fn read_scope_buf(matrix: &Matrix, sig_idx: usize) -> (Vec, Vec, f32, f32) { + let handle = matrix.get_scope_handle(0).unwrap(); - let mut chain = MatrixCellChain::new(CellDir::B); - chain.node_inp("scope", "in1").place(&mut matrix, 0, 0).unwrap(); - matrix.sync().unwrap(); + let mut min = vec![]; + let mut max = vec![]; + let mut total_min: f32 = 99999.9; + let mut total_max: f32 = -99999.9; - let scope = NodeId::Scope(0); - let in1_p = scope.inp_param("in1").unwrap(); - let in2_p = scope.inp_param("in2").unwrap(); - let in3_p = scope.inp_param("in3").unwrap(); - - matrix.set_param(in1_p, SAtom::param(1.0)); - matrix.set_param(in2_p, SAtom::param(1.0)); - matrix.set_param(in3_p, SAtom::param(1.0)); - let _res = run_for_ms(&mut node_exec, 11.0); - - let scope = matrix.get_scope_handle(0).unwrap(); - let mut v = vec![]; - for x in 0..SCOPE_SAMPLES { - v.push(scope.read(0, x)); + for i in 0..SCOPE_SAMPLES { + let (mi, ma) = handle.read(sig_idx, i); + min.push(mi); + max.push(ma); + total_min = total_min.min(mi); + total_max = total_max.max(ma); } - assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); - let mut v = vec![]; - for x in 0..SCOPE_SAMPLES { - v.push(scope.read(1, x)); - } - assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); - - let mut v = vec![]; - for x in 0..SCOPE_SAMPLES { - v.push(scope.read(2, x)); - } - assert_decimated_feq!(v, 80, vec![0.0022, 0.1836, 0.3650, 0.5464, 0.7278, 0.9093, 1.0]); + (min, max, total_min, total_max) +} + +#[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, min, max) = read_scope_buf(&matrix, sig_idx); + 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); + } } From d6a734089c3baa10d7f442d0cd4075c6b4887b5c Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 18:45:23 +0200 Subject: [PATCH 54/88] covered most of the scope functionality with tests now --- tests/common/mod.rs | 35 ++++++++---- tests/node_scope.rs | 136 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 13 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ab57382..942dc90 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -393,15 +393,28 @@ 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)); } @@ -409,13 +422,13 @@ pub fn pset_n(matrix: &mut Matrix, nid: NodeId, parm: &str, v_norm: f32) { 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).unwrap(); + 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))); } @@ -423,7 +436,7 @@ pub fn pset_d(matrix: &mut Matrix, nid: NodeId, parm: &str, v_denorm: f32) { 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).unwrap(); + let p = nid.inp_param(parm).expect("param exists"); matrix.set_param(p, SAtom::param(p.norm(v_denorm))); } @@ -435,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)] @@ -448,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)); } @@ -469,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_scope.rs b/tests/node_scope.rs index e3bf294..598c76b 100644 --- a/tests/node_scope.rs +++ b/tests/node_scope.rs @@ -16,14 +16,14 @@ fn read_scope_buf(matrix: &Matrix, sig_idx: usize) -> (Vec, Vec, f32, let mut total_max: f32 = -99999.9; for i in 0..SCOPE_SAMPLES { - let (mi, ma) = handle.read(sig_idx, i); + 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); } - (min, max, total_min, total_max) + (max, min, total_max, total_min) } #[test] @@ -45,9 +45,141 @@ fn check_node_scope_inputs() { let _res = run_for_ms(&mut node_exec, 11.0); let (minv, maxv, min, max) = 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 handle = matrix.get_scope_handle(0).unwrap(); + 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 handle = matrix.get_scope_handle(0).unwrap(); + 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, + 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); +} From c495725f48d0a5fe266eb180e39f76bfde176549 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 18:48:50 +0200 Subject: [PATCH 55/88] add a comment --- tests/node_scope.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/node_scope.rs b/tests/node_scope.rs index 598c76b..0a51cd6 100644 --- a/tests/node_scope.rs +++ b/tests/node_scope.rs @@ -138,7 +138,6 @@ fn check_node_scope_sine_2hz() { assert_float_eq!(min, -1.0); } - #[test] fn check_node_scope_sine_oversampled() { let (node_conf, mut node_exec) = new_node_engine(); @@ -164,18 +163,10 @@ fn check_node_scope_sine_oversampled() { 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.4506, 0.4506, 0.4506, 0.3938, 0.3938, 0.3354, 0.3354, 0.2150, 0.2150, 0.1534, 0.1534, 0.1534, ] ); From 1b89fed67d486be750e97c81b378d9588d107d15 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 27 Jul 2022 19:05:12 +0200 Subject: [PATCH 56/88] finished scope tests --- src/dsp/node_scope.rs | 2 +- tests/node_scope.rs | 114 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index ea1298d..39bbe48 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -154,7 +154,7 @@ impl DspNode for Scope { let sample_time = 1.0 / self.srate_ms; let threshold = denorm::Scope::thrsh(thrsh, 0); - self.trig.set_threshold(threshold, threshold + 0.001); + self.trig.set_threshold(threshold, threshold + 0.0001); let trigger_input = if tsrc.i() == 2 { trig } else { in1 }; let trigger_disabled = tsrc.i() == 0; diff --git a/tests/node_scope.rs b/tests/node_scope.rs index 0a51cd6..ceedf24 100644 --- a/tests/node_scope.rs +++ b/tests/node_scope.rs @@ -44,7 +44,7 @@ fn check_node_scope_inputs() { node_pset_d(&mut matrix, "amp", 0, "inp", 1.0); let _res = run_for_ms(&mut node_exec, 11.0); - let (minv, maxv, min, max) = read_scope_buf(&matrix, sig_idx); + 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]); @@ -108,9 +108,7 @@ fn check_node_scope_sine_2hz() { wait_params_smooth(&mut node_exec); - let handle = matrix.get_scope_handle(0).unwrap(); 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!( @@ -156,10 +154,8 @@ fn check_node_scope_sine_oversampled() { wait_params_smooth(&mut node_exec); - let handle = matrix.get_scope_handle(0).unwrap(); let _res = run_for_ms(&mut node_exec, 1000.0); - - let (maxv, minv, max, min) = read_scope_buf(&matrix, 0); + let (maxv, _minv, max, min) = read_scope_buf(&matrix, 0); assert_decimated_feq!( maxv[0..25], 5, @@ -174,3 +170,109 @@ fn check_node_scope_sine_oversampled() { 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); +} From 76a0e5cb88295ae2598a44313899239b86a32b53 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 29 Jul 2022 21:10:51 +0200 Subject: [PATCH 57/88] Integration of WBlockDSP --- Cargo.toml | 5 +++-- src/lib.rs | 4 +++- src/nodes/node_conf.rs | 4 ++++ src/nodes/node_exec.rs | 23 ++++++++++++++++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9746475..2e2a5b6 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 = [ "wblockdsp" ] [dependencies] serde = { version = "1.0", features = ["derive"] } @@ -19,6 +19,7 @@ triple_buffer = "5.0.6" lazy_static = "1.4.0" hound = "3.4.0" num-traits = "0.2.14" +wblockdsp = { path = "../wblockdsp", optional = true } [dev-dependencies] num-complex = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 5ba821d..eab30ed 100644 --- a/src/lib.rs +++ b/src/lib.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. @@ -314,6 +314,8 @@ pub mod monitor; pub mod nodes; pub mod sample_lib; pub mod scope_handle; +#[cfg(feature="wblockdsp")] +pub mod wblockdsp; mod util; pub use cell_dir::CellDir; diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index b9c8dad..36055f3 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -13,6 +13,8 @@ use crate::nodes::drop_thread::DropThread; use crate::util::AtomicFloat; use crate::SampleLibrary; use crate::ScopeHandle; +#[cfg(feature = "wblockdsp")] +use crate::wblockdsp::CodeEngine; use ringbuf::{Producer, RingBuffer}; use std::collections::HashMap; @@ -254,6 +256,8 @@ impl SharedNodeConf { graph_update_con: rb_graph_con, graph_drop_prod: rb_drop_prod, monitor_backend, + #[cfg(feature = "wblockdsp")] + code_backend: None, // TODO: FILL THIS! }, ) } diff --git a/src/nodes/node_exec.rs b/src/nodes/node_exec.rs index 699d026..dbedbfb 100644 --- a/src/nodes/node_exec.rs +++ b/src/nodes/node_exec.rs @@ -9,6 +9,8 @@ use super::{ use crate::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE}; use crate::monitor::{MonitorBackend, MON_SIG_CNT}; use crate::util::{AtomicFloat, Smoother}; +#[cfg(feature = "wblockdsp")] +use crate::wblockdsp::CodeEngineBackend; use crate::log; use std::io::Write; @@ -81,6 +83,9 @@ pub(crate) struct SharedNodeExec { pub(crate) graph_drop_prod: Producer, /// For sending feedback to the frontend thread. pub(crate) monitor_backend: MonitorBackend, + /// For handing over the [crate::wblockdsp::CodeEngineBackend] + #[cfg(feature = "wblockdsp")] + pub(crate) code_backend: Option>, } /// Contains audio driver context informations. Such as the number @@ -168,25 +173,41 @@ impl Default for FeedbackBuffer { /// This is used for instance to implement the feedbackd delay nodes. pub struct NodeExecContext { pub feedback_delay_buffers: Vec, + #[cfg(feature = "wblockdsp")] + pub code_backend: Option>, } impl NodeExecContext { fn new() -> Self { let mut fbdb = vec![]; fbdb.resize_with(MAX_ALLOCATED_NODES, FeedbackBuffer::new); - Self { feedback_delay_buffers: fbdb } + Self { + feedback_delay_buffers: fbdb, + #[cfg(feature = "wblockdsp")] + code_backend: None + } } fn set_sample_rate(&mut self, srate: f32) { for b in self.feedback_delay_buffers.iter_mut() { b.set_sample_rate(srate); } + + #[cfg(feature = "wblockdsp")] + if let Some(code_backend) = self.code_backend.as_mut() { + code_backend.set_sample_rate(srate); + } } fn clear(&mut self) { for b in self.feedback_delay_buffers.iter_mut() { b.clear(); } + + #[cfg(feature = "wblockdsp")] + if let Some(code_backend) = self.code_backend.as_mut() { + code_backend.clear(); + } } } From 36b76f538859a54b523bcdf2d67dcd64d1992ee5 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 29 Jul 2022 21:30:56 +0200 Subject: [PATCH 58/88] Improved WBlockDSP API Integration --- src/nodes/node_conf.rs | 2 - src/nodes/node_exec.rs | 23 +------ src/wblockdsp.rs | 153 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 src/wblockdsp.rs diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 36055f3..addc9ef 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -256,8 +256,6 @@ impl SharedNodeConf { graph_update_con: rb_graph_con, graph_drop_prod: rb_drop_prod, monitor_backend, - #[cfg(feature = "wblockdsp")] - code_backend: None, // TODO: FILL THIS! }, ) } diff --git a/src/nodes/node_exec.rs b/src/nodes/node_exec.rs index dbedbfb..699d026 100644 --- a/src/nodes/node_exec.rs +++ b/src/nodes/node_exec.rs @@ -9,8 +9,6 @@ use super::{ use crate::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE}; use crate::monitor::{MonitorBackend, MON_SIG_CNT}; use crate::util::{AtomicFloat, Smoother}; -#[cfg(feature = "wblockdsp")] -use crate::wblockdsp::CodeEngineBackend; use crate::log; use std::io::Write; @@ -83,9 +81,6 @@ pub(crate) struct SharedNodeExec { pub(crate) graph_drop_prod: Producer, /// For sending feedback to the frontend thread. pub(crate) monitor_backend: MonitorBackend, - /// For handing over the [crate::wblockdsp::CodeEngineBackend] - #[cfg(feature = "wblockdsp")] - pub(crate) code_backend: Option>, } /// Contains audio driver context informations. Such as the number @@ -173,41 +168,25 @@ impl Default for FeedbackBuffer { /// This is used for instance to implement the feedbackd delay nodes. pub struct NodeExecContext { pub feedback_delay_buffers: Vec, - #[cfg(feature = "wblockdsp")] - pub code_backend: Option>, } impl NodeExecContext { fn new() -> Self { let mut fbdb = vec![]; fbdb.resize_with(MAX_ALLOCATED_NODES, FeedbackBuffer::new); - Self { - feedback_delay_buffers: fbdb, - #[cfg(feature = "wblockdsp")] - code_backend: None - } + Self { feedback_delay_buffers: fbdb } } fn set_sample_rate(&mut self, srate: f32) { for b in self.feedback_delay_buffers.iter_mut() { b.set_sample_rate(srate); } - - #[cfg(feature = "wblockdsp")] - if let Some(code_backend) = self.code_backend.as_mut() { - code_backend.set_sample_rate(srate); - } } fn clear(&mut self) { for b in self.feedback_delay_buffers.iter_mut() { b.clear(); } - - #[cfg(feature = "wblockdsp")] - if let Some(code_backend) = self.code_backend.as_mut() { - code_backend.clear(); - } } } diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs new file mode 100644 index 0000000..cbb5bd9 --- /dev/null +++ b/src/wblockdsp.rs @@ -0,0 +1,153 @@ +// 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 wblockdsp::*; + +use ringbuf::{Consumer, Producer, RingBuffer}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +const MAX_RINGBUF_SIZE: usize = 128; +const MAX_CONTEXTS: usize = 32; + +enum CodeUpdateMsg { + UpdateFun(Box), +} + +enum CodeReturnMsg { + DestroyFun(Box), +} + +pub struct CodeEngine { + dsp_ctx: Rc>, + lib: Rc>, + update_prod: Producer, + update_cons: Option>, + return_cons: Consumer, + return_prod: Option>, +} + +impl CodeEngine { + pub fn new() -> Self { + let rb = RingBuffer::new(MAX_RINGBUF_SIZE); + let (update_prod, update_cons) = rb.split(); + let rb = RingBuffer::new(MAX_RINGBUF_SIZE); + let (return_prod, return_cons) = rb.split(); + + let lib = get_default_library(); + + Self { + lib, + dsp_ctx: DSPNodeContext::new_ref(), + update_prod, + update_cons: Some(update_cons), + return_prod: Some(return_prod), + return_cons, + } + } + + pub fn upload( + &mut self, + code_instance: usize, + ast: Box, + ) -> Result<(), JITCompileError> { + + let jit = JIT::new(self.lib.clone(), self.dsp_ctx.clone()); + let fun = jit.compile(ASTFun::new(ast))?; + self.update_prod.push(CodeUpdateMsg::UpdateFun(fun)); + + Ok(()) + } + + pub fn cleanup(&self, fun: Box) { + self.dsp_ctx.borrow_mut().cleanup_dsp_fun_after_user(fun); + } + + pub fn query_returns(&mut self) { + while let Some(msg) = self.return_cons.pop() { + match msg { + CodeReturnMsg::DestroyFun(fun) => { + self.cleanup(fun); + } + } + } + } + + pub fn get_backend(&mut self) -> Option { + if let Some(update_cons) = self.update_cons.take() { + if let Some(return_prod) = self.return_prod.take() { + let function = get_nop_function(self.lib.clone(), self.dsp_ctx.clone()); + return Some(CodeEngineBackend::new(function, update_cons, return_prod)); + } + } + + None + } +} + +impl Drop for CodeEngine { + fn drop(&mut self) { + self.dsp_ctx.borrow_mut().free(); + } +} + + +pub struct CodeEngineBackend { + sample_rate: f32, + function: Box, + update_cons: Consumer, + return_prod: Producer, +} + +impl CodeEngineBackend { + fn new(function: Box, update_cons: Consumer, return_prod: Producer) -> Self { + Self { sample_rate: 0.0, function, update_cons, return_prod } + } + + #[inline] + pub fn process( + &mut self, + in1: f32, + in2: f32, + a: f32, + b: f32, + d: f32, + g: f32, + ) -> (f32, f32, f32) { + let mut s1 = 0.0_f64; + let mut s2 = 0.0_f64; + let res = self + .function + .exec(in1 as f64, in2 as f64, a as f64, b as f64, d as f64, g as f64, &mut s1, &mut s2); + (s1 as f32, s2 as f32, res as f32) + } + + pub fn swap_fun(&mut self, srate: f32, mut fun: Box) -> Box { + std::mem::swap(&mut self.function, &mut fun); + self.function.init(srate as f64, Some(&fun)); + fun + } + + pub fn set_sample_rate(&mut self, srate: f32) { + self.sample_rate = srate; + self.function.set_sample_rate(srate as f64); + } + + pub fn clear(&mut self) { + self.function.reset(); + } + + pub fn process_updates(&mut self) { + while let Some(msg) = self.update_cons.pop() { + match msg { + CodeUpdateMsg::UpdateFun(mut fun) => { + std::mem::swap(&mut self.function, &mut fun); + self.function.init(self.sample_rate as f64, Some(&fun)); + self.return_prod.push(CodeReturnMsg::DestroyFun(fun)); + } + } + } + } +} From 70f369af706e0a6c994066a67822e4709d2c7cb1 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 29 Jul 2022 22:00:46 +0200 Subject: [PATCH 59/88] Added Code node --- src/dsp/mod.rs | 19 +++++-- src/dsp/node_code.rs | 109 +++++++++++++++++++++++++++++++++++++++++ src/nodes/mod.rs | 1 + src/nodes/node_conf.rs | 6 ++- src/wblockdsp.rs | 28 ++++++----- 5 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 src/dsp/node_code.rs diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 219c707..9193b58 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -537,6 +537,8 @@ mod node_tseq; mod node_tslfo; #[allow(non_upper_case_globals)] mod node_vosc; +#[allow(non_upper_case_globals)] +mod node_code; pub mod biquad; pub mod dattorro; @@ -607,6 +609,7 @@ use node_sin::Sin; use node_smap::SMap; use node_test::Test; use node_tseq::TSeq; +use node_code::Code; use node_tslfo::TsLFO; use node_vosc::VOsc; @@ -1367,11 +1370,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} diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs new file mode 100644 index 0000000..0f00176 --- /dev/null +++ b/src/dsp/node_code.rs @@ -0,0 +1,109 @@ +// 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}; +use crate::wblockdsp::CodeEngineBackend; + +use crate::dsp::MAX_BLOCK_SIZE; + +/// A WBlockDSP code execution node for JIT'ed DSP code +pub struct Code { + 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 { + backend: None, + srate: 48000.0, + } + } + + 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; + if let Some(backend) = self.backend.as_mut() { + backend.set_sample_rate(srate); + } + } + + fn reset(&mut self) { + 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::{at, denorm, inp, out}; +// let clock = inp::TSeq::clock(inputs); +// let trig = inp::TSeq::trig(inputs); +// let cmode = at::TSeq::cmode(atoms); + + let backend = if let Some(backend) = &mut self.backend { + backend + } else { + return; + }; + + backend.process_updates(); + + for frame in 0..ctx.nframes() { + let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + } + + ctx_vals[0].set(0.0); + ctx_vals[1].set(0.0); + } +} diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index 06e6e55..fb3f7a0 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -8,6 +8,7 @@ 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. diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index addc9ef..bc4b3d0 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -4,7 +4,7 @@ use super::{ FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS, - MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, + MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, MAX_AVAIL_CODE_ENGINES }; use crate::dsp::tracker::{PatternData, Tracker}; use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom}; @@ -182,6 +182,9 @@ pub struct NodeConfigurator { pub(crate) trackers: Vec, /// Holding the scope buffers: pub(crate) scopes: Vec>, + /// Holding the WBlockDSP code engine backends: + #[cfg(feature = "wblockdsp")] + pub(crate) code_engines: Vec, /// The shared parts of the [NodeConfigurator] /// and the [crate::nodes::NodeExecutor]. pub(crate) shared: SharedNodeConf, @@ -287,6 +290,7 @@ impl NodeConfigurator { atom_values: std::collections::HashMap::new(), node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], + code_engines: vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES], scopes, }, shared_exec, diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs index cbb5bd9..115be8a 100644 --- a/src/wblockdsp.rs +++ b/src/wblockdsp.rs @@ -24,9 +24,13 @@ pub struct CodeEngine { dsp_ctx: Rc>, lib: Rc>, update_prod: Producer, - update_cons: Option>, return_cons: Consumer, - return_prod: Option>, +} + +impl Clone for CodeEngine { + fn clone(&self) -> Self { + CodeEngine::new() + } } impl CodeEngine { @@ -42,8 +46,6 @@ impl CodeEngine { lib, dsp_ctx: DSPNodeContext::new_ref(), update_prod, - update_cons: Some(update_cons), - return_prod: Some(return_prod), return_cons, } } @@ -75,15 +77,17 @@ impl CodeEngine { } } - pub fn get_backend(&mut self) -> Option { - if let Some(update_cons) = self.update_cons.take() { - if let Some(return_prod) = self.return_prod.take() { - let function = get_nop_function(self.lib.clone(), self.dsp_ctx.clone()); - return Some(CodeEngineBackend::new(function, update_cons, return_prod)); - } - } + pub fn get_backend(&mut self) -> CodeEngineBackend { + let rb = RingBuffer::new(MAX_RINGBUF_SIZE); + let (update_prod, update_cons) = rb.split(); + let rb = RingBuffer::new(MAX_RINGBUF_SIZE); + let (return_prod, return_cons) = rb.split(); - None + self.update_prod = update_prod; + self.return_cons = return_cons; + + let function = get_nop_function(self.lib.clone(), self.dsp_ctx.clone()); + CodeEngineBackend::new(function, update_cons, return_prod) } } From 4fc1dc75fd87845489568749af039a83f187f9cd Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 30 Jul 2022 00:26:01 +0200 Subject: [PATCH 60/88] working on the wblockdsp integration, found some weird bug in wblockdsp though... --- src/dsp/node_code.rs | 31 ++++++++++++++++++++----------- src/nodes/node_conf.rs | 9 +++++++++ src/wblockdsp.rs | 4 ++-- tests/node_ad.rs | 2 +- tests/wblockdsp.rs | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 tests/wblockdsp.rs diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index 0f00176..f06ae5e 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -4,12 +4,14 @@ use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::{NodeAudioContext, NodeExecContext}; +#[cfg(feature = "wblockdsp")] use crate::wblockdsp::CodeEngineBackend; use crate::dsp::MAX_BLOCK_SIZE; /// A WBlockDSP code execution node for JIT'ed DSP code pub struct Code { + #[cfg(feature = "wblockdsp")] backend: Option>, srate: f64, } @@ -29,11 +31,13 @@ impl Clone for Code { impl Code { pub fn new(_nid: &NodeId) -> Self { Self { + #[cfg(feature = "wblockdsp")] backend: None, srate: 48000.0, } } + #[cfg(feature = "wblockdsp")] pub fn set_backend(&mut self, backend: CodeEngineBackend) { self.backend = Some(Box::new(backend)); } @@ -64,12 +68,14 @@ impl DspNode for Code { fn set_sample_rate(&mut self, srate: f32) { self.srate = srate as f64; + #[cfg(feature = "wblockdsp")] if let Some(backend) = self.backend.as_mut() { backend.set_sample_rate(srate); } } fn reset(&mut self) { + #[cfg(feature = "wblockdsp")] if let Some(backend) = self.backend.as_mut() { backend.clear(); } @@ -91,19 +97,22 @@ impl DspNode for Code { // let trig = inp::TSeq::trig(inputs); // let cmode = at::TSeq::cmode(atoms); - let backend = if let Some(backend) = &mut self.backend { - backend - } else { - return; - }; + #[cfg(feature = "wblockdsp")] + { + let backend = if let Some(backend) = &mut self.backend { + backend + } else { + return; + }; - backend.process_updates(); + backend.process_updates(); - for frame in 0..ctx.nframes() { - let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + for frame in 0..ctx.nframes() { + let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + } + + ctx_vals[0].set(0.0); + ctx_vals[1].set(0.0); } - - ctx_vals[0].set(0.0); - ctx_vals[1].set(0.0); } } diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index bc4b3d0..c638644 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -290,6 +290,7 @@ impl NodeConfigurator { atom_values: std::collections::HashMap::new(), node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], + #[cfg(feature = "wblockdsp")] code_engines: vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES], scopes, }, @@ -694,6 +695,14 @@ impl NodeConfigurator { } } + #[cfg(feature = "wblockdsp")] + 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()); diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs index 115be8a..fcbbaa4 100644 --- a/src/wblockdsp.rs +++ b/src/wblockdsp.rs @@ -36,9 +36,9 @@ impl Clone for CodeEngine { impl CodeEngine { pub fn new() -> Self { let rb = RingBuffer::new(MAX_RINGBUF_SIZE); - let (update_prod, update_cons) = rb.split(); + let (update_prod, _update_cons) = rb.split(); let rb = RingBuffer::new(MAX_RINGBUF_SIZE); - let (return_prod, return_cons) = rb.split(); + let (_return_prod, return_cons) = rb.split(); let lib = get_default_library(); diff --git a/tests/node_ad.rs b/tests/node_ad.rs index 59dfccf..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. diff --git a/tests/wblockdsp.rs b/tests/wblockdsp.rs new file mode 100644 index 0000000..8eb7129 --- /dev/null +++ b/tests/wblockdsp.rs @@ -0,0 +1,38 @@ +// 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::*; + +#[test] +fn check_wblockdsp_init() { + let mut engine = CodeEngine::new(); + + let backend = engine.get_backend(); +} + +#[test] +fn check_wblockdsp_code_node() { + 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("code", "sig") + .node_io("code", "in1", "sig") + .node_inp("out", "ch1") + .place(&mut matrix, 0, 0) + .unwrap(); + matrix.sync().unwrap(); + + let mut chain = MatrixCellChain::new(CellDir::B); + chain + .node_out("code", "sig") + .node_io("code", "in1", "sig") + .node_inp("out", "ch1") + .place(&mut matrix, 0, 0) + .unwrap(); + matrix.sync().unwrap(); +} From 89c1ffe79eb468de47fef0be61ab280a812184b0 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 30 Jul 2022 01:52:30 +0200 Subject: [PATCH 61/88] rough PoC for Code node --- src/dsp/node_code.rs | 2 ++ src/nodes/node_conf.rs | 21 +++++++++++++++++---- src/wblockdsp.rs | 21 +++++++-------------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index f06ae5e..7b444fa 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -96,6 +96,7 @@ impl DspNode for Code { // let clock = inp::TSeq::clock(inputs); // let trig = inp::TSeq::trig(inputs); // let cmode = at::TSeq::cmode(atoms); + let out = out::Code::sig(outputs); #[cfg(feature = "wblockdsp")] { @@ -109,6 +110,7 @@ impl DspNode for Code { for frame in 0..ctx.nframes() { let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + out.write(frame, ret); } ctx_vals[0].set(0.0); diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index c638644..6a589f0 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -3,18 +3,18 @@ // See README.md and COPYING for details. use super::{ - FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS, - MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, MAX_AVAIL_CODE_ENGINES + FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES, + MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, }; 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; -use crate::SampleLibrary; -use crate::ScopeHandle; #[cfg(feature = "wblockdsp")] use crate::wblockdsp::CodeEngine; +use crate::SampleLibrary; +use crate::ScopeHandle; use ringbuf::{Producer, RingBuffer}; use std::collections::HashMap; @@ -700,6 +700,19 @@ impl NodeConfigurator { let code_idx = ni.instance(); if let Some(cod) = self.code_engines.get_mut(code_idx) { node.set_backend(cod.get_backend()); + use wblockdsp::build::*; + cod.upload(stmts(&[ + assign( + "*phase", + op_add(var("*phase"), op_mul(literal(440.0), var("israte"))), + ), + _if( + op_gt(var("*phase"), literal(1.0)), + assign("*phase", op_sub(var("*phase"), literal(1.0))), + None, + ), + var("*phase"), + ])); } } diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs index fcbbaa4..b05490c 100644 --- a/src/wblockdsp.rs +++ b/src/wblockdsp.rs @@ -42,20 +42,10 @@ impl CodeEngine { let lib = get_default_library(); - Self { - lib, - dsp_ctx: DSPNodeContext::new_ref(), - update_prod, - return_cons, - } + Self { lib, dsp_ctx: DSPNodeContext::new_ref(), update_prod, return_cons } } - pub fn upload( - &mut self, - code_instance: usize, - ast: Box, - ) -> Result<(), JITCompileError> { - + pub fn upload(&mut self, ast: Box) -> Result<(), JITCompileError> { let jit = JIT::new(self.lib.clone(), self.dsp_ctx.clone()); let fun = jit.compile(ASTFun::new(ast))?; self.update_prod.push(CodeUpdateMsg::UpdateFun(fun)); @@ -97,7 +87,6 @@ impl Drop for CodeEngine { } } - pub struct CodeEngineBackend { sample_rate: f32, function: Box, @@ -106,7 +95,11 @@ pub struct CodeEngineBackend { } impl CodeEngineBackend { - fn new(function: Box, update_cons: Consumer, return_prod: Producer) -> Self { + fn new( + function: Box, + update_cons: Consumer, + return_prod: Producer, + ) -> Self { Self { sample_rate: 0.0, function, update_cons, return_prod } } From 6229d9b3101058bb8f4e5ddc3208eba568b2d7c9 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 30 Jul 2022 13:28:27 +0200 Subject: [PATCH 62/88] moved blocklang over from HexoTK --- src/blocklang.rs | 1583 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 1584 insertions(+) create mode 100644 src/blocklang.rs diff --git a/src/blocklang.rs b/src/blocklang.rs new file mode 100644 index 0000000..c3be6c8 --- /dev/null +++ b/src/blocklang.rs @@ -0,0 +1,1583 @@ +// 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; + +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); + } + } + } + } +} + +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. + 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. + sources: HashSet<(i64, i64)>, + /// Stores the positions of blocks that only have input ports. + 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 + } +} + +#[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_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, +} + +#[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 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 + } +} diff --git a/src/lib.rs b/src/lib.rs index eab30ed..6e671bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -316,6 +316,7 @@ pub mod sample_lib; pub mod scope_handle; #[cfg(feature="wblockdsp")] pub mod wblockdsp; +pub mod blocklang; mod util; pub use cell_dir::CellDir; From 40fc83c1f4e1d322acd37de1a327c66900223e80 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 30 Jul 2022 13:50:34 +0200 Subject: [PATCH 63/88] Mention the JIT --- README.md | 21 ++++++++++++++------- src/lib.rs | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b8e00fc..d340564 100644 --- a/README.md +++ b/README.md @@ -7,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. @@ -15,14 +15,20 @@ 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: @@ -40,6 +46,7 @@ And following DSP nodes: | 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 | diff --git a/src/lib.rs b/src/lib.rs index 6e671bd..f42d10d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,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. @@ -17,14 +17,20 @@ 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: @@ -42,6 +48,7 @@ And following DSP nodes: | 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 | From 5571def6ddd2daf5e9f413a351460fbb529c9850 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 30 Jul 2022 13:57:18 +0200 Subject: [PATCH 64/88] Update date --- README.md | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d340564..2366c6b 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ let (out_l, out_r) = node_exec.test_run(0.11, true); ### 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. diff --git a/src/lib.rs b/src/lib.rs index f42d10d..160cec6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,7 +167,7 @@ let (out_l, out_r) = node_exec.test_run(0.11, true); ## 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. From f5f242041afa714ec38fe2d0efcc0cd31e40ab68 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 30 Jul 2022 23:11:33 +0200 Subject: [PATCH 65/88] use synfx-dsp-jit now --- Cargo.toml | 4 ++-- src/blocklang.rs | 4 ++++ src/dsp/node_code.rs | 14 +++++++------- src/lib.rs | 3 ++- src/nodes/node_conf.rs | 10 +++++----- src/wblockdsp.rs | 4 ++-- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2e2a5b6..799d6f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ keywords = ["audio", "music", "real-time", "synthesis", "synthesizer", "dsp", categories = ["multimedia::audio", "multimedia", "algorithms", "mathematics"] [features] -default = [ "wblockdsp" ] +default = [ "synfx-dsp-jit" ] [dependencies] serde = { version = "1.0", features = ["derive"] } @@ -19,7 +19,7 @@ triple_buffer = "5.0.6" lazy_static = "1.4.0" hound = "3.4.0" num-traits = "0.2.14" -wblockdsp = { path = "../wblockdsp", optional = true } +synfx-dsp-jit = { path = "../synfx-dsp-jit", optional = true } [dev-dependencies] num-complex = "0.2" diff --git a/src/blocklang.rs b/src/blocklang.rs index c3be6c8..d95cb36 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -964,6 +964,10 @@ impl BlockFun { } } + 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) diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index 7b444fa..44278c6 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -4,14 +4,14 @@ use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::{NodeAudioContext, NodeExecContext}; -#[cfg(feature = "wblockdsp")] +#[cfg(feature = "synfx-dsp-jit")] use crate::wblockdsp::CodeEngineBackend; use crate::dsp::MAX_BLOCK_SIZE; /// A WBlockDSP code execution node for JIT'ed DSP code pub struct Code { - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] backend: Option>, srate: f64, } @@ -31,13 +31,13 @@ impl Clone for Code { impl Code { pub fn new(_nid: &NodeId) -> Self { Self { - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] backend: None, srate: 48000.0, } } - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] pub fn set_backend(&mut self, backend: CodeEngineBackend) { self.backend = Some(Box::new(backend)); } @@ -68,14 +68,14 @@ impl DspNode for Code { fn set_sample_rate(&mut self, srate: f32) { self.srate = srate as f64; - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] if let Some(backend) = self.backend.as_mut() { backend.set_sample_rate(srate); } } fn reset(&mut self) { - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] if let Some(backend) = self.backend.as_mut() { backend.clear(); } @@ -98,7 +98,7 @@ impl DspNode for Code { // let cmode = at::TSeq::cmode(atoms); let out = out::Code::sig(outputs); - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] { let backend = if let Some(backend) = &mut self.backend { backend diff --git a/src/lib.rs b/src/lib.rs index 160cec6..234960f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -321,9 +321,10 @@ pub mod monitor; pub mod nodes; pub mod sample_lib; pub mod scope_handle; -#[cfg(feature="wblockdsp")] +#[cfg(feature="synfx-dsp-jit")] pub mod wblockdsp; pub mod blocklang; +pub mod blocklang_def; mod util; pub use cell_dir::CellDir; diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 6a589f0..99d7878 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -11,7 +11,7 @@ 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 = "wblockdsp")] +#[cfg(feature = "synfx-dsp-jit")] use crate::wblockdsp::CodeEngine; use crate::SampleLibrary; use crate::ScopeHandle; @@ -183,7 +183,7 @@ pub struct NodeConfigurator { /// Holding the scope buffers: pub(crate) scopes: Vec>, /// Holding the WBlockDSP code engine backends: - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] pub(crate) code_engines: Vec, /// The shared parts of the [NodeConfigurator] /// and the [crate::nodes::NodeExecutor]. @@ -290,7 +290,7 @@ impl NodeConfigurator { atom_values: std::collections::HashMap::new(), node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], - #[cfg(feature = "wblockdsp")] + #[cfg(feature = "synfx-dsp-jit")] code_engines: vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES], scopes, }, @@ -695,12 +695,12 @@ impl NodeConfigurator { } } - #[cfg(feature = "wblockdsp")] + #[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()); - use wblockdsp::build::*; + use synfx_dsp_jit::build::*; cod.upload(stmts(&[ assign( "*phase", diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs index b05490c..0e97cfa 100644 --- a/src/wblockdsp.rs +++ b/src/wblockdsp.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 wblockdsp::*; +use synfx_dsp_jit::*; use ringbuf::{Consumer, Producer, RingBuffer}; use std::cell::RefCell; @@ -40,7 +40,7 @@ impl CodeEngine { let rb = RingBuffer::new(MAX_RINGBUF_SIZE); let (_return_prod, return_cons) = rb.split(); - let lib = get_default_library(); + let lib = get_standard_library(); Self { lib, dsp_ctx: DSPNodeContext::new_ref(), update_prod, return_cons } } From 9b0f93c92be9a8124f17a0bed100ba3cd7a7b46f Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 31 Jul 2022 08:15:48 +0200 Subject: [PATCH 66/88] Added TDD test for blocklang --- src/matrix.rs | 22 +++++++++++++++-- src/nodes/node_conf.rs | 54 +++++++++++++++++++++++++++++++++++++++++ tests/blocklang.rs | 55 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 tests/blocklang.rs diff --git a/src/matrix.rs b/src/matrix.rs index 1e1a408..26a70b8 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -10,6 +10,7 @@ pub use crate::nodes::MinMaxMonitorSamples; use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES}; pub use crate::CellDir; use crate::ScopeHandle; +use crate::blocklang::BlockFun; use std::collections::{HashMap, HashSet}; @@ -578,20 +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) } + /// 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 pattern data updates need to be sent to the - /// DSP thread. + /// 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 [get_block_function]. + pub fn check_block_function(&mut self, id: usize) { + 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 [check_block_function]. + pub fn get_block_function(&mut 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 diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 99d7878..bd7bc21 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -6,6 +6,8 @@ use super::{ FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES, MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, }; +use crate::blocklang::*; +use crate::blocklang_def; 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}; @@ -185,6 +187,10 @@ pub struct NodeConfigurator { /// 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 [check_block_function]. + pub(crate) block_functions: Vec<(u64, Arc>)>, /// The shared parts of the [NodeConfigurator] /// and the [crate::nodes::NodeExecutor]. pub(crate) shared: SharedNodeConf, @@ -274,6 +280,12 @@ impl NodeConfigurator { let mut scopes = vec![]; scopes.resize_with(MAX_SCOPES, || ScopeHandle::new_shared()); + let lang = blocklang_def::setup_hxdsp_block_language(); + let mut block_functions = vec![]; + block_functions.resize_with(MAX_AVAIL_CODE_ENGINES, || { + (0, Arc::new(Mutex::new(BlockFun::new(lang.clone())))) + }); + ( NodeConfigurator { nodes, @@ -292,6 +304,7 @@ impl NodeConfigurator { trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], #[cfg(feature = "synfx-dsp-jit")] code_engines: vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES], + block_functions, scopes, }, shared_exec, @@ -652,10 +665,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; @@ -664,6 +679,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; @@ -672,6 +691,41 @@ 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 [get_block_function]. + pub fn check_block_function(&mut self, id: usize) { + 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 ast = block_compiler::compile(block_fun); + if let Some(cod) = self.code_engines.get_mut(id) { + use synfx_dsp_jit::build::*; + cod.upload(stmts(&[ + assign( + "*phase", + op_add(var("*phase"), op_mul(literal(440.0), var("israte"))), + ), + _if( + op_gt(var("*phase"), literal(1.0)), + assign("*phase", op_sub(var("*phase"), literal(1.0))), + None, + ), + var("*phase"), + ])); + } + } + } + } + } + + /// Retrieve a handle to the block function `id`. In case you modify the block function, + /// make sure to call [check_block_function]. + pub fn get_block_function(&mut self, id: usize) -> Option>> { + self.block_functions.get(id).map(|pair| pair.1.clone()) + } + pub fn delete_nodes(&mut self) { self.node2idx.clear(); self.nodes.fill_with(|| (NodeInfo::from_node_id(NodeId::Nop), None)); diff --git a/tests/blocklang.rs b/tests/blocklang.rs new file mode 100644 index 0000000..6e02cd9 --- /dev/null +++ b/tests/blocklang.rs @@ -0,0 +1,55 @@ +// 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::*; + +#[test] +fn check_blocklang_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("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(); + + let code = NodeId::Code(0); + + let block_fun = matrix.get_block_function(0).expect("block fun exists"); + { + let mut block_fun = block_fun.lock().expect("matrix lock"); + + block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 0, 1, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 1, 0, "+", None); + block_fun.instanciate_at(0, 0, 1, "set", Some("&sig1".to_string())); + } + + matrix.check_block_function(0); + + let res = run_for_ms(&mut node_exec, 25.0); + assert_decimated_feq!( + res.0, + 50, + vec![ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + ] + ); +} From f5f8ed545c17aeca547f8b6871462a2ea260789a Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 31 Jul 2022 12:18:42 +0200 Subject: [PATCH 67/88] working on the block language to JIT compiler --- src/block_compiler.rs | 194 +++++++++++++++++++++++++++++++++ src/blocklang_def.rs | 236 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/nodes/node_conf.rs | 4 + tests/blocklang.rs | 13 ++- 5 files changed, 446 insertions(+), 2 deletions(-) create mode 100644 src/block_compiler.rs create mode 100644 src/blocklang_def.rs diff --git a/src/block_compiler.rs b/src/block_compiler.rs new file mode 100644 index 0000000..1317b78 --- /dev/null +++ b/src/block_compiler.rs @@ -0,0 +1,194 @@ +// 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 synfx_dsp_jit::ASTNode; +use crate::blocklang::*; + +#[derive(Debug)] +struct JASTNode { + id: usize, + typ: String, + lbl: String, + nodes: Vec<(String, String, ASTNodeRef)>, +} + +#[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 { + pub fn first_child_ref(&self) -> Option { + self.0.borrow().nodes.get(0).map(|n| n.2.clone()) + } + + pub fn first_child(&self) -> Option<(String, String, ASTNodeRef)> { + self.0.borrow().nodes.get(0).cloned() + } + + pub fn nth_child(&self, i: usize) -> Option<(String, String, ASTNodeRef)> { + self.0.borrow().nodes.get(i).cloned() + } + + 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 { + Root { child: BlkASTRef }, + Area { childs: Vec }, + Set { var: String, expr: BlkASTRef }, + Get { id: usize, use_count: usize, var: String, expr: BlkASTRef }, + Node { id: usize, use_count: usize, typ: String, lbl: String, childs: Vec }, +} + +impl BlkASTNode { + pub fn new_root(child: BlkASTRef) -> BlkASTRef { + Rc::new(RefCell::new(BlkASTNode::Root { child })) + } + + pub fn new_area(childs: Vec) -> BlkASTRef { + Rc::new(RefCell::new(BlkASTNode::Area { childs })) + } + + pub fn new_set(var: &str, expr: BlkASTRef) -> BlkASTRef { + Rc::new(RefCell::new(BlkASTNode::Set { var: var.to_string(), expr })) + } + + pub fn new_node(id: usize, typ: &str, lbl: &str, childs: Vec) -> BlkASTRef { + Rc::new(RefCell::new(BlkASTNode::Node { id, typ: typ.to_string(), lbl: lbl.to_string(), use_count: 1, childs })) + } +} + + +#[derive(Debug, Clone)] +pub enum BlkJITCompileError { + UnknownError, + BadTree(ASTNodeRef), +} + +pub struct Block2JITCompiler { + id_node_map: HashMap, +} + +// 1. compile the weird tree into a graph +// - make references where IDs go +// - add a use count to each node, so that we know when to make temporary variables + +impl Block2JITCompiler { + pub fn new() -> Self { + Self { + id_node_map: HashMap::new(), + } + } + + pub fn trans2bjit(&self, node: &ASTNodeRef) -> Result { + match &node.0.borrow().typ[..] { + "" => { + if let Some(first) = node.first_child_ref() { + let child = self.trans2bjit(&first)?; + Ok(BlkASTNode::new_root(child)) + } else { + Err(BlkJITCompileError::BadTree(node.clone())) + } + } + "" => { + let mut childs = vec![]; + + let mut i = 0; + while let Some((_in, _out, child)) = node.nth_child(i) { + let child = self.trans2bjit(&child)?; + 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(first) = node.first_child_ref() { + self.trans2bjit(&first) + } else { + Err(BlkJITCompileError::BadTree(node.clone())) + } + } + "set" => { + if let Some(first) = node.first_child_ref() { + let expr = self.trans2bjit(&first)?; + Ok(BlkASTNode::new_set(&node.0.borrow().lbl, expr)) + + } else { + Err(BlkJITCompileError::BadTree(node.clone())) + } + } + optype => { + let mut childs = vec![]; + + let mut i = 0; + while let Some((_in, _out, child)) = node.nth_child(i) { + let child = self.trans2bjit(&child)?; + childs.push(child); + i += 1; + } + + Ok(BlkASTNode::new_node( + node.0.borrow().id, + &node.0.borrow().typ, + &node.0.borrow().lbl, + childs)) + } + } + } + + pub fn compile(&self, fun: &BlockFun) -> Result { + let tree = fun.generate_tree::("zero").unwrap(); + println!("{}", tree.walk_dump("", "", 0)); + + let blkast = self.trans2bjit(&tree); + println!("R: {:#?}", blkast); + + Err(BlkJITCompileError::UnknownError) + } +} diff --git a/src/blocklang_def.rs b/src/blocklang_def.rs new file mode 100644 index 0000000..822826a --- /dev/null +++ b/src/blocklang_def.rs @@ -0,0 +1,236 @@ +// 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::blocklang::{BlockLanguage, BlockUserInput, BlockType}; +use std::cell::RefCell; +use std::rc::Rc; + +pub fn setup_hxdsp_block_language() -> Rc> { + let mut lang = BlockLanguage::new(); + + lang.define(BlockType { + category: "source".to_string(), + name: "phse".to_string(), + rows: 1, + inputs: vec![Some("f".to_string())], + outputs: vec![Some("".to_string())], + area_count: 0, + user_input: BlockUserInput::None, + description: "A phasor, returns a saw tooth wave to scan through things or use as modulator.".to_string(), + color: 2, + }); + + 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: "SR".to_string(), + rows: 1, + inputs: vec![], + outputs: vec![Some("".to_string())], + area_count: 0, + user_input: BlockUserInput::None, + description: "The sample rate".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, + }); + } + + 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/lib.rs b/src/lib.rs index 234960f..6cae805 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -325,6 +325,7 @@ pub mod scope_handle; pub mod wblockdsp; pub mod blocklang; pub mod blocklang_def; +mod block_compiler; mod util; pub use cell_dir::CellDir; diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index bd7bc21..f896e10 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -8,6 +8,7 @@ use super::{ }; use crate::blocklang::*; use crate::blocklang_def; +use crate::block_compiler::Block2JITCompiler; 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}; @@ -699,6 +700,9 @@ impl NodeConfigurator { if let Ok(block_fun) = block_fun.lock() { if *generation != block_fun.generation() { *generation = block_fun.generation(); + let mut compiler = Block2JITCompiler::new(); + compiler.compile(&block_fun); + // let ast = block_compiler::compile(block_fun); if let Some(cod) = self.code_engines.get_mut(id) { use synfx_dsp_jit::build::*; diff --git a/tests/blocklang.rs b/tests/blocklang.rs index 6e02cd9..d111384 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -27,9 +27,18 @@ fn check_blocklang_1() { let mut block_fun = block_fun.lock().expect("matrix lock"); block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 0, 1, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); block_fun.instanciate_at(0, 1, 0, "+", None); - block_fun.instanciate_at(0, 0, 1, "set", Some("&sig1".to_string())); + block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); + + block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 3, 1, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 4, 0, "-", None); + block_fun.instanciate_at(0, 5, 0, "->3", None); + block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); + block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); + block_fun.instanciate_at(0, 6, 0, "->", None); + block_fun.instanciate_at(0, 7, 0, "->2", None); } matrix.check_block_function(0); From 053aceec4d7ddc40065bdd95a340b54b841708e3 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 31 Jul 2022 14:14:40 +0200 Subject: [PATCH 68/88] extended test case and made notes about multiple outputs --- src/block_compiler.rs | 23 +++++++++++++++++++++++ src/blocklang_def.rs | 12 ++++++++++++ tests/blocklang.rs | 10 +++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index 1317b78..62251fa 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -123,6 +123,26 @@ impl Block2JITCompiler { } pub fn trans2bjit(&self, node: &ASTNodeRef) -> Result { + // TODO: Deal with multiple outputs. + // If we encounter a node with multiple outputs, assign each output + // to a temporary variable and save that. + // Store the name of the temporary in a id+output mapping. + // => XXX + // That means: If we have a single output, things are easy, just plug them into + // the JIT ast: + // outer(inner()) + // But if we have multiple outputs: + // assign(a = inner()) + // assign(b = %1) + // outer_x(a) + // outer_y(b) + + // TODO: Filter out -> nodes from the AST + // TODO: For ->2 and ->3, save the input in some variable + // and reserve a id+output variable for this. + + // XXX: SSA form of cranelift should take care of the rest! + match &node.0.borrow().typ[..] { "" => { if let Some(first) = node.first_child_ref() { @@ -173,6 +193,9 @@ impl Block2JITCompiler { i += 1; } + // TODO: Reorder the childs/arguments according to the input + // order in the BlockLanguage + Ok(BlkASTNode::new_node( node.0.borrow().id, &node.0.borrow().typ, diff --git a/src/blocklang_def.rs b/src/blocklang_def.rs index 822826a..5cc7762 100644 --- a/src/blocklang_def.rs +++ b/src/blocklang_def.rs @@ -202,6 +202,18 @@ pub fn setup_hxdsp_block_language() -> Rc> { color: 8, }); + lang.define(BlockType { + category: "arithmetics".to_string(), + name: "/%".to_string(), + rows: 2, + inputs: vec![Some("a".to_string()), Some("b".to_string())], + outputs: vec![Some("div".to_string()), Some("rem".to_string())], + area_count: 0, + user_input: BlockUserInput::None, + description: "Computes the integer division and remainder of a / b".to_string(), + color: 8, + }); + for fun_name in &["+", "-", "*", "/"] { lang.define(BlockType { category: "arithmetics".to_string(), diff --git a/tests/blocklang.rs b/tests/blocklang.rs index d111384..f9e2992 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -32,13 +32,21 @@ fn check_blocklang_1() { block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 3, 1, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); block_fun.instanciate_at(0, 4, 0, "-", None); block_fun.instanciate_at(0, 5, 0, "->3", None); block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); block_fun.instanciate_at(0, 6, 0, "->", None); block_fun.instanciate_at(0, 7, 0, "->2", None); + + block_fun.instanciate_at(0, 0, 3, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 0, 4, "get", Some("in2".to_string())); + block_fun.instanciate_at(0, 1, 3, "/%", None); + block_fun.instanciate_at(0, 2, 3, "->", None); + block_fun.instanciate_at(0, 3, 3, "/%", None); + block_fun.instanciate_at(0, 4, 3, "set", Some("&sig2".to_string())); + block_fun.instanciate_at(0, 4, 4, "set", Some("*ap".to_string())); } matrix.check_block_function(0); From d9b4dcd984e8b3c6c6102614f19b36883f460c39 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 31 Jul 2022 17:53:52 +0200 Subject: [PATCH 69/88] figured out the output/input stuff better --- src/block_compiler.rs | 187 ++++++++++++++++++++++++++++++++---------- tests/blocklang.rs | 26 +++--- 2 files changed, 157 insertions(+), 56 deletions(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index 62251fa..1a95483 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -3,17 +3,17 @@ // See README.md and COPYING for details. use std::cell::RefCell; -use std::rc::Rc; use std::collections::HashMap; +use std::rc::Rc; -use synfx_dsp_jit::ASTNode; use crate::blocklang::*; +use synfx_dsp_jit::ASTNode; #[derive(Debug)] struct JASTNode { - id: usize, - typ: String, - lbl: String, + id: usize, + typ: String, + lbl: String, nodes: Vec<(String, String, ASTNodeRef)>, } @@ -24,9 +24,9 @@ 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![], + typ: typ.to_string(), + lbl: lbl.to_string(), + nodes: vec![], }))) } @@ -51,17 +51,18 @@ impl ASTNodeRef { 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 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); + 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); @@ -75,14 +76,75 @@ type BlkASTRef = Rc>; #[derive(Debug, Clone)] enum BlkASTNode { - Root { child: BlkASTRef }, - Area { childs: Vec }, - Set { var: String, expr: BlkASTRef }, - Get { id: usize, use_count: usize, var: String, expr: BlkASTRef }, - Node { id: usize, use_count: usize, typ: String, lbl: String, childs: Vec }, + Root { + child: BlkASTRef, + }, + Area { + childs: Vec, + }, + Set { + var: String, + expr: BlkASTRef, + }, + Get { + id: usize, + use_count: usize, + var: String, + }, + Node { + id: usize, + out: Option, + use_count: usize, + typ: String, + lbl: String, + childs: Vec<(Option, BlkASTRef)>, + }, } 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); + } + + match self { + BlkASTNode::Root { child } => { + format!("{}* Root\n", indent_str) + &child.borrow().dump(indent + 1, None) + } + BlkASTNode::Area { childs } => { + let mut s = format!("{}* Area\n", indent_str); + for c in childs.iter() { + s += &c.borrow().dump(indent + 1, None); + } + s + } + BlkASTNode::Set { var, expr } => { + format!("{}set '{}'=\n", indent_str, var) + &expr.borrow().dump(indent + 1, None) + } + BlkASTNode::Get { id, use_count, var } => { + format!("{}get '{}' (id={}, use={})\n", indent_str, var, id, use_count) + } + BlkASTNode::Node { id, out, use_count, typ, lbl, childs } => { + let lbl = if *typ == *lbl { "".to_string() } else { format!("[{}]", lbl) }; + + let mut s = if let Some(out) = out { + format!( + "{}{}{} (id={}/{}, use={})\n", + indent_str, typ, lbl, id, out, use_count + ) + } else { + format!("{}{}{} (id={}, use={})\n", indent_str, typ, lbl, id, use_count) + }; + for (inp, c) in childs.iter() { + s += &format!("{}", c.borrow().dump(indent + 1, inp.as_ref().map(|s| &s[..]))); + } + s + } + } + } + pub fn new_root(child: BlkASTRef) -> BlkASTRef { Rc::new(RefCell::new(BlkASTNode::Root { child })) } @@ -95,12 +157,28 @@ impl BlkASTNode { Rc::new(RefCell::new(BlkASTNode::Set { var: var.to_string(), expr })) } - pub fn new_node(id: usize, typ: &str, lbl: &str, childs: Vec) -> BlkASTRef { - Rc::new(RefCell::new(BlkASTNode::Node { id, typ: typ.to_string(), lbl: lbl.to_string(), use_count: 1, childs })) + pub fn new_get(id: usize, var: &str) -> BlkASTRef { + Rc::new(RefCell::new(BlkASTNode::Get { id, var: var.to_string(), use_count: 1 })) + } + + pub fn new_node( + id: usize, + out: Option, + typ: &str, + lbl: &str, + childs: Vec<(Option, BlkASTRef)>, + ) -> BlkASTRef { + Rc::new(RefCell::new(BlkASTNode::Node { + id, + out, + typ: typ.to_string(), + lbl: lbl.to_string(), + use_count: 1, + childs, + })) } } - #[derive(Debug, Clone)] pub enum BlkJITCompileError { UnknownError, @@ -117,12 +195,14 @@ pub struct Block2JITCompiler { impl Block2JITCompiler { pub fn new() -> Self { - Self { - id_node_map: HashMap::new(), - } + Self { id_node_map: HashMap::new() } } - pub fn trans2bjit(&self, node: &ASTNodeRef) -> Result { + pub fn trans2bjit( + &self, + node: &ASTNodeRef, + my_out: Option, + ) -> Result { // TODO: Deal with multiple outputs. // If we encounter a node with multiple outputs, assign each output // to a temporary variable and save that. @@ -145,8 +225,9 @@ impl Block2JITCompiler { match &node.0.borrow().typ[..] { "" => { - if let Some(first) = node.first_child_ref() { - let child = self.trans2bjit(&first)?; + if let Some((_in, out, first)) = node.first_child() { + let out = if out.len() > 0 { Some(out) } else { None }; + let child = self.trans2bjit(&first, out)?; Ok(BlkASTNode::new_root(child)) } else { Err(BlkJITCompileError::BadTree(node.clone())) @@ -156,8 +237,9 @@ impl Block2JITCompiler { let mut childs = vec![]; let mut i = 0; - while let Some((_in, _out, child)) = node.nth_child(i) { - let child = self.trans2bjit(&child)?; + 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; } @@ -168,39 +250,58 @@ impl Block2JITCompiler { // 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(first) = node.first_child_ref() { - self.trans2bjit(&first) + 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())) } } "set" => { - if let Some(first) = node.first_child_ref() { - let expr = self.trans2bjit(&first)?; + 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)?; Ok(BlkASTNode::new_set(&node.0.borrow().lbl, expr)) - } else { Err(BlkJITCompileError::BadTree(node.clone())) } } + "get" => Ok(BlkASTNode::new_get(node.0.borrow().id, &node.0.borrow().lbl)), optype => { let mut childs = vec![]; let mut i = 0; - while let Some((_in, _out, child)) = node.nth_child(i) { - let child = self.trans2bjit(&child)?; - childs.push(child); + 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; } + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + // TODO: Check here if the optype has multiple outputs. + // when it has, make a sub-collection of statements + // and make temporary variables with ::Set + // then return the output with a final ::Get to the + // output "my_out". + // If no output is given in "my_out" it's an error! + //"""""""""""""""""""""""""""""""""""""""""""""""""""""" + // TODO: Reorder the childs/arguments according to the input // order in the BlockLanguage Ok(BlkASTNode::new_node( node.0.borrow().id, + my_out, &node.0.borrow().typ, &node.0.borrow().lbl, - childs)) + childs, + )) } } } @@ -209,8 +310,8 @@ impl Block2JITCompiler { let tree = fun.generate_tree::("zero").unwrap(); println!("{}", tree.walk_dump("", "", 0)); - let blkast = self.trans2bjit(&tree); - println!("R: {:#?}", blkast); + let blkast = self.trans2bjit(&tree, None); + println!("R: {}", blkast.unwrap().borrow().dump(0, None)); Err(BlkJITCompileError::UnknownError) } diff --git a/tests/blocklang.rs b/tests/blocklang.rs index f9e2992..e76ecb4 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -26,19 +26,19 @@ fn check_blocklang_1() { { let mut block_fun = block_fun.lock().expect("matrix lock"); - block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); - block_fun.instanciate_at(0, 1, 0, "+", None); - block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); - - block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); - block_fun.instanciate_at(0, 4, 0, "-", None); - block_fun.instanciate_at(0, 5, 0, "->3", None); - block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); - block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); - block_fun.instanciate_at(0, 6, 0, "->", None); - block_fun.instanciate_at(0, 7, 0, "->2", None); +// block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); +// block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); +// block_fun.instanciate_at(0, 1, 0, "+", None); +// block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); +// +// block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); +// block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); +// block_fun.instanciate_at(0, 4, 0, "-", None); +// block_fun.instanciate_at(0, 5, 0, "->3", None); +// block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); +// block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); +// block_fun.instanciate_at(0, 6, 0, "->", None); +// block_fun.instanciate_at(0, 7, 0, "->2", None); block_fun.instanciate_at(0, 0, 3, "get", Some("in1".to_string())); block_fun.instanciate_at(0, 0, 4, "get", Some("in2".to_string())); From bf79157f8af3e934e106d4b5374240d61a6c5e6e Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sun, 31 Jul 2022 20:55:04 +0200 Subject: [PATCH 70/88] First steps of the block language compiler are done --- src/block_compiler.rs | 168 +++++++++++++++++++++++++++++++---------- src/blocklang.rs | 34 +++++++++ src/nodes/node_conf.rs | 2 +- tests/blocklang.rs | 32 ++++---- 4 files changed, 182 insertions(+), 54 deletions(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index 1a95483..e81d3ef 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -99,6 +99,9 @@ enum BlkASTNode { lbl: String, childs: Vec<(Option, BlkASTRef)>, }, + Literal { + value: f64, + } } impl BlkASTNode { @@ -107,14 +110,16 @@ impl BlkASTNode { if let Some(inp) = inp { indent_str += &format!("{}<= ", inp); + } else { + indent_str += "<= "; } match self { BlkASTNode::Root { child } => { - format!("{}* Root\n", indent_str) + &child.borrow().dump(indent + 1, None) + format!("{}Root\n", indent_str) + &child.borrow().dump(indent + 1, None) } BlkASTNode::Area { childs } => { - let mut s = format!("{}* Area\n", indent_str); + let mut s = format!("{}Area\n", indent_str); for c in childs.iter() { s += &c.borrow().dump(indent + 1, None); } @@ -126,14 +131,14 @@ impl BlkASTNode { BlkASTNode::Get { id, use_count, var } => { format!("{}get '{}' (id={}, use={})\n", indent_str, var, id, use_count) } + BlkASTNode::Literal { value } => { + format!("{}{}\n", indent_str, value) + } BlkASTNode::Node { id, out, use_count, typ, lbl, childs } => { let lbl = if *typ == *lbl { "".to_string() } else { format!("[{}]", lbl) }; let mut s = if let Some(out) = out { - format!( - "{}{}{} (id={}/{}, use={})\n", - indent_str, typ, lbl, id, out, use_count - ) + format!("{}{}{} (id={}/{}, use={})\n", indent_str, typ, lbl, id, out, use_count) } else { format!("{}{}{} (id={}, use={})\n", indent_str, typ, lbl, id, use_count) }; @@ -161,6 +166,14 @@ impl BlkASTNode { Rc::new(RefCell::new(BlkASTNode::Get { id, var: var.to_string(), use_count: 1 })) } + pub fn new_literal(val: &str) -> Result { + if let Ok(value) = val.parse::() { + Ok(Rc::new(RefCell::new(BlkASTNode::Literal { value }))) + } else { + Err(BlkJITCompileError::BadLiteralNumber(val.to_string())) + } + } + pub fn new_node( id: usize, out: Option, @@ -183,10 +196,17 @@ impl BlkASTNode { pub enum BlkJITCompileError { UnknownError, BadTree(ASTNodeRef), + NoOutputAtIdx(String, usize), + ASTMissingOutputLabel(usize), + NoTmpVarForOutput(usize, String), + BadLiteralNumber(String), } pub struct Block2JITCompiler { id_node_map: HashMap, + idout_var_map: HashMap, + lang: Rc>, + tmpvar_counter: usize, } // 1. compile the weird tree into a graph @@ -194,12 +214,25 @@ pub struct Block2JITCompiler { // - add a use count to each node, so that we know when to make temporary variables impl Block2JITCompiler { - pub fn new() -> Self { - Self { id_node_map: HashMap::new() } + pub fn new(lang: Rc>) -> Self { + Self { id_node_map: HashMap::new(), 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[..]) } pub fn trans2bjit( - &self, + &mut self, node: &ASTNodeRef, my_out: Option, ) -> Result { @@ -223,16 +256,19 @@ impl Block2JITCompiler { // XXX: SSA form of cranelift should take care of the rest! - match &node.0.borrow().typ[..] { - "" => { - if let Some((_in, out, first)) = node.first_child() { - let out = if out.len() > 0 { Some(out) } else { None }; - let child = self.trans2bjit(&first, out)?; - Ok(BlkASTNode::new_root(child)) - } else { - Err(BlkJITCompileError::BadTree(node.clone())) - } + 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![]; @@ -246,10 +282,10 @@ impl Block2JITCompiler { 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. + // 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 }; self.trans2bjit(&first, out) @@ -257,6 +293,9 @@ impl Block2JITCompiler { 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 }; @@ -266,7 +305,22 @@ impl Block2JITCompiler { Err(BlkJITCompileError::BadTree(node.clone())) } } - "get" => Ok(BlkASTNode::new_get(node.0.borrow().id, &node.0.borrow().lbl)), + "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![]; @@ -283,30 +337,64 @@ impl Block2JITCompiler { i += 1; } - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - // TODO: Check here if the optype has multiple outputs. - // when it has, make a sub-collection of statements - // and make temporary variables with ::Set - // then return the output with a final ::Get to the - // output "my_out". - // If no output is given in "my_out" it's an error! - //"""""""""""""""""""""""""""""""""""""""""""""""""""""" - // TODO: Reorder the childs/arguments according to the input // order in the BlockLanguage - Ok(BlkASTNode::new_node( - node.0.borrow().id, - my_out, - &node.0.borrow().typ, - &node.0.borrow().lbl, - childs, - )) + let cnt = self.lang.borrow().type_output_count(optype); + if cnt > 1 { + let mut area = vec![]; + area.push(BlkASTNode::new_node( + id, + my_out.clone(), + &node.0.borrow().typ, + &node.0.borrow().lbl, + childs, + )); + + for i in 0..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, + )) + } } } } - pub fn compile(&self, fun: &BlockFun) -> Result { + pub fn compile(&mut self, fun: &BlockFun) -> Result { let tree = fun.generate_tree::("zero").unwrap(); println!("{}", tree.walk_dump("", "", 0)); diff --git a/src/blocklang.rs b/src/blocklang.rs index d95cb36..e46d80c 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -902,6 +902,40 @@ impl BlockLanguage { identifiers } + pub fn get_type_outputs(&self, typ: &str) -> Option<&[Option]> { + let typ = self.types.get(typ)?; + Some(&typ.outputs) + } + + pub fn get_output_name_at_index(&self, typ: &str, idx: usize) -> Option { + let 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 { diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index f896e10..8f59084 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -700,7 +700,7 @@ impl NodeConfigurator { if let Ok(block_fun) = block_fun.lock() { if *generation != block_fun.generation() { *generation = block_fun.generation(); - let mut compiler = Block2JITCompiler::new(); + let mut compiler = Block2JITCompiler::new(block_fun.block_language()); compiler.compile(&block_fun); // let ast = block_compiler::compile(block_fun); diff --git a/tests/blocklang.rs b/tests/blocklang.rs index e76ecb4..d83ab96 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -26,19 +26,25 @@ fn check_blocklang_1() { { let mut block_fun = block_fun.lock().expect("matrix lock"); -// block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); -// block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); -// block_fun.instanciate_at(0, 1, 0, "+", None); -// block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); -// -// block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); -// block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); -// block_fun.instanciate_at(0, 4, 0, "-", None); -// block_fun.instanciate_at(0, 5, 0, "->3", None); -// block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); -// block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); -// block_fun.instanciate_at(0, 6, 0, "->", None); -// block_fun.instanciate_at(0, 7, 0, "->2", None); + block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); + block_fun.instanciate_at(0, 1, 0, "+", None); + block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); + + block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); + block_fun.instanciate_at(0, 4, 0, "-", None); + block_fun.instanciate_at(0, 5, 0, "->3", None); + + block_fun.instanciate_at(0, 3, 5, "get", Some("in1".to_string())); + block_fun.instanciate_at(0, 4, 5, "if", None); + block_fun.instanciate_at(1, 0, 0, "value", Some("0.5".to_string())); + block_fun.instanciate_at(2, 0, 0, "value", Some("-0.5".to_string())); + + block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); + block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); + block_fun.instanciate_at(0, 6, 0, "->", None); + block_fun.instanciate_at(0, 7, 0, "->2", None); block_fun.instanciate_at(0, 0, 3, "get", Some("in1".to_string())); block_fun.instanciate_at(0, 0, 4, "get", Some("in2".to_string())); From c0dad775779c966e037d17a568c9cdc4725bc0fa Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 1 Aug 2022 03:56:38 +0200 Subject: [PATCH 71/88] Wrote bjit to jit translator and got JIT code running in the Code node already --- src/block_compiler.rs | 98 ++++++++++++++++++++---------------------- src/dsp/node_code.rs | 1 + src/matrix.rs | 3 +- src/nodes/node_conf.rs | 33 +++++++------- tests/blocklang.rs | 47 +++++++++++++++++--- 5 files changed, 109 insertions(+), 73 deletions(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index e81d3ef..37cff2f 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -72,13 +72,10 @@ impl ASTNodeRef { } } -type BlkASTRef = Rc>; +type BlkASTRef = Rc; #[derive(Debug, Clone)] enum BlkASTNode { - Root { - child: BlkASTRef, - }, Area { childs: Vec, }, @@ -88,13 +85,11 @@ enum BlkASTNode { }, Get { id: usize, - use_count: usize, var: String, }, Node { id: usize, out: Option, - use_count: usize, typ: String, lbl: String, childs: Vec<(Option, BlkASTRef)>, @@ -115,60 +110,53 @@ impl BlkASTNode { } match self { - BlkASTNode::Root { child } => { - format!("{}Root\n", indent_str) + &child.borrow().dump(indent + 1, None) - } BlkASTNode::Area { childs } => { let mut s = format!("{}Area\n", indent_str); for c in childs.iter() { - s += &c.borrow().dump(indent + 1, None); + s += &c.dump(indent + 1, None); } s } BlkASTNode::Set { var, expr } => { - format!("{}set '{}'=\n", indent_str, var) + &expr.borrow().dump(indent + 1, None) + format!("{}set '{}'=\n", indent_str, var) + &expr.dump(indent + 1, None) } - BlkASTNode::Get { id, use_count, var } => { - format!("{}get '{}' (id={}, use={})\n", indent_str, var, id, use_count) + BlkASTNode::Get { id, var } => { + format!("{}get '{}' (id={})\n", indent_str, var, id) } BlkASTNode::Literal { value } => { format!("{}{}\n", indent_str, value) } - BlkASTNode::Node { id, out, use_count, typ, lbl, childs } => { + 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={}/{}, use={})\n", indent_str, typ, lbl, id, out, use_count) + format!("{}{}{} (id={}/{})\n", indent_str, typ, lbl, id, out) } else { - format!("{}{}{} (id={}, use={})\n", indent_str, typ, lbl, id, use_count) + format!("{}{}{} (id={})\n", indent_str, typ, lbl, id) }; for (inp, c) in childs.iter() { - s += &format!("{}", c.borrow().dump(indent + 1, inp.as_ref().map(|s| &s[..]))); + s += &format!("{}", c.dump(indent + 1, inp.as_ref().map(|s| &s[..]))); } s } } } - pub fn new_root(child: BlkASTRef) -> BlkASTRef { - Rc::new(RefCell::new(BlkASTNode::Root { child })) - } - pub fn new_area(childs: Vec) -> BlkASTRef { - Rc::new(RefCell::new(BlkASTNode::Area { childs })) + Rc::new(BlkASTNode::Area { childs }) } pub fn new_set(var: &str, expr: BlkASTRef) -> BlkASTRef { - Rc::new(RefCell::new(BlkASTNode::Set { var: var.to_string(), expr })) + Rc::new(BlkASTNode::Set { var: var.to_string(), expr }) } pub fn new_get(id: usize, var: &str) -> BlkASTRef { - Rc::new(RefCell::new(BlkASTNode::Get { id, var: var.to_string(), use_count: 1 })) + 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(RefCell::new(BlkASTNode::Literal { value }))) + Ok(Rc::new(BlkASTNode::Literal { value })) } else { Err(BlkJITCompileError::BadLiteralNumber(val.to_string())) } @@ -181,14 +169,13 @@ impl BlkASTNode { lbl: &str, childs: Vec<(Option, BlkASTRef)>, ) -> BlkASTRef { - Rc::new(RefCell::new(BlkASTNode::Node { + Rc::new(BlkASTNode::Node { id, out, typ: typ.to_string(), lbl: lbl.to_string(), - use_count: 1, childs, - })) + }) } } @@ -236,26 +223,6 @@ impl Block2JITCompiler { node: &ASTNodeRef, my_out: Option, ) -> Result { - // TODO: Deal with multiple outputs. - // If we encounter a node with multiple outputs, assign each output - // to a temporary variable and save that. - // Store the name of the temporary in a id+output mapping. - // => XXX - // That means: If we have a single output, things are easy, just plug them into - // the JIT ast: - // outer(inner()) - // But if we have multiple outputs: - // assign(a = inner()) - // assign(b = %1) - // outer_x(a) - // outer_y(b) - - // TODO: Filter out -> nodes from the AST - // TODO: For ->2 and ->3, save the input in some variable - // and reserve a id+output variable for this. - - // XXX: SSA form of cranelift should take care of the rest! - let id = node.0.borrow().id; if let Some(out) = &my_out { @@ -394,13 +361,40 @@ impl Block2JITCompiler { } } - pub fn compile(&mut self, fun: &BlockFun) -> Result { + pub 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 { id, var: varname } => { + Ok(var(varname)) + }, + BlkASTNode::Node { id, out, typ, lbl, childs } => { + Err(BlkJITCompileError::UnknownError) + } + BlkASTNode::Literal { value } => { + Ok(literal(*value)) + } + } + } + + pub fn compile(&mut self, fun: &BlockFun) -> Result, BlkJITCompileError> { let tree = fun.generate_tree::("zero").unwrap(); println!("{}", tree.walk_dump("", "", 0)); - let blkast = self.trans2bjit(&tree, None); - println!("R: {}", blkast.unwrap().borrow().dump(0, None)); + let blkast = self.trans2bjit(&tree, None)?; + println!("R: {}", blkast.dump(0, None)); - Err(BlkJITCompileError::UnknownError) + self.bjit2jit(&blkast) } } diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index 44278c6..005d62a 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -110,6 +110,7 @@ impl DspNode for Code { for frame in 0..ctx.nframes() { let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + println!("OUT: {}", ret); out.write(frame, ret); } diff --git a/src/matrix.rs b/src/matrix.rs index 26a70b8..b592fca 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -11,6 +11,7 @@ use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_ pub use crate::CellDir; use crate::ScopeHandle; use crate::blocklang::BlockFun; +use crate::block_compiler::BlkJITCompileError; use std::collections::{HashMap, HashSet}; @@ -600,7 +601,7 @@ impl Matrix { /// Checks the block function for the id `id`. If the block function did change, /// updates are then sent to the audio thread. /// See also [get_block_function]. - pub fn check_block_function(&mut self, id: usize) { + pub fn check_block_function(&mut self, id: usize) -> Result<(), BlkJITCompileError> { self.config.check_block_function(id) } diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 8f59084..55217b2 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -8,7 +8,7 @@ use super::{ }; use crate::blocklang::*; use crate::blocklang_def; -use crate::block_compiler::Block2JITCompiler; +use crate::block_compiler::{Block2JITCompiler, BlkJITCompileError}; 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}; @@ -695,33 +695,36 @@ impl NodeConfigurator { /// Checks the block function for the id `id`. If the block function did change, /// updates are then sent to the audio thread. /// See also [get_block_function]. - pub fn check_block_function(&mut self, id: usize) { + pub fn check_block_function(&mut self, id: usize) -> Result<(), BlkJITCompileError> { 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()); - compiler.compile(&block_fun); + let ast = compiler.compile(&block_fun)?; // let ast = block_compiler::compile(block_fun); if let Some(cod) = self.code_engines.get_mut(id) { use synfx_dsp_jit::build::*; - cod.upload(stmts(&[ - assign( - "*phase", - op_add(var("*phase"), op_mul(literal(440.0), var("israte"))), - ), - _if( - op_gt(var("*phase"), literal(1.0)), - assign("*phase", op_sub(var("*phase"), literal(1.0))), - None, - ), - var("*phase"), - ])); + cod.upload(ast); +// stmts(&[ +// assign( +// "*phase", +// op_add(var("*phase"), op_mul(literal(440.0), var("israte"))), +// ), +// _if( +// op_gt(var("*phase"), literal(1.0)), +// assign("*phase", op_sub(var("*phase"), literal(1.0))), +// None, +// ), +// var("*phase"), +// ])); } } } } + + Ok(()) } /// Retrieve a handle to the block function `id`. In case you modify the block function, diff --git a/tests/blocklang.rs b/tests/blocklang.rs index d83ab96..278eb50 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -5,9 +5,8 @@ mod common; use common::*; -#[test] -fn check_blocklang_1() { - let (node_conf, mut node_exec) = new_node_engine(); +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); @@ -20,7 +19,45 @@ fn check_blocklang_1() { .unwrap(); matrix.sync().unwrap(); - let code = NodeId::Code(0); + (matrix, node_exec) +} + +#[test] +fn check_blocklang_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"); + block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); +// block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); + } + + matrix.check_block_function(0).expect("no compile error"); + + let res = run_for_ms(&mut node_exec, 25.0); + println!("RES: {:?}", res.1); + assert_decimated_feq!( + res.0, + 50, + vec![ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + ] + ); +} +#[test] +fn check_blocklang_2() { + let (mut matrix, mut node_exec) = setup(); let block_fun = matrix.get_block_function(0).expect("block fun exists"); { @@ -55,7 +92,7 @@ fn check_blocklang_1() { block_fun.instanciate_at(0, 4, 4, "set", Some("*ap".to_string())); } - matrix.check_block_function(0); + matrix.check_block_function(0).expect("no compile error"); let res = run_for_ms(&mut node_exec, 25.0); assert_decimated_feq!( From 648c94cf799f43bcf3837f4c7b832104232c7dcc Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 1 Aug 2022 05:31:07 +0200 Subject: [PATCH 72/88] fixed broken test case for blocklang --- src/dsp/node_code.rs | 13 ++++++++--- tests/blocklang.rs | 53 ++++++++++++++------------------------------ 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index 005d62a..6685897 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -92,11 +92,17 @@ impl DspNode for Code { outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals, ) { - use crate::dsp::{at, denorm, inp, out}; + use crate::dsp::{at, denorm, inp, out, out_idx}; // let clock = inp::TSeq::clock(inputs); // let trig = inp::TSeq::trig(inputs); // let cmode = at::TSeq::cmode(atoms); let out = out::Code::sig(outputs); + 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")] { @@ -110,8 +116,9 @@ impl DspNode for Code { for frame in 0..ctx.nframes() { let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); - println!("OUT: {}", ret); - out.write(frame, ret); + sig.write(frame, ret); + sig1.write(frame, s1); + sig2.write(frame, s2); } ctx_vals[0].set(0.0); diff --git a/tests/blocklang.rs b/tests/blocklang.rs index 278eb50..67924b4 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -30,31 +30,28 @@ fn check_blocklang_1() { { let mut block_fun = block_fun.lock().expect("matrix lock"); block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); -// block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); + block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); } matrix.check_block_function(0).expect("no compile error"); let res = run_for_ms(&mut node_exec, 25.0); - println!("RES: {:?}", res.1); - assert_decimated_feq!( - res.0, - 50, - vec![ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - ] - ); + assert_decimated_feq!(res.0, 50, vec![0.3; 10]); } + + +// XXX: Test case with 3 outputs, where the first output writes a value used +// by the computation after the first but before the third output. +/* + 0.3 ->3 set a + => -> + set b + get a + => -> - set a + get b + get a + + get b +*/ + #[test] fn check_blocklang_2() { let (mut matrix, mut node_exec) = setup(); @@ -95,21 +92,5 @@ fn check_blocklang_2() { 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.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - ] - ); + assert_decimated_feq!(res.0, 50, vec![0.2; 100]); } From 41b0b3b7fc88731c92be56b6faa7ca6d9b3befc5 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Mon, 1 Aug 2022 06:58:42 +0200 Subject: [PATCH 73/88] refactored test code a bit --- src/block_compiler.rs | 63 ++++++++++++++++ tests/blocklang.rs | 168 ++++++++++++++++++++---------------------- tests/node_code.rs | 40 ++++++++++ 3 files changed, 182 insertions(+), 89 deletions(-) create mode 100644 tests/node_code.rs diff --git a/src/block_compiler.rs b/src/block_compiler.rs index 37cff2f..fbdd2c9 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -398,3 +398,66 @@ impl Block2JITCompiler { self.bjit2jit(&blkast) } } + +#[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 + ) + } + }; + } + + use synfx_dsp_jit::{get_standard_library, DSPNodeContext, JIT, ASTFun, DSPFunction}; + + fn new_jit_fun(mut f: F) -> (Rc>, Box) { + use crate::block_compiler::{BlkJITCompileError, Block2JITCompiler}; + use crate::blocklang::BlockFun; + use crate::blocklang_def; + + let lang = blocklang_def::setup_hxdsp_block_language(); + 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 lib = get_standard_library(); + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.3".to_string())).unwrap(); + bf.instanciate_at(0, 1, 2, "set", Some("&sig2".to_string())).unwrap(); + bf.instanciate_at(0, 0, 3, "value", Some("-1.3".to_string())).unwrap(); + }); + + 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(); + } + +} diff --git a/tests/blocklang.rs b/tests/blocklang.rs index 67924b4..6eda8cd 100644 --- a/tests/blocklang.rs +++ b/tests/blocklang.rs @@ -5,92 +5,82 @@ mod common; use common::*; -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) -} - -#[test] -fn check_blocklang_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"); - block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); - block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); - } - - 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]); -} - - -// XXX: Test case with 3 outputs, where the first output writes a value used -// by the computation after the first but before the third output. -/* - 0.3 ->3 set a - => -> + set b - get a - => -> - set a - get b - get a + - get b -*/ - -#[test] -fn check_blocklang_2() { - 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"); - - block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); - block_fun.instanciate_at(0, 1, 0, "+", None); - block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); - - block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); - block_fun.instanciate_at(0, 4, 0, "-", None); - block_fun.instanciate_at(0, 5, 0, "->3", None); - - block_fun.instanciate_at(0, 3, 5, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 4, 5, "if", None); - block_fun.instanciate_at(1, 0, 0, "value", Some("0.5".to_string())); - block_fun.instanciate_at(2, 0, 0, "value", Some("-0.5".to_string())); - - block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); - block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); - block_fun.instanciate_at(0, 6, 0, "->", None); - block_fun.instanciate_at(0, 7, 0, "->2", None); - - block_fun.instanciate_at(0, 0, 3, "get", Some("in1".to_string())); - block_fun.instanciate_at(0, 0, 4, "get", Some("in2".to_string())); - block_fun.instanciate_at(0, 1, 3, "/%", None); - block_fun.instanciate_at(0, 2, 3, "->", None); - block_fun.instanciate_at(0, 3, 3, "/%", None); - block_fun.instanciate_at(0, 4, 3, "set", Some("&sig2".to_string())); - block_fun.instanciate_at(0, 4, 4, "set", Some("*ap".to_string())); - } - - 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.2; 100]); -} +//#[test] +//fn check_blocklang_dir_1() { +// use hexodsp::block_compiler::{BlkJITCompileError, Block2JITCompiler}; +// use hexodsp::blocklang::BlockFun; +// use hexodsp::blocklang_def; +// +// let lang = blocklang_def::setup_hxdsp_block_language(); +// let mut bf = BlockFun::new(lang.clone()); +// block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); +// block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); +// +// let mut compiler = Block2JITCompiler::new(block_fun.block_language()); +// let ast = compiler.compile(&block_fun)?; +// let lib = synfx_dsp_jit::get_standard_library(); +// let ctx = synfx_dsp_jit::DSPNodeContext::new_ref(); +// let jit = JIT::new(lib, dsp_ctx.clone()); +// let fun = jit.compile(ASTFun::new(ast))?; +// +// fun.init(44100.0, None); +// +// let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); +// +// ctx.borrow_mut().free(); +//} +// +//// XXX: Test case with 3 outputs, where the first output writes a value used +//// by the computation after the first but before the third output. +// +// 0.3 ->3 set a +// => -> + set b +// get a +// => -> - set a +// get b +// get a + +// get b +//*/ +// +////#[test] +////fn check_blocklang_2() { +//// 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"); +//// +//// block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); +//// block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); +//// block_fun.instanciate_at(0, 1, 0, "+", None); +//// block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); +//// +//// block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); +//// block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); +//// block_fun.instanciate_at(0, 4, 0, "-", None); +//// block_fun.instanciate_at(0, 5, 0, "->3", None); +//// +//// block_fun.instanciate_at(0, 3, 5, "get", Some("in1".to_string())); +//// block_fun.instanciate_at(0, 4, 5, "if", None); +//// block_fun.instanciate_at(1, 0, 0, "value", Some("0.5".to_string())); +//// block_fun.instanciate_at(2, 0, 0, "value", Some("-0.5".to_string())); +//// +//// block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); +//// block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); +//// block_fun.instanciate_at(0, 6, 0, "->", None); +//// block_fun.instanciate_at(0, 7, 0, "->2", None); +//// +//// block_fun.instanciate_at(0, 0, 3, "get", Some("in1".to_string())); +//// block_fun.instanciate_at(0, 0, 4, "get", Some("in2".to_string())); +//// block_fun.instanciate_at(0, 1, 3, "/%", None); +//// block_fun.instanciate_at(0, 2, 3, "->", None); +//// block_fun.instanciate_at(0, 3, 3, "/%", None); +//// block_fun.instanciate_at(0, 4, 3, "set", Some("&sig2".to_string())); +//// block_fun.instanciate_at(0, 4, 4, "set", Some("*ap".to_string())); +//// } +//// +//// 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.2; 100]); +////} diff --git a/tests/node_code.rs b/tests/node_code.rs new file mode 100644 index 0000000..25577a1 --- /dev/null +++ b/tests/node_code.rs @@ -0,0 +1,40 @@ +// 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::*; + +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) +} + +#[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"); + block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); + block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); + } + + 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]); +} From 4d5a4ef86592ec9c46c4d8a59ad41470e17f90ea Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 2 Aug 2022 05:07:18 +0200 Subject: [PATCH 74/88] Nearly finished the bjit2jit compiler. --- src/block_compiler.rs | 109 ++++++++---- src/blocklang.rs | 20 ++- src/blocklang_def.rs | 381 ++++++++++++++++++++++------------------- src/nodes/node_conf.rs | 6 +- src/wblockdsp.rs | 4 + tests/node_code.rs | 1 + 6 files changed, 305 insertions(+), 216 deletions(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index fbdd2c9..4fcd1a7 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -96,7 +96,7 @@ enum BlkASTNode { }, Literal { value: f64, - } + }, } impl BlkASTNode { @@ -169,13 +169,7 @@ impl BlkASTNode { lbl: &str, childs: Vec<(Option, BlkASTRef)>, ) -> BlkASTRef { - Rc::new(BlkASTNode::Node { - id, - out, - typ: typ.to_string(), - lbl: lbl.to_string(), - childs, - }) + Rc::new(BlkASTNode::Node { id, out, typ: typ.to_string(), lbl: lbl.to_string(), childs }) } } @@ -187,6 +181,9 @@ pub enum BlkJITCompileError { ASTMissingOutputLabel(usize), NoTmpVarForOutput(usize, String), BadLiteralNumber(String), + NodeWithoutID(String), + UnknownType(String), + TooManyInputs(String, usize), } pub struct Block2JITCompiler { @@ -260,9 +257,7 @@ impl Block2JITCompiler { Err(BlkJITCompileError::BadTree(node.clone())) } } - "value" => { - Ok(BlkASTNode::new_literal(&node.0.borrow().lbl)?) - } + "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 }; @@ -283,7 +278,6 @@ impl Block2JITCompiler { 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())) } @@ -327,11 +321,7 @@ impl Block2JITCompiler { &tmp_var, BlkASTNode::new_get(0, &format!("%{}", i)), )); - self.store_idout_var( - id, - &oname, - &tmp_var, - ); + self.store_idout_var(id, &oname, &tmp_var); } else { return Err(BlkJITCompileError::NoOutputAtIdx(optype.to_string(), i)); } @@ -371,20 +361,44 @@ impl Block2JITCompiler { stmt.push(self.bjit2jit(&c)?); } Ok(stmts(&stmt[..])) - }, + } BlkASTNode::Set { var, expr } => { let e = self.bjit2jit(&expr)?; Ok(assign(var, e)) } - BlkASTNode::Get { id, var: varname } => { - Ok(var(varname)) + BlkASTNode::Get { id, var: varname } => Ok(var(varname)), + BlkASTNode::Node { id, out, typ, lbl, childs } => match &typ[..] { + "if" => Err(BlkJITCompileError::UnknownError), + "zero" => Ok(literal(0.0)), + node => { + 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)); + } + + for input_name in inputs.iter() { + for (inp, c) in childs.iter() { + if inp == input_name { + args.push(self.bjit2jit(&c)?); + } + } + } + } else { + return Err(BlkJITCompileError::UnknownType(typ.to_string())); + } + + Ok(call(typ, *id as u64, &args[..])) + } }, - BlkASTNode::Node { id, out, typ, lbl, childs } => { - Err(BlkJITCompileError::UnknownError) - } - BlkASTNode::Literal { value } => { - Ok(literal(*value)) - } + BlkASTNode::Literal { value } => Ok(literal(*value)), } } @@ -416,21 +430,23 @@ mod test { }; } - use synfx_dsp_jit::{get_standard_library, DSPNodeContext, JIT, ASTFun, DSPFunction}; + use synfx_dsp_jit::{get_standard_library, ASTFun, DSPFunction, DSPNodeContext, JIT}; - fn new_jit_fun(mut f: F) -> (Rc>, Box) { + fn new_jit_fun( + mut f: F, + ) -> (Rc>, Box) { use crate::block_compiler::{BlkJITCompileError, Block2JITCompiler}; use crate::blocklang::BlockFun; use crate::blocklang_def; - let lang = blocklang_def::setup_hxdsp_block_language(); + let lib = get_standard_library(); + let lang = blocklang_def::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 lib = get_standard_library(); let ctx = DSPNodeContext::new_ref(); let jit = JIT::new(lib, ctx.clone()); let mut fun = jit.compile(ASTFun::new(ast)).expect("jit compiles"); @@ -440,7 +456,6 @@ mod test { (ctx, fun) } - #[test] fn check_blocklang_sig1() { let (ctx, mut fun) = new_jit_fun(|bf| { @@ -460,4 +475,36 @@ mod test { ctx.borrow_mut().free(); } + #[test] + fn check_blocklang_accum_shift() { + let (ctx, mut fun) = new_jit_fun(|bf| { + bf.instanciate_at(0, 1, 1, "accum", None); + bf.shift_port(0, 1, 1, 1, false); + bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); + bf.instanciate_at(0, 0, 1, "get", Some("*reset".to_string())); + }); + + 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); + + let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); + let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); + let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); + let (s1, s2, ret) = 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(); + } } diff --git a/src/blocklang.rs b/src/blocklang.rs index e46d80c..1a6c715 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -907,15 +907,21 @@ impl BlockLanguage { 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 { - let 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()); + 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; } - i += 1; } } diff --git a/src/blocklang_def.rs b/src/blocklang_def.rs index 5cc7762..20a35e8 100644 --- a/src/blocklang_def.rs +++ b/src/blocklang_def.rs @@ -2,11 +2,14 @@ // This file is a part of HexoDSP. Released under GPL-3.0-or-later. // See README.md and COPYING for details. -use crate::blocklang::{BlockLanguage, BlockUserInput, BlockType}; +use crate::blocklang::{BlockLanguage, BlockType, BlockUserInput}; use std::cell::RefCell; use std::rc::Rc; +use synfx_dsp_jit::DSPNodeTypeLibrary; -pub fn setup_hxdsp_block_language() -> Rc> { +pub fn setup_hxdsp_block_language( + dsp_lib: Rc>, +) -> Rc> { let mut lang = BlockLanguage::new(); lang.define(BlockType { @@ -17,222 +20,248 @@ pub fn setup_hxdsp_block_language() -> Rc> { outputs: vec![Some("".to_string())], area_count: 0, user_input: BlockUserInput::None, - description: "A phasor, returns a saw tooth wave to scan through things or use as modulator.".to_string(), + description: + "A phasor, returns a saw tooth wave to scan through things or use as modulator." + .to_string(), color: 2, }); 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, + 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, + 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, + 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: "SR".to_string(), - rows: 1, - inputs: vec![], - outputs: vec![Some("".to_string())], - area_count: 0, - user_input: BlockUserInput::None, - description: "The sample rate".to_string(), - color: 1, + 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: "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, + 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: "->".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, + 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: "->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, + 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: "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, + 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: "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, + 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: "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, + 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: "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, + // }); 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, - }); - - lang.define(BlockType { - category: "arithmetics".to_string(), - name: "/%".to_string(), - rows: 2, - inputs: vec![Some("a".to_string()), Some("b".to_string())], - outputs: vec![Some("div".to_string()), Some("rem".to_string())], - area_count: 0, - user_input: BlockUserInput::None, - description: "Computes the integer division and remainder of a / b".to_string(), - color: 8, + category: "arithmetics".to_string(), + name: "/%".to_string(), + rows: 2, + inputs: vec![Some("a".to_string()), Some("b".to_string())], + outputs: vec![Some("div".to_string()), Some("rem".to_string())], + area_count: 0, + user_input: BlockUserInput::None, + description: "Computes the integer division and remainder of a / b".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, + 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"); diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 55217b2..73163b6 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -281,7 +281,9 @@ impl NodeConfigurator { let mut scopes = vec![]; scopes.resize_with(MAX_SCOPES, || ScopeHandle::new_shared()); - let lang = blocklang_def::setup_hxdsp_block_language(); + let code_engines = vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES]; + + let lang = blocklang_def::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())))) @@ -304,7 +306,7 @@ impl NodeConfigurator { node2idx: HashMap::new(), trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], #[cfg(feature = "synfx-dsp-jit")] - code_engines: vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES], + code_engines, block_functions, scopes, }, diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs index 0e97cfa..60b9b86 100644 --- a/src/wblockdsp.rs +++ b/src/wblockdsp.rs @@ -45,6 +45,10 @@ impl CodeEngine { Self { lib, dsp_ctx: DSPNodeContext::new_ref(), update_prod, return_cons } } + pub fn get_lib(&self) -> Rc> { + self.lib.clone() + } + pub fn upload(&mut self, ast: Box) -> Result<(), JITCompileError> { let jit = JIT::new(self.lib.clone(), self.dsp_ctx.clone()); let fun = jit.compile(ASTFun::new(ast))?; diff --git a/tests/node_code.rs b/tests/node_code.rs index 25577a1..0e699fb 100644 --- a/tests/node_code.rs +++ b/tests/node_code.rs @@ -38,3 +38,4 @@ fn check_node_code_1() { let res = run_for_ms(&mut node_exec, 25.0); assert_decimated_feq!(res.0, 50, vec![0.3; 10]); } + From 4c673ec19815559fd8d6ab2562314310a77bfa76 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 2 Aug 2022 05:12:09 +0200 Subject: [PATCH 75/88] Removed dummy jit code --- src/nodes/node_conf.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 73163b6..de70b06 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -709,18 +709,6 @@ impl NodeConfigurator { if let Some(cod) = self.code_engines.get_mut(id) { use synfx_dsp_jit::build::*; cod.upload(ast); -// stmts(&[ -// assign( -// "*phase", -// op_add(var("*phase"), op_mul(literal(440.0), var("israte"))), -// ), -// _if( -// op_gt(var("*phase"), literal(1.0)), -// assign("*phase", op_sub(var("*phase"), literal(1.0))), -// None, -// ), -// var("*phase"), -// ])); } } } @@ -763,19 +751,6 @@ impl NodeConfigurator { let code_idx = ni.instance(); if let Some(cod) = self.code_engines.get_mut(code_idx) { node.set_backend(cod.get_backend()); - use synfx_dsp_jit::build::*; - cod.upload(stmts(&[ - assign( - "*phase", - op_add(var("*phase"), op_mul(literal(440.0), var("israte"))), - ), - _if( - op_gt(var("*phase"), literal(1.0)), - assign("*phase", op_sub(var("*phase"), literal(1.0))), - None, - ), - var("*phase"), - ])); } } From 45857b064be19f9f0427d3c1649c8545a0126e7d Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Tue, 2 Aug 2022 20:12:06 +0200 Subject: [PATCH 76/88] implemented most of the arithmetics --- src/block_compiler.rs | 126 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 4 deletions(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index 4fcd1a7..746531f 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -184,6 +184,8 @@ pub enum BlkJITCompileError { NodeWithoutID(String), UnknownType(String), TooManyInputs(String, usize), + WrongNumberOfChilds(String, usize, usize), + UnassignedInput(String, usize, String), } pub struct Block2JITCompiler { @@ -384,10 +386,38 @@ impl Block2JITCompiler { return Err(BlkJITCompileError::TooManyInputs(typ.to_string(), *id)); } - for input_name in inputs.iter() { + if inputs.len() > 0 && inputs[0] == Some("".to_string()) { + // We assume all inputs are unnamed: + if inputs.len() != childs.len() { + return Err(BlkJITCompileError::WrongNumberOfChilds( + typ.to_string(), + *id, + childs.len(), + )); + } + for (inp, c) in childs.iter() { - if inp == input_name { - args.push(self.bjit2jit(&c)?); + 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), + )); } } } @@ -395,7 +425,29 @@ impl Block2JITCompiler { return Err(BlkJITCompileError::UnknownType(typ.to_string())); } - Ok(call(typ, *id as u64, &args[..])) + 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)), @@ -507,4 +559,70 @@ mod test { ctx.borrow_mut().free(); } + + #[test] + fn check_blocklang_arithmetics() { + // Check + and * + let (ctx, mut fun) = new_jit_fun(|bf| { + bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); + bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); + bf.instanciate_at(0, 1, 1, "+", None); + bf.shift_port(0, 1, 1, 1, true); + bf.instanciate_at(0, 1, 3, "value", Some("2.0".to_string())); + bf.instanciate_at(0, 2, 2, "*", None); + }); + + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); + bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); + bf.instanciate_at(0, 1, 1, "-", None); + bf.shift_port(0, 1, 1, 1, true); + bf.instanciate_at(0, 1, 3, "value", Some("2.0".to_string())); + bf.instanciate_at(0, 2, 2, "/", None); + }); + + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); + bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); + bf.instanciate_at(0, 1, 1, "-", None); + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); + bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); + bf.instanciate_at(0, 1, 1, "/", None); + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); + bf.instanciate_at(0, 0, 2, "value", Some("0.0".to_string())); + bf.instanciate_at(0, 1, 1, "/", None); + }); + + let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); + assert_float_eq!(ret, 0.5 / 0.0); + ctx.borrow_mut().free(); + } } From 561781e3df2fbd49ab62a8c6ee15680858587f0a Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 3 Aug 2022 18:48:10 +0200 Subject: [PATCH 77/88] boilerplate for serializing the block code --- src/blocklang.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/blocklang.rs b/src/blocklang.rs index 1a6c715..43c09fc 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -9,6 +9,8 @@ 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; @@ -805,6 +807,33 @@ impl BlockArea { true } + + pub fn serialize(&self) -> Value { + let mut v = json!({}); + + v + } + + pub fn deserialize(s: &str) -> Result, serde_json::Error> { + let v: Value = serde_json::from_str(s)?; + + let blocks = HashMap::new(); + let size = (0, 0); + let auto_shrink = false; + let header = "".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)] @@ -974,6 +1003,38 @@ pub struct BlockFunSnapshot { cur_id: usize, } +impl BlockFunSnapshot { + pub fn serialize(&self) -> String { + 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.to_string() + } + + pub fn deserialize(s: &str) -> Result { + let v: Value = serde_json::from_str(s)?; + + let mut areas = vec![]; + + Ok(BlockFunSnapshot { + areas, + cur_id: v["current_block_id_counter"].as_i64().unwrap_or(0) as usize, + }) + } +} + #[derive(Debug, Clone)] pub struct BlockFun { language: Rc>, From 9683213c2805c43c641a36c0760a68959a0b2d01 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Wed, 3 Aug 2022 19:36:12 +0200 Subject: [PATCH 78/88] Started serializing the BlockFun structure --- src/blocklang.rs | 172 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 16 deletions(-) diff --git a/src/blocklang.rs b/src/blocklang.rs index 43c09fc..0856c56 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -177,6 +177,91 @@ impl Block { } } } + + /// 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 }; + let mut contains = json!([c0, c1]); + json!({ + "id": self.id as i64, + "rows": self.rows as i64, + "contains": contains, + "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 { @@ -808,27 +893,54 @@ impl BlockArea { true } + /// Serializes this [BlockArea] to a JSON [Value]. + /// Usually called by [BlockFunSnapshot::serialize]. pub fn serialize(&self) -> Value { - let mut v = json!({}); + 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 } - pub fn deserialize(s: &str) -> Result, serde_json::Error> { - let v: Value = serde_json::from_str(s)?; + /// 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 blocks = HashMap::new(); - let size = (0, 0); - let auto_shrink = false; - let header = "".to_string(); + 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 mut ba = Box::new(BlockArea { - blocks, - origin_map: HashMap::new(), - size, - auto_shrink, - header, - }); + 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(); @@ -1026,10 +1138,17 @@ impl BlockFunSnapshot { pub fn deserialize(s: &str) -> Result { let v: Value = serde_json::from_str(s)?; - let mut areas = vec![]; + 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, + areas: a, cur_id: v["current_block_id_counter"].as_i64().unwrap_or(0) as usize, }) } @@ -1686,3 +1805,24 @@ impl BlockCodeView for BlockFun { self.generation } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn check_blockfun_serialize_empty() { + let dsp_lib = synfx_dsp_jit::get_standard_library(); + let lang = crate::blocklang_def::setup_hxdsp_block_language(dsp_lib); + let mut bf = BlockFun::new(lang.clone()); + + let sn = bf.save_snapshot(); + let serialized = sn.serialize(); + assert_eq!(serialized, "{\"VERSION\":1,\"areas\":[{\"auto_shrink\":false,\"blocks\":[],\"header\":\"\",\"size\":[16,16]}],\"current_block_id_counter\":0}"); + + let sn = BlockFunSnapshot::deserialize(&serialized).expect("No deserialization error"); + let mut bf2 = BlockFun::new(lang); + let bf2 = bf2.load_snapshot(&sn); + } +} + From 0c75da09123e2ec6d8a4e8f7e32001c81ff65ced Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Thu, 4 Aug 2022 03:46:56 +0200 Subject: [PATCH 79/88] Made serialization working --- src/blocklang.rs | 40 ++++++++++++++++++++++++++++++++-------- src/matrix.rs | 26 +++++++++++++++++++++++--- src/matrix_repr.rs | 25 ++++++++++++++++++++++++- src/nodes/node_conf.rs | 2 +- 4 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/blocklang.rs b/src/blocklang.rs index 0856c56..b88c59b 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -1116,7 +1116,7 @@ pub struct BlockFunSnapshot { } impl BlockFunSnapshot { - pub fn serialize(&self) -> String { + pub fn serialize(&self) -> Value { let mut v = json!({ "VERSION": 1, }); @@ -1132,12 +1132,10 @@ impl BlockFunSnapshot { v["areas"] = areas; - v.to_string() + v } - pub fn deserialize(s: &str) -> Result { - let v: Value = serde_json::from_str(s)?; - + pub fn deserialize(v: &Value) -> Result { let mut a = vec![]; let areas = &v["areas"]; @@ -1184,6 +1182,10 @@ impl BlockFun { } } + pub fn is_unset(&self) -> bool { + self.generation == 0 + } + pub fn block_language(&self) -> Rc> { self.language.clone() } @@ -1817,12 +1819,34 @@ mod test { let mut bf = BlockFun::new(lang.clone()); let sn = bf.save_snapshot(); - let serialized = sn.serialize(); + 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 sn = BlockFunSnapshot::deserialize(&serialized).expect("No deserialization error"); + 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::blocklang_def::setup_hxdsp_block_language(dsp_lib); + let mut bf = BlockFun::new(lang.clone()); + + bf.instanciate_at(0, 0, 0, "+", None); + + 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/matrix.rs b/src/matrix.rs index b592fca..9817541 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -10,7 +10,7 @@ pub use crate::nodes::MinMaxMonitorSamples; use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES}; pub use crate::CellDir; use crate::ScopeHandle; -use crate::blocklang::BlockFun; +use crate::blocklang::{BlockFun, BlockFunSnapshot}; use crate::block_compiler::BlkJITCompileError; use std::collections::{HashMap, HashSet}; @@ -607,7 +607,7 @@ impl Matrix { /// Retrieve a handle to the block function `id`. In case you modify the block function, /// make sure to call [check_block_function]. - pub fn get_block_function(&mut self, id: usize) -> Option>> { + pub fn get_block_function(&self, id: usize) -> Option>> { self.config.get_block_function(id) } @@ -845,9 +845,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] @@ -879,6 +891,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 { diff --git a/src/matrix_repr.rs b/src/matrix_repr.rs index 93866d5..6611d0a 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::blocklang::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/node_conf.rs b/src/nodes/node_conf.rs index de70b06..e44850a 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -719,7 +719,7 @@ impl NodeConfigurator { /// Retrieve a handle to the block function `id`. In case you modify the block function, /// make sure to call [check_block_function]. - pub fn get_block_function(&mut self, id: usize) -> Option>> { + pub fn get_block_function(&self, id: usize) -> Option>> { self.block_functions.get(id).map(|pair| pair.1.clone()) } From 8781fc72f286d38acc532410faec34b5f62ae22d Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Thu, 4 Aug 2022 20:48:38 +0200 Subject: [PATCH 80/88] Removed non existing node types from blocklang --- src/blocklang_def.rs | 50 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/blocklang_def.rs b/src/blocklang_def.rs index 20a35e8..6833ef9 100644 --- a/src/blocklang_def.rs +++ b/src/blocklang_def.rs @@ -12,20 +12,20 @@ pub fn setup_hxdsp_block_language( ) -> Rc> { let mut lang = BlockLanguage::new(); - lang.define(BlockType { - category: "source".to_string(), - name: "phse".to_string(), - rows: 1, - inputs: vec![Some("f".to_string())], - outputs: vec![Some("".to_string())], - area_count: 0, - user_input: BlockUserInput::None, - description: - "A phasor, returns a saw tooth wave to scan through things or use as modulator." - .to_string(), - color: 2, - }); - +// lang.define(BlockType { +// category: "source".to_string(), +// name: "phse".to_string(), +// rows: 1, +// inputs: vec![Some("f".to_string())], +// outputs: vec![Some("".to_string())], +// area_count: 0, +// user_input: BlockUserInput::None, +// description: +// "A phasor, returns a saw tooth wave to scan through things or use as modulator." +// .to_string(), +// color: 2, +// }); +// lang.define(BlockType { category: "literals".to_string(), name: "zero".to_string(), @@ -198,17 +198,17 @@ pub fn setup_hxdsp_block_language( // color: 8, // }); - lang.define(BlockType { - category: "arithmetics".to_string(), - name: "/%".to_string(), - rows: 2, - inputs: vec![Some("a".to_string()), Some("b".to_string())], - outputs: vec![Some("div".to_string()), Some("rem".to_string())], - area_count: 0, - user_input: BlockUserInput::None, - description: "Computes the integer division and remainder of a / b".to_string(), - color: 8, - }); +// lang.define(BlockType { +// category: "arithmetics".to_string(), +// name: "/%".to_string(), +// rows: 2, +// inputs: vec![Some("a".to_string()), Some("b".to_string())], +// outputs: vec![Some("div".to_string()), Some("rem".to_string())], +// area_count: 0, +// user_input: BlockUserInput::None, +// description: "Computes the integer division and remainder of a / b".to_string(), +// color: 8, +// }); for fun_name in &["+", "-", "*", "/"] { lang.define(BlockType { From 147e5ee18ab89ccbffe9c143cad8e6c9f51fe4bf Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Thu, 4 Aug 2022 22:51:16 +0200 Subject: [PATCH 81/88] Improved compiler error output and handling return values --- src/block_compiler.rs | 148 +++++++++++++++++++++++++++++++++++++---- src/nodes/node_conf.rs | 7 +- 2 files changed, 141 insertions(+), 14 deletions(-) diff --git a/src/block_compiler.rs b/src/block_compiler.rs index 746531f..75ec9ca 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::rc::Rc; use crate::blocklang::*; -use synfx_dsp_jit::ASTNode; +use synfx_dsp_jit::{ASTNode, JITCompileError}; #[derive(Debug)] struct JASTNode { @@ -186,6 +186,7 @@ pub enum BlkJITCompileError { TooManyInputs(String, usize), WrongNumberOfChilds(String, usize, usize), UnassignedInput(String, usize, String), + JITCompileError(JITCompileError), } pub struct Block2JITCompiler { @@ -251,7 +252,19 @@ impl Block2JITCompiler { // 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) @@ -260,11 +273,15 @@ impl Block2JITCompiler { } } "value" => Ok(BlkASTNode::new_literal(&node.0.borrow().lbl)?), - "set" => { + "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)?; - Ok(BlkASTNode::new_set(&node.0.borrow().lbl, expr)) + 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())) } @@ -306,16 +323,30 @@ impl Block2JITCompiler { let cnt = self.lang.borrow().type_output_count(optype); if cnt > 1 { let mut area = vec![]; - area.push(BlkASTNode::new_node( - id, - my_out.clone(), - &node.0.borrow().typ, - &node.0.borrow().lbl, - childs, - )); - for i in 0..cnt { + 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); @@ -323,6 +354,7 @@ impl Block2JITCompiler { &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)); @@ -625,4 +657,96 @@ mod test { 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "/%", None); + bf.instanciate_at(0, 2, 2, "set", Some("&sig1".to_string())).unwrap(); + }); + + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "/%", None); + bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + }); + + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "/%", None); + bf.instanciate_at(0, 2, 2, "set", Some("&sig1".to_string())).unwrap(); + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "/%", None); + bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "/%", None); + bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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| { + bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); + bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); + bf.instanciate_at(0, 1, 1, "/%", None); + bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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/nodes/node_conf.rs b/src/nodes/node_conf.rs index e44850a..7caeeea 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -6,9 +6,9 @@ use super::{ FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES, MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, }; +use crate::block_compiler::{BlkJITCompileError, Block2JITCompiler}; use crate::blocklang::*; use crate::blocklang_def; -use crate::block_compiler::{Block2JITCompiler, BlkJITCompileError}; 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}; @@ -708,7 +708,10 @@ impl NodeConfigurator { // let ast = block_compiler::compile(block_fun); if let Some(cod) = self.code_engines.get_mut(id) { use synfx_dsp_jit::build::*; - cod.upload(ast); + match cod.upload(ast) { + Err(e) => return Err(BlkJITCompileError::JITCompileError(e)), + Ok(()) => (), + } } } } From d6b37e68d0196ba6f1bbfe517ce002b0abed732b Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 5 Aug 2022 05:14:26 +0200 Subject: [PATCH 82/88] Check the phase oscillator of synfx-dsp-jit and make sure everything works as expected --- tests/node_code.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/node_code.rs b/tests/node_code.rs index 0e699fb..b5f1c83 100644 --- a/tests/node_code.rs +++ b/tests/node_code.rs @@ -39,3 +39,53 @@ fn check_node_code_1() { 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"); + block_fun.instanciate_at(0, 0, 2, "value", Some("220.0".to_string())); + block_fun.instanciate_at(0, 1, 2, "phase", None); + block_fun.instanciate_at(0, 1, 3, "value", Some("2.0".to_string())); + block_fun.instanciate_at(0, 2, 2, "*", None); + block_fun.instanciate_at(0, 3, 1, "-", None); + block_fun.instanciate_at(0, 2, 1, "value", Some("1.0".to_string())); + block_fun.instanciate_at(0, 4, 1, "set", Some("&sig1".to_string())); + } + + 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) + ] + ); +} From 808547dc16de49d2e791a38ecfd3ca9bac98125c Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 5 Aug 2022 06:45:06 +0200 Subject: [PATCH 83/88] Refactored out the DSP helper stuff into the synfx-dsp crate --- Cargo.toml | 3 +- src/dsp/biquad.rs | 272 ---- src/dsp/dattorro.rs | 443 ------ src/dsp/helpers.rs | 2697 ---------------------------------- src/dsp/mod.rs | 18 +- src/dsp/node_ad.rs | 2 +- src/dsp/node_allp.rs | 2 +- src/dsp/node_biqfilt.rs | 2 +- src/dsp/node_bosc.rs | 2 +- src/dsp/node_bowstri.rs | 3 +- src/dsp/node_comb.rs | 6 +- src/dsp/node_cqnt.rs | 2 +- src/dsp/node_delay.rs | 2 +- src/dsp/node_mux9.rs | 2 +- src/dsp/node_noise.rs | 2 +- src/dsp/node_pverb.rs | 3 +- src/dsp/node_quant.rs | 2 +- src/dsp/node_rndwk.rs | 2 +- src/dsp/node_sampl.rs | 2 +- src/dsp/node_scope.rs | 2 +- src/dsp/node_sfilter.rs | 2 +- src/dsp/node_sin.rs | 2 +- src/dsp/node_test.rs | 2 +- src/dsp/node_tseq.rs | 2 +- src/dsp/node_tslfo.rs | 2 +- src/dsp/node_vosc.rs | 3 +- src/dsp/tracker/sequencer.rs | 2 +- src/nodes/mod.rs | 2 +- tests/delay_buffer.rs | 255 ---- tests/quant.rs | 2 +- 30 files changed, 36 insertions(+), 3707 deletions(-) delete mode 100644 src/dsp/biquad.rs delete mode 100644 src/dsp/dattorro.rs delete mode 100644 src/dsp/helpers.rs delete mode 100644 tests/delay_buffer.rs diff --git a/Cargo.toml b/Cargo.toml index 9746475..d3ffbb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ 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 = "0.5.1" +#synfx-dsp = { git = "https://github.com/WeirdConstructor/synfx-dsp" } [dev-dependencies] num-complex = "0.2" 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 6ce44b5..0000000 --- a/src/dsp/helpers.rs +++ /dev/null @@ -1,2697 +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 - } -} - -/// Apply linear interpolation between the value a and b. -/// -/// * `a` - value at x=0.0 -/// * `b` - value at x=1.0 -/// * `x` - value between 0.0 and 1.0 to blend between `a` and `b`. -#[inline] -pub fn lerp(x: f32, a: f32, b: f32) -> f32 { - (a * (1.0 - x)) + (b * x) -} - -/// Apply 64bit linear interpolation between the value a and b. -/// -/// * `a` - value at x=0.0 -/// * `b` - value at x=1.0 -/// * `x` - value between 0.0 and 1.0 to blend between `a` and `b`. -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; - -/// Trigger signal generator for HexoDSP nodes. -/// -/// A trigger in HexoSynth and HexoDSP is commonly 2.0 milliseconds. -/// This generator generates a trigger signal when [TrigSignal::trigger] is called. -#[derive(Debug, Clone, Copy)] -pub struct TrigSignal { - length: u32, - scount: u32, -} - -impl TrigSignal { - /// Create a new trigger generator - pub fn new() -> Self { - Self { length: ((44100.0 * TRIG_SIGNAL_LENGTH_MS) / 1000.0).ceil() as u32, scount: 0 } - } - - /// Reset the trigger generator. - pub fn reset(&mut self) { - self.scount = 0; - } - - /// Set the sample rate to calculate the amount of samples for the trigger signal. - pub fn set_sample_rate(&mut self, srate: f32) { - self.length = ((srate * TRIG_SIGNAL_LENGTH_MS) / 1000.0).ceil() as u32; - self.scount = 0; - } - - /// Enable sending a trigger impulse the next time [TrigSignal::next] is called. - #[inline] - pub fn trigger(&mut self) { - self.scount = self.length; - } - - /// Trigger signal output. - #[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() - } -} - -/// Signal change detector that emits a trigger when the input signal changed. -/// -/// This is commonly used for control signals. It has not much use for audio signals. -#[derive(Debug, Clone, Copy)] -pub struct ChangeTrig { - ts: TrigSignal, - last: f32, -} - -impl ChangeTrig { - /// Create a new change detector - pub fn new() -> Self { - Self { - ts: TrigSignal::new(), - last: -100.0, // some random value :-) - } - } - - /// Reset internal state. - pub fn reset(&mut self) { - self.ts.reset(); - self.last = -100.0; - } - - /// Set the sample rate for the trigger signal generator - pub fn set_sample_rate(&mut self, srate: f32) { - self.ts.set_sample_rate(srate); - } - - /// Feed a new input signal sample. - /// - /// The return value is the trigger signal. - #[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() - } -} - -/// Trigger signal detector for HexoDSP. -/// -/// Whenever you need to detect a trigger on an input you can use this component. -/// A trigger in HexoDSP is any signal over [TRIG_HIGH_THRES]. The internal state is -/// resetted when the signal drops below [TRIG_LOW_THRES]. -#[derive(Debug, Clone, Copy)] -pub struct Trigger { - triggered: bool, -} - -impl Trigger { - /// Create a new trigger detector. - pub fn new() -> Self { - Self { triggered: false } - } - - /// Reset the internal state of the trigger detector. - #[inline] - pub fn reset(&mut self) { - self.triggered = false; - } - - /// Checks the input signal for a trigger and returns true when the signal - /// surpassed [TRIG_HIGH_THRES] and has not fallen below [TRIG_LOW_THRES] yet. - #[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 - } - } -} - -/// Trigger signal detector with custom range. -/// -/// Whenever you need to detect a trigger with a custom threshold. -#[derive(Debug, Clone, Copy)] -pub struct CustomTrigger { - triggered: bool, - low_thres: f32, - high_thres: f32, -} - -impl CustomTrigger { - /// Create a new trigger detector. - pub fn new(low_thres: f32, high_thres: f32) -> Self { - Self { triggered: false, low_thres, high_thres } - } - - pub fn set_threshold(&mut self, low_thres: f32, high_thres: f32) { - self.low_thres = low_thres; - self.high_thres = high_thres; - } - - /// Reset the internal state of the trigger detector. - #[inline] - pub fn reset(&mut self) { - self.triggered = false; - } - - /// Checks the input signal for a trigger and returns true when the signal - /// surpassed the high threshold and has not fallen below low threshold yet. - #[inline] - pub fn check_trigger(&mut self, input: f32) -> bool { - // println!("TRIG CHECK: {} <> {}", input, self.high_thres); - if self.triggered { - if input <= self.low_thres { - self.triggered = false; - } - - false - } else if input > self.high_thres { - self.triggered = true; - true - } else { - false - } - } -} - -/// Generates a phase signal from a trigger/gate input signal. -/// -/// This helper allows you to measure the distance between trigger or gate pulses -/// and generates a phase signal for you that increases from 0.0 to 1.0. -#[derive(Debug, Clone, Copy)] -pub struct TriggerPhaseClock { - clock_phase: f64, - clock_inc: f64, - prev_trigger: bool, - clock_samples: u32, -} - -impl TriggerPhaseClock { - /// Create a new phase clock. - pub fn new() -> Self { - Self { clock_phase: 0.0, clock_inc: 0.0, prev_trigger: true, clock_samples: 0 } - } - - /// Reset the phase clock. - #[inline] - pub fn reset(&mut self) { - self.clock_samples = 0; - self.clock_inc = 0.0; - self.prev_trigger = true; - self.clock_samples = 0; - } - - /// Restart the phase clock. It will count up from 0.0 again on [TriggerPhaseClock::next_phase]. - #[inline] - pub fn sync(&mut self) { - self.clock_phase = 0.0; - } - - /// Generate the phase signal of this clock. - /// - /// * `clock_limit` - The maximum number of samples to detect two trigger signals in. - /// * `trigger_in` - Trigger signal input. - #[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)) -} - -/// Hermite / Cubic interpolation of a buffer full of samples at the given _index_. -/// _len_ is the buffer length to consider and wrap the index into. And _fract_ is the -/// fractional part of the index. -/// -/// This function is generic over f32 and f64. That means you can use your preferred float size. -/// -/// Commonly used like this: -/// -///``` -/// use hexodsp::dsp::helpers::cubic_interpolate; -/// -/// let buf : [f32; 9] = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2]; -/// let pos = 3.3_f32; -/// -/// let i = pos.floor() as usize; -/// let f = pos.fract(); -/// -/// let res = cubic_interpolate(&buf[..], buf.len(), i, f); -/// assert!((res - 0.67).abs() < 0.2_f32); -///``` -#[inline] -pub fn cubic_interpolate(data: &[F], len: usize, index: usize, fract: F) -> F { - let index = index + len; - // 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[(index - 1) % len]; - let x0 = data[index % len]; - let x1 = data[(index + 1) % len]; - let x2 = data[(index + 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 res = (((a * fract) - b_neg) * fract + c) * fract + x0; - - // let rr2 = - // x0 + f::(0.5) * fract * ( - // x1 - xm1 + fract * ( - // f::(4.0) * x1 - // + f::(2.0) * xm1 - // - f::(5.0) * x0 - // - x2 - // + fract * (f::(3.0) * (x0 - x1) - xm1 + x2))); - - // eprintln!( - // "index={} fract={:6.4} xm1={:6.4} x0={:6.4} x1={:6.4} x2={:6.4} = {:6.4} <> {:6.4}", - // index, fract.to_f64().unwrap(), xm1.to_f64().unwrap(), x0.to_f64().unwrap(), x1.to_f64().unwrap(), x2.to_f64().unwrap(), - // res.to_f64().unwrap(), - // rr2.to_f64().unwrap() - // ); - - // eprintln!( - // "index={} fract={:6.4} xm1={:6.4} x0={:6.4} x1={:6.4} x2={:6.4} = {:6.4}", - // index, fract.to_f64().unwrap(), xm1.to_f64().unwrap(), x0.to_f64().unwrap(), x1.to_f64().unwrap(), x2.to_f64().unwrap(), - // res.to_f64().unwrap(), - // ); - - res -} - -/// This is a delay buffer/line with linear and cubic interpolation. -/// -/// It's the basic building block underneath the all-pass filter, comb filters and delay effects. -/// You can use linear and cubic and no interpolation to access samples in the past. Either -/// by sample offset or time (millisecond) based. -#[derive(Debug, Clone, Default)] -pub struct DelayBuffer { - data: Vec, - wr: usize, - srate: F, -} - -impl DelayBuffer { - /// Creates a delay buffer with about 5 seconds of capacity at 8*48000Hz sample rate. - pub fn new() -> Self { - Self { data: vec![f(0.0); DEFAULT_DELAY_BUFFER_SAMPLES], wr: 0, srate: f(44100.0) } - } - - /// Creates a delay buffer with the given amount of samples capacity. - pub fn new_with_size(size: usize) -> Self { - Self { data: vec![f(0.0); size], wr: 0, srate: f(44100.0) } - } - - /// Sets the sample rate that is used for milliseconds => sample conversion. - pub fn set_sample_rate(&mut self, srate: F) { - self.srate = srate; - } - - /// Reset the delay buffer contents and write position. - 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 tim with linear interpolation. - /// - /// * `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 with linear interpolation. - /// - /// * `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(); - - // one extra offset, because feed() advances self.wr to the next writing position! - let i = (self.wr + len) - (offs + 1); - let x0 = data[i % len]; - let x1 = data[(i - 1) % len]; - - let res = x0 + fract * (x1 - x0); - //d// eprintln!( - //d// "INTERP: {:6.4} x0={:6.4} x1={:6.4} fract={:6.4} => {:6.4}", - //d// s_offs.to_f64().unwrap_or(0.0), - //d// x0.to_f64().unwrap(), - //d// x1.to_f64().unwrap(), - //d// fract.to_f64().unwrap(), - //d// res.to_f64().unwrap(), - //d// ); - res - } - - /// Fetch a sample from the delay buffer at the given time with cubic interpolation. - /// - /// * `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 with cubic interpolation. - /// - /// * `s_offs` - Sample offset in samples into the past of the [DelayBuffer] - /// from the current write (or the "now") position. - #[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(); - - // (offs + 1) offset for compensating that self.wr points to the next - // unwritten position. - // Additional (offs + 1 + 1) offset for cubic_interpolate, which - // interpolates into the past through the delay buffer. - let i = (self.wr + len) - (offs + 2); - let res = cubic_interpolate(data, len, i, f::(1.0) - fract); - // eprintln!( - // "cubic at={} ({:6.4}) res={:6.4}", - // i % len, - // s_offs.to_f64().unwrap(), - // res.to_f64().unwrap() - // ); - res - } - - /// Fetch a sample from the delay buffer at the given time without any interpolation. - /// - /// * `delay_time_ms` - Delay time in milliseconds. - #[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; - // (offs + 1) one extra offset, because feed() advances - // self.wr to the next writing position! - let idx = ((self.wr + len) - (offs + 1)) % len; - self.data[idx] - } - - /// Fetch a sample from the delay buffer at the given number of samples in the past. - #[inline] - pub fn at(&self, delay_sample_count: usize) -> F { - let len = self.data.len(); - // (delay_sample_count + 1) one extra offset, because feed() advances self.wr to - // the next writing position! - let idx = ((self.wr + len) - (delay_sample_count + 1)) % len; - self.data[idx] - } -} - -/// Default size of the delay buffer: 1 seconds at 8 times 48kHz -const DEFAULT_ALLPASS_COMB_SAMPLES: usize = 8 * 48000; - -/// An all-pass filter based on a delay line. -#[derive(Debug, Clone, Default)] -pub struct AllPass { - delay: DelayBuffer, -} - -impl AllPass { - /// Creates a new all-pass filter with about 1 seconds space for samples. - pub fn new() -> Self { - Self { delay: DelayBuffer::new_with_size(DEFAULT_ALLPASS_COMB_SAMPLES) } - } - - /// Set the sample rate for millisecond based access. - pub fn set_sample_rate(&mut self, srate: F) { - self.delay.set_sample_rate(srate); - } - - /// Reset the internal delay buffer. - pub fn reset(&mut self) { - self.delay.reset(); - } - - /// Access the internal delay at the given amount of milliseconds in the past. - #[inline] - pub fn delay_tap_n(&self, time_ms: F) -> F { - self.delay.tap_n(time_ms) - } - - /// Retrieve the next (cubic interpolated) sample from the all-pass - /// filter while feeding in the next. - /// - /// * `time_ms` - Delay time in milliseconds. - /// * `g` - Feedback factor (usually something around 0.7 is common) - /// * `v` - The new input sample to feed the filter. - #[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 - } - - /// Retrieve the next (linear interpolated) sample from the all-pass - /// filter while feeding in the next. - /// - /// * `time_ms` - Delay time in milliseconds. - /// * `g` - Feedback factor (usually something around 0.7 is common) - /// * `v` - The new input sample to feed the filter. - #[inline] - pub fn next_linear(&mut self, time_ms: F, g: F, v: F) -> F { - let s = self.delay.linear_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; - } - - #[inline] - 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 - }; - - 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 219c707..f77e6b8 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -200,14 +200,15 @@ the help documentation: 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. +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 `dsp/helpers.rs` +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 super::helpers::{TriSawLFO, Trigger}; + use synfx_dsp::{TriSawLFO, Trigger}; #[derive(Debug, Clone)] pub struct TsLFO { @@ -233,7 +234,7 @@ to the HexoDSP node interface. } ``` -The code for `TriSawLFO` in `dsp/helpers.rs` is then independent and reusable else where. +The code for `TriSawLFO` in `synfx-dsp` is then independent and reusable else where. ### Node Parameter/Inputs @@ -538,9 +539,6 @@ 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; @@ -564,7 +562,7 @@ use crate::fa_cqnt; use crate::fa_cqnt_omax; use crate::fa_cqnt_omin; use crate::fa_delay_mode; -use crate::fa_distort; +use synfx_dsp::fa_distort; use crate::fa_map_clip; use crate::fa_mux9_in_cnt; use crate::fa_noise_mode; @@ -1586,7 +1584,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; @@ -1966,7 +1964,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_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 ce51506..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::{cubic_interpolate, 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}; diff --git a/src/dsp/node_scope.rs b/src/dsp/node_scope.rs index 39bbe48..76a6f46 100644 --- a/src/dsp/node_scope.rs +++ b/src/dsp/node_scope.rs @@ -8,7 +8,7 @@ // Copyright by Andrew Belt, 2021 //use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger}; -use crate::dsp::helpers::CustomTrigger; +use synfx_dsp::CustomTrigger; use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::SCOPE_SAMPLES; use crate::nodes::{NodeAudioContext, NodeExecContext}; 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/nodes/mod.rs b/src/nodes/mod.rs index 06e6e55..8023adc 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -89,7 +89,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/tests/delay_buffer.rs b/tests/delay_buffer.rs deleted file mode 100644 index d27a1d5..0000000 --- a/tests/delay_buffer.rs +++ /dev/null @@ -1,255 +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. - -mod common; -use common::*; - -#[test] -fn check_delaybuffer_linear_interpolation() { - let mut buf = crate::helpers::DelayBuffer::new(); - - buf.feed(0.0); - buf.feed(0.1); - buf.feed(0.2); - buf.feed(0.3); - buf.feed(0.4); - buf.feed(0.5); - buf.feed(0.6); - buf.feed(0.7); - buf.feed(0.8); - buf.feed(0.9); - buf.feed(1.0); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.5; - for _ in 0..20 { - samples_out.push(buf.linear_interpolate_at_s(pos)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35000002, 0.3, - 0.25, 0.2, 0.15, 0.1, 0.05 - ] - ); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.2; - for _ in 0..30 { - samples_out.push(buf.linear_interpolate_at_s(pos)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, 0.98, 0.96, 0.94, 0.91999996, 0.9, 0.88, 0.85999995, 0.84, 0.82, 0.8, 0.78, 0.76, - 0.73999995, 0.71999997, 0.6999999, 0.67999995, 0.65999997, 0.6399999, 0.61999995, - 0.59999996, 0.58, 0.56, 0.54, 0.52000004, 0.50000006, 0.48000008, 0.4600001, - 0.44000012, 0.42000014 - ] - ); -} - -#[test] -fn check_delaybuffer_nearest() { - let mut buf = crate::helpers::DelayBuffer::new(); - - buf.feed(0.0); - buf.feed(0.1); - buf.feed(0.2); - buf.feed(0.3); - buf.feed(0.4); - buf.feed(0.5); - buf.feed(0.6); - buf.feed(0.7); - buf.feed(0.8); - buf.feed(0.9); - buf.feed(1.0); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.5; - for _ in 0..20 { - samples_out.push(buf.at(pos as usize)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, 1.0, 0.9, 0.9, 0.8, 0.8, 0.7, 0.7, 0.6, 0.6, 0.5, 0.5, 0.4, 0.4, 0.3, 0.3, 0.2, - 0.2, 0.1, 0.1 - ] - ); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.2; - for _ in 0..30 { - samples_out.push(buf.at(pos as usize)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.8, 0.8, 0.8, 0.8, 0.7, 0.7, - 0.7, 0.7, 0.7, 0.6, 0.6, 0.6, 0.6, 0.6, 0.5, 0.5, 0.5, 0.5, 0.5 - ] - ); -} - -#[test] -fn check_cubic_interpolate() { - use crate::helpers::cubic_interpolate; - let data = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]; - - let mut samples_out = vec![]; - let mut pos = 0.0_f32; - let pos_inc = 0.5_f32; - for _ in 0..30 { - let i = pos.floor() as usize; - let f = pos.fract(); - samples_out.push(cubic_interpolate(&data[..], data.len(), i, f)); - pos += pos_inc; - } - assert_vec_feq!( - samples_out, - vec![ - 1.0, - 1.01875, - 0.9, - 0.85, - 0.8, - 0.75, - 0.7, - 0.65, - 0.6, - 0.55, - 0.5, - 0.45, - 0.4, - 0.35000002, - 0.3, - 0.25, - 0.2, - 0.15, - 0.1, - -0.018750004, - 0.0, - 0.49999997, - 1.0, - 1.01875, - 0.9, - 0.85, - 0.8, - 0.75, - 0.7, - 0.65 - ] - ); - - let mut samples_out = vec![]; - let mut pos = 0.0_f32; - let pos_inc = 0.1_f32; - for _ in 0..30 { - let i = pos.floor() as usize; - let f = pos.fract(); - samples_out.push(cubic_interpolate(&data[..], data.len(), i, f)); - pos += pos_inc; - } - assert_vec_feq!( - samples_out, - vec![ - 1.0, 1.03455, 1.0504, 1.05085, 1.0392, 1.01875, 0.99279994, 0.9646499, 0.9375999, - 0.91494995, 0.9, 0.89, 0.87999994, 0.86999995, 0.85999995, 0.84999996, 0.84, 0.83, - 0.82, 0.80999994, 0.8, 0.79, 0.78000003, 0.77000004, 0.76, 0.75, 0.74, 0.73, 0.72, - 0.71000004 - ] - ); -} - -#[test] -fn check_delaybuffer_cubic_interpolation() { - let mut buf = crate::helpers::DelayBuffer::new(); - - buf.feed(0.0); - buf.feed(0.1); - buf.feed(0.2); - buf.feed(0.3); - buf.feed(0.4); - buf.feed(0.5); - buf.feed(0.6); - buf.feed(0.7); - buf.feed(0.8); - buf.feed(0.9); - buf.feed(1.0); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.1; - for _ in 0..30 { - samples_out.push(buf.cubic_interpolate_at_s(pos)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, 1.03455, 1.0504, 1.05085, 1.0392, 1.01875, 0.99279994, 0.9646499, 0.9375999, - 0.91494995, 0.9, 0.89, 0.87999994, 0.86999995, 0.85999995, 0.84999996, 0.84, 0.83, - 0.82, 0.80999994, 0.8, 0.79, 0.78000003, 0.77000004, 0.76, 0.75, 0.74, 0.73, 0.72, - 0.71000004 - ] - ); - - let mut samples_out = vec![]; - let mut pos = 0.0; - let pos_inc = 0.5; - for _ in 0..30 { - samples_out.push(buf.cubic_interpolate_at_s(pos)); - pos += pos_inc; - } - - assert_vec_feq!( - samples_out, - vec![ - 1.0, - 1.01875, - 0.9, - 0.85, - 0.8, - 0.75, - 0.7, - 0.65, - 0.6, - 0.55, - 0.5, - 0.45, - 0.4, - 0.35000002, - 0.3, - 0.25, - 0.2, - 0.15, - 0.1, - 0.043750003, - 0.0, - -0.00625, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ] - ); -} diff --git a/tests/quant.rs b/tests/quant.rs index 2761e57..a5ee47c 100644 --- a/tests/quant.rs +++ b/tests/quant.rs @@ -6,7 +6,7 @@ mod common; //use common::*; use hexodsp::d_pit; -use hexodsp::dsp::helpers::Quantizer; +use synfx_dsp::Quantizer; #[test] fn check_quant_pos_neg_exact() { From db01caaac0972b063d7a76b00573271787c42254 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 5 Aug 2022 19:44:40 +0200 Subject: [PATCH 84/88] Fix a lot of warnings --- Cargo.toml | 2 +- examples/jack_demo_node_api.rs | 15 --------------- src/block_compiler.rs | 17 ++++++++--------- src/blocklang.rs | 6 ++++-- src/dsp/node_code.rs | 34 ++++++++++++++++++++++++---------- src/nodes/node_conf.rs | 2 -- src/wblockdsp.rs | 6 ++---- tests/node_code.rs | 28 +++++++++++++++++++--------- 8 files changed, 58 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a30420e..35e4260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ 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/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/block_compiler.rs b/src/block_compiler.rs index f150f85..44bbe83 100644 --- a/src/block_compiler.rs +++ b/src/block_compiler.rs @@ -193,7 +193,6 @@ pub enum BlkJITCompileError { } pub struct Block2JITCompiler { - id_node_map: HashMap, idout_var_map: HashMap, lang: Rc>, tmpvar_counter: usize, @@ -210,7 +209,7 @@ enum ASTNode { impl Block2JITCompiler { pub fn new(lang: Rc>) -> Self { - Self { id_node_map: HashMap::new(), idout_var_map: HashMap::new(), lang, tmpvar_counter: 0 } + Self { idout_var_map: HashMap::new(), lang, tmpvar_counter: 0 } } pub fn next_tmpvar_name(&mut self, extra: &str) -> String { @@ -226,7 +225,7 @@ impl Block2JITCompiler { self.idout_var_map.get(&format!("{}/{}", id, out)).map(|s| &s[..]) } - pub fn trans2bjit( + fn trans2bjit( &mut self, node: &ASTNodeRef, my_out: Option, @@ -392,7 +391,7 @@ impl Block2JITCompiler { } #[cfg(feature = "synfx-dsp-jit")] - pub fn bjit2jit(&mut self, ast: &BlkASTRef) -> Result, BlkJITCompileError> { + fn bjit2jit(&mut self, ast: &BlkASTRef) -> Result, BlkJITCompileError> { use synfx_dsp_jit::build::*; match &**ast { @@ -407,11 +406,11 @@ impl Block2JITCompiler { let e = self.bjit2jit(&expr)?; Ok(assign(var, e)) } - BlkASTNode::Get { id, var: varname } => Ok(var(varname)), - BlkASTNode::Node { id, out, typ, lbl, childs } => match &typ[..] { + BlkASTNode::Get { var: varname, .. } => Ok(var(varname)), + BlkASTNode::Node { id, typ, childs, .. } => match &typ[..] { "if" => Err(BlkJITCompileError::UnknownError), "zero" => Ok(literal(0.0)), - node => { + _ => { if *id == 0 { return Err(BlkJITCompileError::NodeWithoutID(typ.to_string())); } @@ -426,7 +425,6 @@ impl Block2JITCompiler { } if inputs.len() > 0 && inputs[0] == Some("".to_string()) { - // We assume all inputs are unnamed: if inputs.len() != childs.len() { return Err(BlkJITCompileError::WrongNumberOfChilds( typ.to_string(), @@ -435,7 +433,8 @@ impl Block2JITCompiler { )); } - for (inp, c) in childs.iter() { + // We assume all inputs are unnamed: + for (_inp, c) in childs.iter() { args.push(self.bjit2jit(&c)?); } } else { diff --git a/src/blocklang.rs b/src/blocklang.rs index cb600e4..61e5c8e 100644 --- a/src/blocklang.rs +++ b/src/blocklang.rs @@ -197,11 +197,10 @@ impl Block { 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 }; - let mut contains = json!([c0, c1]); json!({ "id": self.id as i64, "rows": self.rows as i64, - "contains": contains, + "contains": json!([c0, c1]), "expanded": self.expanded, "typ": self.typ, "lbl": self.lbl, @@ -343,12 +342,15 @@ impl BlockView for Block { #[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. diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index 6685897..2835211 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -7,7 +7,7 @@ use crate::nodes::{NodeAudioContext, NodeExecContext}; #[cfg(feature = "synfx-dsp-jit")] use crate::wblockdsp::CodeEngineBackend; -use crate::dsp::MAX_BLOCK_SIZE; +//use crate::dsp::MAX_BLOCK_SIZE; /// A WBlockDSP code execution node for JIT'ed DSP code pub struct Code { @@ -87,17 +87,20 @@ impl DspNode for Code { ctx: &mut T, _ectx: &mut NodeExecContext, _nctx: &NodeContext, - atoms: &[SAtom], + _atoms: &[SAtom], inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals, ) { - use crate::dsp::{at, denorm, inp, out, out_idx}; -// let clock = inp::TSeq::clock(inputs); -// let trig = inp::TSeq::trig(inputs); -// let cmode = at::TSeq::cmode(atoms); - let out = out::Code::sig(outputs); + 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]; @@ -114,15 +117,26 @@ impl DspNode for Code { 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() { - let (s1, s2, ret) = backend.process(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + (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(0.0); - ctx_vals[1].set(0.0); + ctx_vals[0].set(ret); + ctx_vals[1].set(s1); } } } diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 266693d..a0c63bc 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -713,9 +713,7 @@ impl NodeConfigurator { let mut compiler = Block2JITCompiler::new(block_fun.block_language()); let ast = compiler.compile(&block_fun)?; - // let ast = block_compiler::compile(block_fun); if let Some(cod) = self.code_engines.get_mut(id) { - use synfx_dsp_jit::build::*; match cod.upload(ast) { Err(e) => return Err(BlkJITCompileError::JITCompileError(e)), Ok(()) => (), diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs index 60b9b86..e056a02 100644 --- a/src/wblockdsp.rs +++ b/src/wblockdsp.rs @@ -6,11 +6,9 @@ use synfx_dsp_jit::*; use ringbuf::{Consumer, Producer, RingBuffer}; use std::cell::RefCell; -use std::collections::HashMap; use std::rc::Rc; const MAX_RINGBUF_SIZE: usize = 128; -const MAX_CONTEXTS: usize = 32; enum CodeUpdateMsg { UpdateFun(Box), @@ -52,7 +50,7 @@ impl CodeEngine { pub fn upload(&mut self, ast: Box) -> Result<(), JITCompileError> { let jit = JIT::new(self.lib.clone(), self.dsp_ctx.clone()); let fun = jit.compile(ASTFun::new(ast))?; - self.update_prod.push(CodeUpdateMsg::UpdateFun(fun)); + let _ = self.update_prod.push(CodeUpdateMsg::UpdateFun(fun)); Ok(()) } @@ -146,7 +144,7 @@ impl CodeEngineBackend { CodeUpdateMsg::UpdateFun(mut fun) => { std::mem::swap(&mut self.function, &mut fun); self.function.init(self.sample_rate as f64, Some(&fun)); - self.return_prod.push(CodeReturnMsg::DestroyFun(fun)); + let _ = self.return_prod.push(CodeReturnMsg::DestroyFun(fun)); } } } diff --git a/tests/node_code.rs b/tests/node_code.rs index b5f1c83..69fcf39 100644 --- a/tests/node_code.rs +++ b/tests/node_code.rs @@ -5,6 +5,8 @@ mod common; use common::*; +use hexodsp::blocklang::BlockFun; + fn setup() -> (Matrix, NodeExecutor) { let (node_conf, node_exec) = new_node_engine(); let mut matrix = Matrix::new(node_conf, 3, 3); @@ -22,6 +24,14 @@ fn setup() -> (Matrix, NodeExecutor) { (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(); @@ -29,8 +39,8 @@ fn check_node_code_1() { let block_fun = matrix.get_block_function(0).expect("block fun exists"); { let mut block_fun = block_fun.lock().expect("matrix lock"); - block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); - block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); + 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"); @@ -46,13 +56,13 @@ fn check_node_code_state() { let block_fun = matrix.get_block_function(0).expect("block fun exists"); { let mut block_fun = block_fun.lock().expect("matrix lock"); - block_fun.instanciate_at(0, 0, 2, "value", Some("220.0".to_string())); - block_fun.instanciate_at(0, 1, 2, "phase", None); - block_fun.instanciate_at(0, 1, 3, "value", Some("2.0".to_string())); - block_fun.instanciate_at(0, 2, 2, "*", None); - block_fun.instanciate_at(0, 3, 1, "-", None); - block_fun.instanciate_at(0, 2, 1, "value", Some("1.0".to_string())); - block_fun.instanciate_at(0, 4, 1, "set", Some("&sig1".to_string())); + 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"); From 4c5d832036096e4e5eccfe8383371c8cbaf39ff3 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Fri, 5 Aug 2022 22:58:55 +0200 Subject: [PATCH 85/88] Moved AtomicFloat to synfx-dsp --- CHANGELOG.md | 2 + Cargo.toml | 4 +- src/dsp/mod.rs | 10 ++-- src/nodes/node_conf.rs | 2 +- src/nodes/node_exec.rs | 3 +- src/scope_handle.rs | 2 +- src/util.rs | 121 ----------------------------------------- 7 files changed, 13 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b1bc9..3c58b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,5 @@ to the adjacent cells. 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 35e4260..4369ef9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ triple_buffer = "5.0.6" lazy_static = "1.4.0" hound = "3.4.0" synfx-dsp-jit = { path = "../synfx-dsp-jit", optional = true } -synfx-dsp = "0.5.1" -#synfx-dsp = { git = "https://github.com/WeirdConstructor/synfx-dsp" } +#synfx-dsp = "0.5.1" +synfx-dsp = { git = "https://github.com/WeirdConstructor/synfx-dsp" } [dev-dependencies] num-complex = "0.2" diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index d3d340e..5d59676 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -497,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; @@ -538,8 +540,6 @@ mod node_tseq; mod node_tslfo; #[allow(non_upper_case_globals)] mod node_vosc; -#[allow(non_upper_case_globals)] -mod node_code; mod satom; pub mod tracker; @@ -547,8 +547,8 @@ 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]; @@ -564,7 +564,6 @@ use crate::fa_cqnt; use crate::fa_cqnt_omax; use crate::fa_cqnt_omin; use crate::fa_delay_mode; -use synfx_dsp::fa_distort; use crate::fa_map_clip; use crate::fa_mux9_in_cnt; use crate::fa_noise_mode; @@ -580,6 +579,7 @@ 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; @@ -587,6 +587,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; @@ -607,7 +608,6 @@ use node_sin::Sin; use node_smap::SMap; use node_test::Test; use node_tseq::TSeq; -use node_code::Code; use node_tslfo::TsLFO; use node_vosc::VOsc; diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index a0c63bc..2403b4c 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -13,7 +13,6 @@ 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 crate::wblockdsp::CodeEngine; use crate::SampleLibrary; @@ -23,6 +22,7 @@ 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 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 index 23895b4..5af06c8 100644 --- a/src/scope_handle.rs +++ b/src/scope_handle.rs @@ -3,9 +3,9 @@ // See README.md and COPYING for details. use crate::nodes::SCOPE_SAMPLES; -use crate::util::{AtomicFloat, AtomicFloatPair}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use synfx_dsp::{AtomicFloat, AtomicFloatPair}; #[derive(Debug)] pub struct ScopeHandle { diff --git a/src/util.rs b/src/util.rs index 6ccb4fc..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, AtomicU64, Ordering}; - const SMOOTHING_TIME_MS: f32 = 10.0; pub struct Smoother { @@ -95,122 +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() - } -} - -/// The AtomicFloatPair can store two `f32` numbers atomically. -/// -/// This is useful for storing eg. min and max values of a sampled signal. -pub struct AtomicFloatPair { - atomic: AtomicU64, -} - -impl AtomicFloatPair { - /// New atomic float with initial value `value`. - pub fn new(v: (f32, f32)) -> AtomicFloatPair { - AtomicFloatPair { - atomic: AtomicU64::new(((v.0.to_bits() as u64) << 32) | (v.1.to_bits() as u64)), - } - } - - /// Get the current value of the atomic float. - #[inline] - pub fn get(&self) -> (f32, f32) { - let v = self.atomic.load(Ordering::Relaxed); - (f32::from_bits((v >> 32 & 0xFFFFFFFF) as u32), f32::from_bits((v & 0xFFFFFFFF) as u32)) - } - - /// Set the value of the atomic float to `value`. - #[inline] - pub fn set(&self, v: (f32, f32)) { - let v = ((v.0.to_bits() as u64) << 32) | (v.1.to_bits()) as u64; - self.atomic.store(v, Ordering::Relaxed) - } -} - -impl Default for AtomicFloatPair { - fn default() -> Self { - AtomicFloatPair::new((0.0, 0.0)) - } -} - -impl std::fmt::Debug for AtomicFloatPair { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let v = self.get(); - write!(f, "({}, {})", v.0, v.1) - } -} - -impl std::fmt::Display for AtomicFloatPair { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let v = self.get(); - write!(f, "({}, {})", v.0, v.1) - } -} - -impl From<(f32, f32)> for AtomicFloatPair { - fn from(value: (f32, f32)) -> Self { - AtomicFloatPair::new((value.0, value.1)) - } -} - -impl From for (f32, f32) { - fn from(value: AtomicFloatPair) -> Self { - value.get() - } -} From 87104ded313f44a830a034d04cb94342864fbffb Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 6 Aug 2022 09:28:30 +0200 Subject: [PATCH 86/88] Refactored WBlockDSP into it's own sub module and removed the old wblockdsp.rs --- src/dsp/node_code.rs | 2 +- src/lib.rs | 4 - src/matrix.rs | 7 +- src/matrix_repr.rs | 2 +- src/nodes/node_conf.rs | 16 +- src/wblockdsp.rs | 152 ------------------ .../compiler.rs} | 152 ++++++++++-------- .../definition.rs} | 32 +--- src/{blocklang.rs => wblockdsp/language.rs} | 6 +- src/wblockdsp/mod.rs | 15 ++ tests/blocklang.rs | 86 ---------- tests/node_code.rs | 2 +- tests/wblockdsp.rs | 41 ----- 13 files changed, 119 insertions(+), 398 deletions(-) delete mode 100644 src/wblockdsp.rs rename src/{block_compiler.rs => wblockdsp/compiler.rs} (83%) rename src/{blocklang_def.rs => wblockdsp/definition.rs} (89%) rename src/{blocklang.rs => wblockdsp/language.rs} (99%) create mode 100644 src/wblockdsp/mod.rs delete mode 100644 tests/blocklang.rs delete mode 100644 tests/wblockdsp.rs diff --git a/src/dsp/node_code.rs b/src/dsp/node_code.rs index 2835211..4a23385 100644 --- a/src/dsp/node_code.rs +++ b/src/dsp/node_code.rs @@ -5,7 +5,7 @@ use crate::dsp::{DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::{NodeAudioContext, NodeExecContext}; #[cfg(feature = "synfx-dsp-jit")] -use crate::wblockdsp::CodeEngineBackend; +use synfx_dsp_jit::engine::CodeEngineBackend; //use crate::dsp::MAX_BLOCK_SIZE; diff --git a/src/lib.rs b/src/lib.rs index 6cae805..f944009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -321,11 +321,7 @@ pub mod monitor; pub mod nodes; pub mod sample_lib; pub mod scope_handle; -#[cfg(feature="synfx-dsp-jit")] pub mod wblockdsp; -pub mod blocklang; -pub mod blocklang_def; -mod block_compiler; mod util; pub use cell_dir::CellDir; diff --git a/src/matrix.rs b/src/matrix.rs index 9817541..ee2fb50 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -10,8 +10,7 @@ pub use crate::nodes::MinMaxMonitorSamples; use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES}; pub use crate::CellDir; use crate::ScopeHandle; -use crate::blocklang::{BlockFun, BlockFunSnapshot}; -use crate::block_compiler::BlkJITCompileError; +use crate::wblockdsp::{BlockFun, BlockFunSnapshot, BlkJITCompileError}; use std::collections::{HashMap, HashSet}; @@ -600,13 +599,13 @@ impl Matrix { /// Checks the block function for the id `id`. If the block function did change, /// updates are then sent to the audio thread. - /// See also [get_block_function]. + /// 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 [check_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) } diff --git a/src/matrix_repr.rs b/src/matrix_repr.rs index 6611d0a..3d34aa3 100644 --- a/src/matrix_repr.rs +++ b/src/matrix_repr.rs @@ -4,7 +4,7 @@ use crate::dsp::{NodeId, ParamId, SAtom}; use serde_json::{json, Value}; -use crate::blocklang::BlockFunSnapshot; +use crate::wblockdsp::BlockFunSnapshot; #[derive(Debug, Clone, Copy)] pub struct CellRepr { diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 2403b4c..4fb8f50 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -6,15 +6,13 @@ use super::{ FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES, MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, }; -use crate::block_compiler::{BlkJITCompileError, Block2JITCompiler}; -use crate::blocklang::*; -use crate::blocklang_def; +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; #[cfg(feature = "synfx-dsp-jit")] -use crate::wblockdsp::CodeEngine; +use synfx_dsp_jit::engine::CodeEngine; use crate::SampleLibrary; use crate::ScopeHandle; @@ -190,7 +188,7 @@ pub struct NodeConfigurator { 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 [check_block_function]. + /// in [NodeConfigurator::check_block_function]. #[cfg(feature = "synfx-dsp-jit")] pub(crate) block_functions: Vec<(u64, Arc>)>, /// The shared parts of the [NodeConfigurator] @@ -284,9 +282,9 @@ impl NodeConfigurator { #[cfg(feature = "synfx-dsp-jit")] let (code_engines, block_functions) = { - let code_engines = vec![CodeEngine::new(); MAX_AVAIL_CODE_ENGINES]; + let code_engines = vec![CodeEngine::new_stdlib(); MAX_AVAIL_CODE_ENGINES]; - let lang = blocklang_def::setup_hxdsp_block_language(code_engines[0].get_lib()); + 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())))) @@ -703,7 +701,7 @@ impl NodeConfigurator { /// Checks the block function for the id `id`. If the block function did change, /// updates are then sent to the audio thread. - /// See also [get_block_function]. + /// 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((generation, block_fun)) = self.block_functions.get_mut(id) { @@ -727,7 +725,7 @@ impl NodeConfigurator { } /// Retrieve a handle to the block function `id`. In case you modify the block function, - /// make sure to call [check_block_function]. + /// make sure to call [NodeConfigurator::check_block_function]. pub fn get_block_function(&self, id: usize) -> Option>> { #[cfg(feature = "synfx-dsp-jit")] { diff --git a/src/wblockdsp.rs b/src/wblockdsp.rs deleted file mode 100644 index e056a02..0000000 --- a/src/wblockdsp.rs +++ /dev/null @@ -1,152 +0,0 @@ -// 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 synfx_dsp_jit::*; - -use ringbuf::{Consumer, Producer, RingBuffer}; -use std::cell::RefCell; -use std::rc::Rc; - -const MAX_RINGBUF_SIZE: usize = 128; - -enum CodeUpdateMsg { - UpdateFun(Box), -} - -enum CodeReturnMsg { - DestroyFun(Box), -} - -pub struct CodeEngine { - dsp_ctx: Rc>, - lib: Rc>, - update_prod: Producer, - return_cons: Consumer, -} - -impl Clone for CodeEngine { - fn clone(&self) -> Self { - CodeEngine::new() - } -} - -impl CodeEngine { - pub fn new() -> Self { - let rb = RingBuffer::new(MAX_RINGBUF_SIZE); - let (update_prod, _update_cons) = rb.split(); - let rb = RingBuffer::new(MAX_RINGBUF_SIZE); - let (_return_prod, return_cons) = rb.split(); - - let lib = get_standard_library(); - - Self { lib, dsp_ctx: DSPNodeContext::new_ref(), update_prod, return_cons } - } - - pub fn get_lib(&self) -> Rc> { - self.lib.clone() - } - - pub fn upload(&mut self, ast: Box) -> Result<(), JITCompileError> { - let jit = JIT::new(self.lib.clone(), self.dsp_ctx.clone()); - let fun = jit.compile(ASTFun::new(ast))?; - let _ = self.update_prod.push(CodeUpdateMsg::UpdateFun(fun)); - - Ok(()) - } - - pub fn cleanup(&self, fun: Box) { - self.dsp_ctx.borrow_mut().cleanup_dsp_fun_after_user(fun); - } - - pub fn query_returns(&mut self) { - while let Some(msg) = self.return_cons.pop() { - match msg { - CodeReturnMsg::DestroyFun(fun) => { - self.cleanup(fun); - } - } - } - } - - pub fn get_backend(&mut self) -> CodeEngineBackend { - let rb = RingBuffer::new(MAX_RINGBUF_SIZE); - let (update_prod, update_cons) = rb.split(); - let rb = RingBuffer::new(MAX_RINGBUF_SIZE); - let (return_prod, return_cons) = rb.split(); - - self.update_prod = update_prod; - self.return_cons = return_cons; - - let function = get_nop_function(self.lib.clone(), self.dsp_ctx.clone()); - CodeEngineBackend::new(function, update_cons, return_prod) - } -} - -impl Drop for CodeEngine { - fn drop(&mut self) { - self.dsp_ctx.borrow_mut().free(); - } -} - -pub struct CodeEngineBackend { - sample_rate: f32, - function: Box, - update_cons: Consumer, - return_prod: Producer, -} - -impl CodeEngineBackend { - fn new( - function: Box, - update_cons: Consumer, - return_prod: Producer, - ) -> Self { - Self { sample_rate: 0.0, function, update_cons, return_prod } - } - - #[inline] - pub fn process( - &mut self, - in1: f32, - in2: f32, - a: f32, - b: f32, - d: f32, - g: f32, - ) -> (f32, f32, f32) { - let mut s1 = 0.0_f64; - let mut s2 = 0.0_f64; - let res = self - .function - .exec(in1 as f64, in2 as f64, a as f64, b as f64, d as f64, g as f64, &mut s1, &mut s2); - (s1 as f32, s2 as f32, res as f32) - } - - pub fn swap_fun(&mut self, srate: f32, mut fun: Box) -> Box { - std::mem::swap(&mut self.function, &mut fun); - self.function.init(srate as f64, Some(&fun)); - fun - } - - pub fn set_sample_rate(&mut self, srate: f32) { - self.sample_rate = srate; - self.function.set_sample_rate(srate as f64); - } - - pub fn clear(&mut self) { - self.function.reset(); - } - - pub fn process_updates(&mut self) { - while let Some(msg) = self.update_cons.pop() { - match msg { - CodeUpdateMsg::UpdateFun(mut fun) => { - std::mem::swap(&mut self.function, &mut fun); - self.function.init(self.sample_rate as f64, Some(&fun)); - let _ = self.return_prod.push(CodeReturnMsg::DestroyFun(fun)); - } - } - } - } -} diff --git a/src/block_compiler.rs b/src/wblockdsp/compiler.rs similarity index 83% rename from src/block_compiler.rs rename to src/wblockdsp/compiler.rs index 44bbe83..608dc46 100644 --- a/src/block_compiler.rs +++ b/src/wblockdsp/compiler.rs @@ -6,7 +6,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use crate::blocklang::*; +use super::language::*; #[cfg(feature = "synfx-dsp-jit")] use synfx_dsp_jit::{ASTNode, JITCompileError}; @@ -18,6 +18,8 @@ struct JASTNode { 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>); @@ -37,18 +39,26 @@ impl BlockASTNode for ASTNodeRef { } 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); @@ -528,17 +538,21 @@ mod test { }; } + 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) { - use crate::block_compiler::{BlkJITCompileError, Block2JITCompiler}; - use crate::blocklang::BlockFun; - use crate::blocklang_def; - let lib = get_standard_library(); - let lang = blocklang_def::setup_hxdsp_block_language(lib.clone()); + let lang = crate::wblockdsp::setup_hxdsp_block_language(lib.clone()); let mut bf = BlockFun::new(lang.clone()); f(&mut bf); @@ -557,11 +571,11 @@ mod test { #[test] fn check_blocklang_sig1() { let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.3".to_string())).unwrap(); - bf.instanciate_at(0, 1, 2, "set", Some("&sig2".to_string())).unwrap(); - bf.instanciate_at(0, 0, 3, "value", Some("-1.3".to_string())).unwrap(); + 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); @@ -576,31 +590,31 @@ mod test { #[test] fn check_blocklang_accum_shift() { let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 1, 1, "accum", None); + put_n(bf, 0, 1, 1, "accum"); bf.shift_port(0, 1, 1, 1, false); - bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); - bf.instanciate_at(0, 0, 1, "get", Some("*reset".to_string())); + 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); + 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); + 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); - let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); - let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); - let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); - let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); - let (s1, s2, ret) = 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); + 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(); @@ -610,64 +624,64 @@ mod test { fn check_blocklang_arithmetics() { // Check + and * let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); - bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); - bf.instanciate_at(0, 1, 1, "+", None); + 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); - bf.instanciate_at(0, 1, 3, "value", Some("2.0".to_string())); - bf.instanciate_at(0, 2, 2, "*", None); + 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); + 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| { - bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); - bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); - bf.instanciate_at(0, 1, 1, "-", None); + 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); - bf.instanciate_at(0, 1, 3, "value", Some("2.0".to_string())); - bf.instanciate_at(0, 2, 2, "/", None); + 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); + 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| { - bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); - bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); - bf.instanciate_at(0, 1, 1, "-", None); + 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); + 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| { - bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); - bf.instanciate_at(0, 0, 2, "value", Some("0.01".to_string())); - bf.instanciate_at(0, 1, 1, "/", None); + 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); + 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| { - bf.instanciate_at(0, 0, 1, "value", Some("0.50".to_string())); - bf.instanciate_at(0, 0, 2, "value", Some("0.0".to_string())); - bf.instanciate_at(0, 1, 1, "/", None); + 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); + let (_s1, _s2, ret) = fun.exec_2in_2out(0.0, 0.0); assert_float_eq!(ret, 0.5 / 0.0); ctx.borrow_mut().free(); } @@ -676,10 +690,10 @@ mod test { fn check_blocklang_divrem() { // &sig1 on second output: let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "/%", None); - bf.instanciate_at(0, 2, 2, "set", Some("&sig1".to_string())).unwrap(); + 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); @@ -690,10 +704,10 @@ mod test { // &sig1 on first output: let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "/%", None); - bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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); @@ -704,10 +718,10 @@ mod test { // &sig1 on second output, but swapped outputs: let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "/%", None); - bf.instanciate_at(0, 2, 2, "set", Some("&sig1".to_string())).unwrap(); + 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); }); @@ -719,10 +733,10 @@ mod test { // &sig1 on first output, but swapped outputs: let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "/%", None); - bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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); }); @@ -734,10 +748,10 @@ mod test { // &sig1 on first output, but swapped inputs: let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "/%", None); - bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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); }); @@ -749,10 +763,10 @@ mod test { // &sig1 on first output, but swapped inputs and outputs: let (ctx, mut fun) = new_jit_fun(|bf| { - bf.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())).unwrap(); - bf.instanciate_at(0, 0, 2, "value", Some("-0.4".to_string())).unwrap(); - bf.instanciate_at(0, 1, 1, "/%", None); - bf.instanciate_at(0, 2, 1, "set", Some("&sig1".to_string())).unwrap(); + 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); }); diff --git a/src/blocklang_def.rs b/src/wblockdsp/definition.rs similarity index 89% rename from src/blocklang_def.rs rename to src/wblockdsp/definition.rs index d7949ce..f73a00a 100644 --- a/src/blocklang_def.rs +++ b/src/wblockdsp/definition.rs @@ -2,32 +2,22 @@ // This file is a part of HexoDSP. Released under GPL-3.0-or-later. // See README.md and COPYING for details. -use crate::blocklang::{BlockLanguage, BlockType, BlockUserInput}; +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: "source".to_string(), -// name: "phse".to_string(), -// rows: 1, -// inputs: vec![Some("f".to_string())], -// outputs: vec![Some("".to_string())], -// area_count: 0, -// user_input: BlockUserInput::None, -// description: -// "A phasor, returns a saw tooth wave to scan through things or use as modulator." -// .to_string(), -// color: 2, -// }); -// lang.define(BlockType { category: "literals".to_string(), name: "zero".to_string(), @@ -200,18 +190,6 @@ pub fn setup_hxdsp_block_language( // color: 8, // }); -// lang.define(BlockType { -// category: "arithmetics".to_string(), -// name: "/%".to_string(), -// rows: 2, -// inputs: vec![Some("a".to_string()), Some("b".to_string())], -// outputs: vec![Some("div".to_string()), Some("rem".to_string())], -// area_count: 0, -// user_input: BlockUserInput::None, -// description: "Computes the integer division and remainder of a / b".to_string(), -// color: 8, -// }); - for fun_name in &["+", "-", "*", "/"] { lang.define(BlockType { category: "arithmetics".to_string(), diff --git a/src/blocklang.rs b/src/wblockdsp/language.rs similarity index 99% rename from src/blocklang.rs rename to src/wblockdsp/language.rs index 61e5c8e..17dc369 100644 --- a/src/blocklang.rs +++ b/src/wblockdsp/language.rs @@ -1818,7 +1818,7 @@ mod test { #[test] fn check_blockfun_serialize_empty() { let dsp_lib = synfx_dsp_jit::get_standard_library(); - let lang = crate::blocklang_def::setup_hxdsp_block_language(dsp_lib); + let lang = crate::wblockdsp::setup_hxdsp_block_language(dsp_lib); let mut bf = BlockFun::new(lang.clone()); let sn = bf.save_snapshot(); @@ -1834,10 +1834,10 @@ mod test { #[test] fn check_blockfun_serialize_1() { let dsp_lib = synfx_dsp_jit::get_standard_library(); - let lang = crate::blocklang_def::setup_hxdsp_block_language(dsp_lib); + let lang = crate::wblockdsp::setup_hxdsp_block_language(dsp_lib); let mut bf = BlockFun::new(lang.clone()); - bf.instanciate_at(0, 0, 0, "+", None); + bf.instanciate_at(0, 0, 0, "+", None).unwrap(); let sn = bf.save_snapshot(); let serialized = sn.serialize().to_string(); 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/blocklang.rs b/tests/blocklang.rs deleted file mode 100644 index 6eda8cd..0000000 --- a/tests/blocklang.rs +++ /dev/null @@ -1,86 +0,0 @@ -// 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::*; - -//#[test] -//fn check_blocklang_dir_1() { -// use hexodsp::block_compiler::{BlkJITCompileError, Block2JITCompiler}; -// use hexodsp::blocklang::BlockFun; -// use hexodsp::blocklang_def; -// -// let lang = blocklang_def::setup_hxdsp_block_language(); -// let mut bf = BlockFun::new(lang.clone()); -// block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); -// block_fun.instanciate_at(0, 1, 1, "set", Some("&sig1".to_string())); -// -// let mut compiler = Block2JITCompiler::new(block_fun.block_language()); -// let ast = compiler.compile(&block_fun)?; -// let lib = synfx_dsp_jit::get_standard_library(); -// let ctx = synfx_dsp_jit::DSPNodeContext::new_ref(); -// let jit = JIT::new(lib, dsp_ctx.clone()); -// let fun = jit.compile(ASTFun::new(ast))?; -// -// fun.init(44100.0, None); -// -// let (s1, s2, ret) = fun.exec_2in_2out(0.0, 0.0); -// -// ctx.borrow_mut().free(); -//} -// -//// XXX: Test case with 3 outputs, where the first output writes a value used -//// by the computation after the first but before the third output. -// -// 0.3 ->3 set a -// => -> + set b -// get a -// => -> - set a -// get b -// get a + -// get b -//*/ -// -////#[test] -////fn check_blocklang_2() { -//// 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"); -//// -//// block_fun.instanciate_at(0, 0, 0, "get", Some("in1".to_string())); -//// block_fun.instanciate_at(0, 0, 1, "value", Some("0.3".to_string())); -//// block_fun.instanciate_at(0, 1, 0, "+", None); -//// block_fun.instanciate_at(0, 2, 0, "set", Some("&sig1".to_string())); -//// -//// block_fun.instanciate_at(0, 3, 0, "get", Some("in1".to_string())); -//// block_fun.instanciate_at(0, 3, 1, "get", Some("in2".to_string())); -//// block_fun.instanciate_at(0, 4, 0, "-", None); -//// block_fun.instanciate_at(0, 5, 0, "->3", None); -//// -//// block_fun.instanciate_at(0, 3, 5, "get", Some("in1".to_string())); -//// block_fun.instanciate_at(0, 4, 5, "if", None); -//// block_fun.instanciate_at(1, 0, 0, "value", Some("0.5".to_string())); -//// block_fun.instanciate_at(2, 0, 0, "value", Some("-0.5".to_string())); -//// -//// block_fun.instanciate_at(0, 6, 1, "set", Some("*a".to_string())); -//// block_fun.instanciate_at(0, 6, 2, "set", Some("x".to_string())); -//// block_fun.instanciate_at(0, 6, 0, "->", None); -//// block_fun.instanciate_at(0, 7, 0, "->2", None); -//// -//// block_fun.instanciate_at(0, 0, 3, "get", Some("in1".to_string())); -//// block_fun.instanciate_at(0, 0, 4, "get", Some("in2".to_string())); -//// block_fun.instanciate_at(0, 1, 3, "/%", None); -//// block_fun.instanciate_at(0, 2, 3, "->", None); -//// block_fun.instanciate_at(0, 3, 3, "/%", None); -//// block_fun.instanciate_at(0, 4, 3, "set", Some("&sig2".to_string())); -//// block_fun.instanciate_at(0, 4, 4, "set", Some("*ap".to_string())); -//// } -//// -//// 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.2; 100]); -////} diff --git a/tests/node_code.rs b/tests/node_code.rs index 69fcf39..45ec533 100644 --- a/tests/node_code.rs +++ b/tests/node_code.rs @@ -5,7 +5,7 @@ mod common; use common::*; -use hexodsp::blocklang::BlockFun; +use hexodsp::wblockdsp::BlockFun; fn setup() -> (Matrix, NodeExecutor) { let (node_conf, node_exec) = new_node_engine(); diff --git a/tests/wblockdsp.rs b/tests/wblockdsp.rs deleted file mode 100644 index 4f51935..0000000 --- a/tests/wblockdsp.rs +++ /dev/null @@ -1,41 +0,0 @@ -// 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::*; -#[cfg(feature="synfx-dsp-jit")] -use hexodsp::wblockdsp::*; - -#[cfg(feature="synfx-dsp-jit")] -#[test] -fn check_wblockdsp_init() { - let mut engine = CodeEngine::new(); - - let backend = engine.get_backend(); -} - -#[cfg(feature="synfx-dsp-jit")] -#[test] -fn check_wblockdsp_code_node() { - 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("code", "sig") - .node_io("code", "in1", "sig") - .node_inp("out", "ch1") - .place(&mut matrix, 0, 0) - .unwrap(); - matrix.sync().unwrap(); - - let mut chain = MatrixCellChain::new(CellDir::B); - chain - .node_out("code", "sig") - .node_io("code", "in1", "sig") - .node_inp("out", "ch1") - .place(&mut matrix, 0, 0) - .unwrap(); - matrix.sync().unwrap(); -} From d3b032a1a40cfdac390aa1ee369b87d9dc507d41 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 6 Aug 2022 09:34:41 +0200 Subject: [PATCH 87/88] Prepare for merge --- Cargo.toml | 5 +++-- src/wblockdsp/compiler.rs | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4369ef9..56bc1c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,9 +18,10 @@ ringbuf = "0.2.2" triple_buffer = "5.0.6" lazy_static = "1.4.0" hound = "3.4.0" -synfx-dsp-jit = { path = "../synfx-dsp-jit", optional = true } -#synfx-dsp = "0.5.1" +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" diff --git a/src/wblockdsp/compiler.rs b/src/wblockdsp/compiler.rs index 608dc46..2492d60 100644 --- a/src/wblockdsp/compiler.rs +++ b/src/wblockdsp/compiler.rs @@ -208,12 +208,8 @@ pub struct Block2JITCompiler { tmpvar_counter: usize, } -// 1. compile the weird tree into a graph -// - make references where IDs go -// - add a use count to each node, so that we know when to make temporary variables - #[cfg(not(feature = "synfx-dsp-jit"))] -enum ASTNode { +pub enum ASTNode { NoSynfxDSPJit } From e96e6351632e0de384109746b8ab330bab949ec2 Mon Sep 17 00:00:00 2001 From: Weird Constructor Date: Sat, 6 Aug 2022 14:19:39 +0200 Subject: [PATCH 88/88] properly call query_returns for now --- src/nodes/node_conf.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 4fb8f50..422e8be 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -703,6 +703,11 @@ impl NodeConfigurator { /// 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() {