├── .gitignore ├── CHANGELOG.md ├── COPYING ├── Cargo.toml ├── README.md ├── _clippy.sh ├── _update_readme.sh ├── examples ├── bench_core.rs ├── cpal_demo_node_api.rs ├── cpal_synth_constructor.rs ├── jack_demo_node_api.rs └── list_synth_reference.rs ├── rustfmt.toml ├── scripts └── pitch_quantize_prototype.wl ├── src ├── build.rs ├── cell_dir.rs ├── chain_builder.rs ├── dsp │ ├── mod.rs │ ├── node_ad.rs │ ├── node_adsr.rs │ ├── node_allp.rs │ ├── node_amp.rs │ ├── node_biqfilt.rs │ ├── node_bosc.rs │ ├── node_bowstri.rs │ ├── node_code.rs │ ├── node_comb.rs │ ├── node_cqnt.rs │ ├── node_delay.rs │ ├── node_ext.rs │ ├── node_fbwr_fbrd.rs │ ├── node_formfm.rs │ ├── node_fvafilt.rs │ ├── node_inp.rs │ ├── node_map.rs │ ├── node_midicc.rs │ ├── node_midip.rs │ ├── node_mix3.rs │ ├── node_mux9.rs │ ├── node_noise.rs │ ├── node_out.rs │ ├── node_pverb.rs │ ├── node_quant.rs │ ├── node_rndwk.rs │ ├── node_rust.rs │ ├── node_sampl.rs │ ├── node_scope.rs │ ├── node_sfilter.rs │ ├── node_sin.rs │ ├── node_smap.rs │ ├── node_test.rs │ ├── node_tseq.rs │ ├── node_tslfo.rs │ ├── node_vosc.rs │ ├── satom.rs │ └── tracker │ │ ├── mod.rs │ │ ├── pattern.rs │ │ └── sequencer.rs ├── dsp_nodes_ref.md ├── global.rs ├── lib.rs ├── log.rs ├── matrix.rs ├── matrix_repr.rs ├── monitor.rs ├── nodes │ ├── drop_thread.rs │ ├── feedback_filter.rs │ ├── midi.rs │ ├── mod.rs │ ├── node_conf.rs │ ├── node_exec.rs │ ├── node_graph_ordering.rs │ ├── node_prog.rs │ └── visual_sampling_filter.rs ├── sample_lib.rs ├── scope_handle.rs ├── shared_feedback.rs ├── synth_constructor.rs ├── util.rs └── wblockdsp │ ├── compiler.rs │ ├── definition.rs │ ├── language.rs │ └── mod.rs └── tests ├── basics.rs ├── common └── mod.rs ├── constructor_api.rs ├── hr.wav ├── matrix_observer.rs ├── modamt.rs ├── node_ad.rs ├── node_adsr.rs ├── node_allp.rs ├── node_bosc.rs ├── node_code.rs ├── node_comb.rs ├── node_cqnt.rs ├── node_delay.rs ├── node_exta.rs ├── node_formfm.rs ├── node_fvafilt.rs ├── node_inp.rs ├── node_map.rs ├── node_midicc.rs ├── node_midip.rs ├── node_mix3.rs ├── node_mux9.rs ├── node_noise.rs ├── node_pverb.rs ├── node_quant.rs ├── node_rndwk.rs ├── node_sampl.rs ├── node_scope.rs ├── node_sfilter.rs ├── node_smap.rs ├── node_test.rs ├── node_tslfo.rs ├── node_vosc.rs ├── quant.rs ├── sample_sin.wav └── sample_sin_long.wav /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*.wav 3 | /*.hxy 4 | /*.lock 5 | *.history 6 | /_pub_doc.sh 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.2.3 (unreleased) 2 | ================== 3 | 4 | * Bugfix: `MidiP` and `MidiCC` channels did get up to 16. Limited that to 15 now. 5 | 6 | 0.2.2 (2024-01-04) 7 | ================== 8 | 9 | * Feature: Finished the implementation of the `FVaFilt` virtual analog filter node. 10 | * Bugfix: Fixed non compiling `synfx-dsp` (Adjustment to Rust's unstable SIMD API). 11 | * Change: Bumped cpal version to 0.15.2 (used for the examples). 12 | 13 | 0.2.1 (2022-11-06) 14 | ================== 15 | 16 | * Breaking Change: `Amp` gain knob got a more useful -24dB to 24dB range. 17 | * Breaking Change: `Mix3` gain knobs were replaced by volume knobs 18 | with proper dB range. 19 | * Breaking Change: `Inp` got a volume knob with proper dB range. 20 | * Breaking Change: `Out` got a volume knob with proper dB range. 21 | * Breaking Change: `FbRd` got a volume knob with proper dB range. 22 | * Breaking Change: `BigFilt` got also an output gain knob with dB range. 23 | * Breaking Change: `Scope` gain knobs now have a -24db to 24db range. 24 | * Change: Removed any fixed limits regarding the maximum number of nodes in a graph. 25 | * Feature: Added PM input to the Sin node for phase modulation/distortion. 26 | This allows to get a more linear FM sound from that oscillator. 27 | * Change: Changed `Ad` envelope node to be properly retriggerable. Refactored 28 | out it's DSP code to `synfx_dsp::EnvRetrigAD`. 29 | * Change: Changed the `att` parameter of `Amp` to be linear now. For non linear attenuation 30 | you can use the `gain` input with a mod amount modification. 31 | * Feature: Added `Matrix::load_patch_from_mem` and `Matrix::save_patch_to_mem`. 32 | * Feature: The `BOsc` node has now a pulse mode with DC correction. 33 | * Bugfix: The rounding of the parameters could snap outside the UI min/max range of that 34 | parameter. It's now clamped properly. 35 | * Feature: Added `Adsr` node for an ADSR envelope generator. 36 | * Feature: Added the `SynthConstructor` API. 37 | 38 | 0.2.0 (2022-08-28) 39 | ================== 40 | 41 | * Documentation: Added a guide in the hexodsp::dsp module documentation 42 | about implementing new DSP nodes. 43 | * Bugfix: TriSawLFO (TsLFO) node did output too high values if the `rev` 44 | parameter was changed or modulated at runtime. 45 | * Bugfix: Found a bug in cubic interpolation in the sample player and 46 | similar bugs in the delay line (and all-pass & comb filters). Refactored 47 | the cubic interpolation and tested it seperately now. 48 | * Change: Moved DSP code over to `synfx-dsp` crate. 49 | * Feature: Matrix::get\_connections() returns information about the connections 50 | to the adjacent cells. 51 | * Feature: Added the MatrixCellChain abstraction for easy creation of DSP 52 | chains on the hexagonal Matrix. 53 | * Feature: Added Scope DSP node and NodeConfigurator/Matrix API for retrieving 54 | the scope handles for access to it's capture buffers. 55 | * Feature: Added WBlockDSP visual programming language utilizing the `synfx-dsp-jit` crate. 56 | * Feature: Added the `FormFM` node that was contributed by Dimas Leenman (aka Skythedragon). 57 | * Feature: Added `MidiP` node for MIDI pitch/note input. 58 | * Feature: Added `MidiCC` node for MIDI CC input. 59 | * Feature: Added `ExtA` to `ExtF` nodes for plugin parameter access. 60 | * Feature: Added `Inp` input node for the two audio channels. 61 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hexodsp" 3 | version = "0.2.2" 4 | authors = ["Weird Constructor "] 5 | license = "GPL-3.0-or-later" 6 | edition = "2021" 7 | description = "Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth." 8 | repository = "https://github.com/WeirdConstructor/HexoDSP" 9 | readme = "README.md" 10 | keywords = ["audio", "real-time", "synthesis", "synthesizer", "dsp"] 11 | categories = ["multimedia::audio", "multimedia", "algorithms", "mathematics"] 12 | 13 | [features] 14 | default = [ "synfx-dsp-jit" ] 15 | 16 | [dependencies] 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | ringbuf = "0.2.2" 20 | triple_buffer = "5.0.6" 21 | lazy_static = "1.4.0" 22 | hound = "3.4.0" 23 | synfx-dsp-jit = { version = "0.6.2", optional = true } 24 | #synfx-dsp-jit = { git = "https://github.com/WeirdConstructor/synfx-dsp-jit.git", optional = true } 25 | synfx-dsp = { version = "0.5.6" } 26 | #synfx-dsp = { git = "https://github.com/WeirdConstructor/synfx-dsp.git" } 27 | 28 | [dev-dependencies] 29 | num-complex = "0.2" 30 | jack = "0.10.0" 31 | rustfft = "6.0.0" 32 | cpal = "0.15.2" 33 | anyhow = "1.0.58" 34 | 35 | [lib] 36 | path = "src/lib.rs" 37 | name = "hexodsp" 38 | crate-type = ["lib"] 39 | 40 | #[patch.'https://github.com/WeirdConstructor/synfx-dsp.git'] 41 | #synfx-dsp = { path = "../synfx-dsp" } 42 | -------------------------------------------------------------------------------- /_clippy.sh: -------------------------------------------------------------------------------- 1 | cargo clippy --color=always 2>&1 | less -R 2 | -------------------------------------------------------------------------------- /_update_readme.sh: -------------------------------------------------------------------------------- 1 | cargo readme --no-title --no-indent-headings > README.md 2 | -------------------------------------------------------------------------------- /examples/bench_core.rs: -------------------------------------------------------------------------------- 1 | // This example demonstrates the SynthConstructor API with the CPAL backend 2 | // for standalone audio output. 3 | // 4 | // Execute with: 5 | // $ cargo +nightly run --release --example cpal_synth_constructor 6 | 7 | use hexodsp::synth_constructor::SynthConstructor; 8 | use hexodsp::*; 9 | 10 | use anyhow; 11 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 12 | 13 | fn main() { 14 | // The SynthConstructor encapsulates the whole audio graph engine in HexoDSP: 15 | let mut sc = SynthConstructor::new(); 16 | 17 | use hexodsp::build::*; 18 | 19 | // Setup a sawtooth oscillator with 440Hz: 20 | let saw = bosc(0).set().wtype(2).set().freq(440.0); 21 | // Setup an amplifier node with a low gain: 22 | let mut am = amp(0).set().gain(0.2).input().inp(&saw.output().sig()); 23 | 24 | let mut am = sin(0).input().freq(&am.output().sig()); 25 | for i in 1..100 { 26 | am = sin(i).input().freq(&am.output().sig()); 27 | } 28 | 29 | // // Insert your own custom Rust function via a NodeId::Rust1x1 node 30 | // // into the DSP graph: 31 | // use hexodsp::dsp::{DynamicNode1x1, DynNode1x1Context}; 32 | let r1x1 = rust1x1(0).input().inp(&am.output().sig()); 33 | let r1x1 = r1x1.set().alpha(0.75); 34 | // // You may replace this function anytime at runtime: 35 | // sc.set_dynamic_node1x1(0, Box::new(|inp: &[f32], out: &mut [f32], ctx: &DynNode1x1Context| { 36 | // let alpha = ctx.alpha_slice(); 37 | // for (i, in_sample) in inp.iter().enumerate() { 38 | // out[i] = in_sample * alpha[i]; 39 | // } 40 | // 41 | // // This sets an atomic float that can be read out using SynthConstructor::led_value()! 42 | // ctx.led_value().set(out[0]); 43 | // })); 44 | // 45 | // Assign amplifier node output to the two input channels 46 | // of the audio device output node: 47 | // let out = out(0).input().ch1(&r1x1.output().sig()); 48 | let out = out(0).input().ch1(&am.output().sig()); 49 | let out = out.input().ch2(&r1x1.output().sig()); 50 | 51 | // Setup a triangle LFO with a cycletime of 8 seconds. 52 | let lfo = tslfo(0).set().time(8000.0); 53 | // let out = out.input().ch2(&lfo.output().sig()); 54 | // Setup the "att"enuator input to 0.3 with a modulation amount of 0.0 to 0.7. 55 | // Redirect the output of the LFO (which oscillated between 0.0 and 1.0) to the 56 | // "att" input of the Amp node here: 57 | // am.set_mod().att(0.3, 0.7).input().att(&lfo.output().sig()); 58 | 59 | // Upload the program: 60 | sc.upload(&out).unwrap(); 61 | 62 | use std::time::Instant; 63 | let mut exec = sc.executor().unwrap(); 64 | let mut avg = 0; 65 | let mut cnt = 0; 66 | exec.test_run(10.0, false, &[]); 67 | for i in 0..10 { 68 | let now = Instant::now(); 69 | // exec.dummy_run(100.0); 70 | exec.test_run(10.0, false, &[]); 71 | let millis = now.elapsed().as_millis(); 72 | cnt += 1; 73 | avg += millis; 74 | } 75 | println!("avg: {}", avg / cnt); 76 | } 77 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | use_field_init_shorthand = true 3 | -------------------------------------------------------------------------------- /scripts/pitch_quantize_prototype.wl: -------------------------------------------------------------------------------- 1 | # -1.0 == 21 => A0 2 | # -0.1 == 69 - 12 => A3 3 | # 0.0 == 69 => A4 4 | # 0.1 == 69 + 12 => A5 5 | # 0.2 == 69 + 12 => A6 6 | # 0.3 == 69 + 12 => A7 7 | # 0.4 == 69 + 12 => A8 8 | # 0.5 == 69 + 12 => A9 9 | # 0.6 == 69 + 12 => A10 10 | # 0.7 == 69 + 12 => A11 11 | # 0.7 == 69 + 12 => A12 12 | # 0.9 == 69 + 12 => A13 13 | # 1.0 == 69 + 12 => A14 14 | !f2note = { 69.0 + _ * 120.0 }; 15 | !note2pitch = { 440.0 * 2.0 ^ ((_ - 69) / 12) }; 16 | 17 | !f2p = { 440.0 * 2.0 ^ (_ * 10.0); }; 18 | 19 | !pitch2note = { 20 | 12.0 * std:num:log2[_ / 440.0] + 69.0 21 | }; 22 | 23 | !note2f = { _ / 120.0 - 0.575 }; 24 | 25 | !p2f = { 0.1 * std:num:log2[_ / 440.0] }; 26 | 27 | # ((($x as f32).max(0.01) / 440.0).log2() / 10.0) 28 | 29 | !in = (1.0 / 12.0) / 10.0; 30 | std:displayln :f2note ~ f2note in; 31 | std:displayln :note2pitch ~ note2pitch ~ f2note in; 32 | std:displayln :f2p " "~ f2p in; 33 | !f = f2p in; 34 | std:displayln :pitch2note " "~ pitch2note f; 35 | std:displayln :note2f " "~ note2f ~ pitch2note f; 36 | std:displayln :p2f " "~ p2f f; 37 | 38 | !eucMod = { 39 | !a = int _; 40 | !b = int _1; 41 | !q = a % b; 42 | if q < 0 { return ~ if b > 0 { q + b } { q - b } }; 43 | q 44 | }; 45 | 46 | !eucDiv = { 47 | !a = int _; 48 | !b = int _1; 49 | !div = a / b; 50 | !mod = a % b; 51 | if mod < 0 { .div -= 1; }; 52 | div 53 | }; 54 | 55 | # gets the index in the 24 half semitone array 56 | !get_note_idx = { 57 | !num = _ * 240.0; # twice the resolution, for quater notes and a more even quantization 58 | !r = int ~ std:num:floor num; 59 | !o = eucDiv r 24; 60 | .r -= o * 24; 61 | # std:displayln ~ $F " in={:8.5} r={:2} o={:2}" _ r o; 62 | r 63 | }; 64 | 65 | iter f $[ 66 | $[10, 0, 0], 67 | $[-10, 0, 0], 68 | $[0, 1, -11], 69 | $[0, 1, 6], 70 | $[0, 1, 0], 71 | $[0, -11, -5], 72 | $[0, -1, 0], 73 | # $[0, -1, 0], 74 | # $[0, -1, -1], 75 | # $[0, -1, 1], 76 | # $[-1, -1, 0], 77 | # $[-1, 1, 0], 78 | # $[1, -1, 0], 79 | # $[1, 1, 0], 80 | # $[2, -1, 1], 81 | # $[2, 1, 1], 82 | $[2, -11, 0], 83 | $[2, 11, 0], 84 | $[-9, -1, 0], 85 | ] { 86 | !num = 87 | float[f.0] * 0.1 88 | + (float[f.1] / 120.0) 89 | + (float[f.2] / 1200.0); 90 | #-0.2, -0.3, -0.4, -0.5, 0.2, 0.3, 0.4, 0.5, -1.0, 1.0] { 91 | std:displayln ~ 92 | $F "o={:2} n={:2} c={:2} | {:6.3} => {:3}" 93 | f.0 f.1 f.2 num (get_note_idx num); 94 | std:displayln[]; 95 | }; 96 | 97 | !n = 1000; 98 | iter f 0 => n { 99 | !x = (float[f] / float[n]) - 0.5; 100 | std:displayln ~ $F "[{:5.3}] => {}" x get_note_idx[x]; 101 | }; 102 | 103 | # Taken from VCV Rack Fundamental Modules Quantizer.cpp 104 | # Under GPL-3.0-or-later 105 | # 106 | # void process(const ProcessArgs& args) override { 107 | # bool playingNotes[12] = {}; 108 | # int channels = std::max(inputs[PITCH_INPUT].getChannels(), 1); 109 | # 110 | # for (int c = 0; c < channels; c++) { 111 | # float pitch = inputs[PITCH_INPUT].getVoltage(c); 112 | # int range = std::floor(pitch * 24); // 1.1 => 26 113 | # int octave = eucDiv(range, 24); // 26 => 1 114 | # range -= octave * 24; // 26 => 2 115 | # int note = ranges[range] + octave * 12; 116 | # playingNotes[eucMod(note, 12)] = true; 117 | # pitch = float(note) / 12; 118 | # outputs[PITCH_OUTPUT].setVoltage(pitch, c); 119 | # } 120 | # outputs[PITCH_OUTPUT].setChannels(channels); 121 | # std::memcpy(this->playingNotes, playingNotes, sizeof(playingNotes)); 122 | # } 123 | # 124 | # void updateRanges() { 125 | # // Check if no notes are enabled 126 | # bool anyEnabled = false; 127 | # for (int note = 0; note < 12; note++) { 128 | # if (enabledNotes[note]) { 129 | # anyEnabled = true; 130 | # break; 131 | # } 132 | # } 133 | # // Find closest notes for each range 134 | # for (int i = 0; i < 24; i++) { // => Oversampling, for taking care of the rounding?! 135 | # int closestNote = 0; 136 | # int closestDist = INT_MAX; 137 | # for (int note = -12; note <= 24; note++) { 138 | # int dist = std::abs((i + 1) / 2 - note); 139 | # // Ignore enabled state if no notes are enabled 140 | # if (anyEnabled && !enabledNotes[eucMod(note, 12)]) { 141 | # continue; 142 | # } 143 | # if (dist < closestDist) { 144 | # closestNote = note; 145 | # closestDist = dist; 146 | # } 147 | # else { 148 | # // If dist increases, we won't find a better one. 149 | # break; 150 | # } 151 | # } 152 | # ranges[i] = closestNote; 153 | # } 154 | # } 155 | 156 | !mk_pitch_lookup_table = {!enabled = _; 157 | !any = $f; 158 | iter n enabled { if n { .any = $t } }; 159 | 160 | !tbl = $[]; 161 | 162 | iter i 0 => 24 { 163 | !minDistNote = 0; 164 | !minDist = 10000000000; 165 | 166 | iter note -12 => 25 { 167 | !dist = std:num:abs[ (i + 1) / 2 - note ]; 168 | 169 | !idx = eucMod note 12; 170 | if any &and not[enabled.(idx)] { 171 | next[]; 172 | }; 173 | std:displayln "DIST" (i + 1) / 2 note idx "=>" dist; 174 | if dist < minDist { 175 | .minDistNote = idx; 176 | .minDist = dist; 177 | } { break[] }; 178 | }; 179 | 180 | tbl.(i) = minDistNote; 181 | }; 182 | 183 | tbl 184 | }; 185 | 186 | !lkup = mk_pitch_lookup_table $[ 187 | $f, $f, $f, $f, $f, $f, 188 | $f, $f, $f, $t, $t, $t, 189 | ]; 190 | 191 | std:displayln ~ eucMod -1 12; 192 | std:displayln lkup; 193 | !k = ${}; 194 | !i = 0; 195 | while i < 24 { 196 | std:displayln (i / 2) + 1 " => " lkup.(i) lkup.(i + 1); 197 | k.(lkup.(i)) = k.(lkup.(i)) + 1; 198 | k.(lkup.(i + 1)) = k.(lkup.(i + 1)) + 1; 199 | .i += 2; 200 | }; 201 | std:displayln k; 202 | #std:displayln ~ get_note_offs ~ note2f (0.1 + (4.0 / 12.0) / 10.0); 203 | -------------------------------------------------------------------------------- /src/cell_dir.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] 6 | pub enum CellDir { 7 | TR, 8 | BR, 9 | B, 10 | BL, 11 | TL, 12 | T, 13 | /// Center 14 | C, 15 | } 16 | 17 | impl CellDir { 18 | pub fn from(edge: u8) -> Self { 19 | match edge { 20 | 0 => CellDir::TR, 21 | 1 => CellDir::BR, 22 | 2 => CellDir::B, 23 | 3 => CellDir::BL, 24 | 4 => CellDir::TL, 25 | 5 => CellDir::T, 26 | _ => CellDir::C, 27 | } 28 | } 29 | 30 | pub fn flip(&self) -> Self { 31 | match self { 32 | CellDir::TR => CellDir::BL, 33 | CellDir::BR => CellDir::TL, 34 | CellDir::B => CellDir::T, 35 | CellDir::BL => CellDir::TR, 36 | CellDir::TL => CellDir::BR, 37 | CellDir::T => CellDir::B, 38 | CellDir::C => CellDir::T, 39 | } 40 | } 41 | 42 | #[inline] 43 | pub fn is_output(&self) -> bool { 44 | let e = self.as_edge(); 45 | e <= 2 46 | } 47 | 48 | #[inline] 49 | pub fn is_input(&self) -> bool { 50 | !self.is_output() 51 | } 52 | 53 | #[inline] 54 | pub fn as_edge(&self) -> u8 { 55 | *self as u8 56 | } 57 | 58 | pub fn as_menu_pos(&self) -> (i32, i32) { 59 | match self { 60 | // out 1 - TR 61 | CellDir::TR => (0, 1), 62 | // out 2 - BR 63 | CellDir::BR => (1, 1), 64 | // out 3 - B 65 | CellDir::B => (0, 1), 66 | // in 3 - BL 67 | CellDir::BL => (-1, 1), 68 | // in 2 - TL 69 | CellDir::TL => (-1, 0), 70 | // in 1 - T 71 | CellDir::T => (0, -1), 72 | _ => (0, 0), 73 | } 74 | } 75 | 76 | pub fn path_from_to(mut a: (usize, usize), b: (usize, usize)) -> Vec { 77 | let mut path = vec![]; 78 | 79 | let mut defensive_max: i32 = 1024; 80 | 81 | while (a.0 != b.0 || a.1 != b.1) && defensive_max > 0 { 82 | //d// println!("ITER START: A={:?} B={:?}", a, b); 83 | defensive_max -= 1; 84 | 85 | let mut min_distance = 99999.0; 86 | let mut min_dir = CellDir::C; 87 | let mut min_new_a = a; 88 | 89 | for e in 0..6 { 90 | let dir = Self::from(e); 91 | 92 | if let Some(new_pos) = dir.offs_pos(a) { 93 | let dist = (b.0 as f32 - new_pos.0 as f32).powf(2.0) 94 | + (b.1 as f32 - new_pos.1 as f32).powf(2.0); 95 | 96 | //d// println!("DIST={:5.3} FOR {:?} (B={:?})", dist, new_pos, b); 97 | 98 | if dist < min_distance { 99 | min_distance = dist; 100 | min_dir = dir; 101 | min_new_a = new_pos; 102 | } 103 | } else { 104 | //d// println!("NOPOS {:?} {:?}", dir, a); 105 | } 106 | } 107 | 108 | if min_distance < 99999.0 { 109 | //d// println!("A={:?} => {:?} DIR={:?} B={:?}", a, min_new_a, min_dir, b); 110 | a = min_new_a; 111 | path.push(min_dir); 112 | } else { 113 | //d// println!("ITER BREAK"); 114 | break; 115 | } 116 | 117 | //d// println!("ITER END: A={:?} B={:?} MAX={}", a, b, defensive_max); 118 | } 119 | 120 | //d// println!("PATH: {:?}", path); 121 | 122 | path 123 | } 124 | 125 | pub fn offs_pos(&self, pos: (usize, usize)) -> Option<(usize, usize)> { 126 | let offs = self.as_offs(pos.0); 127 | 128 | let new_pos = (pos.0 as i32 + offs.0, pos.1 as i32 + offs.1); 129 | 130 | if new_pos.0 >= 0 && new_pos.1 >= 0 { 131 | Some((new_pos.0 as usize, new_pos.1 as usize)) 132 | } else { 133 | None 134 | } 135 | } 136 | 137 | pub fn as_offs(&self, x: usize) -> (i32, i32) { 138 | let even = x % 2 == 0; 139 | match self { 140 | // out 1 - TR 141 | CellDir::TR => (1, if even { -1 } else { 0 }), 142 | // out 2 - BR 143 | CellDir::BR => (1, i32::from(!even)), 144 | // out 3 - B 145 | CellDir::B => (0, 1), 146 | // in 3 - BL 147 | CellDir::BL => (-1, i32::from(!even)), 148 | // in 2 - TL 149 | CellDir::TL => (-1, if even { -1 } else { 0 }), 150 | // in 1 - T 151 | CellDir::T => (0, -1), 152 | _ => (0, 0), 153 | } 154 | } 155 | 156 | /// If it returns 0 they are not adjacent, 157 | /// if it returns 1 the data flow direction is pos_a => pos_b 158 | /// if it returns -1 the data flow direction is pos_b => pos_a 159 | pub fn are_adjacent(pos_a: (usize, usize), pos_b: (usize, usize)) -> Option { 160 | let ipos_a = (pos_a.0 as i32, pos_a.1 as i32); 161 | let ipos_b = (pos_b.0 as i32, pos_b.1 as i32); 162 | 163 | let (ox, oy) = CellDir::T.as_offs(pos_a.0); 164 | if ipos_b == (ipos_a.0 + ox, ipos_a.1 + oy) { 165 | return Some(CellDir::T); 166 | } 167 | let (ox, oy) = CellDir::TL.as_offs(pos_a.0); 168 | if ipos_b == (ipos_a.0 + ox, ipos_a.1 + oy) { 169 | return Some(CellDir::TL); 170 | } 171 | let (ox, oy) = CellDir::BL.as_offs(pos_a.0); 172 | if ipos_b == (ipos_a.0 + ox, ipos_a.1 + oy) { 173 | return Some(CellDir::BL); 174 | } 175 | 176 | let (ox, oy) = CellDir::TR.as_offs(pos_a.0); 177 | if ipos_b == (ipos_a.0 + ox, ipos_a.1 + oy) { 178 | return Some(CellDir::TR); 179 | } 180 | let (ox, oy) = CellDir::BR.as_offs(pos_a.0); 181 | if ipos_b == (ipos_a.0 + ox, ipos_a.1 + oy) { 182 | return Some(CellDir::BR); 183 | } 184 | let (ox, oy) = CellDir::B.as_offs(pos_a.0); 185 | if ipos_b == (ipos_a.0 + ox, ipos_a.1 + oy) { 186 | return Some(CellDir::B); 187 | } 188 | 189 | None 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/dsp/node_allp.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp::AllPass; 10 | 11 | /// A simple amplifier 12 | #[derive(Debug, Clone)] 13 | pub struct AllP { 14 | allpass: Box>, 15 | } 16 | 17 | impl AllP { 18 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 19 | Self { allpass: Box::new(AllPass::new()) } 20 | } 21 | 22 | pub const inp: &'static str = "The signal input for the allpass filter."; 23 | pub const g: &'static str = "The internal factor for the allpass filter."; 24 | pub const time: &'static str = "The allpass delay time."; 25 | pub const sig: &'static str = "The output of allpass filter."; 26 | 27 | pub const DESC: &'static str = r#"Single Allpass Filter 28 | This is an allpass filter that can be used to build reverbs 29 | or anything you might find it useful for. 30 | "#; 31 | pub const HELP: &'static str = r#"A Simple Single Allpass Filter 32 | 33 | This is an allpass filter that can be used to build reverbs 34 | or anything you might find it useful for. 35 | 36 | Typical arrangements are (Schroeder Reverb): 37 | 38 | ```text 39 | t=4.5ms 40 | g=0.7 -> Comb 41 | AllP -> AllP -> AllP -> -> Comb 42 | t=42ms t=13.5ms -> Comb 43 | g=0.7 g=0.7 -> Comb 44 | ``` 45 | 46 | Or: 47 | 48 | ```text 49 | Comb -> t=0.48ms 50 | Comb -> g=0.7 51 | Comb -> AllP -> AllP -> AllP 52 | Comb -> t=5ms t=1.68ms 53 | g=0.7 g=0.7 54 | ``` 55 | 56 | Typical values for the comb filters are in the range ~~g~~=**0.6** to **0.9** 57 | and time in the range of **30ms** to **250ms**. 58 | 59 | Feel free to deviate from this and experiment around. 60 | 61 | Building your own reverbs is fun! 62 | 63 | (And don't forget that you can create feedback using the `FbWr` and `FbRd` nodes!) 64 | "#; 65 | 66 | pub fn graph_fun() -> Option { 67 | None 68 | } 69 | } 70 | 71 | impl DspNode for AllP { 72 | fn set_sample_rate(&mut self, srate: f32) { 73 | self.allpass.set_sample_rate(srate as f64); 74 | } 75 | 76 | fn reset(&mut self) { 77 | self.allpass.reset(); 78 | } 79 | 80 | #[inline] 81 | fn process( 82 | &mut self, 83 | ctx: &mut dyn NodeAudioContext, 84 | _ectx: &mut NodeExecContext, 85 | _nctx: &NodeContext, 86 | _atoms: &[SAtom], 87 | inputs: &[ProcBuf], 88 | outputs: &mut [ProcBuf], 89 | ctx_vals: LedPhaseVals, 90 | ) { 91 | use crate::dsp::{denorm, inp, out}; 92 | 93 | let inp = inp::AllP::inp(inputs); 94 | let time = inp::AllP::time(inputs); 95 | let g = inp::AllP::g(inputs); 96 | let out = out::AllP::sig(outputs); 97 | 98 | let ap = &mut *self.allpass; 99 | 100 | for frame in 0..ctx.nframes() { 101 | let v = inp.read(frame); 102 | 103 | out.write( 104 | frame, 105 | ap.next( 106 | denorm::AllP::time(time, frame) as f64, 107 | denorm::AllP::g(g, frame) as f64, 108 | v as f64, 109 | ) as f32, 110 | ); 111 | } 112 | 113 | let last_frame = ctx.nframes() - 1; 114 | ctx_vals[0].set(out.read(last_frame)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/dsp/node_amp.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | 10 | #[macro_export] 11 | macro_rules! fa_amp_neg_att { 12 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 13 | let s = match ($v.round() as usize) { 14 | 0 => "Allow", 15 | 1 => "Clip", 16 | _ => "?", 17 | }; 18 | write!($formatter, "{}", s) 19 | }}; 20 | } 21 | 22 | /// A simple amplifier 23 | #[derive(Debug, Clone)] 24 | pub struct Amp {} 25 | 26 | impl Amp { 27 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 28 | Self {} 29 | } 30 | pub const inp: &'static str = "Signal input"; 31 | pub const att: &'static str = 32 | "Attenuate input. Does only attenuate the signal, not amplify it.\n\ 33 | Use this for envelope input."; 34 | pub const gain: &'static str = "Gain input. This control can actually amplify the signal."; 35 | pub const neg_att: &'static str = 36 | "If this is set to 'Clip', only the **0.0**-**1.0** input range of the \ 37 | ~~att~~ input port is used. Negative values are clipped to **0.0**."; 38 | pub const sig: &'static str = "Amplified signal output"; 39 | pub const DESC: &'static str = r#"Signal Amplifier 40 | 41 | This is a simple amplifier to amplify or attenuate a signal. 42 | "#; 43 | pub const HELP: &'static str = r#"Signal Amplifier 44 | 45 | It serves the simple purpose of taking an input signal and attenuate (either 46 | with the ~~att~~ or the ~~gain~~ parameter) or just amplifying it with 47 | the ~~gain~~ parameter. 48 | 49 | You can even use it as simple fixed control signal source if you leave the 50 | ~~inp~~ port unconnected and just dial in the desired output value with the 51 | parameter. 52 | 53 | The main idea with the ~~gain~~ and ~~att~~ parameters is, that you can set 54 | the desired amplification with the ~~gain~~ parameter and automate it using 55 | the ~~att~~ parameter. The ~~neg~~ setting then defines what happens with 56 | negative inputs on the ~~att~~ port. 57 | "#; 58 | 59 | pub fn graph_fun() -> Option { 60 | None 61 | } 62 | } 63 | 64 | impl DspNode for Amp { 65 | fn set_sample_rate(&mut self, _srate: f32) {} 66 | fn reset(&mut self) {} 67 | 68 | #[inline] 69 | fn process( 70 | &mut self, 71 | ctx: &mut dyn NodeAudioContext, 72 | _ectx: &mut NodeExecContext, 73 | _nctx: &NodeContext, 74 | atoms: &[SAtom], 75 | inputs: &[ProcBuf], 76 | outputs: &mut [ProcBuf], 77 | ctx_vals: LedPhaseVals, 78 | ) { 79 | use crate::dsp::{at, denorm, denorm_v, inp, inp_dir, out}; 80 | 81 | let gain = inp::Amp::gain(inputs); 82 | let att = inp::Amp::att(inputs); 83 | let inp = inp::Amp::inp(inputs); 84 | let out = out::Amp::sig(outputs); 85 | let neg = at::Amp::neg_att(atoms); 86 | 87 | let last_frame = ctx.nframes() - 1; 88 | 89 | let last_val = if neg.i() > 0 { 90 | for frame in 0..ctx.nframes() { 91 | out.write( 92 | frame, 93 | inp.read(frame) 94 | * denorm_v::Amp::att(inp_dir::Amp::att(att, frame).max(0.0)) 95 | * denorm::Amp::gain(gain, frame), 96 | ); 97 | } 98 | 99 | inp.read(last_frame) 100 | * denorm_v::Amp::att(inp_dir::Amp::att(att, last_frame).max(0.0)) 101 | * denorm::Amp::gain(gain, last_frame) 102 | } else { 103 | for frame in 0..ctx.nframes() { 104 | out.write( 105 | frame, 106 | inp.read(frame) 107 | * denorm_v::Amp::att(inp_dir::Amp::att(att, frame).abs()) 108 | * denorm::Amp::gain(gain, frame), 109 | ); 110 | } 111 | 112 | inp.read(last_frame) 113 | * denorm_v::Amp::att(inp_dir::Amp::att(att, last_frame).abs()) 114 | * denorm::Amp::gain(gain, last_frame) 115 | }; 116 | 117 | ctx_vals[0].set(last_val); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/dsp/node_biqfilt.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp::*; 10 | 11 | #[macro_export] 12 | macro_rules! fa_biqfilt_type { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | let s = match ($v.round() as usize) { 15 | 0 => "BtW LP", 16 | 1 => "Res", 17 | _ => "?", 18 | }; 19 | write!($formatter, "{}", s) 20 | }}; 21 | } 22 | 23 | #[macro_export] 24 | macro_rules! fa_biqfilt_ord { 25 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 26 | let s = match ($v.round() as usize) { 27 | 0 => "1", 28 | 1 => "2", 29 | 2 => "3", 30 | 3 => "4", 31 | _ => "?", 32 | }; 33 | write!($formatter, "{}", s) 34 | }}; 35 | } 36 | 37 | /// A simple amplifier 38 | #[derive(Debug, Clone)] 39 | pub struct BiqFilt { 40 | cascade: Vec, 41 | srate: f32, 42 | ofreq: f32, 43 | oq: f32, 44 | ogain: f32, 45 | otype: u8, 46 | } 47 | 48 | impl BiqFilt { 49 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 50 | Self { 51 | cascade: vec![Biquad::new(); 4], 52 | srate: 1.0 / 44100.0, 53 | otype: 99, // value that can't be set by the user 54 | ofreq: -2.0, // value that can't be set by the user 55 | oq: -2.0, // value that can't be set by the user 56 | ogain: -2.0, // value that can't be set by the user 57 | } 58 | } 59 | pub const inp: &'static str = "Signal input"; 60 | pub const freq: &'static str = "Filter cutoff frequency."; 61 | pub const q: &'static str = "Filter Q factor."; 62 | pub const gain: &'static str = "Filter gain."; 63 | pub const ftype: &'static str = "'BtW LP' Butterworth Low-Pass, 'Res' Resonator"; 64 | pub const order: &'static str = ""; 65 | pub const sig: &'static str = "Filtered signal output."; 66 | pub const DESC: &'static str = r#"Biquad Filter 67 | 68 | This is the implementation of a biquad filter cascade. 69 | It is not meant for fast automation. Please use other nodes 70 | like eg. `SFilter` for that. 71 | "#; 72 | pub const HELP: &'static str = r#"Biquad Filter (Cascade) 73 | 74 | This is the implementation of a biquad filter cascade. 75 | It is not meant for fast automation and might blow up if you 76 | treat it too rough. Please use other nodes like eg. `SFilter` for that. 77 | "#; 78 | 79 | pub fn graph_fun() -> Option { 80 | None 81 | } 82 | } 83 | 84 | impl DspNode for BiqFilt { 85 | fn set_sample_rate(&mut self, srate: f32) { 86 | self.srate = srate; 87 | self.otype = 99; // cause recalculation of the filter 88 | 89 | for b in &mut self.cascade { 90 | b.reset(); 91 | } 92 | } 93 | 94 | fn reset(&mut self) { 95 | for b in &mut self.cascade { 96 | b.reset(); 97 | } 98 | } 99 | 100 | #[inline] 101 | fn process( 102 | &mut self, 103 | ctx: &mut dyn NodeAudioContext, 104 | _ectx: &mut NodeExecContext, 105 | _nctx: &NodeContext, 106 | atoms: &[SAtom], 107 | inputs: &[ProcBuf], 108 | outputs: &mut [ProcBuf], 109 | ctx_vals: LedPhaseVals, 110 | ) { 111 | use crate::dsp::{at, denorm, inp, out}; 112 | 113 | let inp = inp::BiqFilt::inp(inputs); 114 | let freq = inp::BiqFilt::freq(inputs); 115 | let q = inp::BiqFilt::q(inputs); 116 | let gain = inp::BiqFilt::gain(inputs); 117 | let ftype = at::BiqFilt::ftype(atoms); 118 | let order = at::BiqFilt::order(atoms); 119 | let out = out::BiqFilt::sig(outputs); 120 | 121 | let ftype = ftype.i() as u8; 122 | let cfreq = denorm::BiqFilt::freq(freq, 0); 123 | let cfreq = cfreq.clamp(0.0, 22000.0); 124 | let cq = denorm::BiqFilt::q(q, 0); 125 | let cgain = denorm::BiqFilt::gain(gain, 0); 126 | 127 | if ftype != self.otype 128 | || (cfreq - self.ofreq).abs() > 0.0001 129 | || (cq - self.oq).abs() > 0.0001 130 | || (cgain - self.ogain).abs() > 0.0001 131 | { 132 | // recalculate coeffs of all in the cascade 133 | let coefs = match ftype { 134 | 1 => BiquadCoefs::resonator(self.srate, cfreq, cq), 135 | _ => BiquadCoefs::butter_lowpass(self.srate, cfreq), 136 | }; 137 | 138 | for o in &mut self.cascade { 139 | o.set_coefs(coefs); 140 | o.reset(); 141 | } 142 | 143 | self.otype = ftype; 144 | self.ofreq = cfreq; 145 | self.oq = cq; 146 | self.ogain = cgain; 147 | } 148 | 149 | let mut order = order.i() as u8; 150 | if ftype == 1 { 151 | // The resonator just blows up with higher orders. 152 | order = 0; 153 | } 154 | 155 | for frame in 0..ctx.nframes() { 156 | // let freq = denorm::BiqFilt::freq(freq, frame); 157 | // let freq = freq.clamp($minfreq, $maxfreq); 158 | // let q = denorm::BiqFilt::q(q, frame); 159 | // let gain = denorm::BiqFilt::gain(gain, frame); 160 | 161 | let mut s = inp.read(frame); 162 | for i in 0..=order { 163 | s = self.cascade[i as usize].tick(s); 164 | } 165 | 166 | out.write(frame, s); 167 | } 168 | 169 | ctx_vals[0].set(out.read(ctx.nframes() - 1)); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/dsp/node_code.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | #[cfg(feature = "synfx-dsp-jit")] 10 | use synfx_dsp_jit::engine::CodeEngineBackend; 11 | 12 | //use crate::dsp::MAX_BLOCK_SIZE; 13 | 14 | /// A WBlockDSP code execution node for JIT'ed DSP code 15 | pub struct Code { 16 | #[cfg(feature = "synfx-dsp-jit")] 17 | backend: Option, 18 | srate: f64, 19 | } 20 | 21 | impl std::fmt::Debug for Code { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 23 | write!(f, "Code") 24 | } 25 | } 26 | 27 | impl Clone for Code { 28 | fn clone(&self) -> Self { 29 | Self { 30 | #[cfg(feature = "synfx-dsp-jit")] 31 | backend: None, 32 | srate: self.srate, 33 | } 34 | } 35 | } 36 | 37 | impl Code { 38 | pub fn new(nid: &NodeId, node_global: &NodeGlobalRef) -> Self { 39 | let backend = if let Ok(mut handle) = node_global.lock() { 40 | #[cfg(feature = "synfx-dsp-jit")] 41 | { 42 | Some(handle.get_code_engine_backend(nid.instance() as usize)) 43 | } 44 | #[cfg(not(feature = "synfx-dsp-jit"))] 45 | { 46 | None 47 | } 48 | } else { 49 | None 50 | }; 51 | 52 | Self { 53 | #[cfg(feature = "synfx-dsp-jit")] 54 | backend, 55 | srate: 48000.0, 56 | } 57 | } 58 | 59 | pub const in1: &'static str = "Input Signal 1"; 60 | pub const in2: &'static str = "Input Signal 2"; 61 | pub const alpha: &'static str = "Input Parameter Alpha"; 62 | pub const beta: &'static str = "Input Parameter Beta"; 63 | pub const delta: &'static str = "Input Parameter Delta"; 64 | pub const gamma: &'static str = "Input Parameter Gamma"; 65 | pub const sig: &'static str = "Return output"; 66 | pub const sig1: &'static str = "Signal channel 1 output"; 67 | pub const sig2: &'static str = "Signal channel 2 output"; 68 | 69 | pub const DESC: &'static str = "WBlockDSP Code Execution\n\n\ 70 | This node executes just in time compiled code as fast as machine code. \ 71 | Use this to implement real time DSP code yourself. The inputs are freely \ 72 | useable in your code. All the ports (input and output) can be used either \ 73 | for audio or for control signals."; 74 | pub const HELP: &'static str = r#"WBlockDSP Code Execution 75 | 76 | This node executes just in time compiled code as fast as machine code. 77 | Use this to implement real time DSP code yourself. The inputs are freely 78 | useable in your code. All the ports (input and output) can be used either 79 | for audio or for control signals. 80 | 81 | The inputs ~~in1~~ and ~~in2~~ are thought to be a stereo signal input. But 82 | you are free to repurpose them as you like. 83 | 84 | The inputs ~~alpha~~, ~~beta~~, ~~delta~~ and ~~gamma~~ can be used as parameters 85 | in your code. But are also not restricted, so you may use them as audio signal 86 | inputs. 87 | 88 | The outputs ~~sig~~, ~~sig1~~ and ~~sig3~~ are also freely useable. 89 | 90 | Some ideas how to use this, you can build your own: 91 | 92 | - Waveshapers 93 | - Signal Generators (Oscillators) 94 | - Custom LFO 95 | - Control Signal shapers or generators 96 | - Sequencers 97 | - ... and many more things! 98 | "#; 99 | 100 | pub fn graph_fun() -> Option { 101 | None 102 | } 103 | } 104 | 105 | impl DspNode for Code { 106 | fn set_sample_rate(&mut self, srate: f32) { 107 | self.srate = srate as f64; 108 | #[cfg(feature = "synfx-dsp-jit")] 109 | if let Some(backend) = self.backend.as_mut() { 110 | backend.set_sample_rate(srate); 111 | } 112 | } 113 | 114 | fn reset(&mut self) { 115 | #[cfg(feature = "synfx-dsp-jit")] 116 | if let Some(backend) = self.backend.as_mut() { 117 | backend.clear(); 118 | } 119 | } 120 | 121 | #[inline] 122 | fn process( 123 | &mut self, 124 | ctx: &mut dyn NodeAudioContext, 125 | _ectx: &mut NodeExecContext, 126 | _nctx: &NodeContext, 127 | _atoms: &[SAtom], 128 | inputs: &[ProcBuf], 129 | outputs: &mut [ProcBuf], 130 | ctx_vals: LedPhaseVals, 131 | ) { 132 | use crate::dsp::{inp, out_idx}; 133 | let in1 = inp::Code::in1(inputs); 134 | let in2 = inp::Code::in2(inputs); 135 | let a = inp::Code::alpha(inputs); 136 | let b = inp::Code::beta(inputs); 137 | let d = inp::Code::delta(inputs); 138 | let g = inp::Code::gamma(inputs); 139 | let out_i = out_idx::Code::sig1(); 140 | 141 | let (sig, sig1) = outputs.split_at_mut(out_i); 142 | let (sig1, sig2) = sig1.split_at_mut(1); 143 | let sig = &mut sig[0]; 144 | let sig1 = &mut sig1[0]; 145 | let sig2 = &mut sig2[0]; 146 | 147 | #[cfg(feature = "synfx-dsp-jit")] 148 | { 149 | let backend = if let Some(backend) = &mut self.backend { 150 | backend 151 | } else { 152 | return; 153 | }; 154 | 155 | backend.process_updates(); 156 | 157 | let mut ret = 0.0; 158 | let mut s1 = 0.0; 159 | #[allow(unused_assignments)] 160 | let mut s2 = 0.0; 161 | for frame in 0..ctx.nframes() { 162 | (s1, s2, ret) = backend.process( 163 | in1.read(frame), 164 | in2.read(frame), 165 | a.read(frame), 166 | b.read(frame), 167 | d.read(frame), 168 | g.read(frame), 169 | ); 170 | sig.write(frame, ret); 171 | sig1.write(frame, s1); 172 | sig2.write(frame, s2); 173 | } 174 | 175 | ctx_vals[0].set(ret); 176 | ctx_vals[1].set(s1); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/dsp/node_comb.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp; 10 | 11 | #[macro_export] 12 | macro_rules! fa_comb_mode { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | let s = match ($v.round() as usize) { 15 | 0 => "FedBack", 16 | 1 => "FedForw", 17 | _ => "?", 18 | }; 19 | write!($formatter, "{}", s) 20 | }}; 21 | } 22 | 23 | /// A simple amplifier 24 | #[derive(Debug, Clone)] 25 | pub struct Comb { 26 | comb: Box, 27 | } 28 | 29 | impl Comb { 30 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 31 | Self { comb: Box::new(synfx_dsp::Comb::new()) } 32 | } 33 | 34 | pub const inp: &'static str = "The signal input for the comb filter."; 35 | pub const g: &'static str = 36 | "The internal factor for the comb filter. Be careful with high ~~g~~ \ 37 | values (> **0.75**) in feedback mode, you will probably have to attenuate \ 38 | the output a bit yourself."; 39 | pub const time: &'static str = "The comb delay time."; 40 | pub const sig: &'static str = "The output of comb filter."; 41 | pub const mode: &'static str = "The mode of the comb filter, whether it's a \ 42 | feedback or feedforward comb filter."; 43 | pub const DESC: &'static str = r#"Comb Filter 44 | 45 | A very simple comb filter. It has interesting filtering effects 46 | and can also be used to build custom reverbs. 47 | "#; 48 | pub const HELP: &'static str = r#"A Simple Comb Filter 49 | 50 | This is a comb filter that can be used for filtering 51 | as well as for building reverbs or anything you might 52 | find it useful for. 53 | 54 | Attention: Be careful with high ~~g~~ values, you might need to 55 | attenuate the output manually for the feedback combfilter mode, 56 | because the feedback adds up quickly. 57 | 58 | For typical arrangements in combination with allpass filters, 59 | see the documentation of the `AllP` node! 60 | "#; 61 | 62 | pub fn graph_fun() -> Option { 63 | None 64 | } 65 | } 66 | 67 | impl DspNode for Comb { 68 | fn set_sample_rate(&mut self, srate: f32) { 69 | self.comb.set_sample_rate(srate); 70 | } 71 | 72 | fn reset(&mut self) { 73 | self.comb.reset(); 74 | } 75 | 76 | #[inline] 77 | fn process( 78 | &mut self, 79 | ctx: &mut dyn NodeAudioContext, 80 | _ectx: &mut NodeExecContext, 81 | _nctx: &NodeContext, 82 | atoms: &[SAtom], 83 | inputs: &[ProcBuf], 84 | outputs: &mut [ProcBuf], 85 | ctx_vals: LedPhaseVals, 86 | ) { 87 | use crate::dsp::{at, denorm, inp, out}; 88 | 89 | let inp = inp::Comb::inp(inputs); 90 | let time = inp::Comb::time(inputs); 91 | let g = inp::Comb::g(inputs); 92 | let out = out::Comb::sig(outputs); 93 | let mode = at::Comb::mode(atoms); 94 | 95 | let c = &mut *self.comb; 96 | 97 | if mode.i() == 0 { 98 | for frame in 0..ctx.nframes() { 99 | let v = inp.read(frame); 100 | 101 | out.write( 102 | frame, 103 | c.next_feedback(denorm::Comb::time(time, frame), denorm::Comb::g(g, frame), v), 104 | ); 105 | } 106 | } else { 107 | for frame in 0..ctx.nframes() { 108 | let v = inp.read(frame); 109 | 110 | out.write( 111 | frame, 112 | c.next_feedforward( 113 | denorm::Comb::time(time, frame), 114 | denorm::Comb::g(g, frame), 115 | v, 116 | ), 117 | ); 118 | } 119 | } 120 | 121 | let last_frame = ctx.nframes() - 1; 122 | ctx_vals[0].set(out.read(last_frame)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/dsp/node_cqnt.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp::{ChangeTrig, CtrlPitchQuantizer}; 10 | 11 | #[macro_export] 12 | macro_rules! fa_cqnt { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | write!($formatter, "?") 15 | }}; 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! fa_cqnt_omin { 20 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 21 | let s = match ($v.round() as usize) { 22 | 0 => "-0", 23 | 1 => "-1", 24 | 2 => "-2", 25 | 3 => "-3", 26 | 4 => "-4", 27 | _ => "?", 28 | }; 29 | write!($formatter, "{}", s) 30 | }}; 31 | } 32 | 33 | #[macro_export] 34 | macro_rules! fa_cqnt_omax { 35 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 36 | let s = match ($v.round() as usize) { 37 | 0 => "+0", 38 | 1 => "+1", 39 | 2 => "+2", 40 | 3 => "+3", 41 | 4 => "+4", 42 | _ => "?", 43 | }; 44 | write!($formatter, "{}", s) 45 | }}; 46 | } 47 | 48 | /// A control signal to pitch quantizer/converter 49 | #[derive(Debug, Clone)] 50 | pub struct CQnt { 51 | quant: Box, 52 | change_trig: ChangeTrig, 53 | } 54 | 55 | impl CQnt { 56 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 57 | Self { quant: Box::new(CtrlPitchQuantizer::new()), change_trig: ChangeTrig::new() } 58 | } 59 | pub const inp: &'static str = "The unipolar input signal that is to be mapped to the \ 60 | selected pitch range."; 61 | pub const oct: &'static str = "The octave offset from A4."; 62 | pub const omin: &'static str = "The minimum octave of the range. If **0** it will be ~~oct~~."; 63 | pub const omax: &'static str = "The maximum octave of the range. If **0** it will be ~~oct~~."; 64 | pub const sig: &'static str = "The output pitch signal."; 65 | pub const t: &'static str = "Everytime the quantizer snaps to a new pitch, it will \ 66 | emit a short trigger on this signal output. This is useful \ 67 | to trigger for example an envelope."; 68 | pub const keys: &'static str = "Here you can select the individual notes of the range. \ 69 | If no note is selected, it's the same as if all notes were selected."; 70 | pub const DESC: &'static str = r#"Ctrl Pitch Quantizer 71 | 72 | This special quantizer maps the unipolar **0**..**1** control signal 73 | input range on ~~inp~~ evenly to the selected keys and octaves. 74 | "#; 75 | pub const HELP: &'static str = r#"A control signal to pitch quantizer 76 | 77 | This is a specialized control signal quantizer to generate a pitch/frequency 78 | from a signal within the **0**..**1** range. It does not quantize a typical **-1**..**1** 79 | frequency signal like the `Quant` node. 80 | 81 | In contrast to `Quant`, this quantizer maps the incoming signal evenly 82 | to the available note range. It will result in more evenly played notes 83 | if you sweep across the input signal range. 84 | "#; 85 | 86 | pub fn graph_fun() -> Option { 87 | None 88 | } 89 | } 90 | 91 | impl DspNode for CQnt { 92 | fn set_sample_rate(&mut self, srate: f32) { 93 | self.change_trig.set_sample_rate(srate); 94 | } 95 | 96 | fn reset(&mut self) { 97 | self.change_trig.reset(); 98 | } 99 | 100 | #[inline] 101 | fn process( 102 | &mut self, 103 | ctx: &mut dyn NodeAudioContext, 104 | _ectx: &mut NodeExecContext, 105 | _nctx: &NodeContext, 106 | atoms: &[SAtom], 107 | inputs: &[ProcBuf], 108 | outputs: &mut [ProcBuf], 109 | ctx_vals: LedPhaseVals, 110 | ) { 111 | use crate::dsp::{at, denorm, inp, out_buf}; 112 | 113 | let inp = inp::CQnt::inp(inputs); 114 | let oct = inp::CQnt::oct(inputs); 115 | let mut out = out_buf::CQnt::sig(outputs); 116 | let mut t = out_buf::CQnt::t(outputs); 117 | let keys = at::CQnt::keys(atoms); 118 | let omin = at::CQnt::omin(atoms); 119 | let omax = at::CQnt::omax(atoms); 120 | 121 | self.quant.update_keys(keys.i(), omin.i(), omax.i()); 122 | 123 | for frame in 0..ctx.nframes() { 124 | let pitch = self.quant.signal_to_pitch(denorm::CQnt::inp(inp, frame)); 125 | 126 | t.write(frame, self.change_trig.next(pitch)); 127 | out.write(frame, pitch + denorm::CQnt::oct(oct, frame)); 128 | } 129 | 130 | let last_pitch = self.quant.last_key_pitch(); 131 | ctx_vals[1].set(last_pitch * 10.0 + 0.0001); 132 | ctx_vals[0].set((last_pitch * 10.0 - 0.5) * 2.0); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/dsp/node_delay.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp::{crossfade, DelayBuffer, TriggerSampleClock}; 10 | 11 | #[macro_export] 12 | macro_rules! fa_delay_mode { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | let s = match ($v.round() as usize) { 15 | 0 => "Time", 16 | 1 => "Sync", 17 | _ => "?", 18 | }; 19 | write!($formatter, "{}", s) 20 | }}; 21 | } 22 | 23 | /// A simple amplifier 24 | #[derive(Debug, Clone)] 25 | pub struct Delay { 26 | buffer: Box>, 27 | clock: TriggerSampleClock, 28 | } 29 | 30 | impl Delay { 31 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 32 | Self { buffer: Box::new(DelayBuffer::new()), clock: TriggerSampleClock::new() } 33 | } 34 | 35 | pub const inp: &'static str = "The signal input for the delay. You can mix in this \ 36 | input to the output with the ~~mix~~ parameter."; 37 | pub const trig: &'static str = "If you set ~~mode~~ to **Sync** the delay time will be \ 38 | synchronized to the trigger signals received on this input."; 39 | pub const time: &'static str = "The delay time. It can be freely modulated to your \ 40 | likings."; 41 | pub const fb: &'static str = "The feedback amount of the delay output to it's input. \ 42 | "; 43 | pub const mix: &'static str = "The dry/wet mix of the delay."; 44 | pub const mode: &'static str = "Allows different operating modes of the delay. \ 45 | **Time** is the default, and means that the ~~time~~ input \ 46 | specifies the delay time. **Sync** will synchronize the delay time \ 47 | with the trigger signals on the ~~trig~~ input."; 48 | pub const sig: &'static str = "The output of the dry/wet mix."; 49 | 50 | pub const DESC: &'static str = r#"Simple Delay Line 51 | 52 | This is a very simple single buffer delay node. 53 | It provides an internal feedback and dry/wet mix. 54 | "#; 55 | pub const HELP: &'static str = r#"A Simple Delay Line 56 | 57 | This node provides a very simple delay line with the bare minimum of 58 | parameters. Most importantly a freely modulateable ~~time~~ parameter 59 | and a feedback ~~fb~~ parameter. 60 | 61 | Via the ~~mix~~ parameter you can mix in the input signal to the output. 62 | 63 | You can use this node to delay any kind of signal, from a simple control 64 | signal to an audio signal. 65 | 66 | For other kinds of delay/feedback please see also the `FbWr`/`FbRd` nodes. 67 | "#; 68 | 69 | pub fn graph_fun() -> Option { 70 | None 71 | } 72 | } 73 | 74 | impl DspNode for Delay { 75 | fn set_sample_rate(&mut self, srate: f32) { 76 | self.buffer.set_sample_rate(srate); 77 | } 78 | 79 | fn reset(&mut self) { 80 | self.buffer.reset(); 81 | self.clock.reset(); 82 | } 83 | 84 | #[inline] 85 | fn process( 86 | &mut self, 87 | ctx: &mut dyn NodeAudioContext, 88 | _ectx: &mut NodeExecContext, 89 | _nctx: &NodeContext, 90 | atoms: &[SAtom], 91 | inputs: &[ProcBuf], 92 | outputs: &mut [ProcBuf], 93 | ctx_vals: LedPhaseVals, 94 | ) { 95 | use crate::dsp::{at, denorm, inp, out}; 96 | 97 | let buffer = &mut *self.buffer; 98 | 99 | let mode = at::Delay::mode(atoms); 100 | let inp = inp::Delay::inp(inputs); 101 | let trig = inp::Delay::trig(inputs); 102 | let time = inp::Delay::time(inputs); 103 | let fb = inp::Delay::fb(inputs); 104 | let mix = inp::Delay::mix(inputs); 105 | let out = out::Delay::sig(outputs); 106 | 107 | if mode.i() == 0 { 108 | for frame in 0..ctx.nframes() { 109 | let dry = inp.read(frame); 110 | 111 | let out_sample = buffer.cubic_interpolate_at(denorm::Delay::time(time, frame)); 112 | 113 | buffer.feed(dry + out_sample * denorm::Delay::fb(fb, frame)); 114 | 115 | out.write( 116 | frame, 117 | crossfade(dry, out_sample, denorm::Delay::mix(mix, frame).clamp(0.0, 1.0)), 118 | ); 119 | } 120 | } else { 121 | for frame in 0..ctx.nframes() { 122 | let dry = inp.read(frame); 123 | 124 | let clock_samples = self.clock.next(denorm::Delay::trig(trig, frame)); 125 | let out_sample = buffer.at(clock_samples as usize); 126 | 127 | buffer.feed(dry + out_sample * denorm::Delay::fb(fb, frame)); 128 | 129 | out.write( 130 | frame, 131 | crossfade(dry, out_sample, denorm::Delay::mix(mix, frame).clamp(0.0, 1.0)), 132 | ); 133 | } 134 | } 135 | 136 | let last_frame = ctx.nframes() - 1; 137 | ctx_vals[0].set(out.read(last_frame)); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/dsp/node_ext.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | denorm, inp, out_idx, DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, 7 | ProcBuf, SAtom, 8 | }; 9 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 10 | use synfx_dsp::SlewValue; 11 | 12 | macro_rules! define_ext { 13 | ($name: ident, $p1: ident, $p2: ident, $p3: ident) => { 14 | #[derive(Debug, Clone)] 15 | pub struct $name { 16 | slew1: SlewValue, 17 | slew2: SlewValue, 18 | slew3: SlewValue, 19 | } 20 | 21 | impl $name { 22 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 23 | Self { slew1: SlewValue::new(), slew2: SlewValue::new(), slew3: SlewValue::new() } 24 | } 25 | 26 | pub const slew: &'static str = "Slew limiter for the 3 parameters"; 27 | pub const atv1: &'static str = "Attenuverter for the A1 parameter"; 28 | pub const atv2: &'static str = "Attenuverter for the A2 parameter"; 29 | pub const atv3: &'static str = "Attenuverter for the A3 parameter"; 30 | 31 | pub const sig1: &'static str = "A-F1 output channel"; 32 | pub const sig2: &'static str = "A-F2 output channel"; 33 | pub const sig3: &'static str = "A-F3 output channel"; 34 | 35 | pub const DESC: &'static str = "Ext. Param. Set A-F Input\n\n\ 36 | This node gives access to the 24 input parameters of the HexoSynth VST3/CLAP plugin. \ 37 | A ~~slew~~ limiter allows you to smooth out quick changes a bit if you need it. \ 38 | Attenuverters (attenuators that can also invert) allow to reduce the amplitude \ 39 | or invert the signal."; 40 | pub const HELP: &'static str = r#"External Parameter Set A-F Input 41 | 42 | This node gives access to the 24 input parameters of the 43 | HexoSynth VST3/CLAP plugin. A ~~slew~~ limiter allows you to smooth out quick 44 | changes a bit if you need it. Attenuverters (attenuators that can also invert) 45 | allow to reduce the amplitude or invert the signal. 46 | 47 | All instances of the nodes `ExtA`, `ExtB`, ..., `ExtF` have access to the same 48 | 3 input parameters (~~A1~~-~~A3~~, ~~B1~~-~~B3~~, ..., ~~F1~~-~~F3~~). That means there is no 49 | difference whether you use the same instance of different ones. 50 | Except that you can of course set the ~~atv~~ and ~~slew~~ parameters to different 51 | values. 52 | 53 | If you absolutely need more parameters to control the HexoSynth patch: 54 | Keep in mind, that there is also the `MidiCC` node, that allows HexoSynth to 55 | react to MIDI CC messages. 56 | "#; 57 | 58 | pub fn graph_fun() -> Option { None } 59 | } 60 | 61 | impl DspNode for $name { 62 | fn set_sample_rate(&mut self, _srate: f32) {} 63 | fn reset(&mut self) {} 64 | 65 | #[inline] 66 | fn process( 67 | &mut self, 68 | ctx: &mut dyn NodeAudioContext, 69 | ectx: &mut NodeExecContext, 70 | _nctx: &NodeContext, 71 | _atoms: &[SAtom], 72 | inputs: &[ProcBuf], 73 | outputs: &mut [ProcBuf], 74 | ctx_vals: LedPhaseVals, 75 | ) { 76 | let slew = inp::$name::slew(inputs); 77 | let atv1 = inp::$name::atv1(inputs); 78 | let atv2 = inp::$name::atv2(inputs); 79 | let atv3 = inp::$name::atv3(inputs); 80 | let sig2_i = out_idx::$name::sig2(); 81 | let (sig1, r) = outputs.split_at_mut(sig2_i); 82 | let (sig2, sig3) = r.split_at_mut(1); 83 | let sig1 = &mut sig1[0]; 84 | let sig2 = &mut sig2[0]; 85 | let sig3 = &mut sig3[0]; 86 | 87 | if let Some(params) = &ectx.ext_param { 88 | let p1 = params.$p1(); 89 | let p2 = params.$p2(); 90 | let p3 = params.$p3(); 91 | 92 | for frame in 0..ctx.nframes() { 93 | let slew_ms = denorm::$name::slew(slew, frame); 94 | sig1.write( 95 | frame, 96 | denorm::$name::atv1(atv1, frame) * self.slew1.next(p1, slew_ms), 97 | ); 98 | sig2.write( 99 | frame, 100 | denorm::$name::atv2(atv2, frame) * self.slew2.next(p2, slew_ms), 101 | ); 102 | sig3.write( 103 | frame, 104 | denorm::$name::atv3(atv3, frame) * self.slew3.next(p3, slew_ms), 105 | ); 106 | } 107 | } 108 | 109 | let last_frame = ctx.nframes() - 1; 110 | ctx_vals[0].set(sig1.read(last_frame)); 111 | } 112 | } 113 | 114 | } 115 | } 116 | 117 | define_ext! {ExtA, a1, a2, a3} 118 | define_ext! {ExtB, b1, b2, b3} 119 | define_ext! {ExtC, c1, c2, c3} 120 | define_ext! {ExtD, d1, d2, d3} 121 | define_ext! {ExtE, e1, e2, e3} 122 | define_ext! {ExtF, f1, f2, f3} 123 | -------------------------------------------------------------------------------- /src/dsp/node_fbwr_fbrd.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use crate::{SharedFeedback, SharedFeedbackReader, SharedFeedbackWriter}; 10 | 11 | /// A simple amplifier 12 | #[derive(Debug, Clone)] 13 | pub struct FbWr { 14 | fb_wr: Box, 15 | } 16 | 17 | impl FbWr { 18 | pub fn new(nid: &NodeId, node_global: &NodeGlobalRef) -> Self { 19 | let fb_wr = if let Ok(mut node_global) = node_global.lock() { 20 | node_global.get_feedback_writer(nid.instance() as usize) 21 | } else { 22 | // If we can't get the lock, other issues are active and I would 23 | // rather not crash, so I just make a dummy feedback buffer: 24 | let sfb = SharedFeedback::new(44100.0); 25 | Box::new(SharedFeedbackWriter::new(&sfb)) 26 | }; 27 | Self { fb_wr } 28 | } 29 | pub const inp: &'static str = "Signal input"; 30 | 31 | pub const DESC: &'static str = "Feedback Delay Writer\n\n\ 32 | HexoSynth does not allow direct feedback cycles in it's graph.\n\ 33 | To make feedback possible anyways the `FbWr` and `FbRd` nodes are provided.\n\ 34 | This node allows you to write a signal into the corresponsing signal delay buffer.\n\ 35 | Use `FbRd` for using the signal. The delay is **3.14ms**."; 36 | pub const HELP: &'static str = r#"Feedback Delay Writer 37 | 38 | HexoSynth does not allow direct feedback cycles in it's graph. 39 | To make feedback possible anyways the `FbWr` and `FbRd` nodes are provided. 40 | This node allows you to send a signal into the corresponding `FbWr` signal 41 | delay. 42 | 43 | The instance id of the node defines which `FbWr` and `FbRd` are connected. 44 | That means `FbRd 0` is connected to the corresponding `FbWr 0`. You can use 45 | the signal multiple times by connecting the `FbRd 0` ~~sig~~ port to multiple 46 | inputs. 47 | 48 | The delay is always **3.14ms**, regardless of the sampling rate the synthesizer 49 | is running at. 50 | "#; 51 | 52 | pub fn graph_fun() -> Option { 53 | None 54 | } 55 | } 56 | 57 | impl DspNode for FbWr { 58 | fn set_sample_rate(&mut self, _srate: f32) {} 59 | fn reset(&mut self) {} 60 | 61 | #[inline] 62 | fn process( 63 | &mut self, 64 | ctx: &mut dyn NodeAudioContext, 65 | _ectx: &mut NodeExecContext, 66 | _nctx: &NodeContext, 67 | _atoms: &[SAtom], 68 | inputs: &[ProcBuf], 69 | _outputs: &mut [ProcBuf], 70 | ctx_vals: LedPhaseVals, 71 | ) { 72 | use crate::dsp::inp; 73 | 74 | let inp = inp::FbWr::inp(inputs); 75 | 76 | for frame in 0..ctx.nframes() { 77 | self.fb_wr.write(inp.read(frame)); 78 | } 79 | 80 | ctx_vals[0].set(inp.read(ctx.nframes() - 1)); 81 | } 82 | } 83 | 84 | /// A simple amplifier 85 | #[derive(Debug, Clone)] 86 | pub struct FbRd { 87 | fb_rd: Box, 88 | } 89 | 90 | impl FbRd { 91 | pub fn new(nid: &NodeId, node_global: &NodeGlobalRef) -> Self { 92 | let fb_rd = if let Ok(mut node_global) = node_global.lock() { 93 | node_global.get_feedback_reader(nid.instance() as usize) 94 | } else { 95 | // If we can't get the lock, other issues are active and I would 96 | // rather not crash, so I just make a dummy feedback buffer: 97 | let sfb = SharedFeedback::new(44100.0); 98 | Box::new(SharedFeedbackReader::new(&sfb)) 99 | }; 100 | Self { fb_rd } 101 | } 102 | pub const vol: &'static str = "Volume of the input.\n\ 103 | Use this to adjust the feedback amount."; 104 | pub const sig: &'static str = "Feedback signal output."; 105 | 106 | pub const DESC: &'static str = "Feedback Delay Reader\n\n\ 107 | HexoSynth does not allow direct feedback cycles in it's graph.\n\ 108 | To make feedback possible anyways the `FbWr` and `FbRd` nodes are provided.\n\ 109 | This node allows you to tap into the corresponding `FbWr` signal delay \ 110 | for feedback. The delay is **3.14ms**."; 111 | pub const HELP: &'static str = r#"Feedback Delay Reader 112 | 113 | HexoSynth does not allow direct feedback cycles in it's graph. 114 | To make feedback possible anyways the `FbWr` and `FbRd` nodes are provided. 115 | This node allows you to tap into the corresponding `FbWr` signal delay for 116 | feedback. 117 | 118 | The instance id of the node defines which `FbWr` and `FbRd` are connected. 119 | That means `FbRd 0` is connected to the corresponding `FbWr 0`. You can use 120 | the signal multiple times by connecting the `FbRd 0` ~~sig~~ port to multiple 121 | inputs. 122 | 123 | The delay is always **3.14ms**, regardless of the sampling rate the synthesizer 124 | is running at. 125 | 126 | The ~~vol~~ parameter is a convenience parameter to allow to control the 127 | volume of the feedback. 128 | "#; 129 | 130 | pub fn graph_fun() -> Option { 131 | None 132 | } 133 | } 134 | 135 | impl DspNode for FbRd { 136 | fn set_sample_rate(&mut self, _srate: f32) {} 137 | fn reset(&mut self) {} 138 | 139 | #[inline] 140 | fn process( 141 | &mut self, 142 | ctx: &mut dyn NodeAudioContext, 143 | _ectx: &mut NodeExecContext, 144 | _nctx: &NodeContext, 145 | _atoms: &[SAtom], 146 | inputs: &[ProcBuf], 147 | outputs: &mut [ProcBuf], 148 | ctx_vals: LedPhaseVals, 149 | ) { 150 | use crate::dsp::{denorm, inp, out}; 151 | 152 | let vol = inp::FbRd::vol(inputs); 153 | let sig = out::FbRd::sig(outputs); 154 | 155 | let mut last_val = 0.0; 156 | for frame in 0..ctx.nframes() { 157 | last_val = self.fb_rd.read(); 158 | last_val *= denorm::FbRd::vol(vol, frame); 159 | sig.write(frame, last_val); 160 | } 161 | 162 | ctx_vals[0].set(last_val); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/dsp/node_formfm.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Dimas Leenman 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | 10 | /// A simple amplifier 11 | #[derive(Debug, Clone)] 12 | pub struct FormFM { 13 | inv_sample_rate: f32, 14 | phase: f32, 15 | } 16 | 17 | impl FormFM { 18 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 19 | Self { inv_sample_rate: 1.0 / 44100.0, phase: 0.0 } 20 | } 21 | pub const freq: &'static str = "Base frequency to oscillate at\n"; 22 | pub const det: &'static str = "Detune the oscillator in semitones and cents.\n"; 23 | pub const form: &'static str = 24 | "Frequency of the formant\nThis affects how much lower or higher tones the sound has."; 25 | pub const side: &'static str = 26 | "Which side the peak of the wave is. Values more towards **0.0** or **1.0** make the base frequency more pronounced"; 27 | pub const peak: &'static str = 28 | "How high the peak amplitude is. Lower values make the effect more pronounced"; 29 | pub const sig: &'static str = "Generated formant signal"; 30 | pub const DESC: &'static str = r#"Formant oscillator 31 | 32 | Simple formant oscillator that generates a formant like sound. 33 | Loosely based on the ModFM synthesis method. 34 | "#; 35 | pub const HELP: &'static str = r#"Direct formant synthesizer 36 | 37 | This is a formant synthesizer that directly generates 38 | the audio of a single formant. 39 | 40 | This can be seen as passing a saw wave with frequency ~~freq~~ 41 | into a bandpass filter with the cutoff at ~~form~~ 42 | 43 | ~~freq~~ controls the base frequency of the formant. 44 | ~~form~~ controls the formant frequency. Lower values give more bass to the sound, 45 | and higher values give the high frequencies more sound. 46 | 47 | ~~side~~ controls where the peak of the carrier wave is, 48 | and in turn controls the bandwidth of the effect. The more towards **0.0** or **1.0**, 49 | the more the formant is audible. 50 | 51 | ~~peak~~ controls how high the peak of the carrier wave is. 52 | This also controls the bandwidth of the effect, where lower means a higher 53 | bandwidth, and thus more audible formant. 54 | "#; 55 | 56 | pub fn graph_fun() -> Option { 57 | None 58 | } 59 | } 60 | 61 | impl DspNode for FormFM { 62 | fn set_sample_rate(&mut self, srate: f32) { 63 | self.inv_sample_rate = 1.0 / srate; 64 | } 65 | 66 | fn reset(&mut self) { 67 | self.phase = 0.0; 68 | } 69 | 70 | #[inline] 71 | fn process( 72 | &mut self, 73 | ctx: &mut dyn NodeAudioContext, 74 | _ectx: &mut NodeExecContext, 75 | _nctx: &NodeContext, 76 | _atoms: &[SAtom], 77 | inputs: &[ProcBuf], 78 | outputs: &mut [ProcBuf], 79 | _ctx_vals: LedPhaseVals, 80 | ) { 81 | use crate::dsp::{denorm, denorm_offs, inp, out}; 82 | 83 | let base_freq = inp::FormFM::freq(inputs); 84 | let det = inp::FormFM::det(inputs); 85 | let formant_freq = inp::FormFM::form(inputs); 86 | let side_val = inp::FormFM::side(inputs); 87 | let peak_val = inp::FormFM::peak(inputs); 88 | let out = out::FormFM::sig(outputs); 89 | 90 | for frame in 0..ctx.nframes() { 91 | // get the inputs 92 | let base_freq = denorm_offs::FormFM::freq(base_freq, det.read(frame), frame); 93 | let formant_freq = denorm::FormFM::form(formant_freq, frame); 94 | let side_val = denorm::FormFM::side(side_val, frame).min(1.0 - 1e-6).max(1e-6); 95 | let peak_val = denorm::FormFM::peak(peak_val, frame); 96 | 97 | // make a triangle wave, with the peak at carrier center 98 | let carrier_base = (self.phase / side_val).min((1.0 - self.phase) / (1.0 - side_val)); 99 | 100 | // smoothstep 101 | let carrier = 1.0 102 | - ((1.0 - peak_val) * (carrier_base * carrier_base * (3.0 - 2.0 * carrier_base))); 103 | 104 | // multiple of the frequency the modulators are at 105 | let multiple = formant_freq / base_freq.max(1e-6); 106 | 107 | // round them to the closest integer of the formant freq 108 | let freq_a = multiple.floor(); 109 | let freq_b = freq_a + 1.0; 110 | 111 | // and how much to lerp between them 112 | let blend = multiple.fract(); 113 | 114 | // get the true modulator 115 | let modulator = (1.0 - blend) 116 | * if multiple < 1.0 { 117 | 0.0 118 | } else { 119 | (std::f32::consts::TAU * self.phase * freq_a).cos() 120 | } 121 | + blend * (std::f32::consts::TAU * self.phase * freq_b).cos(); 122 | 123 | // entire wave 124 | let wave = carrier * modulator; 125 | 126 | // increment phase (very imporant) 127 | self.phase += base_freq * self.inv_sample_rate; 128 | 129 | // wrap around 130 | self.phase = self.phase.fract(); 131 | 132 | out.write(frame, wave); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/dsp/node_inp.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | denorm, inp, out_idx, DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, 7 | ProcBuf, SAtom, 8 | }; 9 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 10 | 11 | /// The (stereo) input port of the plugin 12 | #[derive(Debug, Clone)] 13 | pub struct Inp {} 14 | 15 | impl Inp { 16 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 17 | Self {} 18 | } 19 | 20 | pub const vol: &'static str = 21 | "The volume of the two plugin input ports, applied to all channels. \ 22 | Please note that this is a linear control, to prevent inaccuracies for **1.0**. \ 23 | "; 24 | pub const sig1: &'static str = "Audio input channel 1 (left)"; 25 | pub const sig2: &'static str = "Audio input channel 2 (right)"; 26 | 27 | pub const DESC: &'static str = "Audio Input Port\n\n\ 28 | This node gives you access to the two input ports of the HexoSynth plugin. \ 29 | Build effects or what ever you can imagine with this! 30 | "; 31 | pub const HELP: &'static str = r#"Audio Input Port 32 | 33 | This node gives you access to the two input ports of the HexoSynth plugin. 34 | You can build an effects plugin with this node and the `Out` node. 35 | Or a synthesizer that reacts to audio rate control signals on these two 36 | input ports. 37 | "#; 38 | 39 | pub fn graph_fun() -> Option { 40 | None 41 | } 42 | } 43 | 44 | impl DspNode for Inp { 45 | fn set_sample_rate(&mut self, _srate: f32) {} 46 | fn reset(&mut self) {} 47 | 48 | #[inline] 49 | fn process( 50 | &mut self, 51 | ctx: &mut dyn NodeAudioContext, 52 | _ectx: &mut NodeExecContext, 53 | _nctx: &NodeContext, 54 | _atoms: &[SAtom], 55 | inputs: &[ProcBuf], 56 | outputs: &mut [ProcBuf], 57 | ctx_vals: LedPhaseVals, 58 | ) { 59 | let vol = inp::Inp::vol(inputs); 60 | 61 | let sig_i = out_idx::Inp::sig2(); 62 | let (sig1, sig2) = outputs.split_at_mut(sig_i); 63 | let sig1 = &mut sig1[0]; 64 | let sig2 = &mut sig2[0]; 65 | 66 | for frame in 0..ctx.nframes() { 67 | let vol = denorm::Inp::vol(vol, frame); 68 | sig1.write(frame, vol * ctx.input(0, frame)); 69 | sig2.write(frame, vol * ctx.input(1, frame)); 70 | } 71 | 72 | let last_val = sig1.read(ctx.nframes() - 1); 73 | ctx_vals[0].set(last_val); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/dsp/node_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | 10 | #[macro_export] 11 | macro_rules! fa_map_clip { 12 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 13 | let s = match ($v.round() as usize) { 14 | 0 => "Off", 15 | 1 => "Clip", 16 | _ => "?", 17 | }; 18 | write!($formatter, "{}", s) 19 | }}; 20 | } 21 | 22 | /// A simple amplifier 23 | #[derive(Debug, Clone)] 24 | pub struct Map {} 25 | 26 | impl Map { 27 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 28 | Self {} 29 | } 30 | pub const inp: &'static str = "Signal input"; 31 | pub const atv: &'static str = 32 | "Input signal attenuverter, to attenuate or invert the input signal."; 33 | pub const offs: &'static str = "Input signal offset after ~~atv~~ has been applied."; 34 | pub const imin: &'static str = "Minimum of the input signal range, \ 35 | it's mapped to the ~~min~~ output signal range."; 36 | pub const imax: &'static str = "Maximum of the input signal range, \ 37 | it's mapped to the ~~max~~ output signal range."; 38 | pub const min: &'static str = "Minimum of the output signal range."; 39 | pub const max: &'static str = "Maximum of the output signal range."; 40 | pub const clip: &'static str = "The ~~clip~~ mode allows you to limit the output \ 41 | exactly to the ~~min~~/~~max~~ range. If this is off, the output \ 42 | may be outside the output signal range if the input signal is \ 43 | outside the input signal range."; 44 | pub const sig: &'static str = "Mapped signal output"; 45 | pub const DESC: &'static str = r#"Range Mapper 46 | 47 | This node allows to map an input signal range to a precise output signal range. 48 | It's mostly useful to map control signals to modulate inputs. 49 | 50 | See also the `SMap` node, which is a simplified version of this node. 51 | "#; 52 | pub const HELP: &'static str = r#"Range Mapper 53 | 54 | This node allows to map an input signal range to a precise output signal 55 | range. It's main use is for precise control of an input of another node. 56 | 57 | It processes the input signal as follows. First the input is attenuverted 58 | using the ~~atv~~ parameter and then the ~~offs~~ offset parameter is added: 59 | 60 | ```text 61 | inp * atv + offs 62 | ``` 63 | 64 | The resulting signal is then processed by the mapping, that maps 65 | the input signal range ~~imin~~/~~imax~~ to the ouput signal range ~~min~~/~~max~~. 66 | 67 | The ~~clip~~ mode allows you to limit the output exactly to the ~~min~~/~~max~~ 68 | range. If this is off, the output may be outside the output signal 69 | range if the input signal is outside the input signal range. 70 | 71 | This can also be used to invert the signal. 72 | 73 | For a more simplified version of this node see also `SMap`. 74 | "#; 75 | 76 | pub fn graph_fun() -> Option { 77 | None 78 | } 79 | } 80 | 81 | impl DspNode for Map { 82 | fn set_sample_rate(&mut self, _srate: f32) {} 83 | fn reset(&mut self) {} 84 | 85 | #[inline] 86 | fn process( 87 | &mut self, 88 | ctx: &mut dyn NodeAudioContext, 89 | _ectx: &mut NodeExecContext, 90 | _nctx: &NodeContext, 91 | atoms: &[SAtom], 92 | inputs: &[ProcBuf], 93 | outputs: &mut [ProcBuf], 94 | ctx_vals: LedPhaseVals, 95 | ) { 96 | use crate::dsp::{at, inp, out}; 97 | 98 | let inp = inp::Map::inp(inputs); 99 | let atv = inp::Map::atv(inputs); 100 | let offs = inp::Map::offs(inputs); 101 | let imin = inp::Map::imin(inputs); 102 | let imax = inp::Map::imax(inputs); 103 | let min = inp::Map::min(inputs); 104 | let max = inp::Map::max(inputs); 105 | let out = out::Map::sig(outputs); 106 | 107 | let clip = at::Map::clip(atoms); 108 | 109 | let mut last_val = 0.0; 110 | 111 | if clip.i() == 0 { 112 | for frame in 0..ctx.nframes() { 113 | let s = (inp.read(frame) * atv.read(frame)) + offs.read(frame); 114 | 115 | let imin = imin.read(frame); 116 | let imax = imax.read(frame); 117 | let min = min.read(frame); 118 | let max = max.read(frame); 119 | 120 | let x = if (imax - imin).abs() < std::f32::EPSILON { 121 | 1.0 122 | } else { 123 | ((s - imin) / (imax - imin)).abs() 124 | }; 125 | last_val = x; 126 | let s = min + (max - min) * x; 127 | 128 | out.write(frame, s); 129 | } 130 | } else { 131 | for frame in 0..ctx.nframes() { 132 | let s = (inp.read(frame) * atv.read(frame)) + offs.read(frame); 133 | 134 | let imin = imin.read(frame); 135 | let imax = imax.read(frame); 136 | let min = min.read(frame); 137 | let max = max.read(frame); 138 | 139 | let x = if (imax - imin).abs() < std::f32::EPSILON { 140 | 1.0 141 | } else { 142 | ((s - imin) / (imax - imin)).abs() 143 | }; 144 | last_val = x; 145 | let s = min + (max - min) * x; 146 | 147 | out.write(frame, if min < max { s.clamp(min, max) } else { s.clamp(max, min) }); 148 | } 149 | } 150 | 151 | ctx_vals[0].set(last_val); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/dsp/node_midicc.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | at, denorm, inp, out_idx, DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, 7 | ProcBuf, SAtom, 8 | }; 9 | use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext}; 10 | use synfx_dsp::SlewValue; 11 | 12 | #[macro_export] 13 | macro_rules! fa_midicc_cc { 14 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 15 | write!($formatter, "{}", $v.round() as usize) 16 | }}; 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct MidiCC { 21 | cur_cc1: f32, 22 | cur_cc2: f32, 23 | cur_cc3: f32, 24 | slew_cc1: SlewValue, 25 | slew_cc2: SlewValue, 26 | slew_cc3: SlewValue, 27 | } 28 | 29 | impl MidiCC { 30 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 31 | Self { 32 | cur_cc1: 0.0, 33 | cur_cc2: 0.0, 34 | cur_cc3: 0.0, 35 | slew_cc1: SlewValue::new(), 36 | slew_cc2: SlewValue::new(), 37 | slew_cc3: SlewValue::new(), 38 | } 39 | } 40 | 41 | pub const chan: &'static str = "MIDI Channel 0 to 15\n"; 42 | pub const slew: &'static str = "Slew limiter for the 3 CCs"; 43 | pub const cc1: &'static str = "MIDI selected CC 1"; 44 | pub const cc2: &'static str = "MIDI selected CC 2"; 45 | pub const cc3: &'static str = "MIDI selected CC 3"; 46 | 47 | pub const sig1: &'static str = "CC output channel 1"; 48 | pub const sig2: &'static str = "CC output channel 2"; 49 | pub const sig3: &'static str = "CC output channel 3"; 50 | 51 | pub const DESC: &'static str = "MIDI CC Input\n\n\ 52 | This node is an input of MIDI CC events/values into the DSP graph. \ 53 | You get 3 CC value outputs: ~~sig1~~, ~~sig2~~ and ~~sig3~~. To set which CC \ 54 | gets which output you have to set the corresponding ~~cc1~~, ~~cc2~~ and \ 55 | ~~cc3~~ parameters."; 56 | pub const HELP: &'static str = r#"MIDI CC Input 57 | 58 | This node is an input of MIDI CC events/values into the DSP graph. 59 | You get 3 CC value outputs: ~~sig1~~, ~~sig2~~ and ~~sig3~~. To set which CC 60 | gets which output you have to set the corresponding ~~cc1~~, ~~cc2~~ and 61 | ~~cc3~~ parameters. 62 | 63 | If the CC values change to rapidly or you hear audible artifacts, you can 64 | try to limit the speed of change with the ~~slew~~ limiter. 65 | 66 | If you need different ~~slew~~ values for the CCs, I recommend creating other 67 | `MidiCC` instances with different ~~slew~~ settings. 68 | "#; 69 | 70 | pub fn graph_fun() -> Option { 71 | None 72 | } 73 | } 74 | 75 | impl DspNode for MidiCC { 76 | fn set_sample_rate(&mut self, _srate: f32) {} 77 | fn reset(&mut self) {} 78 | 79 | #[inline] 80 | fn process( 81 | &mut self, 82 | ctx: &mut dyn NodeAudioContext, 83 | ectx: &mut NodeExecContext, 84 | _nctx: &NodeContext, 85 | atoms: &[SAtom], 86 | inputs: &[ProcBuf], 87 | outputs: &mut [ProcBuf], 88 | ctx_vals: LedPhaseVals, 89 | ) { 90 | let slew = inp::MidiCC::slew(inputs); 91 | let chan = at::MidiCC::chan(atoms); 92 | let cc1 = at::MidiCC::cc1(atoms); 93 | let cc2 = at::MidiCC::cc2(atoms); 94 | let cc3 = at::MidiCC::cc3(atoms); 95 | let sig2_i = out_idx::MidiCC::sig2(); 96 | let (sig1, r) = outputs.split_at_mut(sig2_i); 97 | let (sig2, sig3) = r.split_at_mut(1); 98 | let sig1 = &mut sig1[0]; 99 | let sig2 = &mut sig2[0]; 100 | let sig3 = &mut sig3[0]; 101 | 102 | let midicc_channel = (chan.i() as usize % 16) as u8; 103 | let midicc_cc1 = (cc1.i() as usize % 128) as u8; 104 | let midicc_cc2 = (cc2.i() as usize % 128) as u8; 105 | let midicc_cc3 = (cc3.i() as usize % 128) as u8; 106 | 107 | let mut ptr = MidiEventPointer::new(&ectx.midi_ccs[..]); 108 | 109 | let mut change = false; 110 | 111 | for frame in 0..ctx.nframes() { 112 | let slew_ms = denorm::MidiCC::slew(slew, frame); 113 | 114 | while let Some(ev) = ptr.next_at(frame) { 115 | if let HxMidiEvent::CC { channel, cc, value } = ev { 116 | if channel != midicc_channel { 117 | continue; 118 | } 119 | 120 | if cc == midicc_cc1 { 121 | self.cur_cc1 = value; 122 | change = true; 123 | } else if cc == midicc_cc2 { 124 | self.cur_cc2 = value; 125 | change = true; 126 | } else if cc == midicc_cc3 { 127 | self.cur_cc3 = value; 128 | change = true; 129 | } 130 | } 131 | } 132 | 133 | sig1.write(frame, self.slew_cc1.next(self.cur_cc1, slew_ms)); 134 | sig2.write(frame, self.slew_cc2.next(self.cur_cc2, slew_ms)); 135 | sig3.write(frame, self.slew_cc3.next(self.cur_cc3, slew_ms)); 136 | } 137 | 138 | ctx_vals[0].set(if change { 1.0 } else { 0.0 }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/dsp/node_mix3.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | 10 | /// A 3 channel signal mixer 11 | #[derive(Debug, Clone)] 12 | pub struct Mix3 {} 13 | 14 | impl Mix3 { 15 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 16 | Self {} 17 | } 18 | pub const ch1: &'static str = "Channel 1 Signal input"; 19 | pub const ch2: &'static str = "Channel 2 Signal input"; 20 | pub const ch3: &'static str = "Channel 3 Signal input"; 21 | pub const vol1: &'static str = "Channel 1 volume"; 22 | pub const vol2: &'static str = "Channel 2 volume"; 23 | pub const vol3: &'static str = "Channel 3 volume"; 24 | pub const ovol: &'static str = "Output volume of the sum"; 25 | pub const sig: &'static str = "Mixed signal output"; 26 | pub const DESC: &'static str = r#"3 Ch. Signal Mixer 27 | 28 | A very simple 3 channel signal mixer. 29 | You can mix anything, from audio signals to control signals. 30 | "#; 31 | pub const HELP: &'static str = r#"3 Channel Signal Mixer 32 | 33 | Just a small 3 channel mixer to create a sum of multiple signals. 34 | You can mix anything, from audio signals to control signals. 35 | 36 | There is even a convenient output volume knob, 37 | to turn down the output. 38 | "#; 39 | 40 | pub fn graph_fun() -> Option { 41 | None 42 | } 43 | } 44 | 45 | impl DspNode for Mix3 { 46 | fn set_sample_rate(&mut self, _srate: f32) {} 47 | fn reset(&mut self) {} 48 | 49 | #[inline] 50 | fn process( 51 | &mut self, 52 | ctx: &mut dyn NodeAudioContext, 53 | _ectx: &mut NodeExecContext, 54 | _nctx: &NodeContext, 55 | _atoms: &[SAtom], 56 | inputs: &[ProcBuf], 57 | outputs: &mut [ProcBuf], 58 | ctx_vals: LedPhaseVals, 59 | ) { 60 | use crate::dsp::{denorm, inp, out}; 61 | 62 | let inp1 = inp::Mix3::ch1(inputs); 63 | let inp2 = inp::Mix3::ch2(inputs); 64 | let inp3 = inp::Mix3::ch3(inputs); 65 | let g1 = inp::Mix3::vol1(inputs); 66 | let g2 = inp::Mix3::vol2(inputs); 67 | let g3 = inp::Mix3::vol3(inputs); 68 | let og = inp::Mix3::ovol(inputs); 69 | let out = out::Mix3::sig(outputs); 70 | 71 | for frame in 0..ctx.nframes() { 72 | let sum = inp1.read(frame) * denorm::Mix3::vol1(g1, frame) 73 | + inp2.read(frame) * denorm::Mix3::vol2(g2, frame) 74 | + inp3.read(frame) * denorm::Mix3::vol3(g3, frame); 75 | out.write(frame, sum * denorm::Mix3::ovol(og, frame)); 76 | } 77 | 78 | ctx_vals[0].set(out.read(ctx.nframes() - 1)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/dsp/node_noise.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp::Rng; 10 | 11 | #[macro_export] 12 | macro_rules! fa_noise_mode { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | let s = match ($v.round() as usize) { 15 | 0 => "Bipolar", 16 | 1 => "Unipolar", 17 | _ => "?", 18 | }; 19 | write!($formatter, "{}", s) 20 | }}; 21 | } 22 | 23 | /// A simple noise generator 24 | #[derive(Debug, Clone)] 25 | pub struct Noise { 26 | seed: u64, 27 | rng: Rng, 28 | } 29 | 30 | impl Noise { 31 | pub fn new(nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 32 | let mut rng = Rng::new(); 33 | rng.seed((0x193a67f4a8a6d769_u64).wrapping_add(0x131415 * (nid.instance() as u64 + 1))); 34 | 35 | Self { seed: nid.instance() as u64, rng } 36 | } 37 | 38 | pub const atv: &'static str = "Attenuverter input, to attenuate or invert \ 39 | the noise"; 40 | pub const offs: &'static str = "Offset input, that is added to the output \ 41 | signal after attenuvertig it."; 42 | pub const mode: &'static str = "You can switch between **Bipolar** noise, which \ 43 | uses the full range from **-1** to **1**, or **Unipolar** noise that \ 44 | only uses the range from **0** to **1**."; 45 | pub const sig: &'static str = "The noise output."; 46 | 47 | pub const DESC: &'static str = r#"Noise Oscillator 48 | 49 | This is a very simple noise oscillator, which can be used for any kind of audio rate noise. 50 | And as a source for sample & hold like nodes to generate low frequency modulation. The white 51 | noise is uniformly distributed and not normal distributed (which could be a bit more natural 52 | in some contexts). See also the `XNoise` node for more noise alternatives. 53 | "#; 54 | pub const HELP: &'static str = r#"A Simple Noise Oscillator 55 | 56 | This is a very simple noise oscillator, which can be used for 57 | any kind of audio rate noise. And as a source for sample & hold 58 | like nodes to generate low frequency modulation. 59 | 60 | The noise follows a uniform distribution. That means all amplitudes are equally likely to occur. 61 | While it might sound similar, white noise is usually following a normal distribution, which makes 62 | some amplitudes more likely to occur than others. 63 | See also the `XNoise` node for more noise alternatives. 64 | 65 | The ~~atv~~ attenuverter and ~~offs~~ parameters control the value range 66 | of the noise, and the ~~mode~~ allows to switch the oscillator between 67 | unipolar and bipolar output. 68 | "#; 69 | 70 | pub fn graph_fun() -> Option { 71 | None 72 | } 73 | } 74 | 75 | impl DspNode for Noise { 76 | fn set_sample_rate(&mut self, _srate: f32) {} 77 | 78 | fn reset(&mut self) { 79 | self.rng.seed((0x193a67f4a8a6d769_u64).wrapping_add(0x131415 * (self.seed + 1))); 80 | } 81 | 82 | #[inline] 83 | fn process( 84 | &mut self, 85 | ctx: &mut dyn NodeAudioContext, 86 | _ectx: &mut NodeExecContext, 87 | _nctx: &NodeContext, 88 | atoms: &[SAtom], 89 | inputs: &[ProcBuf], 90 | outputs: &mut [ProcBuf], 91 | ctx_vals: LedPhaseVals, 92 | ) { 93 | use crate::dsp::{at, denorm, inp, out}; 94 | 95 | let mode = at::Noise::mode(atoms); 96 | let atv = inp::Noise::atv(inputs); 97 | let offs = inp::Noise::offs(inputs); 98 | let out = out::Noise::sig(outputs); 99 | 100 | let rng = &mut self.rng; 101 | 102 | if mode.i() == 0 { 103 | for frame in 0..ctx.nframes() { 104 | let s = (rng.next() * 2.0) - 1.0; 105 | let s = s * denorm::Noise::atv(atv, frame) + denorm::Noise::offs(offs, frame); 106 | out.write(frame, s); 107 | } 108 | } else { 109 | for frame in 0..ctx.nframes() { 110 | let s = 111 | rng.next() * denorm::Noise::atv(atv, frame) + denorm::Noise::offs(offs, frame); 112 | out.write(frame, s); 113 | } 114 | } 115 | 116 | let last_frame = ctx.nframes() - 1; 117 | ctx_vals[0].set(out.read(last_frame)); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/dsp/node_out.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | at, denorm, inp, DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, 7 | SAtom, 8 | }; 9 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 10 | 11 | #[macro_export] 12 | macro_rules! fa_out_mono { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | let s = match ($v.round() as usize) { 15 | 0 => "Stereo", 16 | 1 => "Mono", 17 | _ => "?", 18 | }; 19 | write!($formatter, "{}", s) 20 | }}; 21 | } 22 | 23 | /// The (stereo) output port of the plugin 24 | #[derive(Debug, Clone)] 25 | pub struct Out { 26 | /// - 0: signal channel 1 27 | /// - 1: signal channel 2 28 | #[allow(dead_code)] 29 | input: [f32; 2], 30 | } 31 | 32 | impl Out { 33 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 34 | Self { input: [0.0; 2] } 35 | } 36 | 37 | pub const mono: &'static str = 38 | "If set to **Mono**, ~~ch1~~ will be sent to both output channels.\n(UI only)"; 39 | pub const vol: &'static str = 40 | "The main volume of the synthesizer output, applied to all channels. \ 41 | Please note that this is a linear control, to prevent inaccuracies for **1.0**. \ 42 | "; 43 | pub const ch1: &'static str = "Audio channel 1 (left)"; 44 | pub const ch2: &'static str = "Audio channel 2 (right)"; 45 | 46 | pub const DESC: &'static str = "Audio Output Port\n\n\ 47 | This output port node allows you to send audio signals \ 48 | to audio devices or tracks in your DAW."; 49 | pub const HELP: &'static str = r#"Audio Output Port 50 | 51 | This output port node allows you to send audio signals to audio devices 52 | or tracks in your DAW. If you need a stereo output but only have a mono 53 | signal you can use the ~~mono~~ setting to duplicate the signal on the ~~ch1~~ 54 | input to the second channel ~~ch2~~. 55 | "#; 56 | 57 | pub fn graph_fun() -> Option { 58 | None 59 | } 60 | } 61 | 62 | impl DspNode for Out { 63 | fn set_sample_rate(&mut self, _srate: f32) {} 64 | fn reset(&mut self) {} 65 | 66 | #[inline] 67 | fn process( 68 | &mut self, 69 | ctx: &mut dyn NodeAudioContext, 70 | _ectx: &mut NodeExecContext, 71 | _nctx: &NodeContext, 72 | atoms: &[SAtom], 73 | inputs: &[ProcBuf], 74 | _outputs: &mut [ProcBuf], 75 | ctx_vals: LedPhaseVals, 76 | ) { 77 | let in1 = inp::Out::ch1(inputs); 78 | let vol = inp::Out::vol(inputs); 79 | 80 | if at::Out::mono(atoms).i() > 0 { 81 | for frame in 0..ctx.nframes() { 82 | let vol = denorm::Out::vol(vol, frame); 83 | ctx.output(0, frame, vol * in1.read(frame)); 84 | ctx.output(1, frame, vol * in1.read(frame)); 85 | } 86 | } else { 87 | let in2 = inp::Out::ch2(inputs); 88 | 89 | for frame in 0..ctx.nframes() { 90 | let vol = denorm::Out::vol(vol, frame); 91 | ctx.output(0, frame, vol * in1.read(frame)); 92 | ctx.output(1, frame, vol * in2.read(frame)); 93 | } 94 | } 95 | 96 | let last_val = in1.read(ctx.nframes() - 1); 97 | ctx_vals[0].set(last_val); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/dsp/node_quant.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, SAtom, 7 | }; 8 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 9 | use synfx_dsp::{ChangeTrig, Quantizer}; 10 | 11 | #[macro_export] 12 | macro_rules! fa_quant { 13 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 14 | write!($formatter, "?") 15 | }}; 16 | } 17 | 18 | /// A pitch quantizer 19 | #[derive(Debug, Clone)] 20 | pub struct Quant { 21 | quant: Box, 22 | change_trig: ChangeTrig, 23 | } 24 | 25 | impl Quant { 26 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 27 | Self { quant: Box::new(Quantizer::new()), change_trig: ChangeTrig::new() } 28 | } 29 | pub const freq: &'static str = "Any signal that is to be pitch quantized."; 30 | pub const oct: &'static str = "Pitch offset, the knob is snapping to octave offsets. \ 31 | Feed signal values snapped to **0.1** multiples for exact octave offsets.\ 32 | "; 33 | pub const sig: &'static str = "The quantized output signal that is rounded to \ 34 | the next selected note pitch within the octave of the \ 35 | original input to ~~freq~~."; 36 | pub const keys: &'static str = "Select the notes you want to snap to here. \ 37 | If no notes are selected, the quantizer will snap the \ 38 | incoming signal to any closest note."; 39 | pub const t: &'static str = "Everytime the quantizer snaps to a new pitch, it will \ 40 | emit a short trigger on this signal output. This is useful \ 41 | to trigger for example an envelope."; 42 | pub const DESC: &'static str = r#"Pitch Quantizer 43 | 44 | This is a simple quantizer, that snaps a pitch signal on ~~freq~~ to the closest selected notes within their octave. 45 | "#; 46 | pub const HELP: &'static str = r#"A pitch quantizer 47 | 48 | This is a simple quantizer, that snaps a pitch signal on ~~freq~~ to the 49 | closest selected notes within their octave. 50 | 51 | If you sweep along pitches you will notice that notes that are closer together 52 | are travelled across faster. That means the notes are not evenly distributed 53 | across the pitch input. If you want a more evenly distributed pitch selection 54 | please see also the `CQnt` node. 55 | "#; 56 | 57 | pub fn graph_fun() -> Option { 58 | None 59 | } 60 | } 61 | 62 | impl DspNode for Quant { 63 | fn set_sample_rate(&mut self, srate: f32) { 64 | self.change_trig.set_sample_rate(srate); 65 | } 66 | 67 | fn reset(&mut self) { 68 | self.change_trig.reset(); 69 | } 70 | 71 | #[inline] 72 | fn process( 73 | &mut self, 74 | ctx: &mut dyn NodeAudioContext, 75 | _ectx: &mut NodeExecContext, 76 | _nctx: &NodeContext, 77 | atoms: &[SAtom], 78 | inputs: &[ProcBuf], 79 | outputs: &mut [ProcBuf], 80 | ctx_vals: LedPhaseVals, 81 | ) { 82 | use crate::dsp::{at, denorm, inp, out_buf}; 83 | 84 | let freq = inp::Quant::freq(inputs); 85 | let oct = inp::Quant::oct(inputs); 86 | let keys = at::Quant::keys(atoms); 87 | let mut out = out_buf::CQnt::sig(outputs); 88 | let mut t = out_buf::CQnt::t(outputs); 89 | 90 | self.quant.set_keys(keys.i()); 91 | 92 | for frame in 0..ctx.nframes() { 93 | let pitch = self.quant.process(freq.read(frame)); 94 | 95 | t.write(frame, self.change_trig.next(pitch)); 96 | out.write(frame, pitch + denorm::Quant::oct(oct, frame)); 97 | } 98 | 99 | let last_pitch = self.quant.last_key_pitch(); 100 | ctx_vals[1].set(last_pitch * 10.0 + 0.0001); 101 | ctx_vals[0].set((last_pitch * 10.0 - 0.5) * 2.0); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/dsp/node_sin.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | denorm, denorm_offs, inp, out, DspNode, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, 7 | NodeId, ProcBuf, SAtom, 8 | }; 9 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 10 | use synfx_dsp::fast_sin; 11 | 12 | /// A sine oscillator 13 | #[derive(Debug, Clone)] 14 | pub struct Sin { 15 | /// Sample rate 16 | srate: f32, 17 | /// Oscillator phase 18 | phase: f32, 19 | /// Initial phase offset 20 | init_phase: f32, 21 | } 22 | 23 | const TWOPI: f32 = 2.0 * std::f32::consts::PI; 24 | 25 | impl Sin { 26 | pub fn new(nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 27 | let init_phase = nid.init_phase(); 28 | 29 | Self { srate: 44100.0, phase: init_phase, init_phase } 30 | } 31 | pub const freq: &'static str = "Frequency of the oscillator.\n"; 32 | pub const det: &'static str = "Detune the oscillator in semitones and cents. \ 33 | the input of this value is rounded to semitones on coarse input. \ 34 | Fine input lets you detune in cents (rounded). \ 35 | A signal sent to this port is not rounded.\n\ 36 | Note: The signal input allows detune +-10 octaves.\ 37 | "; 38 | pub const pm: &'static str = 39 | "Phase modulation input or phase offset. Use this for linear FM/PM modulation.\n"; 40 | pub const sig: &'static str = "Oscillator signal output.\n"; 41 | 42 | pub const DESC: &'static str = r#"Sine Oscillator 43 | 44 | This is a very simple oscillator that generates a sine wave. 45 | "#; 46 | 47 | pub const HELP: &'static str = r#"A Sine Oscillator 48 | 49 | This is a very simple oscillator that generates a sine wave. 50 | The ~~freq~~ parameter specifies the frequency, and the ~~det~~ parameter 51 | allows you to detune the oscillator easily. 52 | 53 | You can send any signal to these input ports. The ~~det~~ parameter takes 54 | the same signal range as ~~freq~~, which means, that a value of 0.1 detunes 55 | by one octave. And a value 1.0 detunes by 10 octaves. This means that 56 | for ~~det~~ to be usefully modulated you need to attenuate the modulation input. 57 | 58 | For linear FM, you can use the ~~pm~~ input. It allows you to modulate the phase 59 | of the oscillator linearly. It does so *through zero* which means that the pitch 60 | should not detune by the amount of modulation in low frequencies. 61 | 62 | You can do exponential FM with this node using the ~~det~~ or ~~freq~~ input, 63 | but for easy exponential FM synthesis there might be other nodes available. 64 | "#; 65 | 66 | pub fn graph_fun() -> Option { 67 | None 68 | } 69 | } 70 | 71 | impl DspNode for Sin { 72 | fn set_sample_rate(&mut self, srate: f32) { 73 | self.srate = srate; 74 | } 75 | 76 | fn reset(&mut self) { 77 | self.phase = self.init_phase; 78 | } 79 | 80 | #[inline] 81 | fn process( 82 | &mut self, 83 | ctx: &mut dyn NodeAudioContext, 84 | _ectx: &mut NodeExecContext, 85 | _nctx: &NodeContext, 86 | _atoms: &[SAtom], 87 | inputs: &[ProcBuf], 88 | outputs: &mut [ProcBuf], 89 | ctx_vals: LedPhaseVals, 90 | ) { 91 | let o = out::Sin::sig(outputs); 92 | let freq = inp::Sin::freq(inputs); 93 | let det = inp::Sin::det(inputs); 94 | let pm = inp::Sin::pm(inputs); 95 | let isr = 1.0 / self.srate; 96 | 97 | let mut last_val = 0.0; 98 | for frame in 0..ctx.nframes() { 99 | let freq = denorm_offs::Sin::freq(freq, det.read(frame), frame); 100 | 101 | let mut phase = self.phase + denorm::Sin::pm(pm, frame); 102 | while phase < 0.0 { 103 | phase += 1.0 104 | } 105 | last_val = fast_sin(phase.fract() * TWOPI); 106 | o.write(frame, last_val); 107 | 108 | self.phase += freq * isr; 109 | self.phase = self.phase.fract(); 110 | } 111 | 112 | ctx_vals[0].set(last_val); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/dsp/node_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, 7 | SAtom, 8 | }; 9 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 10 | use synfx_dsp::TrigSignal; 11 | 12 | #[macro_export] 13 | macro_rules! fa_test_s { 14 | ($formatter: expr, $v: expr, $denorm_v: expr) => {{ 15 | let s = match ($v.round() as usize) { 16 | 0 => "Zero", 17 | 1 => "One", 18 | 2 => "Two", 19 | 3 => "Three", 20 | 4 => "Four", 21 | 5 => "Five", 22 | 6 => "Six", 23 | 7 => "Seven", 24 | 8 => "Eigth", 25 | 9 => "Nine", 26 | 10 => "Ten", 27 | _ => "?", 28 | }; 29 | write!($formatter, "{}", s) 30 | }}; 31 | } 32 | 33 | /// A simple amplifier 34 | #[derive(Debug, Clone)] 35 | pub struct Test { 36 | trig_sig: TrigSignal, 37 | trigger: bool, 38 | } 39 | 40 | impl Test { 41 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 42 | Self { trigger: false, trig_sig: TrigSignal::new() } 43 | } 44 | 45 | pub const f: &'static str = "F Test"; 46 | pub const p: &'static str = "An unsmoothed parameter for automated tests."; 47 | pub const trig: &'static str = 48 | "A trigger input, that will create a short pulse on the ~~tsig~~ output."; 49 | pub const sig: &'static str = "The output of p as signal"; 50 | pub const tsig: &'static str = 51 | "A short trigger pulse will be generated when the ~~trig~~ input is triggered."; 52 | pub const out2: &'static str = 53 | "A test output that will emit **1.0** if output ~~sig~~ is connected."; 54 | pub const out3: &'static str = 55 | "A test output that will emit **1.0** if input ~~f~~ is connected."; 56 | pub const out4: &'static str = ""; 57 | pub const outc: &'static str = 58 | "Emits a number that defines the out_connected bitmask. Used only for testing!"; 59 | 60 | pub const DESC: &'static str = r#""#; 61 | pub const HELP: &'static str = r#""#; 62 | 63 | pub fn graph_fun() -> Option { 64 | Some(Box::new(|_gd: &dyn GraphAtomData, _init: bool, x: f32, _xn: f32| -> f32 { x })) 65 | } 66 | } 67 | 68 | impl DspNode for Test { 69 | fn set_sample_rate(&mut self, srate: f32) { 70 | self.trig_sig.set_sample_rate(srate); 71 | } 72 | 73 | fn reset(&mut self) { 74 | self.trig_sig.reset(); 75 | } 76 | 77 | #[inline] 78 | fn process( 79 | &mut self, 80 | ctx: &mut dyn NodeAudioContext, 81 | _ectx: &mut NodeExecContext, 82 | nctx: &NodeContext, 83 | atoms: &[SAtom], 84 | _inputs: &[ProcBuf], 85 | outputs: &mut [ProcBuf], 86 | _led: LedPhaseVals, 87 | ) { 88 | use crate::dsp::{at, is_in_con, is_out_con, out_buf, out_idx}; 89 | 90 | let p = at::Test::p(atoms); 91 | let trig = at::Test::trig(atoms); 92 | let tsig = out_idx::Test::tsig(); 93 | 94 | let mut out2 = out_buf::Test::out2(outputs); 95 | let mut out3 = out_buf::Test::out3(outputs); 96 | let mut outc = out_buf::Test::outc(outputs); 97 | 98 | let (out, tsig) = outputs.split_at_mut(tsig); 99 | let out = &mut out[0]; 100 | let tsig = &mut tsig[0]; 101 | 102 | let mut trigger = trig.i(); 103 | if !self.trigger && trigger > 0 { 104 | self.trigger = true; 105 | } else if !self.trigger && trigger == 0 { 106 | self.trigger = false; 107 | } else if self.trigger { 108 | trigger = 0; 109 | } 110 | 111 | for frame in 0..ctx.nframes() { 112 | if trigger > 0 { 113 | self.trig_sig.trigger(); 114 | trigger = 0; 115 | } 116 | 117 | out.write(frame, p.f()); 118 | let t = self.trig_sig.next(); 119 | tsig.write(frame, t); 120 | 121 | out2.write(frame, if is_out_con::Test::sig(nctx) { 1.0 } else { 0.0 }); 122 | out3.write(frame, if is_in_con::Test::f(nctx) { 1.0 } else { 0.0 }); 123 | outc.write(frame, nctx.out_connected as f32); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/dsp/node_tslfo.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::{ 6 | DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeGlobalRef, NodeId, ProcBuf, 7 | SAtom, 8 | }; 9 | use crate::nodes::{NodeAudioContext, NodeExecContext}; 10 | use synfx_dsp::{TriSawLFO, Trigger}; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct TsLFO { 14 | lfo: Box>, 15 | trig: Trigger, 16 | } 17 | 18 | impl TsLFO { 19 | pub fn new(_nid: &NodeId, _node_global: &NodeGlobalRef) -> Self { 20 | Self { lfo: Box::new(TriSawLFO::new()), trig: Trigger::new() } 21 | } 22 | 23 | pub const time: &'static str = "The frequency or period time of the LFO, goes all the \ 24 | way from **0.1ms** up to **30s**. Please note, that the text entry is always \ 25 | in milliseconds."; 26 | pub const trig: &'static str = "Triggers a phase reset of the LFO."; 27 | pub const rev: &'static str = "The reverse point of the LFO waveform. At **0.5** the LFO \ 28 | will follow a triangle waveform. At **0.0** or **1.0** the LFO waveform will \ 29 | be (almost) a (reversed) saw tooth. Node: A perfect sawtooth can not be \ 30 | achieved with this oscillator, as there will always be a minimal \ 31 | rise/fall time."; 32 | pub const sig: &'static str = "The LFO output."; 33 | pub const DESC: &'static str = r#"TriSaw LFO 34 | 35 | This simple LFO has a configurable waveform. 36 | You can blend between triangular to sawtooth waveforms using the ~~rev~~ parameter. 37 | "#; 38 | pub const HELP: &'static str = r#"TriSaw LFO 39 | 40 | This simple LFO has a configurable waveform. You can blend between 41 | triangular to sawtooth waveforms using the ~~rev~~ parameter. 42 | 43 | Using the ~~trig~~ input you can reset the LFO phase, which allows to use it 44 | kind of like an envelope. 45 | "#; 46 | 47 | pub fn graph_fun() -> Option { 48 | let mut lfo = TriSawLFO::new(); 49 | lfo.set_sample_rate(160.0); 50 | 51 | Some(Box::new(move |gd: &dyn GraphAtomData, init: bool, _x: f32, _xn: f32| -> f32 { 52 | if init { 53 | lfo.reset(); 54 | let time_idx = NodeId::TsLFO(0).inp_param("time").unwrap().inp(); 55 | let rev_idx = NodeId::TsLFO(0).inp_param("rev").unwrap().inp(); 56 | 57 | let time = gd.get_norm(time_idx as u32).sqrt(); 58 | let rev = gd.get_norm(rev_idx as u32); 59 | lfo.set(5.0 * (1.0 - time) + time * 1.0, rev); 60 | } 61 | 62 | lfo.next_unipolar() as f32 63 | })) 64 | } 65 | } 66 | 67 | impl DspNode for TsLFO { 68 | fn set_sample_rate(&mut self, srate: f32) { 69 | self.lfo.set_sample_rate(srate as f64); 70 | } 71 | 72 | fn reset(&mut self) { 73 | self.lfo.reset(); 74 | self.trig.reset(); 75 | } 76 | 77 | #[inline] 78 | fn process( 79 | &mut self, 80 | ctx: &mut dyn NodeAudioContext, 81 | _ectx: &mut NodeExecContext, 82 | _nctx: &NodeContext, 83 | _atoms: &[SAtom], 84 | inputs: &[ProcBuf], 85 | outputs: &mut [ProcBuf], 86 | ctx_vals: LedPhaseVals, 87 | ) { 88 | use crate::dsp::{denorm, inp, out}; 89 | 90 | let time = inp::TsLFO::time(inputs); 91 | let trig = inp::TsLFO::trig(inputs); 92 | let rev = inp::TsLFO::rev(inputs); 93 | let out = out::TsLFO::sig(outputs); 94 | 95 | let lfo = &mut *self.lfo; 96 | 97 | for frame in 0..ctx.nframes() { 98 | if self.trig.check_trigger(denorm::TsLFO::trig(trig, frame)) { 99 | lfo.reset(); 100 | } 101 | 102 | let time_ms = denorm::TsLFO::time(time, frame).clamp(0.1, 300000.0); 103 | 104 | lfo.set((1000.0 / time_ms) as f64, denorm::TsLFO::rev(rev, frame) as f64); 105 | 106 | out.write(frame, lfo.next_unipolar() as f32); 107 | } 108 | 109 | ctx_vals[0].set(out.read(ctx.nframes() - 1)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/dsp/satom.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub enum SAtom { 7 | Str(String), 8 | MicroSample(Vec), 9 | AudioSample((String, Option>>)), 10 | Setting(i64), 11 | Param(f32), 12 | } 13 | 14 | impl SAtom { 15 | pub fn str(s: &str) -> Self { 16 | SAtom::Str(s.to_string()) 17 | } 18 | pub fn setting(s: i64) -> Self { 19 | SAtom::Setting(s) 20 | } 21 | pub fn param(p: f32) -> Self { 22 | SAtom::Param(p) 23 | } 24 | pub fn micro(m: &[f32]) -> Self { 25 | SAtom::MicroSample(m.to_vec()) 26 | } 27 | pub fn audio(s: &str, m: std::sync::Arc>) -> Self { 28 | SAtom::AudioSample((s.to_string(), Some(m))) 29 | } 30 | 31 | pub fn audio_unloaded(s: &str) -> Self { 32 | SAtom::AudioSample((s.to_string(), None)) 33 | } 34 | 35 | pub fn default_of(&self) -> Self { 36 | match self { 37 | SAtom::Str(_) => SAtom::Str("".to_string()), 38 | SAtom::MicroSample(_) => SAtom::MicroSample(vec![]), 39 | SAtom::AudioSample(_) => SAtom::AudioSample(("".to_string(), None)), 40 | SAtom::Setting(_) => SAtom::Setting(0), 41 | SAtom::Param(_) => SAtom::Param(0.0), 42 | } 43 | } 44 | 45 | pub fn is_continous(&self) -> bool { 46 | matches!(self, SAtom::Param(_)) 47 | } 48 | 49 | pub fn i(&self) -> i64 { 50 | match self { 51 | SAtom::Setting(i) => *i, 52 | SAtom::Param(i) => *i as i64, 53 | _ => 0, 54 | } 55 | } 56 | 57 | pub fn s(&self) -> String { 58 | match self { 59 | SAtom::Str(s) => s.clone(), 60 | _ => "".to_string(), 61 | } 62 | } 63 | 64 | pub fn f(&self) -> f32 { 65 | match self { 66 | SAtom::Setting(i) => *i as f32, 67 | SAtom::Param(i) => *i, 68 | _ => 0.0, 69 | } 70 | } 71 | 72 | pub fn v_ref(&self) -> Option<&[f32]> { 73 | match self { 74 | SAtom::MicroSample(v) => Some(&v[..]), 75 | SAtom::AudioSample((_, Some(v))) => Some(&v[..]), 76 | _ => None, 77 | } 78 | } 79 | 80 | pub fn type_str(&self) -> &str { 81 | match self { 82 | SAtom::Str(_) => "str", 83 | SAtom::MicroSample(_) => "micro_sample", 84 | SAtom::AudioSample(_) => "audio_sample", 85 | SAtom::Setting(_) => "setting", 86 | SAtom::Param(_) => "param", 87 | } 88 | } 89 | } 90 | 91 | impl From for SAtom { 92 | fn from(n: f32) -> Self { 93 | SAtom::Param(n) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use lazy_static::lazy_static; 6 | use ringbuf::{Consumer, Producer, RingBuffer}; 7 | use std::cell::RefCell; 8 | 9 | use std::sync::{Arc, Mutex}; 10 | 11 | lazy_static! { 12 | static ref LOG_RECV: Arc> = Arc::new(Mutex::new(LogReceiver::new())); 13 | } 14 | 15 | thread_local! { 16 | pub static LOG: RefCell> = RefCell::new(None); 17 | } 18 | 19 | pub fn retrieve_log_messages(f: F) { 20 | if let Ok(mut lr) = LOG_RECV.lock() { 21 | lr.retrieve_messages(f); 22 | } 23 | } 24 | 25 | #[inline] 26 | pub fn init_thread_logger(name: &'static str) -> bool { 27 | if !LOG.with(|l| l.borrow().is_some()) { 28 | if let Ok(mut lr) = LOG_RECV.lock() { 29 | lr.spawn_global_logger(name); 30 | return true; 31 | } 32 | } 33 | 34 | false 35 | } 36 | 37 | pub fn log)>(f: F) { 38 | use std::borrow::BorrowMut; 39 | 40 | LOG.with(|l| { 41 | let mut lh = l.borrow_mut(); 42 | if let Some(lh) = (*(*lh.borrow_mut())).as_mut() { 43 | lh.log(f); 44 | } 45 | }); 46 | } 47 | 48 | const MAX_LOG_BUFFER: usize = 4096; 49 | 50 | pub struct LogReceiver { 51 | consumers: Vec<(&'static str, Consumer)>, 52 | } 53 | 54 | impl LogReceiver { 55 | pub fn new() -> Self { 56 | Self { consumers: vec![] } 57 | } 58 | 59 | pub fn retrieve_messages(&mut self, mut f: F) { 60 | for (name, consumer) in self.consumers.iter_mut() { 61 | let mut buf = [0; 1024]; 62 | let mut oi = 0; 63 | 64 | while let Some(byte) = consumer.pop() { 65 | if oi >= buf.len() || byte == 0xFF { 66 | f(name, std::str::from_utf8(&buf[0..oi]).unwrap()); 67 | oi = 0; 68 | } else { 69 | buf[oi] = byte; 70 | oi += 1; 71 | } 72 | } 73 | } 74 | } 75 | 76 | pub fn spawn_logger(&mut self, name: &'static str) -> Log { 77 | let rb = RingBuffer::new(MAX_LOG_BUFFER); 78 | let (producer, con) = rb.split(); 79 | 80 | self.consumers.push((name, con)); 81 | Log { producer, buf: [0; 512] } 82 | } 83 | 84 | #[inline] 85 | pub fn spawn_global_logger(&mut self, name: &'static str) { 86 | let hdl = self.spawn_logger(name); 87 | LOG.with(move |f| { 88 | *f.borrow_mut() = Some(hdl); 89 | }); 90 | } 91 | } 92 | 93 | pub struct Log { 94 | producer: Producer, 95 | buf: [u8; 512], 96 | } 97 | 98 | impl Log { 99 | pub fn log_buf(&mut self, data: &[u8]) { 100 | self.producer.push_slice(data); 101 | let _ = self.producer.push(0xFF); 102 | } 103 | 104 | pub fn log)>(&mut self, f: F) { 105 | self.buf.fill(0xFF); 106 | 107 | let len = { 108 | let mut bw = std::io::BufWriter::new(&mut self.buf[..]); 109 | f(&mut bw); 110 | bw.buffer().len() 111 | }; 112 | 113 | if len < (self.buf.len() - 1) { 114 | // include one 0xFF! 115 | self.producer.push_slice(&self.buf[0..len + 1]); 116 | } else { 117 | self.producer.push_slice(&self.buf[0..len]); 118 | } 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn check_threaded_logger() { 128 | std::thread::spawn(|| { 129 | use std::io::Write; 130 | assert!(init_thread_logger("tstlog")); 131 | log(|w| write!(w, "Test Log{}!", 1).unwrap()); 132 | log(|w| write!(w, "Test Log{}!", 2).unwrap()); 133 | }); 134 | 135 | let mut msgs = vec![]; 136 | for _ in 0..100 { 137 | std::thread::sleep(std::time::Duration::from_millis(100)); 138 | 139 | retrieve_log_messages(|name, s| msgs.push(name.to_string() + "/" + s)); 140 | 141 | if msgs.len() > 1 { 142 | assert_eq!(msgs[0], "tstlog/Test Log1!"); 143 | assert_eq!(msgs[1], "tstlog/Test Log2!"); 144 | break; 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/nodes/drop_thread.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use super::DropMsg; 6 | 7 | use ringbuf::Consumer; 8 | 9 | /// For receiving deleted/overwritten nodes from the backend 10 | /// thread and dropping them. 11 | pub(crate) struct DropThread { 12 | terminate: std::sync::Arc, 13 | th: Option>, 14 | } 15 | 16 | impl DropThread { 17 | pub(crate) fn new(mut graph_drop_con: Consumer) -> Self { 18 | let terminate = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); 19 | let th_terminate = terminate.clone(); 20 | 21 | let th = std::thread::spawn(move || { 22 | loop { 23 | if th_terminate.load(std::sync::atomic::Ordering::Relaxed) { 24 | return; 25 | } 26 | 27 | while let Some(_node) = graph_drop_con.pop() { 28 | // drop it ... 29 | //d// println!("Dropped some shit..."); 30 | } 31 | 32 | std::thread::sleep(std::time::Duration::from_millis(250)); 33 | } 34 | }); 35 | 36 | Self { th: Some(th), terminate } 37 | } 38 | } 39 | 40 | impl Drop for DropThread { 41 | fn drop(&mut self) { 42 | self.terminate.store(true, std::sync::atomic::Ordering::Relaxed); 43 | let _ = self.th.take().unwrap().join(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/nodes/feedback_filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use super::VisualSamplingFilter; 6 | use crate::dsp::NodeId; 7 | 8 | use std::collections::HashMap; 9 | 10 | pub struct FeedbackFilter { 11 | led_filters: HashMap, 12 | out_filters: HashMap<(NodeId, u8), VisualSamplingFilter>, 13 | recalc_state: bool, 14 | } 15 | 16 | impl FeedbackFilter { 17 | pub fn new() -> Self { 18 | Self { led_filters: HashMap::new(), out_filters: HashMap::new(), recalc_state: true } 19 | } 20 | 21 | fn get_out_filter_for_node(&mut self, node_id: &NodeId, out: u8) -> &mut VisualSamplingFilter { 22 | self.out_filters.entry((*node_id, out)).or_insert_with(|| VisualSamplingFilter::new()) 23 | } 24 | 25 | fn get_led_filter_for_node(&mut self, node_id: &NodeId) -> &mut VisualSamplingFilter { 26 | self.led_filters.entry(*node_id).or_insert_with(|| VisualSamplingFilter::new()) 27 | } 28 | 29 | pub fn trigger_recalc(&mut self) { 30 | self.recalc_state = !self.recalc_state; 31 | } 32 | 33 | pub fn get_led(&mut self, node_id: &NodeId, sample: f32) -> (f32, f32) { 34 | let recalc_state = self.recalc_state; 35 | let filter = self.get_led_filter_for_node(node_id); 36 | filter.get(recalc_state, sample) 37 | } 38 | 39 | pub fn get_out(&mut self, node_id: &NodeId, out: u8, sample: f32) -> (f32, f32) { 40 | let recalc_state = self.recalc_state; 41 | let filter = self.get_out_filter_for_node(node_id, out); 42 | filter.get(recalc_state, sample) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/nodes/midi.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub struct HxTimedEvent { 7 | /// The frame number in the current block by the audio driver or plugin API/DAW 8 | timing: usize, 9 | kind: HxMidiEvent, 10 | } 11 | 12 | impl HxTimedEvent { 13 | pub fn new_timed(timing: usize, kind: HxMidiEvent) -> Self { 14 | Self { timing, kind } 15 | } 16 | 17 | pub fn is_cc(&self) -> bool { 18 | if let HxMidiEvent::CC { .. } = self.kind { 19 | true 20 | } else { 21 | false 22 | } 23 | } 24 | 25 | pub fn kind(&self) -> HxMidiEvent { 26 | self.kind 27 | } 28 | 29 | pub fn cc(timing: usize, channel: u8, cc: u8, value: f32) -> Self { 30 | Self { timing, kind: HxMidiEvent::CC { channel, cc, value } } 31 | } 32 | 33 | pub fn note_on(timing: usize, channel: u8, note: u8, vel: f32) -> Self { 34 | Self { timing, kind: HxMidiEvent::NoteOn { channel, note, vel } } 35 | } 36 | 37 | pub fn note_off(timing: usize, channel: u8, note: u8) -> Self { 38 | Self { timing, kind: HxMidiEvent::NoteOff { channel, note } } 39 | } 40 | } 41 | 42 | pub struct MidiEventPointer<'a> { 43 | buf: &'a [HxTimedEvent], 44 | idx: usize, 45 | } 46 | 47 | impl<'a> MidiEventPointer<'a> { 48 | pub fn new(buf: &'a [HxTimedEvent]) -> Self { 49 | Self { buf, idx: 0 } 50 | } 51 | 52 | pub fn next_at(&mut self, time: usize) -> Option { 53 | if self.idx < self.buf.len() && self.buf[self.idx].timing <= time { 54 | self.idx += 1; 55 | Some(self.buf[self.idx - 1].kind) 56 | } else { 57 | None 58 | } 59 | } 60 | } 61 | 62 | #[derive(Debug, Clone, Copy)] 63 | pub enum HxMidiEvent { 64 | NoteOn { channel: u8, note: u8, vel: f32 }, 65 | NoteOff { channel: u8, note: u8 }, 66 | CC { channel: u8, cc: u8, value: f32 }, 67 | } 68 | 69 | pub struct EventWindowing { 70 | pub event: Option, 71 | } 72 | 73 | impl EventWindowing { 74 | pub fn new() -> Self { 75 | Self { event: None } 76 | } 77 | 78 | #[inline] 79 | pub fn feed_me(&self) -> bool { 80 | self.event.is_none() 81 | } 82 | 83 | #[inline] 84 | pub fn feed(&mut self, event: HxTimedEvent) { 85 | self.event = Some(event); 86 | } 87 | 88 | #[inline] 89 | pub fn next_event_in_range( 90 | &mut self, 91 | to_time: usize, 92 | block_size: usize, 93 | ) -> Option { 94 | if let Some(event) = self.event.take() { 95 | if event.timing < (to_time + block_size) { 96 | return Some(HxTimedEvent { timing: event.timing - to_time, kind: event.kind }); 97 | } else { 98 | self.event = Some(event); 99 | } 100 | } 101 | 102 | None 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/nodes/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | pub const SCOPE_SAMPLES: usize = 512; 6 | pub const MAX_DSP_NODE_INPUTS: usize = 32; 7 | pub const MAX_SMOOTHERS: usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs 8 | pub const MAX_INJ_MIDI_EVENTS: usize = 64; 9 | 10 | mod drop_thread; 11 | mod feedback_filter; 12 | mod midi; 13 | mod node_conf; 14 | mod node_exec; 15 | mod node_graph_ordering; 16 | mod node_prog; 17 | pub mod visual_sampling_filter; 18 | 19 | pub(crate) use visual_sampling_filter::*; 20 | 21 | pub use feedback_filter::*; 22 | pub use midi::{EventWindowing, HxMidiEvent, HxTimedEvent, MidiEventPointer}; 23 | pub use node_conf::*; 24 | pub use node_exec::*; 25 | pub use node_graph_ordering::NodeGraphOrdering; 26 | pub use node_prog::*; 27 | 28 | use crate::dsp::{Node, SAtom}; 29 | pub use crate::monitor::MinMaxMonitorSamples; 30 | use crate::monitor::MON_SIG_CNT; 31 | 32 | #[derive(Debug)] 33 | #[allow(dead_code)] 34 | pub(crate) enum DropMsg { 35 | Node { node: Node }, 36 | Prog { prog: NodeProg }, 37 | Atom { atom: SAtom }, 38 | } 39 | 40 | /// Messages for updating the [NodeExecutor] thread. 41 | /// Usually used for shoveling NodeProg and Nodes to and from 42 | /// the [NodeExecutor] thread. And also parameter updates of course. 43 | #[derive(Debug)] 44 | pub enum GraphMessage { 45 | NewProg { 46 | prog: NodeProg, 47 | copy_old_out: bool, 48 | }, 49 | Clear { 50 | prog: NodeProg, 51 | }, 52 | 53 | // XXX: Parameter updates used to be separate from the graph update, but this 54 | // became a race condition and I had to revert this premature optimization. 55 | AtomUpdate { 56 | at_idx: usize, 57 | value: SAtom, 58 | }, 59 | ParamUpdate { 60 | input_idx: usize, 61 | value: f32, 62 | }, 63 | ModamtUpdate { 64 | mod_idx: usize, 65 | modamt: f32, 66 | }, 67 | InjectMidi { 68 | midi_ev: HxMidiEvent, 69 | }, 70 | /// Sets the buffer indices to monitor with the FeedbackProcessor. 71 | SetMonitor { 72 | bufs: [usize; MON_SIG_CNT], 73 | }, 74 | } 75 | 76 | /// Message from the DSP graph/backend to the frontend. Such as MIDI events 77 | /// for MIDI learn for instance. 78 | pub enum GraphEvent { 79 | MIDI(HxMidiEvent), 80 | } 81 | 82 | pub const UNUSED_MONITOR_IDX: usize = 99999; 83 | 84 | /// Creates a [NodeConfigurator] and a [NodeExecutor] which are interconnected 85 | /// by ring buffers. 86 | pub fn new_node_engine() -> (NodeConfigurator, NodeExecutor) { 87 | let (nc, shared_exec) = NodeConfigurator::new(); 88 | let ne = NodeExecutor::new(shared_exec); 89 | 90 | // XXX: This is one of the earliest and most consistent points 91 | // in runtime to do this kind of initialization: 92 | synfx_dsp::init_cos_tab(); 93 | 94 | (nc, ne) 95 | } 96 | -------------------------------------------------------------------------------- /src/nodes/visual_sampling_filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | const VALUE_SAMPLING_FILTER_SIZE: usize = 10; 6 | 7 | /// Accumulates the values for a single visible feedback value, 8 | /// like an LED ([crate::Matrix::led_value_for]) or the 9 | /// output feedbacks [crate::Matrix::out_fb_for] from the [crate::Matrix]. 10 | #[derive(Debug, Clone, Copy)] 11 | pub struct VisualSamplingFilter { 12 | /// Holds a state bit, that is used to check if this 13 | /// filter needs to recalculate or not. 14 | recalc_state: bool, 15 | 16 | /// Current write head into the sample buffer. 17 | write_ptr: usize, 18 | 19 | /// Holds a set of the most recent samples to calculate 20 | /// the output. 21 | sample_buffer: [f32; VALUE_SAMPLING_FILTER_SIZE], 22 | 23 | /// Holds the last output, will only be recalculated 24 | /// when necessary. 25 | last_output: (f32, f32), 26 | } 27 | 28 | impl VisualSamplingFilter { 29 | pub fn new() -> Self { 30 | Self { 31 | recalc_state: false, 32 | write_ptr: 0, 33 | sample_buffer: [0.0; VALUE_SAMPLING_FILTER_SIZE], 34 | last_output: (0.0, 0.0), 35 | } 36 | } 37 | 38 | /// Used to check if we need to update this filter. 39 | #[inline] 40 | fn needs_recalc(&mut self, recalc_value: bool) -> bool { 41 | if self.recalc_state != recalc_value { 42 | self.recalc_state = recalc_value; 43 | true 44 | } else { 45 | false 46 | } 47 | } 48 | 49 | /// Retrieves the current output value of the filter. 50 | /// Negate the input for `recalc_value` one each frame, 51 | /// to reduce access to the `retrieve_fn` to be done only 52 | /// once per frame and per [VisualSamplingFilter]. 53 | /// 54 | ///``` 55 | /// use hexodsp::nodes::visual_sampling_filter::*; 56 | /// 57 | /// let mut vsf = VisualSamplingFilter::new(); 58 | /// 59 | /// let inputs = [-0.87, -0.8, 0.2, 0.75, 0.5, 0.0, 0.22]; 60 | /// let mut recalc = true; 61 | /// 62 | /// let mut last_output = (0.0, 0.0); 63 | /// for ip in inputs { 64 | /// last_output = vsf.get(recalc, ip); 65 | /// recalc = !recalc; 66 | /// } 67 | /// 68 | /// assert_eq!(last_output, (0.87, 0.75)); 69 | ///``` 70 | pub fn get(&mut self, recalc_value: bool, sample: f32) -> (f32, f32) { 71 | if self.needs_recalc(recalc_value) { 72 | let write_ptr = (self.write_ptr + 1) % self.sample_buffer.len(); 73 | self.write_ptr = write_ptr; 74 | 75 | self.sample_buffer[write_ptr] = sample; 76 | 77 | let mut neg_max: f32 = 0.0; 78 | let mut pos_max: f32 = 0.0; 79 | 80 | for v in self.sample_buffer.iter() { 81 | if *v >= 0.0 { 82 | pos_max = pos_max.max((*v).abs()); 83 | } else { 84 | neg_max = neg_max.max((*v).abs()); 85 | } 86 | } 87 | 88 | self.last_output = (neg_max, pos_max); 89 | } 90 | 91 | self.last_output 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/sample_lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::dsp::SAtom; 6 | 7 | use hound; 8 | use std::collections::HashMap; 9 | 10 | #[derive(Debug)] 11 | pub enum SampleLoadError { 12 | LoadError(hound::Error), 13 | UnsupportedFormat, 14 | } 15 | 16 | impl From for SampleLoadError { 17 | fn from(err: hound::Error) -> Self { 18 | SampleLoadError::LoadError(err) 19 | } 20 | } 21 | 22 | const MAX_SAMPLE_LEN_S: usize = 60; // 60 seconds of audio is about 20MB 23 | 24 | /// Loads and stores samples, for use as SAtom parameters for 25 | /// nodes. 26 | pub struct SampleLibrary { 27 | loaded_samples: HashMap, 28 | max_length_s: usize, 29 | } 30 | 31 | impl SampleLibrary { 32 | pub fn new() -> Self { 33 | Self { loaded_samples: HashMap::new(), max_length_s: MAX_SAMPLE_LEN_S } 34 | } 35 | 36 | /// Synchronous/blocking loading of a sample from `path`. 37 | /// Returns an SAtom reference that you can clone and send directly 38 | /// to the sampling node of your choice. 39 | /// 40 | /// Keep in mind that blocking on I/O in the UI might not be desireable. 41 | pub fn load<'a>(&'a mut self, path: &str) -> Result<&'a SAtom, SampleLoadError> { 42 | if self.loaded_samples.get(path).is_some() { 43 | return Ok(self.loaded_samples.get(path).unwrap()); 44 | } 45 | 46 | let mut rd = match hound::WavReader::open(path) { 47 | Err(e) => return Err(SampleLoadError::LoadError(e)), 48 | Ok(rd) => rd, 49 | }; 50 | 51 | let channels = rd.spec().channels as usize; 52 | 53 | let mut v = vec![rd.spec().sample_rate as f32]; 54 | 55 | let max_sample_count = self.max_length_s * rd.spec().sample_rate as usize; 56 | 57 | match rd.spec().sample_format { 58 | hound::SampleFormat::Float => { 59 | for s in rd.samples::().step_by(channels).take(max_sample_count) { 60 | v.push(s?); 61 | } 62 | } 63 | // http://blog.bjornroche.com/2009/12/int-float-int-its-jungle-out-there.html 64 | hound::SampleFormat::Int => { 65 | for s in rd.samples::().step_by(channels).take(max_sample_count) { 66 | let s = s?; 67 | let s = s as f32 / (0x8000 as f32); 68 | v.push(s); 69 | } 70 | } 71 | }; 72 | 73 | let atom = SAtom::audio(path, std::sync::Arc::new(v)); 74 | 75 | self.loaded_samples.insert(path.to_string(), atom); 76 | Ok(self.loaded_samples.get(path).unwrap()) 77 | } 78 | } 79 | 80 | impl Default for SampleLibrary { 81 | fn default() -> Self { 82 | Self::new() 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod tests { 88 | use super::*; 89 | 90 | fn save_wav(name: &str, buf: &[f32]) { 91 | let spec = hound::WavSpec { 92 | channels: 1, 93 | sample_rate: 44100, 94 | bits_per_sample: 16, 95 | sample_format: hound::SampleFormat::Int, 96 | }; 97 | 98 | let mut writer = hound::WavWriter::create(name, spec).unwrap(); 99 | for s in buf.iter() { 100 | let amp = i16::MAX as f32; 101 | writer.write_sample((amp * s) as i16).unwrap(); 102 | } 103 | } 104 | 105 | #[test] 106 | fn check_sample_lib() { 107 | let mut sl = SampleLibrary::new(); 108 | 109 | save_wav("check_sample_lib_test.wav", &[0.1, -1.0, 1.0, -0.1]); 110 | 111 | let sat = sl.load("check_sample_lib_test.wav").unwrap(); 112 | 113 | //d// println!("sa: {:?}", sat); 114 | 115 | if let SAtom::AudioSample((_n, Some(v))) = sat { 116 | assert_eq!(v[0], 44100.0); 117 | assert_eq!((v[1] * 1000.0).round() as i32, 100); 118 | assert_eq!((v[2] * 1000.0).round() as i32, -1000); 119 | assert_eq!((v[3] * 1000.0).round() as i32, 1000); 120 | assert_eq!((v[4] * 1000.0).round() as i32, -100); 121 | } else { 122 | assert!(false); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/scope_handle.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use crate::nodes::SCOPE_SAMPLES; 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | use std::sync::Arc; 8 | use synfx_dsp::{AtomicFloat, AtomicFloatPair}; 9 | 10 | #[derive(Debug)] 11 | pub struct ScopeHandle { 12 | bufs: [Vec; 3], 13 | active: [AtomicBool; 3], 14 | offs_gain: [AtomicFloatPair; 3], 15 | threshold: (AtomicBool, AtomicFloat), 16 | } 17 | 18 | impl ScopeHandle { 19 | pub fn new_shared() -> Arc { 20 | let mut v1 = vec![]; 21 | v1.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default()); 22 | let mut v2 = vec![]; 23 | v2.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default()); 24 | let mut v3 = vec![]; 25 | v3.resize_with(SCOPE_SAMPLES, || AtomicFloatPair::default()); 26 | Arc::new(Self { 27 | bufs: [v1, v2, v3], 28 | active: [AtomicBool::new(false), AtomicBool::new(false), AtomicBool::new(false)], 29 | offs_gain: [ 30 | AtomicFloatPair::default(), 31 | AtomicFloatPair::default(), 32 | AtomicFloatPair::default(), 33 | ], 34 | threshold: (AtomicBool::new(false), AtomicFloat::default()), 35 | }) 36 | } 37 | 38 | pub fn write_oversampled(&self, buf_idx: usize, idx: usize, copies: usize, v: f32) { 39 | let end = (idx + copies).min(SCOPE_SAMPLES); 40 | for i in idx..end { 41 | self.bufs[buf_idx % 3][i % SCOPE_SAMPLES].set((v, v)); 42 | } 43 | } 44 | 45 | pub fn set_offs_gain(&self, buf_idx: usize, offs: f32, gain: f32) { 46 | self.offs_gain[buf_idx % 3].set((offs, gain)); 47 | } 48 | 49 | pub fn get_offs_gain(&self, buf_idx: usize) -> (f32, f32) { 50 | self.offs_gain[buf_idx % 3].get() 51 | } 52 | 53 | pub fn set_threshold(&self, thresh: Option) { 54 | if let Some(t) = thresh { 55 | self.threshold.1.set(t); 56 | self.threshold.0.store(true, Ordering::Relaxed); 57 | } else { 58 | self.threshold.0.store(false, Ordering::Relaxed); 59 | } 60 | } 61 | 62 | pub fn get_threshold(&self) -> Option { 63 | if self.threshold.0.load(Ordering::Relaxed) { 64 | Some(self.threshold.1.get()) 65 | } else { 66 | None 67 | } 68 | } 69 | 70 | pub fn write(&self, buf_idx: usize, idx: usize, v: (f32, f32)) { 71 | self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].set(v); 72 | } 73 | 74 | pub fn read(&self, buf_idx: usize, idx: usize) -> (f32, f32) { 75 | self.bufs[buf_idx % 3][idx % SCOPE_SAMPLES].get() 76 | } 77 | 78 | pub fn set_active_from_mask(&self, mask: u64) { 79 | self.active[0].store(mask & 0x1 > 0x0, Ordering::Relaxed); 80 | self.active[1].store(mask & 0x2 > 0x0, Ordering::Relaxed); 81 | self.active[2].store(mask & 0x4 > 0x0, Ordering::Relaxed); 82 | } 83 | 84 | pub fn is_active(&self, idx: usize) -> bool { 85 | self.active[idx % 3].load(Ordering::Relaxed) 86 | } 87 | 88 | pub fn len(&self) -> usize { 89 | SCOPE_SAMPLES 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/shared_feedback.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | //! Provides an implementation for a shared feedback buffer for the DSP node graph. 6 | //! It is used for instance by the `FbWr` and `FbRd` nodes to implement their functionality. 7 | //! 8 | //! See also [crate::NodeGlobalData] which provides the [SharedFeedback] to the nodes. 9 | 10 | use crate::dsp::MAX_BLOCK_SIZE; 11 | use std::sync::Arc; 12 | use synfx_dsp::AtomicFloat; 13 | 14 | pub const FB_DELAY_LENGTH_MS: f32 = 3.14; 15 | 16 | /// The SharedFeedback is a feedback delay buffer for the `FbWr` and `FbRd` nodes. 17 | /// 18 | /// They have a fixed delay of 3.14ms, which should be equal for all sample rates above 42kHz. 19 | /// Below that the delay might be longer to accomodate the [crate::dsp::MAX_BLOCK_SIZE]. 20 | /// 21 | /// See also [crate::NodeGlobalData] which provides the [SharedFeedback] to the DSP nodes. 22 | #[derive(Debug, Clone)] 23 | pub struct SharedFeedback { 24 | buffer: Arc>, 25 | delay_sample_count: usize, 26 | } 27 | 28 | impl SharedFeedback { 29 | pub fn new(sample_rate: f32) -> Self { 30 | let mut buf = vec![]; 31 | let delay_sample_count = ((sample_rate * FB_DELAY_LENGTH_MS) / 1000.0) as usize; 32 | 33 | // Ensure we got at least MAX_BLOCK_SIZE though! 34 | let delay_sample_count = delay_sample_count.max(MAX_BLOCK_SIZE); 35 | 36 | // Multiply by 3, to make ample space for the FB_DELAY_LENGTH_MS, 37 | // probably 2*delay_sample_count would be fine too, 38 | // but I'm anxious about off by one bugs :-) 39 | buf.resize_with(3 * delay_sample_count, || AtomicFloat::new(0.0)); 40 | 41 | Self { buffer: Arc::new(buf), delay_sample_count } 42 | } 43 | } 44 | 45 | /// This instance writes into the [SharedFeedback] buffer. 46 | /// 47 | /// Even though it's safe to have multiple writers of this will not work 48 | /// or produce any meaningful results. The goal is really, that one `FbWr` DSP node 49 | /// in the audio thread writes the buffer, and one (or multiple) `FbRd` DSP nodes 50 | /// read from that [SharedFeedback] buffer via a [SharedFeedbackReader]. 51 | #[derive(Debug, Clone)] 52 | pub struct SharedFeedbackWriter { 53 | buffer: Arc>, 54 | write_ptr: usize, 55 | delay_sample_count: usize, 56 | } 57 | 58 | impl SharedFeedbackWriter { 59 | pub fn new(sfb: &SharedFeedback) -> Self { 60 | let buffer = sfb.buffer.clone(); 61 | Self { 62 | buffer, 63 | delay_sample_count: sfb.delay_sample_count, 64 | write_ptr: sfb.delay_sample_count, 65 | } 66 | } 67 | 68 | /// Write the next sample in to the feedback buffer. 69 | /// 70 | /// Even though it's safe to have multiple writers of this will not work 71 | /// or produce any meaningful results. The goal is really, that one `FbWr` DSP node 72 | /// on the audio thread writing the buffer per buffer iteration. And then one or more 73 | /// `FbRd` DSP node reading from that buffer. 74 | pub fn write(&mut self, s: f32) { 75 | self.buffer[self.write_ptr].set(s); 76 | self.write_ptr = (self.write_ptr + 1) % self.delay_sample_count; 77 | } 78 | } 79 | 80 | /// A reader for the [SharedFeedback] buffer, used to implement the `FbRd` DSP node. 81 | /// 82 | /// Multiple readers are okay, and you can even read from the buffer across the threads. 83 | /// It is sound to read from another thread. But keep in mind, that this is not a ring buffer 84 | /// and you will get partially written buffer contents. There is also only a per sample reading 85 | /// API, that means without the current sample rate you will not know how many samples the 3.14ms 86 | /// buffer is big. 87 | #[derive(Debug, Clone)] 88 | pub struct SharedFeedbackReader { 89 | buffer: Arc>, 90 | read_ptr: usize, 91 | delay_sample_count: usize, 92 | } 93 | 94 | impl SharedFeedbackReader { 95 | pub fn new(sfb: &SharedFeedback) -> Self { 96 | Self { buffer: sfb.buffer.clone(), delay_sample_count: sfb.delay_sample_count, read_ptr: 0 } 97 | } 98 | 99 | /// Read the next sample from the buffer. Wraps around after some internal buffer 100 | /// size (that is consistent with the [SharedFeedback] buffer size). Used by `FbRd` DSP node 101 | /// to do it's functionality. 102 | pub fn read(&mut self) -> f32 { 103 | let ret = self.buffer[self.read_ptr].get(); 104 | self.read_ptr = (self.read_ptr + 1) % self.delay_sample_count; 105 | ret 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | const SMOOTHING_TIME_MS: f32 = 10.0; 6 | 7 | pub struct Smoother { 8 | slope_samples: usize, 9 | value: f32, 10 | inc: f32, 11 | target: f32, 12 | count: usize, 13 | done: bool, 14 | } 15 | 16 | impl Smoother { 17 | pub fn new() -> Self { 18 | Self { slope_samples: 0, value: 0.0, inc: 0.0, count: 0, target: 0.0, done: true } 19 | } 20 | 21 | pub fn set_sample_rate(&mut self, sr: f32) { 22 | self.slope_samples = ((sr * SMOOTHING_TIME_MS) / 1000.0).ceil() as usize; 23 | } 24 | 25 | #[inline] 26 | pub fn is_done(&self) -> bool { 27 | self.done 28 | } 29 | 30 | #[inline] 31 | #[allow(dead_code)] 32 | pub fn stop(&mut self) { 33 | self.done = true; 34 | } 35 | 36 | #[inline] 37 | pub fn set(&mut self, current: f32, target: f32) { 38 | self.value = current; 39 | self.count = self.slope_samples; 40 | self.inc = (target - current) / (self.count as f32); 41 | self.target = target; 42 | self.done = false; 43 | } 44 | 45 | #[allow(dead_code)] 46 | #[inline] 47 | pub fn current(&self) -> f32 { 48 | if self.done { 49 | self.target 50 | } else { 51 | self.value 52 | } 53 | } 54 | 55 | #[inline] 56 | pub fn next(&mut self) -> f32 { 57 | //d// println!("NEXT: count={}, value={:6.3} inc={:6.4}", 58 | //d// self.count, 59 | //d// self.value, 60 | //d// self.inc); 61 | if self.count == 0 { 62 | self.done = true; 63 | 64 | self.target 65 | } else { 66 | self.value += self.inc; 67 | self.count -= 1; 68 | self.value 69 | } 70 | } 71 | } 72 | 73 | pub struct PerfTimer { 74 | lbl: &'static str, 75 | i: std::time::Instant, 76 | off: bool, 77 | // let tb = std::time::Instant::now(); 78 | // let ta = std::time::Instant::now().duration_since(ta); 79 | // let tb = std::time::Instant::now().duration_since(tb); 80 | // println!("ta Elapsed: {:?}", ta); 81 | } 82 | 83 | impl PerfTimer { 84 | #[inline] 85 | pub fn off(mut self) -> Self { 86 | self.off = true; 87 | self 88 | } 89 | 90 | #[inline] 91 | pub fn new(lbl: &'static str) -> Self { 92 | Self { lbl, i: std::time::Instant::now(), off: false } 93 | } 94 | 95 | #[inline] 96 | pub fn print(&mut self, lbl2: &str) { 97 | if self.off { 98 | return; 99 | } 100 | 101 | let t = std::time::Instant::now().duration_since(self.i); 102 | println!("*** PERF[{}/{}] {:?}", self.lbl, lbl2, t); 103 | self.i = std::time::Instant::now(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/wblockdsp/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | /*! Contains the implementation of the visual DSP programming language named WBlockDSP. 6 | 7 | */ 8 | 9 | mod compiler; 10 | mod definition; 11 | mod language; 12 | 13 | pub use compiler::*; 14 | pub use definition::*; 15 | pub use language::*; 16 | -------------------------------------------------------------------------------- /tests/constructor_api.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | use hexodsp::build::*; 6 | use hexodsp::synth_constructor::SynthConstructor; 7 | mod common; 8 | use common::*; 9 | 10 | fn build_basic_api_test_graph() -> Out { 11 | let f = bosc(0).set().wtype(3).set().freq(440.0); 12 | let mix = mix3(0).set().ovol(0.39839).input().ch1(&f.output().sig()); 13 | let mix = mix.input().ch2(&f.output().sig()); 14 | let filt = sfilter(0).input().inp(&mix.output().sig()); 15 | out(0).input().ch1(&filt.output().sig()) 16 | } 17 | 18 | fn check_basic_api_rmsbefore_after(exec: &mut NodeExecutor, mut f: F) { 19 | let rmsmima = run_and_get_l_rms_mimax(exec, 100.0); 20 | assert_rmsmima!(rmsmima, (0.64348, -1.0887, 1.05413)); 21 | f(); 22 | let rmsmima = run_and_get_l_rms_mimax(exec, 100.0); 23 | assert_rmsmima!(rmsmima, (0.5083, -0.9676, 0.9369)); 24 | } 25 | 26 | #[test] 27 | fn check_basic_api() { 28 | let f = bosc(0).set().wtype(3).set().freq(440.0); 29 | let mix = mix3(0).set().ovol(0.39839).input().ch1(&f.output().sig()); 30 | let mix = mix.input().ch2(&f.output().sig()); 31 | let filt = sfilter(0).input().inp(&mix.output().sig()); 32 | 33 | let mut sc = SynthConstructor::new(); 34 | 35 | //d// println!("{:#?}", f.build()); 36 | //d// println!("{:#?}", filt.build()); 37 | let mut exec = sc.executor().unwrap(); 38 | 39 | // Upload the graph: 40 | sc.upload(&out(0).input().ch1(&filt.output().sig())).unwrap(); 41 | 42 | check_basic_api_rmsbefore_after(&mut exec, || { 43 | mix.set_mod().ch2(0.0, 0.7776); 44 | assert!(sc.update_params(&out(0).input().ch1(&filt.output().sig())).unwrap()); 45 | }); 46 | } 47 | 48 | #[test] 49 | fn check_basic_api_update_params() { 50 | let graph = build_basic_api_test_graph(); 51 | 52 | let mut sc = SynthConstructor::new(); 53 | sc.upload(&graph).unwrap(); 54 | 55 | let mut exec = sc.executor().unwrap(); 56 | check_basic_api_rmsbefore_after(&mut exec, || { 57 | let updated_graph = sc.update_params(&mix3(0).set_mod().ch2(0.0, 0.7776)).unwrap(); 58 | assert!(updated_graph); 59 | }); 60 | } 61 | 62 | #[test] 63 | fn check_basic_api_update_params_no_graph() { 64 | let graph = build_basic_api_test_graph(); 65 | 66 | let mut sc = SynthConstructor::new(); 67 | sc.upload(&graph).unwrap(); 68 | 69 | let mut exec = sc.executor().unwrap(); 70 | println!("START"); 71 | check_basic_api_rmsbefore_after(&mut exec, || { 72 | // Change the graph a bit: 73 | let updated_graph = sc.update_params(&mix3(0).set().vol2(0.7776)).unwrap(); 74 | assert!(!updated_graph); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /tests/hr.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeirdConstructor/HexoDSP/5454e65633815f8cc1dc3b144cca40aa34bd0a38/tests/hr.wav -------------------------------------------------------------------------------- /tests/matrix_observer.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | use std::sync::{Arc, Mutex}; 9 | 10 | struct MatrixTestRecorder { 11 | records: Mutex>, 12 | } 13 | 14 | impl MatrixObserver for MatrixTestRecorder { 15 | fn update_prop(&self, _key: &str) {} 16 | fn update_monitor(&self, _cell: &Cell) {} 17 | fn update_param(&self, _param_id: &ParamId) {} 18 | fn update_matrix(&self) {} 19 | fn update_all(&self) {} 20 | fn midi_event(&self, midi_ev: HxMidiEvent) { 21 | self.records.lock().expect("recorder lock ok").push(format!("{:?}", midi_ev)); 22 | } 23 | } 24 | 25 | #[test] 26 | fn check_matrix_observer() { 27 | let (node_conf, mut node_exec) = new_node_engine(); 28 | let mut matrix = Matrix::new(node_conf, 3, 3); 29 | let recorder = Arc::new(MatrixTestRecorder { records: Mutex::new(vec![]) }); 30 | matrix.set_observer(recorder.clone()); 31 | 32 | let mut chain = MatrixCellChain::new(CellDir::B); 33 | chain.node_out("test", "sig").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap(); 34 | matrix.sync().unwrap(); 35 | 36 | matrix.inject_midi_event(HxMidiEvent::NoteOn { channel: 1, note: 57, vel: 0.751 }); 37 | 38 | // matrix.set_param(trig_p, SAtom::param(0.0)); 39 | run_for_ms(&mut node_exec, 10.0); 40 | matrix.handle_graph_events(); 41 | 42 | let rec = 43 | recorder.records.lock().expect("lock recorder for pop").pop().expect("A record present"); 44 | 45 | assert_eq!(rec, "NoteOn { channel: 1, note: 57, vel: 0.751 }"); 46 | } 47 | -------------------------------------------------------------------------------- /tests/modamt.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_param_mod_amt_no_input() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let sin = NodeId::Sin(0); 14 | let out = NodeId::Out(0); 15 | matrix.place(0, 0, Cell::empty(sin).out(None, None, sin.out("sig"))); 16 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 17 | matrix.sync().unwrap(); 18 | 19 | matrix.set_param_modamt(sin.inp_param("freq").unwrap(), Some(0.2)).unwrap(); 20 | 21 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 22 | assert_rmsmima!(rms, (0.4999, -1.0, 1.0)); 23 | } 24 | 25 | #[test] 26 | fn check_param_mod_amt_with_input() { 27 | let (node_conf, mut node_exec) = new_node_engine(); 28 | let mut matrix = Matrix::new(node_conf, 3, 3); 29 | 30 | let sin = NodeId::Sin(0); 31 | let sin2 = NodeId::Sin(1); 32 | let out = NodeId::Out(0); 33 | matrix.place(0, 0, Cell::empty(sin2).out(None, None, sin2.out("sig"))); 34 | matrix.place( 35 | 0, 36 | 1, 37 | Cell::empty(sin).input(sin.inp("freq"), None, None).out(None, None, sin.out("sig")), 38 | ); 39 | matrix.place(0, 2, Cell::empty(out).input(out.inp("ch1"), None, None)); 40 | matrix.sync().unwrap(); 41 | 42 | matrix.set_param_modamt(sin.inp_param("freq").unwrap(), Some(0.2)).unwrap(); 43 | 44 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 45 | assert_rmsmima!(rms, (0.48997, -1.0, 1.0)); 46 | } 47 | 48 | #[test] 49 | fn check_param_mod_amt_set() { 50 | let (node_conf, mut node_exec) = new_node_engine(); 51 | let mut matrix = Matrix::new(node_conf, 3, 3); 52 | 53 | let tst = NodeId::Test(0); 54 | let amp = NodeId::Amp(0); 55 | let out = NodeId::Out(0); 56 | matrix.place(0, 0, Cell::empty(tst).out(None, None, tst.out("sig"))); 57 | matrix.place( 58 | 0, 59 | 1, 60 | Cell::empty(amp).input(amp.inp("inp"), None, None).out(None, None, amp.out("sig")), 61 | ); 62 | matrix.place(0, 2, Cell::empty(out).input(out.inp("ch1"), None, None)); 63 | pset_n(&mut matrix, tst, "p", 1.0); 64 | matrix.sync().unwrap(); 65 | 66 | // Run with no modulation 67 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 68 | assert_rmsmima!(rms, (1.0, 1.0, 1.0)); 69 | 70 | // Enable modulation 71 | matrix.set_param_modamt(amp.inp_param("inp").unwrap(), Some(0.2)).unwrap(); 72 | 73 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 74 | assert_rmsmima!(rms, (0.04, 0.2, 0.2)); 75 | 76 | // Change modulation 77 | matrix.set_param_modamt(amp.inp_param("inp").unwrap(), Some(0.1)).unwrap(); 78 | 79 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 80 | assert_rmsmima!(rms, (0.01, 0.1, 0.1)); 81 | 82 | // Remove modulation 83 | matrix.set_param_modamt(amp.inp_param("inp").unwrap(), None).unwrap(); 84 | 85 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 86 | assert_rmsmima!(rms, (1.0, 1.0, 1.0)); 87 | } 88 | 89 | #[test] 90 | fn check_param_mod_amt_set_bipol() { 91 | let (node_conf, mut node_exec) = new_node_engine(); 92 | let mut matrix = Matrix::new(node_conf, 3, 3); 93 | 94 | let tst = NodeId::Test(0); 95 | let amp = NodeId::Amp(0); 96 | let out = NodeId::Out(0); 97 | matrix.place(0, 0, Cell::empty(tst).out(None, None, tst.out("sig"))); 98 | matrix.place( 99 | 0, 100 | 1, 101 | Cell::empty(amp).input(amp.inp("inp"), None, None).out(None, None, amp.out("sig")), 102 | ); 103 | matrix.place(0, 2, Cell::empty(out).input(out.inp("ch1"), None, None)); 104 | pset_n(&mut matrix, tst, "p", -1.0); 105 | matrix.sync().unwrap(); 106 | 107 | // Run with no modulation 108 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 109 | assert_rmsmima!(rms, (1.0, -1.0, -1.0)); 110 | 111 | // Enable modulation 112 | matrix.set_param_modamt(amp.inp_param("inp").unwrap(), Some(0.2)).unwrap(); 113 | 114 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 115 | assert_rmsmima!(rms, (0.04, -0.2, -0.2)); 116 | 117 | // Change modulation 118 | matrix.set_param_modamt(amp.inp_param("inp").unwrap(), Some(0.1)).unwrap(); 119 | 120 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 121 | assert_rmsmima!(rms, (0.01, -0.1, -0.1)); 122 | 123 | // Remove modulation 124 | matrix.set_param_modamt(amp.inp_param("inp").unwrap(), None).unwrap(); 125 | 126 | let rms = run_and_get_first_rms_mimax(&mut node_exec, 50.0); 127 | assert_rmsmima!(rms, (1.0, -1.0, -1.0)); 128 | } 129 | -------------------------------------------------------------------------------- /tests/node_allp.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_allp() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 4, 4); 12 | 13 | let test = NodeId::Test(0); 14 | let ap = NodeId::AllP(0); 15 | let out = NodeId::Out(0); 16 | matrix.place(0, 0, Cell::empty(test).out(None, None, test.out("tsig"))); 17 | matrix.place( 18 | 0, 19 | 1, 20 | Cell::empty(ap).input(ap.inp("inp"), None, None).out(None, None, ap.out("sig")), 21 | ); 22 | matrix.place(0, 2, Cell::empty(out).input(out.inp("ch1"), None, None).out(None, None, None)); 23 | matrix.place(1, 0, Cell::empty(test).out(None, None, test.out("tsig"))); 24 | matrix.place(1, 1, Cell::empty(out).input(out.inp("ch2"), None, None).out(None, None, None)); 25 | pset_d(&mut matrix, ap, "time", 3.0); 26 | matrix.sync().unwrap(); 27 | 28 | pset_s(&mut matrix, test, "trig", 1); 29 | 30 | let res = run_for_ms(&mut node_exec, 20.0); 31 | 32 | // the original signal on ch2: 2ms trigger up: 33 | let mut v = vec![1.0; (2.0 * 44.1_f32).ceil() as usize]; 34 | v.append(&mut vec![0.0; (18.0 * 44.1_f32).ceil() as usize]); 35 | assert_vec_feq!(res.1, v); 36 | 37 | // now signal on ch1 from the allpass: 38 | // starts with original signal * -0.7 39 | let mut v = vec![0.7; (2.0 * 44.1_f32).ceil() as usize]; 40 | // silence for 1ms, which is the internal delay of the allpass 41 | v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 1]); 42 | 43 | // allpass feedback of the original signal for 2ms: 44 | // XXX: the smearing before and after the allpass is due to the 45 | // cubic interpolation! 46 | v.append(&mut vec![-0.03748519, 0.37841395, 0.5260659]); 47 | v.append(&mut vec![0.51; (2.0 * 44.1_f32).ceil() as usize - 3]); 48 | // 1ms allpass silence like before: 49 | v.append(&mut vec![0.54748523, 0.13158606, -0.016065884]); 50 | v.append(&mut vec![0.0; (1.0 * 44.1_f32).floor() as usize - 4]); 51 | 52 | // 2ms the previous 1.0 * 0.7 fed back into the filter, 53 | // including even more smearing due to cubic interpolation: 54 | v.append(&mut vec![ 55 | -0.0019286226, 56 | 0.04086761, 57 | -0.1813516, 58 | -0.35157663, 59 | -0.36315754, 60 | -0.35664573, 61 | ]); 62 | v.append(&mut vec![-0.357; (2.0 * 44.1_f32).floor() as usize - 5]); 63 | v.append(&mut vec![ 64 | -0.3550714, 65 | -0.39786762, 66 | -0.1756484, 67 | -0.005423375, 68 | 0.006157537, 69 | -0.00035427228, 70 | ]); 71 | v.append(&mut vec![0.0; 10]); 72 | 73 | //d// println!("res={:?}", res.1); 74 | assert_vec_feq!(res.0, v); 75 | } 76 | -------------------------------------------------------------------------------- /tests/node_bosc.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_bosc_1() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 4, 4); 12 | 13 | let bosc = NodeId::BOsc(0); 14 | let out = NodeId::Out(0); 15 | matrix.place(0, 0, Cell::empty(bosc).out(None, None, bosc.out("sig"))); 16 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None).out(None, None, None)); 17 | pset_s(&mut matrix, bosc, "wtype", 0); // Sine 18 | pset_d(&mut matrix, bosc, "freq", 220.0); 19 | matrix.sync().unwrap(); 20 | 21 | run_for_ms(&mut node_exec, 10.0); 22 | let fft = run_and_get_fft4096_now(&mut node_exec, 20); 23 | assert_eq!(fft[0].0, 194); 24 | assert_eq!(fft[1].0, 205); 25 | assert_eq!(fft[2].0, 215); 26 | assert_eq!(fft[3].0, 226); 27 | assert_eq!(fft[4].0, 237); 28 | assert_eq!(fft[5].0, 248); 29 | 30 | pset_s(&mut matrix, bosc, "wtype", 1); // Triangle 31 | let fft = run_and_get_fft4096_now(&mut node_exec, 20); 32 | assert_eq!(fft[0].0, 194); 33 | assert_eq!(fft[1].0, 205); 34 | assert_eq!(fft[2].0, 215); 35 | assert_eq!(fft[3].0, 226); 36 | assert_eq!(fft[4].0, 237); 37 | assert_eq!(fft[5].0, 646); 38 | assert_eq!(fft[6].0, 657); 39 | assert_eq!(fft[7].0, 668); 40 | assert_eq!(fft[8].0, 1098); 41 | assert_eq!(fft[9].0, 1109); 42 | 43 | pset_s(&mut matrix, bosc, "wtype", 2); // Saw 44 | let fft = run_and_get_fft4096_now(&mut node_exec, 120); 45 | assert_eq!(fft[0].0, 205); 46 | assert_eq!(fft[1].0, 215); 47 | assert_eq!(fft[2].0, 226); 48 | assert_eq!(fft[3].0, 431); 49 | assert_eq!(fft[4].0, 441); 50 | assert_eq!(fft[5].0, 452); 51 | assert_eq!(fft[6].0, 657); 52 | assert_eq!(fft[7].0, 668); 53 | assert_eq!(fft[8].0, 883); 54 | assert_eq!(fft[9].0, 1098); 55 | 56 | pset_s(&mut matrix, bosc, "wtype", 3); // Pulse 57 | pset_n(&mut matrix, bosc, "pw", 0.0); // Pulse width no mod 58 | run_for_ms(&mut node_exec, 10.0); 59 | let fft = run_and_get_fft4096_now(&mut node_exec, 120); 60 | assert_eq!(fft[0].0, 205); 61 | assert_eq!(fft[1].0, 215); 62 | assert_eq!(fft[2].0, 226); 63 | assert_eq!(fft[3].0, 237); 64 | assert_eq!(fft[4].0, 646); 65 | assert_eq!(fft[5].0, 657); 66 | assert_eq!(fft[6].0, 668); 67 | assert_eq!(fft[7].0, 1098); 68 | assert_eq!(fft[8].0, 1109); 69 | assert_eq!(fft[9].0, 1540); 70 | assert_eq!(fft[10].0, 1981); 71 | 72 | pset_n(&mut matrix, bosc, "pw", 1.0); // Pulse width mod 73 | run_for_ms(&mut node_exec, 10.0); 74 | let fft = run_and_get_fft4096_now(&mut node_exec, 120); 75 | assert_eq!(fft[0].0, 0); // DC component from short pulse 76 | assert_eq!(fft[1].0, 11); // but full amplitude! 77 | assert_eq!(fft[2].0, 215); 78 | assert_eq!(fft[3].0, 226); 79 | assert_eq!(fft[4].0, 431); 80 | assert_eq!(fft[5].0, 441); 81 | assert_eq!(fft[6].0, 452); 82 | assert_eq!(fft[7].0, 657); 83 | assert_eq!(fft[8].0, 668); 84 | assert_eq!(fft[9].0, 872); 85 | assert_eq!(fft[10].0, 883); 86 | assert_eq!(fft[11].0, 1098); 87 | assert_eq!(fft[12].0, 1109); 88 | assert_eq!(fft[13].0, 1314); 89 | assert_eq!(fft[14].0, 1324); 90 | assert_eq!(fft[15].0, 1540); 91 | 92 | // Old test code that worked with the pulse width DC compensation: 93 | // assert_eq!(fft[0].0, 215); 94 | // assert_eq!(fft[1].0, 226); 95 | // assert_eq!(fft[2].0, 431); 96 | // assert_eq!(fft[3].0, 441); 97 | // assert_eq!(fft[4].0, 452); 98 | // assert_eq!(fft[5].0, 657); 99 | // assert_eq!(fft[6].0, 668); 100 | // assert_eq!(fft[7].0, 872); 101 | // assert_eq!(fft[8].0, 883); 102 | // assert_eq!(fft[9].0, 1098); 103 | // assert_eq!(fft[10].0, 1109); 104 | // assert_eq!(fft[11].0, 1314); 105 | // assert_eq!(fft[12].0, 1324); 106 | // assert_eq!(fft[13].0, 1540); 107 | } 108 | 109 | #[test] 110 | fn check_node_bosc_det() { 111 | let (node_conf, mut node_exec) = new_node_engine(); 112 | let mut matrix = Matrix::new(node_conf, 4, 4); 113 | 114 | let bosc = NodeId::BOsc(0); 115 | let out = NodeId::Out(0); 116 | matrix.place(0, 0, Cell::empty(bosc).out(None, None, bosc.out("sig"))); 117 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None).out(None, None, None)); 118 | pset_s(&mut matrix, bosc, "wtype", 3); // Pulse 119 | pset_n(&mut matrix, bosc, "pw", 0.0); // Pulse width no mod 120 | pset_d(&mut matrix, bosc, "freq", 220.0); 121 | matrix.sync().unwrap(); 122 | 123 | let fft = run_and_get_fft4096_now(&mut node_exec, 120); 124 | // println!("TO {:?}", fft); 125 | assert_eq!(fft[0].0, 205); 126 | assert_eq!(fft[1].0, 215); 127 | assert_eq!(fft[2].0, 226); 128 | assert_eq!(fft[3].0, 237); 129 | assert_eq!(fft[4].0, 646); 130 | assert_eq!(fft[5].0, 657); 131 | assert_eq!(fft[6].0, 668); 132 | assert_eq!(fft[7].0, 1098); 133 | assert_eq!(fft[8].0, 1109); 134 | assert_eq!(fft[9].0, 1540); 135 | assert_eq!(fft[10].0, 1981); 136 | 137 | pset_d_wait(&mut matrix, &mut node_exec, bosc, "freq", 1000.0); 138 | let fft = run_and_get_fft4096_now(&mut node_exec, 200); 139 | // println!("TO {:?}", fft); 140 | assert_eq!(fft[0].0, 991); 141 | assert_eq!(fft[1].0, 1001); 142 | assert_eq!(fft[2].0, 1012); 143 | assert_eq!(fft[3].0, 2993); 144 | assert_eq!(fft[4].0, 3004); 145 | assert_eq!(fft[5].0, 4996); 146 | 147 | pset_n_wait(&mut matrix, &mut node_exec, bosc, "det", 0.1); 148 | let fft = run_and_get_fft4096_now(&mut node_exec, 200); 149 | // println!("TO {:?}", fft); 150 | assert_eq!(fft[0].0, 1992); 151 | assert_eq!(fft[1].0, 2003); 152 | assert_eq!(fft[2].0, 2013); 153 | assert_eq!(fft[3].0, 5997); 154 | assert_eq!(fft[4].0, 6008); 155 | assert_eq!(fft[5].0, 10002); 156 | } 157 | -------------------------------------------------------------------------------- /tests/node_code.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | use hexodsp::wblockdsp::BlockFun; 9 | 10 | fn setup() -> (Matrix, NodeExecutor) { 11 | let (node_conf, node_exec) = new_node_engine(); 12 | let mut matrix = Matrix::new(node_conf, 3, 3); 13 | 14 | let mut chain = MatrixCellChain::new(CellDir::B); 15 | chain 16 | .node_out("code", "sig1") 17 | .set_denorm("in1", 0.5) 18 | .set_denorm("in2", -0.6) 19 | .node_inp("out", "ch1") 20 | .place(&mut matrix, 0, 0) 21 | .unwrap(); 22 | matrix.sync().unwrap(); 23 | 24 | (matrix, node_exec) 25 | } 26 | 27 | fn put_n(bf: &mut BlockFun, a: usize, x: i64, y: i64, s: &str) { 28 | bf.instanciate_at(a, x, y, s, None).expect("no put error"); 29 | } 30 | 31 | fn put_v(bf: &mut BlockFun, a: usize, x: i64, y: i64, s: &str, v: &str) { 32 | bf.instanciate_at(a, x, y, s, Some(v.to_string())).expect("no put error"); 33 | } 34 | 35 | #[test] 36 | fn check_node_code_1() { 37 | let (mut matrix, mut node_exec) = setup(); 38 | 39 | let block_fun = matrix.get_block_function(0).expect("block fun exists"); 40 | { 41 | let mut block_fun = block_fun.lock().expect("matrix lock"); 42 | put_v(&mut block_fun, 0, 0, 1, "value", "0.3"); 43 | put_v(&mut block_fun, 0, 1, 1, "set", "&sig1"); 44 | } 45 | 46 | matrix.check_block_function(0).expect("no compile error"); 47 | 48 | let res = run_for_ms(&mut node_exec, 25.0); 49 | assert_decimated_feq!(res.0, 50, vec![0.3; 10]); 50 | } 51 | 52 | #[test] 53 | fn check_node_code_state() { 54 | let (mut matrix, mut node_exec) = setup(); 55 | 56 | let block_fun = matrix.get_block_function(0).expect("block fun exists"); 57 | { 58 | let mut block_fun = block_fun.lock().expect("matrix lock"); 59 | put_v(&mut block_fun, 0, 0, 3, "value", "220.0"); 60 | put_n(&mut block_fun, 0, 1, 2, "phase"); 61 | block_fun.shift_port(0, 1, 2, 0, false); // move reset up 62 | block_fun.shift_port(0, 1, 2, 0, true); // move output down 63 | put_v(&mut block_fun, 0, 1, 4, "value", "2.0"); 64 | put_n(&mut block_fun, 0, 2, 3, "*"); 65 | put_n(&mut block_fun, 0, 3, 2, "-"); 66 | put_v(&mut block_fun, 0, 2, 2, "value", "1.0"); 67 | put_v(&mut block_fun, 0, 4, 2, "set", "&sig1"); 68 | } 69 | 70 | matrix.check_block_function(0).expect("no compile error"); 71 | 72 | let fft = run_and_get_fft4096_now(&mut node_exec, 50); 73 | // Aliasing sawtooth I expect: 74 | assert_eq!( 75 | fft, 76 | vec![ 77 | (205, 133), 78 | (215, 576), 79 | (226, 527), 80 | (237, 90), 81 | (431, 195), 82 | (441, 322), 83 | (452, 131), 84 | (646, 61), 85 | (657, 204), 86 | (668, 157), 87 | (872, 113), 88 | (883, 155), 89 | (894, 51), 90 | (1098, 127), 91 | (1109, 82), 92 | (1314, 85), 93 | (1324, 98), 94 | (1540, 93), 95 | (1755, 70), 96 | (1766, 67), 97 | (1981, 72), 98 | (2196, 60), 99 | (2422, 57), 100 | (2638, 52) 101 | ] 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /tests/node_comb.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::*; 3 | 4 | #[test] 5 | fn check_node_comb_1() { 6 | let (node_conf, mut node_exec) = new_node_engine(); 7 | let mut matrix = Matrix::new(node_conf, 3, 3); 8 | 9 | let noise_1 = NodeId::Noise(0); 10 | let comb_1 = NodeId::Comb(0); 11 | let out_1 = NodeId::Out(0); 12 | matrix.place( 13 | 0, 14 | 1, 15 | Cell::empty(noise_1).input(None, None, None).out(None, noise_1.out("sig"), None), 16 | ); 17 | matrix.place( 18 | 1, 19 | 1, 20 | Cell::empty(comb_1).input(None, comb_1.inp("inp"), None).out(None, comb_1.out("sig"), None), 21 | ); 22 | matrix.place( 23 | 2, 24 | 2, 25 | Cell::empty(out_1).input(None, out_1.inp("ch1"), out_1.inp("ch1")).out(None, None, None), 26 | ); 27 | 28 | pset_n(&mut matrix, comb_1, "g", 0.950); 29 | pset_n(&mut matrix, comb_1, "time", 0.014); 30 | matrix.sync().unwrap(); 31 | 32 | let fft = run_and_get_avg_fft4096_now(&mut node_exec, 180); 33 | assert_eq!( 34 | fft, 35 | vec![ 36 | (0, 216), 37 | (11, 219), 38 | (22, 210), 39 | (3122, 189), 40 | (3133, 190), 41 | (6266, 181), 42 | (9421, 210), 43 | (9432, 193), 44 | (12565, 224), 45 | (12575, 234) 46 | ] 47 | ); 48 | 49 | pset_n_wait(&mut matrix, &mut node_exec, comb_1, "time", 0.030); 50 | 51 | let fft = run_and_get_avg_fft4096_now(&mut node_exec, 180); 52 | assert_eq!(fft, vec![(980, 219), (3908, 225), (5868, 203), (6848, 195)]); 53 | 54 | pset_n_wait(&mut matrix, &mut node_exec, comb_1, "g", 0.999); 55 | let fft = run_and_get_avg_fft4096_now(&mut node_exec, 1000); 56 | assert_eq!( 57 | fft, 58 | vec![(0, 1979), (11, 1002), (980, 1245), (1960, 1144), (2929, 1569), (2939, 1545)] 59 | ); 60 | } 61 | 62 | #[test] 63 | fn check_node_comb_time() { 64 | let (node_conf, mut node_exec) = new_node_engine(); 65 | let mut matrix = Matrix::new(node_conf, 3, 3); 66 | 67 | let test = NodeId::Test(0); 68 | let comb_1 = NodeId::Comb(0); 69 | let out_1 = NodeId::Out(0); 70 | matrix.place(0, 1, Cell::empty(test).input(None, None, None).out(None, test.out("tsig"), None)); 71 | matrix.place( 72 | 1, 73 | 1, 74 | Cell::empty(comb_1).input(None, comb_1.inp("inp"), None).out(None, comb_1.out("sig"), None), 75 | ); 76 | matrix.place( 77 | 2, 78 | 2, 79 | Cell::empty(out_1).input(None, out_1.inp("ch1"), out_1.inp("ch1")).out(None, None, None), 80 | ); 81 | 82 | pset_n(&mut matrix, comb_1, "g", 0.75); 83 | pset_d(&mut matrix, comb_1, "time", 100.0); 84 | matrix.sync().unwrap(); 85 | 86 | pset_n(&mut matrix, test, "trig", 1.0); 87 | 88 | let (out_l, _) = run_for_ms(&mut node_exec, 300.0); 89 | let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], 25.0); 90 | assert_minmax_of_rms!(rms_mimax[0], (0.0, 1.0)); 91 | assert_minmax_of_rms!(rms_mimax[1], (0.0, 0.0)); 92 | assert_minmax_of_rms!(rms_mimax[2], (0.0, 0.0)); 93 | assert_minmax_of_rms!(rms_mimax[3], (0.0, 0.0)); 94 | assert_minmax_of_rms!(rms_mimax[4], (-0.0001, 0.7502)); 95 | assert_minmax_of_rms!(rms_mimax[5], (0.0, 0.0)); 96 | assert_minmax_of_rms!(rms_mimax[6], (0.0, 0.0)); 97 | assert_minmax_of_rms!(rms_mimax[7], (0.0, 0.0)); 98 | assert_minmax_of_rms!(rms_mimax[8], (-0.00027, 0.56277)); 99 | } 100 | -------------------------------------------------------------------------------- /tests/node_cqnt.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | fn setup_cqnt(matrix: &mut Matrix, trig_out: bool) { 9 | let cqnt = NodeId::CQnt(0); 10 | let out = NodeId::Out(0); 11 | 12 | if trig_out { 13 | matrix.place(0, 0, Cell::empty(cqnt).out(None, None, cqnt.out("t"))); 14 | } else { 15 | matrix.place(0, 0, Cell::empty(cqnt).out(None, None, cqnt.out("sig"))); 16 | } 17 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 18 | 19 | pset_s(matrix, cqnt, "keys", 0); 20 | matrix.sync().unwrap(); 21 | } 22 | 23 | #[test] 24 | fn check_node_cqnt_1() { 25 | init_test!(matrix, node_exec, 3); 26 | setup_cqnt(matrix, false); 27 | 28 | let cqnt = NodeId::CQnt(0); 29 | 30 | let mut v = vec![]; 31 | for i in 0..20 { 32 | let x = i as f32 / 20.0; 33 | 34 | pset_n_wait(matrix, node_exec, cqnt, "inp", x); 35 | let res = run_for_ms(node_exec, 1.0); 36 | v.push(res.0[0]); 37 | } 38 | 39 | assert_vec_feq!( 40 | v, 41 | vec![ 42 | -0.075, -0.075, -0.0666, -0.0666, -0.0583, -0.05, -0.05, -0.04166, -0.04166, -0.0333, 43 | -0.0250, -0.0250, -0.0166, -0.0166, -0.0083, 0.0, 0.0, 0.0083, 0.0083, 0.0166 44 | ] 45 | ); 46 | } 47 | 48 | #[test] 49 | fn check_node_cqnt_one_key() { 50 | init_test!(matrix, node_exec, 3); 51 | setup_cqnt(matrix, false); 52 | 53 | let cqnt = NodeId::CQnt(0); 54 | 55 | pset_s(matrix, cqnt, "keys", 0b100); 56 | 57 | let mut v = vec![]; 58 | for i in 0..10 { 59 | let x = i as f32 / 10.0; 60 | 61 | pset_n_wait(matrix, node_exec, cqnt, "inp", x); 62 | let res = run_for_ms(node_exec, 1.0); 63 | v.push(res.0[0]); 64 | } 65 | 66 | assert_vec_feq!( 67 | v, 68 | vec![-0.05833, -0.05833, -0.05833, -0.05833, -0.05833, -0.05833, -0.05833, -0.05833,] 69 | ); 70 | } 71 | 72 | #[test] 73 | fn check_node_cqnt_one_key_oct() { 74 | init_test!(matrix, node_exec, 3); 75 | setup_cqnt(matrix, false); 76 | 77 | let cqnt = NodeId::CQnt(0); 78 | 79 | pset_s(matrix, cqnt, "keys", 0b100); 80 | pset_s(matrix, cqnt, "omin", 1); 81 | pset_s(matrix, cqnt, "omax", 1); 82 | 83 | let mut v = vec![]; 84 | for i in 0..10 { 85 | let x = i as f32 / 10.0; 86 | 87 | pset_n_wait(matrix, node_exec, cqnt, "inp", x); 88 | let res = run_for_ms(node_exec, 1.0); 89 | v.push(res.0[0]); 90 | } 91 | 92 | assert_vec_feq!( 93 | v, 94 | vec![-0.1583, -0.1583, -0.1583, -0.1583, -0.0583, -0.0583, -0.0583, 0.0416, 0.0416, 0.0416] 95 | ); 96 | } 97 | 98 | #[test] 99 | fn check_node_cqnt_min_max_oct() { 100 | init_test!(matrix, node_exec, 3); 101 | setup_cqnt(matrix, false); 102 | 103 | let cqnt = NodeId::CQnt(0); 104 | 105 | pset_s(matrix, cqnt, "keys", 0b11100); 106 | pset_s(matrix, cqnt, "omin", 1); 107 | pset_s(matrix, cqnt, "omax", 1); 108 | 109 | let mut v = vec![]; 110 | for i in 0..20 { 111 | let x = i as f32 / 20.0; 112 | 113 | pset_n_wait(matrix, node_exec, cqnt, "inp", x); 114 | let res = run_for_ms(node_exec, 1.0); 115 | v.push(res.0[0]); 116 | } 117 | 118 | assert_vec_feq!( 119 | v, 120 | vec![ 121 | -0.1583, -0.1583, -0.1583, -0.15, -0.15, -0.1416, -0.1416, -0.0583, -0.0583, -0.05, 122 | -0.05, -0.05, -0.0416, -0.0416, 0.0416, 0.0416, 0.0499, 0.0499, 0.0583, 0.0583 123 | ] 124 | ); 125 | } 126 | 127 | #[test] 128 | fn check_node_cqnt_min_max_oct_trig_out() { 129 | init_test!(matrix, node_exec, 3); 130 | setup_cqnt(matrix, true); 131 | 132 | let cqnt = NodeId::CQnt(0); 133 | 134 | pset_s(matrix, cqnt, "keys", 0b11100); 135 | pset_s(matrix, cqnt, "omin", 1); 136 | pset_s(matrix, cqnt, "omax", 1); 137 | 138 | let mut v = vec![]; 139 | for i in 0..100 { 140 | let x = i as f32 / 100.0; 141 | 142 | pset_n(matrix, cqnt, "inp", x); 143 | let res = run_for_ms(node_exec, 10.0); 144 | v.extend_from_slice(&res.0[..]); 145 | } 146 | 147 | let idxs_big = collect_signal_changes(&v[..], 50); 148 | 149 | assert_eq!( 150 | idxs_big, 151 | vec![ 152 | (0, 100), 153 | (5341, 100), 154 | (10240, 100), 155 | (15140, 100), 156 | (20041, 100), 157 | (24941, 100), 158 | (29841, 100), 159 | (34740, 100), 160 | (39640, 100) 161 | ] 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /tests/node_exta.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | struct MyParams {} 9 | 10 | impl hexodsp::nodes::ExternalParams for MyParams { 11 | fn a1(&self) -> f32 { 12 | 0.23 13 | } 14 | fn a2(&self) -> f32 { 15 | 0.44 16 | } 17 | fn a3(&self) -> f32 { 18 | -0.33 19 | } 20 | } 21 | 22 | #[test] 23 | fn check_node_exta() { 24 | let (node_conf, mut node_exec) = new_node_engine(); 25 | let mut matrix = Matrix::new(node_conf, 3, 3); 26 | 27 | let myparams = std::sync::Arc::new(MyParams {}); 28 | 29 | let mut chain = MatrixCellChain::new(CellDir::B); 30 | chain.node_out("exta", "sig1").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap(); 31 | let mut chain = MatrixCellChain::new(CellDir::B); 32 | chain.node_out("exta", "sig3").node_inp("out", "ch2").place(&mut matrix, 1, 0).unwrap(); 33 | matrix.sync().unwrap(); 34 | 35 | node_exec.set_external_params(myparams); 36 | 37 | let (ch1, ch2) = node_exec.test_run(0.1, false, &[]); 38 | assert_decimated_feq!(ch1, 10, vec![0.23; 100]); 39 | assert_decimated_feq!(ch2, 10, vec![-0.33; 100]); 40 | 41 | node_pset_n(&mut matrix, "exta", 0, "atv1", -1.0); 42 | node_pset_n(&mut matrix, "exta", 0, "atv3", 0.5); 43 | 44 | let (ch1, ch2) = node_exec.test_run(0.1, false, &[]); 45 | assert_decimated_feq!( 46 | ch1, 47 | 80, 48 | vec![ 49 | 0.22895692, 50 | 0.14551038, 51 | 0.062063817, 52 | -0.021382917, 53 | -0.10482957, 54 | -0.18827613, 55 | -0.23, 56 | -0.23, 57 | -0.23, 58 | -0.23, 59 | -0.23 60 | ] 61 | ); 62 | assert_decimated_feq!( 63 | ch2, 64 | 80, 65 | vec![ 66 | -0.32962584, 67 | -0.29969355, 68 | -0.26976123, 69 | -0.23982893, 70 | -0.20989662, 71 | -0.17996432, 72 | -0.165, 73 | -0.165, 74 | -0.165, 75 | -0.165, 76 | -0.165, 77 | -0.165, 78 | -0.165 79 | ] 80 | ); 81 | } 82 | 83 | #[test] 84 | fn check_node_exta_slew() { 85 | let (node_conf, mut node_exec) = new_node_engine(); 86 | let mut matrix = Matrix::new(node_conf, 3, 3); 87 | 88 | let myparams = std::sync::Arc::new(MyParams {}); 89 | 90 | let mut chain = MatrixCellChain::new(CellDir::B); 91 | chain 92 | .node_out("exta", "sig1") 93 | .set_denorm("slew", 40.0) 94 | .node_inp("out", "ch1") 95 | .place(&mut matrix, 0, 0) 96 | .unwrap(); 97 | let mut chain = MatrixCellChain::new(CellDir::B); 98 | chain 99 | .node_out("exta", "sig3") 100 | .set_denorm("slew", 40.0) 101 | .node_inp("out", "ch2") 102 | .place(&mut matrix, 1, 0) 103 | .unwrap(); 104 | matrix.sync().unwrap(); 105 | 106 | node_exec.set_external_params(myparams); 107 | 108 | let (ch1, ch2) = node_exec.test_run(0.1, false, &[]); 109 | assert_decimated_feq!( 110 | ch1, 111 | 80, 112 | vec![ 113 | 0.00056689343, 114 | 0.045918357, 115 | 0.09126975, 116 | 0.13662128, 117 | 0.18197326, 118 | 0.22732525, 119 | 0.23, 120 | 0.23, 121 | 0.23, 122 | 0.23, 123 | 0.23 124 | ] 125 | ); 126 | assert_decimated_feq!( 127 | ch2, 128 | 80, 129 | vec![ 130 | -0.00056689343, 131 | -0.045918357, 132 | -0.09126975, 133 | -0.13662128, 134 | -0.18197326, 135 | -0.22732525, 136 | -0.2726772, 137 | -0.3180292, 138 | -0.33, 139 | -0.33, 140 | -0.33 141 | ] 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /tests/node_inp.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_inp_1() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let mut chain = MatrixCellChain::new(CellDir::B); 14 | chain.node_out("inp", "sig1").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap(); 15 | let mut chain = MatrixCellChain::new(CellDir::B); 16 | chain.node_out("inp", "sig2").node_inp("out", "ch2").place(&mut matrix, 1, 0).unwrap(); 17 | matrix.sync().unwrap(); 18 | 19 | let (out_l, out_r) = node_exec.test_run_input(&[0.44; 512], false, &[]); 20 | assert!(out_l.len() == 512); 21 | assert_decimated_feq!(out_l, 1, vec![0.44; 512]); 22 | 23 | assert!(out_r.len() == 512); 24 | assert_decimated_feq!(out_r, 1, vec![0.44; 512]); 25 | } 26 | 27 | #[test] 28 | fn check_node_inp_vol() { 29 | let (node_conf, mut node_exec) = new_node_engine(); 30 | let mut matrix = Matrix::new(node_conf, 3, 3); 31 | 32 | let mut chain = MatrixCellChain::new(CellDir::B); 33 | chain 34 | .node_out("inp", "sig1") 35 | .set_denorm("vol", 0.5) 36 | .node_inp("out", "ch1") 37 | .place(&mut matrix, 0, 0) 38 | .unwrap(); 39 | let mut chain = MatrixCellChain::new(CellDir::B); 40 | chain 41 | .node_out("inp", "sig2") 42 | .set_denorm("vol", 0.5) 43 | .node_inp("out", "ch2") 44 | .place(&mut matrix, 1, 0) 45 | .unwrap(); 46 | matrix.sync().unwrap(); 47 | 48 | let (out_l, out_r) = node_exec.test_run_input(&[0.44; 512], false, &[]); 49 | assert!(out_l.len() == 512); 50 | assert_decimated_feq!(out_l, 1, vec![0.22; 512]); 51 | 52 | assert!(out_r.len() == 512); 53 | assert_decimated_feq!(out_r, 1, vec![0.22; 512]); 54 | } 55 | -------------------------------------------------------------------------------- /tests/node_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_map() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let map = NodeId::Map(0); 14 | let out = NodeId::Out(0); 15 | matrix.place(0, 0, Cell::empty(map).out(None, None, map.out("sig"))); 16 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 17 | pset_n(&mut matrix, map, "inp", 0.5); 18 | pset_n(&mut matrix, map, "atv", 0.5); // => 0.25 19 | pset_n(&mut matrix, map, "offs", 0.1); // => 0.35 20 | 21 | pset_n(&mut matrix, map, "imin", 0.0); 22 | pset_n(&mut matrix, map, "imax", 0.7); // middle is at 0.35 23 | 24 | pset_n(&mut matrix, map, "min", -1.0); 25 | pset_n(&mut matrix, map, "max", -0.5); // we expect -0.75 26 | matrix.sync().unwrap(); 27 | 28 | let res = run_for_ms(&mut node_exec, 10.0); 29 | assert_decimated_feq!(res.0, 10, vec![-0.75; 50]); 30 | } 31 | 32 | #[test] 33 | fn check_node_map_clip() { 34 | let (node_conf, mut node_exec) = new_node_engine(); 35 | let mut matrix = Matrix::new(node_conf, 3, 3); 36 | 37 | let map = NodeId::Map(0); 38 | let out = NodeId::Out(0); 39 | matrix.place(0, 0, Cell::empty(map).out(None, None, map.out("sig"))); 40 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 41 | pset_n(&mut matrix, map, "inp", 0.5); 42 | pset_n(&mut matrix, map, "atv", 0.5); // => 0.25 43 | pset_n(&mut matrix, map, "offs", 0.80); // => 1.05 44 | 45 | pset_n(&mut matrix, map, "imin", 0.0); 46 | pset_n(&mut matrix, map, "imax", 0.7); // 1.05 is at 0.7 + 0.35 47 | 48 | pset_n(&mut matrix, map, "min", 0.0); 49 | pset_n(&mut matrix, map, "max", 0.1); // with 50% over imax => 0.15 50 | matrix.sync().unwrap(); 51 | 52 | let res = run_for_ms(&mut node_exec, 10.0); 53 | assert_decimated_feq!(res.0, 10, vec![0.15; 50]); 54 | 55 | pset_s(&mut matrix, map, "clip", 1); // should clip at 0.1 56 | run_for_ms(&mut node_exec, 30.0); 57 | 58 | let res = run_for_ms(&mut node_exec, 10.0); 59 | assert_decimated_feq!(res.0, 10, vec![0.1; 50]); 60 | } 61 | -------------------------------------------------------------------------------- /tests/node_midicc.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_midicc_test_receive() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let mut chain = MatrixCellChain::new(CellDir::B); 14 | chain 15 | .node_out("midicc", "sig1") 16 | .set_atom("cc1", SAtom::setting(10)) 17 | .node_inp("out", "ch1") 18 | .place(&mut matrix, 0, 0) 19 | .unwrap(); 20 | let mut chain = MatrixCellChain::new(CellDir::B); 21 | chain 22 | .node_out("midicc", "sig2") 23 | .set_atom("cc2", SAtom::setting(12)) 24 | .node_inp("out", "ch2") 25 | .place(&mut matrix, 1, 0) 26 | .unwrap(); 27 | matrix.sync().unwrap(); 28 | 29 | // Test run for 5ms with 3 Note On events at sample positions 30 | // 5, 10 and 130 in this block of samples: 31 | let (ch1, ch2) = node_exec.test_run( 32 | 0.005, 33 | false, 34 | &[ 35 | HxTimedEvent::cc(3, 0, 12, 1.55), 36 | HxTimedEvent::cc(5, 0, 10, 0.55), 37 | HxTimedEvent::cc(100, 0, 10, 0.35), 38 | HxTimedEvent::cc(120, 0, 10, 0.15), 39 | HxTimedEvent::cc(190, 0, 10, 0.05), 40 | HxTimedEvent::cc(200, 0, 12, 1.15), 41 | ], 42 | ); 43 | 44 | let changes = collect_signal_changes(&ch1[..], 0); 45 | assert_eq!(changes, &[(5, 55), (100, 35), (120, 15)]); 46 | 47 | let changes = collect_signal_changes(&ch2[..], 0); 48 | assert_eq!(changes, &[(3, 155), (200, 115)]); 49 | } 50 | 51 | #[test] 52 | fn check_node_midicc_test_slew() { 53 | let (node_conf, mut node_exec) = new_node_engine(); 54 | let mut matrix = Matrix::new(node_conf, 3, 3); 55 | 56 | let mut chain = MatrixCellChain::new(CellDir::B); 57 | chain 58 | .node_out("midicc", "sig1") 59 | .set_atom("cc1", SAtom::setting(10)) 60 | .set_denorm("slew", 4.0) 61 | .node_inp("out", "ch1") 62 | .place(&mut matrix, 0, 0) 63 | .unwrap(); 64 | matrix.sync().unwrap(); 65 | 66 | // Test run for 5ms with 3 Note On events at sample positions 67 | // 5, 10 and 130 in this block of samples: 68 | let (ch1, _ch2) = node_exec.test_run( 69 | 0.005, 70 | false, 71 | &[ 72 | HxTimedEvent::cc(3, 0, 12, 1.55), 73 | HxTimedEvent::cc(5, 0, 10, 0.55), 74 | HxTimedEvent::cc(100, 0, 10, 0.35), 75 | HxTimedEvent::cc(120, 0, 10, 0.15), 76 | HxTimedEvent::cc(190, 0, 10, 0.05), 77 | HxTimedEvent::cc(200, 0, 12, 1.15), 78 | ], 79 | ); 80 | 81 | let changes: Vec<(usize, i32)> = collect_signal_changes_flt(&ch1[..], 0.0) 82 | .iter() 83 | .step_by(20) 84 | .map(|(a, b)| (*a, (b * 1000.0).round() as i32)) 85 | .collect(); 86 | assert_eq!( 87 | changes, 88 | &[ 89 | (5, 6), 90 | (25, 119), 91 | (45, 232), 92 | (65, 346), 93 | (85, 459), 94 | (105, 505), 95 | (125, 391), 96 | (145, 278), 97 | (165, 164), 98 | (206, 54) 99 | ] 100 | ); 101 | } 102 | 103 | #[test] 104 | fn check_node_midicc_test_slew2() { 105 | let (node_conf, mut node_exec) = new_node_engine(); 106 | let mut matrix = Matrix::new(node_conf, 3, 3); 107 | 108 | let mut chain = MatrixCellChain::new(CellDir::B); 109 | chain 110 | .node_out("midicc", "sig1") 111 | .set_atom("cc1", SAtom::setting(10)) 112 | .set_denorm("slew", 2.0) 113 | .node_inp("out", "ch1") 114 | .place(&mut matrix, 0, 0) 115 | .unwrap(); 116 | matrix.sync().unwrap(); 117 | 118 | // Test run for 5ms with 3 Note On events at sample positions 119 | // 5, 10 and 130 in this block of samples: 120 | let (ch1, _ch2) = node_exec.test_run( 121 | 0.005, 122 | false, 123 | &[ 124 | HxTimedEvent::cc(3, 0, 12, 1.55), 125 | HxTimedEvent::cc(5, 0, 10, 0.55), 126 | HxTimedEvent::cc(100, 0, 10, 0.35), 127 | HxTimedEvent::cc(120, 0, 10, 0.15), 128 | HxTimedEvent::cc(190, 0, 10, 0.05), 129 | HxTimedEvent::cc(200, 0, 12, 1.15), 130 | ], 131 | ); 132 | 133 | let changes: Vec<(usize, i32)> = collect_signal_changes_flt(&ch1[..], 0.0) 134 | .iter() 135 | .step_by(20) 136 | .map(|(a, b)| (*a, (b * 1000.0).round() as i32)) 137 | .collect(); 138 | assert_eq!(changes, &[(5, 11), (25, 238), (45, 465), (111, 414), (133, 191),]); 139 | } 140 | -------------------------------------------------------------------------------- /tests/node_mix3.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_mix3_1() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 5, 5); 12 | 13 | let amp_1 = NodeId::Amp(0); 14 | let amp_3 = NodeId::Amp(2); 15 | let amp_2 = NodeId::Amp(1); 16 | let mix3_1 = NodeId::Mix3(0); 17 | let out_1 = NodeId::Out(0); 18 | matrix.place( 19 | 1, 20 | 1, 21 | Cell::empty(amp_1).input(None, None, None).out(None, amp_1.out("sig"), None), 22 | ); 23 | matrix.place( 24 | 1, 25 | 2, 26 | Cell::empty(amp_3).input(None, None, None).out(amp_3.out("sig"), None, None), 27 | ); 28 | matrix.place( 29 | 2, 30 | 1, 31 | Cell::empty(amp_2).input(None, None, None).out(None, None, amp_2.out("sig")), 32 | ); 33 | matrix.place( 34 | 2, 35 | 2, 36 | Cell::empty(mix3_1).input(mix3_1.inp("ch1"), mix3_1.inp("ch2"), mix3_1.inp("ch3")).out( 37 | None, 38 | mix3_1.out("sig"), 39 | None, 40 | ), 41 | ); 42 | matrix.place( 43 | 3, 44 | 2, 45 | Cell::empty(out_1).input(None, out_1.inp("ch1"), None).out(None, None, None), 46 | ); 47 | 48 | pset_d(&mut matrix, amp_1, "inp", 0.200); 49 | pset_d(&mut matrix, mix3_1, "vol2", 0.2); // 0.04 50 | 51 | pset_d(&mut matrix, amp_2, "inp", -0.300); 52 | pset_d(&mut matrix, mix3_1, "vol1", 0.1); // -0.03 53 | 54 | pset_d(&mut matrix, amp_3, "inp", 0.500); 55 | pset_d(&mut matrix, mix3_1, "vol3", 0.5); // 0.25 56 | 57 | pset_d(&mut matrix, mix3_1, "ovol", 1.0); 58 | matrix.sync().unwrap(); 59 | 60 | // hexodsp::save_patch_to_file(&mut matrix, "check_matrix_ser_mix3.hxy") 61 | // .unwrap(); 62 | 63 | let res = run_for_ms(&mut node_exec, 25.0); 64 | // The sum is 0.26 65 | assert_float_eq!(res.0[100], 0.26); 66 | 67 | pset_d_wait(&mut matrix, &mut node_exec, mix3_1, "vol1", 1.0); 68 | 69 | let res = run_for_ms(&mut node_exec, 25.0); 70 | // The sum is now (0.25 + 0.04) - 0.3 == -0.01 71 | assert_float_eq!(res.0[100], -0.01); 72 | } 73 | -------------------------------------------------------------------------------- /tests/node_mux9.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | fn setup_mux9_slct(matrix: &mut Matrix) { 9 | let amp = NodeId::Amp(0); 10 | let mux9 = NodeId::Mux9(0); 11 | let out = NodeId::Out(0); 12 | matrix.place(0, 0, Cell::empty(amp).out(None, None, amp.out("sig"))); 13 | matrix.place( 14 | 0, 15 | 1, 16 | Cell::empty(mux9).input(mux9.inp("slct"), None, None).out(None, None, mux9.out("sig")), 17 | ); 18 | matrix.place(0, 2, Cell::empty(out).input(out.inp("ch1"), None, None)); 19 | for i in 1..=9 { 20 | pset_n(matrix, mux9, &format!("in_{}", i), 0.01 * (i as f32)); 21 | } 22 | 23 | matrix.sync().unwrap(); 24 | } 25 | 26 | fn setup_mux9(matrix: &mut Matrix) { 27 | let mux9 = NodeId::Mux9(0); 28 | let out = NodeId::Out(0); 29 | matrix.place(0, 0, Cell::empty(mux9).out(None, None, mux9.out("sig"))); 30 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 31 | for i in 1..=9 { 32 | pset_n(matrix, mux9, &format!("in_{}", i), 0.01 * (i as f32)); 33 | } 34 | 35 | matrix.sync().unwrap(); 36 | } 37 | 38 | #[test] 39 | fn check_node_mux9_9() { 40 | init_test!(matrix, node_exec, 3); 41 | setup_mux9(matrix); 42 | 43 | let mux9 = NodeId::Mux9(0); 44 | 45 | pset_s(matrix, mux9, "in_cnt", 9.into()); 46 | 47 | let (out, _) = run_for_ms(node_exec, 1.0); 48 | assert_float_eq!(out[0], 0.01); 49 | 50 | for i in 2..=9 { 51 | pset_n_wait(matrix, node_exec, mux9, "t_up", 0.0); 52 | pset_n_wait(matrix, node_exec, mux9, "t_up", 1.0); 53 | let (out, _) = run_for_ms(node_exec, 1.0); 54 | assert_float_eq!(out[0], 0.01 * (i as f32)); 55 | } 56 | } 57 | 58 | #[test] 59 | fn check_node_mux9_limit() { 60 | init_test!(matrix, node_exec, 3); 61 | setup_mux9(matrix); 62 | 63 | let mux9 = NodeId::Mux9(0); 64 | 65 | pset_s(matrix, mux9, "in_cnt", 2.into()); 66 | 67 | let (out, _) = run_for_ms(node_exec, 1.0); 68 | assert_float_eq!(out[0], 0.01); 69 | 70 | for i in 2..=3 { 71 | pset_n_wait(matrix, node_exec, mux9, "t_up", 0.0); 72 | pset_n_wait(matrix, node_exec, mux9, "t_up", 1.0); 73 | let (out, _) = run_for_ms(node_exec, 1.0); 74 | assert_float_eq!(out[0], 0.01 * (i as f32)); 75 | } 76 | 77 | for i in 1..=3 { 78 | pset_n_wait(matrix, node_exec, mux9, "t_up", 0.0); 79 | pset_n_wait(matrix, node_exec, mux9, "t_up", 1.0); 80 | let (out, _) = run_for_ms(node_exec, 1.0); 81 | assert_float_eq!(out[0], 0.01 * (i as f32)); 82 | } 83 | } 84 | 85 | #[test] 86 | fn check_node_mux9_reset() { 87 | init_test!(matrix, node_exec, 3); 88 | setup_mux9(matrix); 89 | 90 | let mux9 = NodeId::Mux9(0); 91 | 92 | pset_s(matrix, mux9, "in_cnt", 5.into()); 93 | 94 | let (out, _) = run_for_ms(node_exec, 1.0); 95 | assert_float_eq!(out[0], 0.01); 96 | 97 | for i in 2..=3 { 98 | pset_n_wait(matrix, node_exec, mux9, "t_up", 0.0); 99 | pset_n_wait(matrix, node_exec, mux9, "t_up", 1.0); 100 | let (out, _) = run_for_ms(node_exec, 1.0); 101 | assert_float_eq!(out[0], 0.01 * (i as f32)); 102 | } 103 | 104 | pset_n_wait(matrix, node_exec, mux9, "t_rst", 1.0); 105 | pset_n_wait(matrix, node_exec, mux9, "t_rst", 0.0); 106 | 107 | for i in 1..=3 { 108 | let (out, _) = run_for_ms(node_exec, 1.0); 109 | assert_float_eq!(out[0], 0.01 * (i as f32)); 110 | pset_n_wait(matrix, node_exec, mux9, "t_up", 0.0); 111 | pset_n_wait(matrix, node_exec, mux9, "t_up", 1.0); 112 | } 113 | } 114 | 115 | #[test] 116 | fn check_node_mux9_slct() { 117 | init_test!(matrix, node_exec, 3); 118 | setup_mux9_slct(matrix); 119 | 120 | let mux9 = NodeId::Mux9(0); 121 | let amp = NodeId::Amp(0); 122 | 123 | pset_s(matrix, mux9, "in_cnt", 5.into()); 124 | pset_n_wait(matrix, node_exec, amp, "inp", 0.9); 125 | 126 | let (out, _) = run_for_ms(node_exec, 1.0); 127 | assert_float_eq!(out[0], 0.06); 128 | 129 | pset_n_wait(matrix, node_exec, amp, "inp", 0.0); 130 | let (out, _) = run_for_ms(node_exec, 1.0); 131 | assert_float_eq!(out[0], 0.01); 132 | 133 | pset_n(matrix, amp, "inp", 1.0); 134 | let (out, _) = run_for_ms(node_exec, 20.0); 135 | assert_decimated_feq!(out, 90, vec![0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.06, 0.06, 0.06,]); 136 | 137 | pset_s(matrix, mux9, "in_cnt", 8.into()); 138 | pset_n_wait(matrix, node_exec, amp, "inp", 0.0); 139 | 140 | pset_n(matrix, amp, "inp", 1.0); 141 | let (out, _) = run_for_ms(node_exec, 20.0); 142 | assert_decimated_feq!( 143 | out, 144 | 50, 145 | vec![0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.09, 0.09, 0.09, 0.09,] 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /tests/node_quant.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | fn setup_quant(matrix: &mut Matrix, trig_out: bool) { 9 | let qnt = NodeId::Quant(0); 10 | let out = NodeId::Out(0); 11 | 12 | if trig_out { 13 | matrix.place(0, 0, Cell::empty(qnt).out(None, None, qnt.out("t"))); 14 | } else { 15 | matrix.place(0, 0, Cell::empty(qnt).out(None, None, qnt.out("sig"))); 16 | } 17 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 18 | 19 | pset_s(matrix, qnt, "keys", 0); 20 | matrix.sync().unwrap(); 21 | } 22 | 23 | #[test] 24 | fn check_node_quant_1() { 25 | init_test!(matrix, node_exec, 3); 26 | setup_quant(matrix, false); 27 | 28 | let qnt = NodeId::Quant(0); 29 | 30 | let mut v = vec![]; 31 | for i in 0..20 { 32 | let x = i as f32 / 200.0; 33 | 34 | pset_n_wait(matrix, node_exec, qnt, "freq", x); 35 | let res = run_for_ms(node_exec, 1.0); 36 | v.push(res.0[0]); 37 | } 38 | 39 | assert_vec_feq!( 40 | v, 41 | vec![ 42 | 0.0, 0.0083, 0.0083, 0.0166, 0.0250, 0.0250, 0.0333, 0.0333, 0.0416, 0.0500, 0.0500, 43 | 0.0583, 0.0583, 0.0666, 0.075, 0.075, 0.0833, 0.0833, 0.0916, 0.1 44 | ] 45 | ); 46 | } 47 | 48 | #[test] 49 | fn check_node_quant_2() { 50 | init_test!(matrix, node_exec, 3); 51 | setup_quant(matrix, false); 52 | 53 | let qnt = NodeId::Quant(0); 54 | 55 | let mut v = vec![]; 56 | for i in 0..20 { 57 | let x = i as f32 / 200.0; 58 | 59 | pset_n_wait(matrix, node_exec, qnt, "freq", -1.0 * x); 60 | let res = run_for_ms(node_exec, 1.0); 61 | v.push(res.0[0]); 62 | } 63 | 64 | assert_vec_feq!( 65 | v, 66 | vec![ 67 | 0.0, 68 | 0.0, 69 | -1.0 * 0.0083, 70 | -1.0 * 0.0166, 71 | -1.0 * 0.0166, 72 | -1.0 * 0.0250, 73 | -1.0 * 0.0250, 74 | -1.0 * 0.0333, 75 | -1.0 * 0.0416, 76 | -1.0 * 0.0416, 77 | -1.0 * 0.0500, 78 | -1.0 * 0.0500, 79 | -1.0 * 0.0583, 80 | -1.0 * 0.0666, 81 | -1.0 * 0.0666, 82 | -1.0 * 0.075, 83 | -1.0 * 0.075, 84 | -1.0 * 0.0833, 85 | -1.0 * 0.0916, 86 | -1.0 * 0.0916, 87 | -1.0 * 0.1 88 | ] 89 | ); 90 | } 91 | 92 | #[test] 93 | fn check_node_quant_oct() { 94 | init_test!(matrix, node_exec, 3); 95 | setup_quant(matrix, false); 96 | 97 | let qnt = NodeId::Quant(0); 98 | 99 | pset_n(matrix, qnt, "oct", 0.1); 100 | 101 | let mut v = vec![]; 102 | for i in 0..20 { 103 | let x = i as f32 / 200.0; 104 | 105 | pset_n_wait(matrix, node_exec, qnt, "freq", x); 106 | let res = run_for_ms(node_exec, 1.0); 107 | v.push(res.0[0]); 108 | } 109 | 110 | assert_vec_feq!( 111 | v, 112 | vec![ 113 | 0.1 + 0.0, 114 | 0.1 + 0.0083, 115 | 0.1 + 0.0083, 116 | 0.1 + 0.0166, 117 | 0.1 + 0.0250, 118 | 0.1 + 0.0250, 119 | 0.1 + 0.0333, 120 | 0.1 + 0.0333, 121 | 0.1 + 0.0416, 122 | 0.1 + 0.0500, 123 | 0.1 + 0.0500, 124 | 0.1 + 0.0583, 125 | 0.1 + 0.0583, 126 | 0.1 + 0.0666, 127 | 0.1 + 0.075, 128 | 0.1 + 0.075, 129 | 0.1 + 0.0833, 130 | 0.1 + 0.0833, 131 | 0.1 + 0.0916, 132 | 0.1 + 0.1 133 | ] 134 | ); 135 | } 136 | 137 | #[test] 138 | fn check_node_quant_keys() { 139 | init_test!(matrix, node_exec, 3); 140 | setup_quant(matrix, false); 141 | 142 | let quant = NodeId::Quant(0); 143 | 144 | pset_s(matrix, quant, "keys", 0b11100); 145 | 146 | let mut v = vec![]; 147 | for i in 0..20 { 148 | let x = i as f32 / 200.0; 149 | 150 | pset_n_wait(matrix, node_exec, quant, "freq", x); 151 | let res = run_for_ms(node_exec, 1.0); 152 | v.push(res.0[0]); 153 | } 154 | 155 | assert_vec_feq!( 156 | v, 157 | vec![ 158 | -0.0416, 0.0416, 0.0416, 0.0416, 0.0416, 0.0416, 0.0416, 0.0416, 0.0416, 0.0500, 159 | 0.0500, 0.0583, 0.0583, 0.0583, 0.0583, 0.0583, 0.0583, 0.0583, 0.0583, 0.0583 160 | ] 161 | ); 162 | } 163 | 164 | #[test] 165 | fn check_node_quant_trig_out() { 166 | init_test!(matrix, node_exec, 3); 167 | setup_quant(matrix, true); 168 | 169 | let quant = NodeId::Quant(0); 170 | 171 | pset_s(matrix, quant, "keys", 0b11100); 172 | 173 | let mut v = vec![]; 174 | for i in 0..100 { 175 | let x = i as f32 / 1000.0; 176 | 177 | pset_n(matrix, quant, "freq", x); 178 | let res = run_for_ms(node_exec, 10.0); 179 | v.extend_from_slice(&res.0[..]); 180 | } 181 | 182 | let idxs_big = collect_signal_changes(&v[..], 50); 183 | 184 | assert_eq!(idxs_big, vec![(0, 100), (1359, 100), (19734, 100), (23409, 100)]); 185 | } 186 | -------------------------------------------------------------------------------- /tests/node_smap.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_smap() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let smap = NodeId::SMap(0); 14 | let out = NodeId::Out(0); 15 | matrix.place(0, 0, Cell::empty(smap).out(None, None, smap.out("sig"))); 16 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 17 | pset_n(&mut matrix, smap, "inp", 0.5); 18 | 19 | pset_n(&mut matrix, smap, "min", -1.0); 20 | pset_n(&mut matrix, smap, "max", -0.9); // we expect -0.95 21 | 22 | pset_s(&mut matrix, smap, "mode", 0); // unipolar 23 | matrix.sync().unwrap(); 24 | 25 | let res = run_for_ms(&mut node_exec, 10.0); 26 | assert_decimated_feq!(res.0, 10, vec![-0.95; 50]); 27 | 28 | pset_s(&mut matrix, smap, "mode", 1); // bipolar 29 | let res = run_for_ms(&mut node_exec, 10.0); 30 | assert_decimated_feq!(res.0, 10, vec![-0.925; 50]); 31 | 32 | pset_s(&mut matrix, smap, "mode", 3); // bipolar inverted 33 | let res = run_for_ms(&mut node_exec, 10.0); 34 | assert_decimated_feq!(res.0, 10, vec![-0.975; 50]); 35 | 36 | pset_n(&mut matrix, smap, "inp", 1.0); 37 | run_for_ms(&mut node_exec, 10.0); 38 | 39 | pset_s(&mut matrix, smap, "mode", 2); // unipolar inverted 40 | let res = run_for_ms(&mut node_exec, 10.0); 41 | assert_decimated_feq!(res.0, 10, vec![-1.0; 50]); 42 | } 43 | 44 | #[test] 45 | fn check_node_smap_clip() { 46 | let (node_conf, mut node_exec) = new_node_engine(); 47 | let mut matrix = Matrix::new(node_conf, 3, 3); 48 | 49 | let smap = NodeId::SMap(0); 50 | let out = NodeId::Out(0); 51 | matrix.place(0, 0, Cell::empty(smap).out(None, None, smap.out("sig"))); 52 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 53 | 54 | pset_s(&mut matrix, smap, "mode", 0); // unipolar 55 | pset_n(&mut matrix, smap, "inp", -0.5); 56 | pset_n(&mut matrix, smap, "min", 0.1); 57 | pset_n(&mut matrix, smap, "max", -0.1); 58 | 59 | matrix.sync().unwrap(); 60 | 61 | let res = run_for_ms(&mut node_exec, 10.0); 62 | assert_decimated_feq!(res.0, 10, vec![0.2; 50]); 63 | 64 | pset_s(&mut matrix, smap, "clip", 1); // enable clipping 65 | let res = run_for_ms(&mut node_exec, 10.0); 66 | assert_decimated_feq!(res.0, 10, vec![0.1; 50]); 67 | 68 | pset_s(&mut matrix, smap, "mode", 1); // bipolar 69 | pset_s(&mut matrix, smap, "clip", 0); // disable clipping 70 | 71 | // go to -1.5 input here, which is very much below unipolar 72 | // and a bit below bipolar. 73 | pset_n(&mut matrix, smap, "inp", -1.5); // out of range input 74 | 75 | run_for_ms(&mut node_exec, 10.0); 76 | 77 | let res = run_for_ms(&mut node_exec, 10.0); 78 | assert_decimated_feq!(res.0, 10, vec![0.15; 50]); 79 | 80 | pset_s(&mut matrix, smap, "clip", 1); // enable clipping 81 | let res = run_for_ms(&mut node_exec, 10.0); 82 | assert_decimated_feq!(res.0, 10, vec![0.1; 50]); 83 | 84 | pset_s(&mut matrix, smap, "mode", 0); // unipolar 85 | pset_s(&mut matrix, smap, "clip", 0); // disable clipping 86 | let res = run_for_ms(&mut node_exec, 10.0); 87 | assert_decimated_feq!(res.0, 10, vec![(0.1 * (1.0 + 1.5)) + (-0.1 * -1.5); 50]); 88 | 89 | pset_s(&mut matrix, smap, "clip", 1); // enable clipping 90 | let res = run_for_ms(&mut node_exec, 10.0); 91 | assert_decimated_feq!(res.0, 10, vec![0.1; 50]); 92 | 93 | pset_s(&mut matrix, smap, "mode", 2); // unipolar inverse 94 | pset_s(&mut matrix, smap, "clip", 0); // disable clipping 95 | let res = run_for_ms(&mut node_exec, 10.0); 96 | assert_decimated_feq!(res.0, 10, vec![(-0.1 * (1.0 + 1.5)) + (0.1 * -1.5); 50]); 97 | 98 | pset_s(&mut matrix, smap, "clip", 1); // enable clipping 99 | let res = run_for_ms(&mut node_exec, 10.0); 100 | assert_decimated_feq!(res.0, 10, vec![-0.1; 50]); 101 | 102 | pset_s(&mut matrix, smap, "mode", 3); // bipolar inverse 103 | pset_s(&mut matrix, smap, "clip", 0); // disable clipping 104 | 105 | let res = run_for_ms(&mut node_exec, 10.0); 106 | assert_decimated_feq!(res.0, 10, vec![-0.15; 50]); 107 | 108 | pset_s(&mut matrix, smap, "clip", 1); // enable clipping 109 | let res = run_for_ms(&mut node_exec, 10.0); 110 | assert_decimated_feq!(res.0, 10, vec![-0.1; 50]); 111 | } 112 | -------------------------------------------------------------------------------- /tests/node_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_test_1() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let test = NodeId::Test(0); 14 | let out = NodeId::Out(0); 15 | matrix.place(0, 0, Cell::empty(test).out(None, None, test.out("sig"))); 16 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 17 | matrix.sync().unwrap(); 18 | 19 | let p = test.inp_param("p").unwrap(); 20 | 21 | matrix.set_param(p, SAtom::param(1.0)); 22 | let res = run_for_ms(&mut node_exec, 2.0); 23 | assert_decimated_feq!(res.0, 1, vec![1.0; 10]); 24 | 25 | matrix.set_param(p, SAtom::param(0.5)); 26 | let res = run_for_ms(&mut node_exec, 2.0); 27 | assert_decimated_feq!(res.0, 1, vec![0.5; 10]); 28 | 29 | matrix.set_param(p, SAtom::param(0.0)); 30 | let res = run_for_ms(&mut node_exec, 1.0); 31 | assert_decimated_feq!(res.0, 1, vec![0.0; 10]); 32 | } 33 | 34 | #[test] 35 | fn check_node_test_out_connected() { 36 | let (node_conf, mut node_exec) = new_node_engine(); 37 | let mut matrix = Matrix::new(node_conf, 6, 3); 38 | 39 | let test = NodeId::Test(0); 40 | let out = NodeId::Out(0); 41 | let sin = NodeId::Sin(0); 42 | let sin2 = NodeId::Sin(1); 43 | let sin3 = NodeId::Sin(2); 44 | matrix.place(0, 0, Cell::empty(test).out(None, None, test.out("outc"))); 45 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 46 | 47 | matrix.place(1, 0, Cell::empty(test).out(None, None, test.out("out2"))); 48 | matrix.place(1, 1, Cell::empty(out).input(out.inp("ch2"), None, None)); 49 | 50 | matrix.place(2, 0, Cell::empty(test).out(None, None, test.out("out3"))); 51 | matrix.place(2, 1, Cell::empty(sin).input(sin.inp("freq"), None, None)); 52 | 53 | matrix.place(3, 0, Cell::empty(test).out(None, None, test.out("out4"))); 54 | matrix.place(3, 1, Cell::empty(sin2).input(sin2.inp("freq"), None, None)); 55 | 56 | matrix.place(4, 0, Cell::empty(test).out(None, None, test.out("sig"))); 57 | matrix.place(4, 1, Cell::empty(sin3).input(sin3.inp("freq"), None, None)); 58 | matrix.sync().unwrap(); 59 | 60 | let res = run_for_ms(&mut node_exec, 2.0); 61 | let mask = 0x01 | 0x04 | 0x08 | 0x10 | 0x20; 62 | assert_decimated_feq!(res.0, 1, vec![mask as f32; 10]); 63 | assert_decimated_feq!(res.1, 1, vec![1.0; 10]); 64 | 65 | // Remove a connection for testing: 66 | matrix.place(1, 1, Cell::empty(NodeId::Nop)); 67 | matrix.sync().unwrap(); 68 | 69 | let res = run_for_ms(&mut node_exec, 2.0); 70 | let mask = 0x01 | 0x08 | 0x10 | 0x20; 71 | assert_decimated_feq!(res.0, 1, vec![mask as f32; 10]); 72 | assert_decimated_feq!(res.1, 1, vec![0.0; 10]); 73 | } 74 | 75 | #[test] 76 | fn check_node_test_in_connected() { 77 | let (node_conf, mut node_exec) = new_node_engine(); 78 | let mut matrix = Matrix::new(node_conf, 6, 3); 79 | 80 | let test = NodeId::Test(0); 81 | let tst2 = NodeId::Test(1); 82 | let out = NodeId::Out(0); 83 | matrix.place(0, 0, Cell::empty(test).out(None, None, test.out("sig"))); 84 | matrix.place( 85 | 0, 86 | 1, 87 | Cell::empty(tst2).input(tst2.inp("f"), None, None).out(None, None, test.out("out3")), 88 | ); 89 | matrix.place(0, 2, Cell::empty(out).input(out.inp("ch1"), None, None)); 90 | matrix.sync().unwrap(); 91 | 92 | let res = run_for_ms(&mut node_exec, 2.0); 93 | assert_decimated_feq!(res.0, 1, vec![1.0; 10]); 94 | 95 | matrix.place(0, 0, Cell::empty(NodeId::Nop)); 96 | matrix.sync().unwrap(); 97 | 98 | let res = run_for_ms(&mut node_exec, 2.0); 99 | assert_decimated_feq!(res.0, 1, vec![0.0; 10]); 100 | } 101 | -------------------------------------------------------------------------------- /tests/node_tslfo.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | use common::*; 7 | 8 | #[test] 9 | fn check_node_tslfo_1() { 10 | let (node_conf, mut node_exec) = new_node_engine(); 11 | let mut matrix = Matrix::new(node_conf, 3, 3); 12 | 13 | let tsl = NodeId::TsLFO(0); 14 | let out = NodeId::Out(0); 15 | matrix.place(0, 0, Cell::empty(tsl).out(None, None, tsl.out("sig"))); 16 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 17 | pset_d(&mut matrix, tsl, "time", 0.1); 18 | matrix.sync().unwrap(); 19 | 20 | // Test shortest time 0.1ms: 21 | let (out_l, _) = run_for_ms(&mut node_exec, 1.0); 22 | assert_decimated_feq!( 23 | out_l, 24 | 1, 25 | vec![0.0, 0.4535, 0.9070, 0.6394, 0.1859, 0.2675, 0.7210, 0.8253, 0.37188, 0.0816] 26 | ); 27 | 28 | // Test 1ms: 29 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "time", 1.0); 30 | let (out_l, _) = run_for_ms(&mut node_exec, 5.0); 31 | assert_decimated_feq!( 32 | out_l, 33 | 10, 34 | vec![0.7103, 0.8361, 0.3826, 0.0708, 0.5244, 0.9779, 0.5685, 0.1150, 0.3384] 35 | ); 36 | 37 | // Test 300000.0 ms 38 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "time", 300000.0); 39 | let (out_l, _) = run_for_ms(&mut node_exec, 3000.0); 40 | let ramp_slope = 1.0_f64 / (300.0 * 44100.0); 41 | let tri_slope = ramp_slope * 2.0; 42 | assert_float_eq!((out_l[0] - out_l[10000]).abs(), (tri_slope * 10000.0) as f32); 43 | assert_float_eq!((out_l[10000] - out_l[11000]).abs(), (tri_slope * 1000.0) as f32); 44 | assert_decimated_feq!( 45 | out_l, 46 | 10000, 47 | vec![ 48 | 0.7566, // => Slope is ~0.0015 per 10000 samples 49 | 0.7551, 0.7536, 0.7521, 0.7506, 0.7490, 0.7475, 50 | ] 51 | ); 52 | } 53 | 54 | #[test] 55 | fn check_node_tslfo_trig_slopes() { 56 | let (node_conf, mut node_exec) = new_node_engine(); 57 | let mut matrix = Matrix::new(node_conf, 3, 3); 58 | 59 | let tsl = NodeId::TsLFO(0); 60 | let out = NodeId::Out(0); 61 | matrix.place(0, 0, Cell::empty(tsl).out(None, None, tsl.out("sig"))); 62 | matrix.place(0, 1, Cell::empty(out).input(out.inp("ch1"), None, None)); 63 | matrix.sync().unwrap(); 64 | 65 | // Test 1ms but at full ramp up and resync/trigger: 66 | pset_d(&mut matrix, tsl, "rev", 1.0); 67 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "time", 10.0); 68 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "trig", 1.0); 69 | let (out_l, _) = run_for_ms(&mut node_exec, 15.0); 70 | let ramp_slope = 1.0_f64 / ((10.0 / 1000.0) * 44100.0); 71 | assert_float_eq!((out_l[0] - out_l[1]).abs(), ramp_slope as f32); 72 | assert_decimated_feq!( 73 | out_l, 74 | 50, 75 | vec![ 76 | 0.00000022911095, 77 | 0.11349243, 78 | 0.22698463, 79 | 0.34047684, 80 | 0.45396903, 81 | 0.56746125, 82 | 0.68095344, 83 | 0.79444563, 84 | 0.9079378, 85 | 0.020429054, 86 | 0.13392125, 87 | 0.24741346, 88 | 0.36090568, 89 | 0.47439787 90 | ] 91 | ); 92 | 93 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "trig", 0.0); 94 | 95 | // Test 1ms but at full ramp down and resync/trigger: 96 | pset_d(&mut matrix, tsl, "rev", 0.0); 97 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "time", 10.0); 98 | pset_d_wait(&mut matrix, &mut node_exec, tsl, "trig", 1.0); 99 | let (out_l, _) = run_for_ms(&mut node_exec, 15.0); 100 | let ramp_slope = 1.0_f64 / ((10.0 / 1000.0) * 44100.0); 101 | assert_float_eq!((out_l[1] - out_l[2]).abs(), ramp_slope as f32); 102 | assert_decimated_feq!( 103 | out_l, 104 | 50, 105 | vec![ 106 | 0.0022888184, 107 | 0.88670975, 108 | 0.77331966, 109 | 0.65992963, 110 | 0.5465396, 111 | 0.43314955, 112 | 0.3197595, 113 | 0.20636943, 114 | 0.09297939, 115 | 0.97968936, 116 | 0.8662993, 117 | 0.75290924, 118 | 0.6395192, 119 | 0.5261291 120 | ] 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /tests/quant.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Weird Constructor 2 | // This file is a part of HexoDSP. Released under GPL-3.0-or-later. 3 | // See README.md and COPYING for details. 4 | 5 | mod common; 6 | //use common::*; 7 | 8 | use hexodsp::d_pit; 9 | use synfx_dsp::Quantizer; 10 | 11 | #[test] 12 | fn check_quant_pos_neg_exact() { 13 | let mut q = Quantizer::new(); 14 | q.set_keys(0x0); 15 | 16 | let v = (0..=12).map(|i| d_pit!(q.process(i as f32 * (0.1 / 12.0)))).collect::>(); 17 | 18 | assert_vec_feq!( 19 | v, 20 | vec![ 21 | 440.0, 466.1638, 493.8833, 523.2511, 554.3653, 587.3295, 622.25397, 659.2551, 698.4565, 22 | 739.98883, 783.9909, 830.6094, 880.0 23 | ] 24 | ); 25 | 26 | let v = (0..=12).map(|i| d_pit!(q.process(i as f32 * (-0.1 / 12.0)))).collect::>(); 27 | 28 | assert_vec_feq!( 29 | v, 30 | vec![ 31 | 440.0, 415.3047, 391.99542, 369.99442, 349.22824, 329.62756, 311.12698, 293.66476, 32 | 277.18265, 261.62555, 246.94165, 233.08186, 220.0 33 | ] 34 | ); 35 | } 36 | 37 | #[test] 38 | fn check_quant_precise_01_range() { 39 | let mut q = Quantizer::new(); 40 | q.set_keys(0x0); 41 | 42 | let v = 43 | (0..=48).map(|i| d_pit!(q.process(i as f32 * ((0.1 * 0.25) / 12.0)))).collect::>(); 44 | 45 | assert_vec_feq!( 46 | v, 47 | vec![ 48 | 440.0, 466.1638, 466.1638, 466.1638, 466.1638, 493.8833, 493.8833, 493.8833, 493.8833, 49 | 523.2511, 523.2511, 523.2511, 523.2511, 554.3653, 554.3653, 554.3653, 554.3653, 50 | 587.3295, 587.3295, 587.3295, 587.3295, 622.25397, 622.25397, 622.25397, 622.25397, 51 | 659.2551, 659.2551, 659.2551, 659.2551, 698.4565, 698.4565, 698.4565, 698.4565, 52 | 739.98883, 739.98883, 739.98883, 739.98883, 783.9909, 783.9909, 783.9909, 783.9909, 53 | 830.6094, 830.6094, 830.6094, 830.6094, 880.0, 880.0, 880.0, 880.0 54 | ] 55 | ); 56 | } 57 | 58 | #[test] 59 | fn check_quant_neg_pos_range() { 60 | let mut q = Quantizer::new(); 61 | q.set_keys(0x0); 62 | 63 | let v = (0..=48) 64 | .map(|i| { 65 | let i = i - 24; 66 | d_pit!(q.process(i as f32 * ((0.1 * 0.25) / 12.0))) 67 | }) 68 | .collect::>(); 69 | 70 | assert_vec_feq!( 71 | v, 72 | vec![ 73 | 311.12698, 311.12698, 329.62756, 329.62756, 329.62756, 329.62756, 349.22824, 349.22824, 74 | 349.22824, 349.22824, 369.99442, 369.99442, 369.99442, 369.99442, 391.99542, 391.99542, 75 | 391.99542, 391.99542, 415.3047, 415.3047, 415.3047, 415.3047, 440.0, 440.0, 440.0, 76 | 466.1638, 466.1638, 466.1638, 466.1638, 493.8833, 493.8833, 493.8833, 493.8833, 77 | 523.2511, 523.2511, 523.2511, 523.2511, 554.3653, 554.3653, 554.3653, 554.3653, 78 | 587.3295, 587.3295, 587.3295, 587.3295, 622.25397, 622.25397, 622.25397, 622.25397 79 | ] 80 | ); 81 | } 82 | 83 | #[test] 84 | fn check_quant_edge_oct_offs() { 85 | let mut q = Quantizer::new(); 86 | q.set_keys(0b1001_0000_0000); 87 | 88 | let v = 89 | (0..=24).map(|i| d_pit!(q.process(i as f32 * ((0.1 * 0.5) / 12.0)))).collect::>(); 90 | 91 | assert_vec_feq!( 92 | v, 93 | vec![ 94 | 415.3047, 493.8833, 493.8833, 493.8833, 493.8833, 493.8833, 493.8833, 493.8833, 95 | 493.8833, 493.8833, 493.8833, 493.8833, 493.8833, 830.6094, 830.6094, 830.6094, 96 | 830.6094, 830.6094, 830.6094, 830.6094, 830.6094, 830.6094, 830.6094, 830.6094, 97 | 830.6094 98 | ] 99 | ); 100 | } 101 | 102 | #[test] 103 | fn check_quant_one_key() { 104 | let mut q = Quantizer::new(); 105 | q.set_keys(0b0010_0000_0000); 106 | 107 | let v = (0..=48) 108 | .map(|i| { 109 | let i = i - 24; 110 | d_pit!(q.process(i as f32 * ((0.1 * 0.5) / 12.0))) 111 | }) 112 | .collect::>(); 113 | 114 | assert_vec_feq!( 115 | v, 116 | vec![ 117 | 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 220.0, 118 | 220.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 119 | 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 440.0, 120 | 440.0, 880.0, 880.0, 880.0, 880.0, 880.0, 880.0, 880.0, 880.0, 880.0, 880.0, 880.0, 121 | 880.0 122 | ] 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /tests/sample_sin.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeirdConstructor/HexoDSP/5454e65633815f8cc1dc3b144cca40aa34bd0a38/tests/sample_sin.wav -------------------------------------------------------------------------------- /tests/sample_sin_long.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeirdConstructor/HexoDSP/5454e65633815f8cc1dc3b144cca40aa34bd0a38/tests/sample_sin_long.wav --------------------------------------------------------------------------------