diff --git a/Cargo.toml b/Cargo.toml index d078e71..545df8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,22 @@ [package] -name = "HexoDSP" -version = "0.1.0" -authors = ["Weird Constructor "] -license = "AGPL-3.0-or-later" -edition = "2018" +name = "HexoDSP" +version = "0.1.0" +authors = ["Weird Constructor "] +license = "AGPL-3.0-or-later" +edition = "2018" description = "Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth." -keywords = ["audio", "music", "real-time", "synthesis", "synthesizer", "dsp", "sound"] -categories = ["multimedia::audio", "multimedia", "algorithms", "mathematics"] +keywords = ["audio", "music", "real-time", "synthesis", "synthesizer", "dsp", "sound"] +categories = ["multimedia::audio", "multimedia", "algorithms", "mathematics"] + +#[features] +#default = [ "hexotk" ] [dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -ringbuf = "0.2.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +ringbuf = "0.2.2" triple_buffer = "5.0.6" +hexotk = { optional = true, git = "https://github.com/WeirdConstructor/HexoTK.git" } [dev-dependencies] microfft = "0.3.1" @@ -21,5 +25,5 @@ hound = "3.4.0" jack = "0.6.6" [lib] -name="hexodsp" +name = "hexodsp" crate-type = ["lib"] diff --git a/README.md b/README.md index 82fef34..dcacffe 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ samples. For a real time application of this library please refer to the examples that come with this library. ```rust -use hexosynth::*; +use hexodsp::*; let (mut node_conf, mut node_exec) = new_node_engine(); diff --git a/src/cell_dir.rs b/src/cell_dir.rs new file mode 100644 index 0000000..6a2da52 --- /dev/null +++ b/src/cell_dir.rs @@ -0,0 +1,99 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] +pub enum CellDir { + TR, + BR, + B, + BL, + TL, + T, + /// Center + C +} + +impl CellDir { + pub fn from(edge: u8) -> Self { + match edge { + 0 => CellDir::TR, + 1 => CellDir::BR, + 2 => CellDir::B, + 3 => CellDir::BL, + 4 => CellDir::TL, + 5 => CellDir::T, + _ => CellDir::C, + } + } + + #[inline] + pub fn is_output(&self) -> bool { + let e = self.to_edge(); + e <= 2 + } + + #[inline] + pub fn is_input(&self) -> bool { + !self.is_output() + } + + #[inline] + pub fn to_edge(&self) -> u8 { + *self as u8 + } + + pub fn to_menu_pos(&self) -> (i32, i32) { + match self { + // out 1 - TR + CellDir::TR => (0, 1), + // out 2 - BR + CellDir::BR => (1, 1), + // out 3 - B + CellDir::B => (0, 1), + // in 3 - BL + CellDir::BL => (-1, 1), + // in 2 - TL + CellDir::TL => (-1, 0), + // in 1 - T + CellDir::T => (0, -1), + _ => (0, 0), + } + } + + pub fn to_offs(&self, x: usize) -> (i32, i32) { + let even = x % 2 == 0; + match self { + // out 1 - TR + CellDir::TR => (1, if even { -1 } else { 0 }), + // out 2 - BR + CellDir::BR => (1, if even { 0 } else { 1 }), + // out 3 - B + CellDir::B => (0, 1), + // in 3 - BL + CellDir::BL => (-1, if even { 0 } else { 1 }), + // in 2 - TL + CellDir::TL => (-1, if even { -1 } else { 0 }), + // in 1 - T + CellDir::T => (0, -1), + _ => (0, 0), + } + } +} + +#[cfg(feature="hexotk")] +use hexotk::widgets::HexDir; + +#[cfg(feature="hexotk")] +impl From for CellDir { + fn from(h: HexDir) -> Self { + CellDir::from(h.to_edge()) + } +} + +#[cfg(feature="hexotk")] +impl From for HexDir { + fn from(c: CellDir) -> Self { + HexDir::from(c.to_edge()) + } +} diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs new file mode 100644 index 0000000..6b42393 --- /dev/null +++ b/src/dsp/helpers.rs @@ -0,0 +1,420 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +static FAST_COS_TAB_LOG2_SIZE : usize = 9; +static FAST_COS_TAB_SIZE : usize = 1 << FAST_COS_TAB_LOG2_SIZE; // =512 +static mut FAST_COS_TAB : [f32; 513] = [0.0; 513]; + +pub fn init_cos_tab() { + for i in 0..(FAST_COS_TAB_SIZE+1) { + let phase : f32 = + (i as f32) + * ((std::f32::consts::PI * 2.0) + / (FAST_COS_TAB_SIZE as f32)); + unsafe { + // XXX: note: mutable statics can be mutated by multiple + // threads: aliasing violations or data races + // will cause undefined behavior + FAST_COS_TAB[i] = phase.cos(); + } + } +} + +const PHASE_SCALE : f32 = 1.0_f32 / (std::f32::consts::PI * 2.0_f32); + +pub fn fast_cos(mut x: f32) -> f32 { + x = x.abs(); // cosine is symmetrical around 0, let's get rid of negative values + + // normalize range from 0..2PI to 1..2 + let phase = x * PHASE_SCALE; + + let index = FAST_COS_TAB_SIZE as f32 * phase; + + let fract = index.fract(); + let index = index.floor() as usize; + + unsafe { + // XXX: note: mutable statics can be mutated by multiple + // threads: aliasing violations or data races + // will cause undefined behavior + let left = FAST_COS_TAB[index as usize]; + let right = FAST_COS_TAB[index as usize + 1]; + + return left + (right - left) * fract; + } +} + +pub fn fast_sin(x: f32) -> f32 { + fast_cos(x - (std::f32::consts::PI / 2.0)) +} + +static mut WHITE_NOISE_TAB: [f64; 1024] = [0.0; 1024]; + +pub fn init_white_noise_tab() { + let mut rng = RandGen::new(); + unsafe { + for i in 0..WHITE_NOISE_TAB.len() { + WHITE_NOISE_TAB[i as usize] = rng.next_open01(); + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct RandGen { + r: [u64; 2], +} + +// Taken from xoroshiro128 crate under MIT License +// Implemented by Matthew Scharley (Copyright 2016) +// https://github.com/mscharley/rust-xoroshiro128 +pub fn next_xoroshiro128(state: &mut [u64; 2]) -> u64 { + let s0: u64 = state[0]; + let mut s1: u64 = state[1]; + let result: u64 = s0.wrapping_add(s1); + + s1 ^= s0; + state[0] = s0.rotate_left(55) ^ s1 ^ (s1 << 14); // a, b + state[1] = s1.rotate_left(36); // c + + result +} + +// Taken from rand::distributions +// Licensed under the Apache License, Version 2.0 +// Copyright 2018 Developers of the Rand project. +pub fn u64_to_open01(u: u64) -> f64 { + use core::f64::EPSILON; + let float_size = std::mem::size_of::() as u32 * 8; + let fraction = u >> (float_size - 52); + let exponent_bits: u64 = (1023 as u64) << 52; + f64::from_bits(fraction | exponent_bits) - (1.0 - EPSILON / 2.0) +} + +impl RandGen { + pub fn new() -> Self { + RandGen { + r: [0x193a6754a8a7d469, 0x97830e05113ba7bb], + } + } + + pub fn next(&mut self) -> u64 { + next_xoroshiro128(&mut self.r) + } + + pub fn next_open01(&mut self) -> f64 { + u64_to_open01(self.next()) + } +} + + +//- splitmix64 (http://xoroshiro.di.unimi.it/splitmix64.c) +//""" +// Written in 2015 by Sebastiano Vigna (vigna@acm.org) +// +// To the extent possible under law, the author has dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// See . +//""" +// +// Written by Alexander Stocko +// +// To the extent possible under law, the author has dedicated all copyright +// and related and neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// See +use std::num::Wrapping as w; + +/// The `SplitMix64` random number generator. +#[derive(Copy, Clone)] +pub struct SplitMix64(pub u64); + +impl SplitMix64 { + pub fn new(seed: u64) -> Self { Self(seed) } + pub fn new_from_i64(seed: i64) -> Self { + Self::new(u64::from_be_bytes(seed.to_be_bytes())) + } + + #[inline] + pub fn next_u64(&mut self) -> u64 { + let mut z = w(self.0) + w(0x9E37_79B9_7F4A_7C15_u64); + self.0 = z.0; + z = (z ^ (z >> 30)) * w(0xBF58_476D_1CE4_E5B9_u64); + z = (z ^ (z >> 27)) * w(0x94D0_49BB_1331_11EB_u64); + (z ^ (z >> 31)).0 + } + + #[inline] + pub fn next_i64(&mut self) -> i64 { + i64::from_be_bytes( + self.next_u64().to_be_bytes()) + } + + #[inline] + pub fn next_open01(&mut self) -> f64 { + u64_to_open01(self.next_u64()) + } +} + +pub fn mix(v1: f32, v2: f32, mix: f32) -> f32 { + v1 * (1.0 - mix) + v2 * mix +} + +pub fn clamp(f: f32, min: f32, max: f32) -> f32 { + if f < min { min } + else if f > max { max } + else { f } +} + +pub fn square_135(phase: f32) -> f32 { + fast_sin(phase) + + fast_sin(phase * 3.0) / 3.0 + + fast_sin(phase * 5.0) / 5.0 +} + +pub fn square_35(phase: f32) -> f32 { + fast_sin(phase * 3.0) / 3.0 + + fast_sin(phase * 5.0) / 5.0 +} + +// note: MIDI note value? +pub fn note_to_freq(note: f32) -> f32 { + 440.0 * (2.0_f32).powf((note - 69.0) / 12.0) +} + +// Ported from LMMS under GPLv2 +// * DspEffectLibrary.h - library with template-based inline-effects +// * Copyright (c) 2006-2014 Tobias Doerffel +// +/// Signal distortion +/// ```text +/// gain: 0.1 - 5.0 default = 1.0 +/// threshold: 0.0 - 100.0 default = 0.8 +/// i: signal +/// ``` +pub fn f_distort(gain: f32, threshold: f32, i: f32) -> f32 { + gain * ( + i * ( i.abs() + threshold ) + / ( i * i + (threshold - 1.0) * i.abs() + 1.0 )) +} + +// Ported from LMMS under GPLv2 +// * DspEffectLibrary.h - library with template-based inline-effects +// * Copyright (c) 2006-2014 Tobias Doerffel +// +/// Foldback Signal distortion +/// ```text +/// gain: 0.1 - 5.0 default = 1.0 +/// threshold: 0.0 - 100.0 default = 0.8 +/// i: signal +/// ``` +pub fn f_fold_distort(gain: f32, threshold: f32, i: f32) -> f32 { + if i >= threshold || i < -threshold { + gain + * (( + ((i - threshold) % threshold * 4.0).abs() + - threshold * 2.0).abs() + - threshold) + } else { + gain * i + } +} + +pub fn lerp(x: f32, a: f32, b: f32) -> f32 { + (a * (1.0 - x)) + (b * x) +} + +pub fn lerp64(x: f64, a: f64, b: f64) -> f64 { + (a * (1.0 - x)) + (b * x) +} + +pub fn p2range(x: f32, a: f32, b: f32) -> f32 { + lerp(x, a, b) +} + +pub fn p2range_exp(x: f32, a: f32, b: f32) -> f32 { + let x = x * x; + (a * (1.0 - x)) + (b * x) +} + +pub fn p2range_exp4(x: f32, a: f32, b: f32) -> f32 { + let x = x * x * x * x; + (a * (1.0 - x)) + (b * x) +} + + +pub fn range2p(v: f32, a: f32, b: f32) -> f32 { + ((v - a) / (b - a)).abs() +} + +pub fn range2p_exp(v: f32, a: f32, b: f32) -> f32 { + (((v - a) / (b - a)).abs()).sqrt() +} + +pub fn range2p_exp4(v: f32, a: f32, b: f32) -> f32 { + (((v - a) / (b - a)).abs()).sqrt().sqrt() +} + +/// ```text +/// gain: 24.0 - -90.0 default = 0.0 +/// ``` +pub fn gain2coef(gain: f32) -> f32 { + if gain > -90.0 { + 10.0_f32.powf(gain * 0.05) + } else { + 0.0 + } +} + +// quickerTanh / quickerTanh64 credits to mopo synthesis library: +// Under GPLv3 or any later. +// Little IO +// Matt Tytel +pub fn quicker_tanh64(v: f64) -> f64 { + let square = v * v; + v / (1.0 + square / (3.0 + square / 5.0)) +} + +pub fn quicker_tanh(v: f32) -> f32 { + let square = v * v; + v / (1.0 + square / (3.0 + square / 5.0)) +} + +// quickTanh / quickTanh64 credits to mopo synthesis library: +// Under GPLv3 or any later. +// Little IO +// Matt Tytel +pub fn quick_tanh64(v: f64) -> f64 { + let abs_v = v.abs(); + let square = v * v; + let num = + v * (2.45550750702956 + + 2.45550750702956 * abs_v + + square * (0.893229853513558 + + 0.821226666969744 * abs_v)); + let den = + 2.44506634652299 + + (2.44506634652299 + square) + * (v + 0.814642734961073 * v * abs_v).abs(); + + num / den +} + +pub fn quick_tanh(v: f32) -> f32 { + let abs_v = v.abs(); + let square = v * v; + let num = + v * (2.45550750702956 + + 2.45550750702956 * abs_v + + square * (0.893229853513558 + + 0.821226666969744 * abs_v)); + let den = + 2.44506634652299 + + (2.44506634652299 + square) + * (v + 0.814642734961073 * v * abs_v).abs(); + + num / den +} + +/// A helper function for exponential envelopes: +#[inline] +pub fn sqrt4_to_pow4(x: f32, v: f32) -> f32 { + if v > 0.75 { + let xsq1 = x.sqrt(); + let xsq = xsq1.sqrt(); + let v = (v - 0.75) * 4.0; + xsq1 * (1.0 - v) + xsq * v + + } else if v > 0.5 { + let xsq = x.sqrt(); + let v = (v - 0.5) * 4.0; + x * (1.0 - v) + xsq * v + + } else if v > 0.25 { + let xx = x * x; + let v = (v - 0.25) * 4.0; + x * v + xx * (1.0 - v) + + } else { + let xx = x * x; + let xxxx = xx * xx; + let v = v * 4.0; + xx * v + xxxx * (1.0 - v) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TriggerClock { + clock_phase: f64, + clock_inc: f64, + prev_trigger: bool, + clock_samples: u32, +} + +impl TriggerClock { + pub fn new() -> Self { + Self { + clock_phase: 0.0, + clock_inc: 0.0, + prev_trigger: true, + clock_samples: 0, + } + } + + #[inline] + pub fn reset(&mut self) { + self.clock_samples = 0; + self.clock_inc = 0.0; + self.prev_trigger = true; + self.clock_samples = 0; + } + + #[inline] + pub fn next_phase(&mut self, trigger_in: f32) -> f64 { + if self.prev_trigger { + if trigger_in <= 0.25 { + self.prev_trigger = false; + } + + } else if trigger_in > 0.75 { + self.prev_trigger = true; + + if self.clock_samples > 0 { + self.clock_inc = + 1.0 / (self.clock_samples as f64); + } + + self.clock_samples = 0; + } + + self.clock_samples += 1; + + self.clock_phase += self.clock_inc; + + self.clock_phase + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_range2p_exp() { + let a = p2range_exp(0.5, 1.0, 100.0); + let x = range2p_exp(a, 1.0, 100.0); + + assert!((x - 0.5).abs() < std::f32::EPSILON); + } + + #[test] + fn check_range2p() { + let a = p2range(0.5, 1.0, 100.0); + let x = range2p(a, 1.0, 100.0); + + assert!((x - 0.5).abs() < std::f32::EPSILON); + } +} diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs new file mode 100644 index 0000000..d561b14 --- /dev/null +++ b/src/dsp/mod.rs @@ -0,0 +1,1180 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +#[allow(non_upper_case_globals)] +mod node_amp; +#[allow(non_upper_case_globals)] +mod node_sin; +#[allow(non_upper_case_globals)] +mod node_out; +#[allow(non_upper_case_globals)] +mod node_test; +#[allow(non_upper_case_globals)] +mod node_tseq; + +pub mod tracker; +mod satom; +pub mod helpers; + +use crate::nodes::NodeAudioContext; + +use crate::util::AtomicFloat; +use std::sync::Arc; + +pub type LedPhaseVals<'a> = &'a [Arc]; + +pub use satom::*; + +use node_amp::Amp; +use node_sin::Sin; +use node_out::Out; +use node_test::Test; +use node_tseq::TSeq; + +pub const MIDI_MAX_FREQ : f32 = 13289.75; + +pub const MAX_BLOCK_SIZE : usize = 128; + +/// This trait is an interface between the graph functions +/// and the AtomDataModel of the UI. +pub trait GraphAtomData { + fn get(&self, node_id: usize, param_idx: u32) -> Option; + fn get_denorm(&self, node_id: usize, param_idx: u32) -> f32; +} + + +pub type GraphFun = Box f32>; + +/// This trait represents a DspNode for the [crate::matrix::Matrix] +pub trait DspNode { + /// Number of outputs this node has. + fn outputs() -> usize; + + /// Updates the sample rate for the node. + fn set_sample_rate(&mut self, _srate: f32); + + /// Reset any internal state of the node. + fn reset(&mut self); + + /// The code DSP function. + /// + /// * `atoms` are un-smoothed parameters. they can hold integer settings, + /// samples or even strings. + /// * `params` are smoother paramters, those who usually have a knob + /// associated with them. + /// * `inputs` contain all the possible inputs. In contrast to `params` + /// these inputs might be overwritten by outputs of other nodes. + /// * `outputs` are the output buffers of this node. + fn process( + &mut self, ctx: &mut T, atoms: &[SAtom], params: &[ProcBuf], + inputs: &[ProcBuf], outputs: &mut [ProcBuf], + led: LedPhaseVals); + + /// A function factory for generating a graph for the generic node UI. + fn graph_fun() -> Option { None } +} + +/// A processing buffer with the exact right maximum size. +#[derive(Clone, Copy)] +pub struct ProcBuf(*mut [f32; MAX_BLOCK_SIZE]); + +impl ProcBuf { + pub fn new() -> Self { + ProcBuf(Box::into_raw(Box::new([0.0; MAX_BLOCK_SIZE]))) + } + + pub fn null() -> Self { + ProcBuf(std::ptr::null_mut()) + } +} + +impl crate::monitor::MonitorSource for &ProcBuf { + fn copy_to(&self, len: usize, slice: &mut [f32]) { + unsafe { slice.copy_from_slice(&(*self.0)[0..len]) } + } +} + +unsafe impl Send for ProcBuf {} +//unsafe impl Sync for HexoSynthShared {} + +impl ProcBuf { + #[inline] + pub fn write(&mut self, idx: usize, v: f32) { + unsafe { + (*self.0)[idx] = v; + } + } + + #[inline] + pub fn write_from(&mut self, slice: &[f32]) { + unsafe { + (*self.0)[0..slice.len()].copy_from_slice(slice); + } + } + + #[inline] + pub fn read(&self, idx: usize) -> f32 { unsafe { (*self.0)[idx] } } + + #[inline] + pub fn fill(&mut self, v: f32) { + unsafe { + (*self.0).fill(v); + } + } + + pub fn free(&self) { + if !self.0.is_null() { + drop(unsafe { Box::from_raw(self.0) }); + } + } +} + +impl std::fmt::Debug for ProcBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unsafe { + write!(f, "ProcBuf(")?; + if self.0.is_null() { + write!(f, "NULL ")?; + } else { + for i in 0..MAX_BLOCK_SIZE { + write!(f, "{:5.4} ", (*self.0)[i])?; + } + } + write!(f, ")") + } + } +} + +impl std::fmt::Display for ProcBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unsafe { + write!(f, "ProcBuf(0: {})", (*self.0)[0]) + } + } +} + +//#[derive(Debug, Clone, Copy)] +//enum UIParamDesc { +// Knob { width: usize, prec: usize, unit: &'static str }, +// Setting { labels: &'static [&'static str], unit: &'static str }, +//} + + +#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] +pub enum UIType { + Generic, + LfoA, + EnvA, + OscA, +} + +#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] +pub enum UICategory { + None, + Osc, + Mod, + NtoM, + Signal, + CV, + IOUtil, +} + +macro_rules! n_id { ($x: expr) => { $x } } +macro_rules! d_id { ($x: expr) => { $x } } + +macro_rules! define_lin { + ($n_id: ident $d_id: ident $min: expr, $max: expr) => { + macro_rules! $n_id { ($x: expr) => { + (($x - $min) / ($max - $min) as f32).abs() + } } + + macro_rules! $d_id { ($x: expr) => { + $min * (1.0 - $x) + $max * $x + } } + } +} + +macro_rules! define_exp { + ($n_id: ident $d_id: ident $min: expr, $max: expr) => { + macro_rules! $n_id { ($x: expr) => { + (($x - $min) / ($max - $min) as f32).abs().sqrt() + } } + macro_rules! $d_id { ($x: expr) => { + { let x : f32 = $x * $x; $min * (1.0 - x) + $max * x } + } } + } +} + +macro_rules! define_exp4 { + ($n_id: ident $d_id: ident $min: expr, $max: expr) => { + macro_rules! $n_id { ($x: expr) => { + (($x - $min) / ($max - $min)).abs().sqrt().sqrt() + } } + macro_rules! $d_id { ($x: expr) => { + { let x : f32 = $x * $x * $x * $x; $min * (1.0 - x) + $max * x } + } } + } +} + +macro_rules! n_pit { ($x: expr) => { + ((($x as f32).max(0.01) / 440.0).log2() / 10.0) +// ((($x as f32).max(0.01) / 440.0).log2() / 5.0) +} } + +macro_rules! d_pit { ($x: expr) => { + { + let note : f32 = ($x as f32) * 10.0; + 440.0 * (2.0_f32).powf(note) + } +} } + +// norm-fun denorm-min +// denorm-fun denorm-max +define_exp!{n_gain d_gain 0.0, 2.0} +define_exp!{n_att d_att 0.0, 1.0} + +// A note about the input-indicies: +// +// Atoms and Input parameters share the same global ID space +// because thats how the client of the Matrix API needs to refer to +// them. Beyond the Matrix API the atom data is actually split apart +// from the parameters, because they are not smoothed. +// +// The index there only matters for addressing the atoms in the global atom vector. +// +// But the actually second index here is for referring to the atom index +// relative to the absolute count of atom data a Node has. +// It is used by the [Matrix] to get the global ParamId for the atom data +// when iterating through the atoms of a Node and initializes the default data +// for new nodes. +macro_rules! node_list { + ($inmacro: ident) => { + $inmacro!{ + nop => Nop, + amp => Amp UIType::Generic UICategory::Signal + // node_param_idx + // name denorm_fun norm norm denorm + // norm_fun min max default + (0 inp n_id d_id -1.0, 1.0, 0.0) + (1 gain n_gain d_gain 0.0, 1.0, 1.0) + (2 att n_att d_att 0.0, 1.0, 1.0) + {3 0 neg_att setting(1) 0 1} + [0 sig], + tseq => TSeq UIType::Generic UICategory::CV + (0 clock n_id d_id 0.0, 1.0, 0.0) + {1 0 cmode setting(1) 0 2} + [0 trk1] + [1 trk2] + [2 trk3] + [3 trk4] + [4 trk5] + [5 trk6], + sin => Sin UIType::Generic UICategory::Osc + (0 freq n_pit d_pit -1.0, 1.0, 440.0) + [0 sig], + out => Out UIType::Generic UICategory::IOUtil + (0 ch1 n_id d_id -1.0, 1.0, 0.0) + (1 ch2 n_id d_id -1.0, 1.0, 0.0) + // node_param_idx + // | atom_idx + // | | name constructor min max + // | | | | defa/lt_/value + // | | | | | | / + {2 0 mono setting(0) 0 1}, + test => Test UIType::Generic UICategory::IOUtil + (0 f n_id d_id 0.0, 1.0, 0.5) + {1 0 s setting(0) 0 10}, + } + } +} + +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod labels { + pub mod Test { + pub const s : [&'static str; 11] = [ + "Zero", "One", "Two", "Three", "Four", + "Five", "Six", "Seven", "Eigth", "Nine", "Ten" + ]; + } + + pub mod Out { + pub const mono : [&'static str; 2] = ["Mono", "Stereo"]; + } + + pub mod Amp { + pub const neg_att : [&'static str; 2] = ["Allow", "Clip"]; + } + + pub mod TSeq { + pub const cmode : [&'static str; 3] = ["RowT", "PatT", "Phase"]; + } +} + +impl UICategory { + #[allow(unused_assignments)] + pub fn get_node_ids(&self, mut skip: usize, mut fun: F) { + macro_rules! make_cat_lister { + ($s1: ident => $v1: ident, + $($str: ident => $variant: ident + UIType:: $gui_type: ident + UICategory:: $ui_cat: ident + $(($in_idx: literal $para: ident + $n_fun: ident $d_fun: ident + $min: expr, $max: expr, $def: expr))* + $({$in_at_idx: literal $at_idx: literal $atom: ident + $at_fun: ident ($at_init: tt) + $amin: literal $amax: literal})* + $([$out_idx: literal $out: ident])* + ,)+ + ) => { + $(if UICategory::$ui_cat == *self { + if skip == 0 { + fun(NodeId::$variant(0)); + } else { + skip -= 1 + } + })+ + } + } + + node_list!{make_cat_lister}; + } +} + +macro_rules! make_node_info_enum { + ($s1: ident => $v1: ident, + $($str: ident => $variant: ident + UIType:: $gui_type: ident + UICategory:: $ui_cat: ident + $(($in_idx: literal $para: ident + $n_fun: ident $d_fun: ident + $min: expr, $max: expr, $def: expr))* + $({$in_at_idx: literal $at_idx: literal $atom: ident + $at_fun: ident ($at_init: expr) + $amin: literal $amax: literal})* + $([$out_idx: literal $out: ident])* + ,)+ + ) => { + /// Holds information about the node type that was allocated. + /// It stores the names of inputs, output and atoms for uniform + /// access. + /// + /// The [crate::NodeConfigurator] allocates and holds instances + /// of this type for access by [NodeId]. + /// See also [crate::NodeConfigurator::node_by_id] and + /// [crate::Matrix::info_for]. + #[derive(Debug, Clone)] + pub enum NodeInfo { + $v1, + $($variant((NodeId, crate::dsp::ni::$variant))),+ + } + + impl NodeInfo { + /// Allocates a new [NodeInfo] from a [NodeId]. + /// Usually you access [NodeInfo] in the UI thread via + /// [crate::NodeConfigurator::node_by_id] + /// or [crate::Matrix::info_for]. + pub fn from_node_id(nid: NodeId) -> NodeInfo { + match nid { + NodeId::$v1 => NodeInfo::$v1, + $(NodeId::$variant(_) => NodeInfo::$variant((nid, crate::dsp::ni::$variant::new()))),+ + } + } + } + + /// Refers to an input paramter or atom of a specific + /// [Node] referred to by a [NodeId]. + /// + /// To obtain a [ParamId] you use one of these: + /// * [NodeId::atom_param_by_idx] + /// * [NodeId::inp_param_by_idx] + /// * [NodeId::param_by_idx] + /// * [NodeId::inp_param] + /// + /// To obtain an input and output index for a port use: + /// * [NodeId::inp] + /// * [NodeId::out] + /// + ///``` + /// use hexodsp::*; + /// let freq_param = NodeId::Sin(2).inp_param("freq").unwrap(); + /// + /// assert!(!freq_param.is_atom()); + /// + /// // Access the min/max values of this paramter: + /// assert_eq!(freq_param.param_min_max().unwrap(), (-1.0, 1.0)); + /// + /// // Access the default value: + /// assert_eq!(freq_param.as_atom_def().f(), 0.0); + /// + /// // Normalize a value (convert frequency to the 0.0 to 1.0 range) + /// assert_eq!(freq_param.norm(220.0), -0.1); + /// + /// // Denormalize a value (convert 0.0 to 1.0 range to frequency) + /// assert_eq!(freq_param.denorm(-0.1), 220.0); + ///``` + #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Ord, Hash)] + pub struct ParamId { + name: &'static str, + node: NodeId, + idx: u8, + } + + impl ParamId { + pub fn none() -> Self { + Self { + name: "NOP", + node: NodeId::Nop, + idx: 0, + } + } + + pub fn node_id(&self) -> NodeId { self.node } + pub fn inp(&self) -> u8 { self.idx } + pub fn name(&self) -> &'static str { self.name } + + /// Returns true if the [ParamId] has been associated with + /// the atoms of a Node, and not the paramters. Even if the + /// Atom is a `param()`. + pub fn is_atom(&self) -> bool { + match self.node { + NodeId::$v1 => false, + $(NodeId::$variant(_) => { + match self.idx { + $($in_idx => false,)* + $($in_at_idx => true,)* + _ => false, + } + }),+ + } + } + + pub fn param_min_max(&self) -> Option<(f32, f32)> { + match self.node { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match self.idx { + $($in_idx => Some(($min, $max)),)* + _ => None, + } + }),+ + } + } + + pub fn setting_lbl(&self, lbl_idx: usize) -> Option<&'static str> { + match self.node { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match self.idx { + $($in_at_idx => + if lbl_idx < crate::dsp::labels::$variant::$atom.len() { + Some(crate::dsp::labels::$variant::$atom[lbl_idx]) + } else { + None + },)* + _ => None, + } + }),+ + } + } + + pub fn setting_min_max(&self) -> Option<(i64, i64)> { + match self.node { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match self.idx { + $($in_at_idx => Some(($amin, $amax)),)* + _ => None, + } + }),+ + } + } + + pub fn as_atom_def(&self) -> SAtom { + match self.node { + NodeId::$v1 => SAtom::param(0.0), + $(NodeId::$variant(_) => { + match self.idx { + $($in_idx => SAtom::param(crate::dsp::norm_def::$variant::$para()),)* + $($in_at_idx => SAtom::$at_fun($at_init),)* + _ => SAtom::param(0.0), + } + }),+ + } + } + + pub fn norm_def(&self) -> f32 { + match self.node { + NodeId::$v1 => 0.0, + $(NodeId::$variant(_) => { + match self.idx { + $($in_idx => crate::dsp::norm_def::$variant::$para(),)* + _ => 0.0, + } + }),+ + } + } + + pub fn norm(&self, v: f32) -> f32 { + match self.node { + NodeId::$v1 => 0.0, + $(NodeId::$variant(_) => { + match self.idx { + $($in_idx => crate::dsp::norm_v::$variant::$para(v),)* + _ => 0.0, + } + }),+ + } + } + + pub fn denorm(&self, v: f32) -> f32 { + match self.node { + NodeId::$v1 => 0.0, + $(NodeId::$variant(_) => { + match self.idx { + $($in_idx => crate::dsp::denorm_v::$variant::$para(v),)* + _ => 0.0, + } + }),+ + } + } + } + + /// This enum is a collection of all implemented modules (aka nodes) + /// that are implemented. The associated `u8` index is the so called + /// _instance_ of the corresponding [Node] type. + /// + /// This is the primary way in this library to refer to a specific node + /// in the node graph that is managed by [crate::NodeConfigurator] + /// and executed by [crate::NodeExecutor]. + /// + /// To see how to actually use this, refer to the documentation + /// of [crate::Cell], where you will find an example. + #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Ord, Hash)] + pub enum NodeId { + $v1, + $($variant(u8)),+ + } + + impl std::fmt::Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeId::$v1 => write!(f, "{}", stringify!($s1)), + $(NodeId::$variant(i) => write!(f, "{} {}", stringify!($str), i)),+ + } + } + } + + impl NodeId { + pub fn to_instance(&self, instance: usize) -> NodeId { + match self { + NodeId::$v1 => NodeId::$v1, + $(NodeId::$variant(_) => NodeId::$variant(instance as u8)),+ + } + } + + pub fn graph_fun(&self) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => crate::dsp::$variant::graph_fun()),+ + } + } + + pub fn eq_variant(&self, other: &NodeId) -> bool { + match self { + NodeId::$v1 => *other == NodeId::$v1, + $(NodeId::$variant(_) => + if let NodeId::$variant(_) = other { true } + else { false }),+ + } + } + + pub fn from_node_info(ni: &NodeInfo) -> NodeId { + match ni { + NodeInfo::$v1 => NodeId::$v1, + $(NodeInfo::$variant(_) => NodeId::$variant(0)),+ + } + } + + pub fn name(&self) -> &'static str { + match self { + NodeId::$v1 => stringify!($s1), + $(NodeId::$variant(_) => stringify!($str)),+ + } + } + + pub fn from_str(name: &str) -> Self { + match name { + stringify!($s1) => NodeId::$v1, + $(stringify!($str) => NodeId::$variant(0)),+, + _ => NodeId::Nop, + } + } + + pub fn ui_type(&self) -> UIType { + match self { + NodeId::$v1 => UIType::Generic, + $(NodeId::$variant(_) => UIType::$gui_type),+ + } + } + + pub fn ui_category(&self) -> UICategory { + match self { + NodeId::$v1 => UICategory::None, + $(NodeId::$variant(_) => UICategory::$ui_cat),+ + } + } + + /// This maps the atom index of the node to the absolute + /// ParamId in the GUI (and in the [crate::matrix::Matrix]). + /// The Atom/Param duality is a bit weird because they share + /// the same ID namespace for the UI. But in the actual + /// backend, they are split. So the actual splitting happens + /// in the [crate::matrix::Matrix]. + pub fn atom_param_by_idx(&self, idx: usize) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match idx { + $($at_idx => Some(ParamId { + node: *self, + name: stringify!($atom), + idx: $in_at_idx, + }),)* + _ => None, + } + }),+ + } + } + + pub fn inp_param_by_idx(&self, idx: usize) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match idx { + $($in_idx => Some(ParamId { + node: *self, + name: stringify!($para), + idx: $in_idx, + }),)* + _ => None, + } + }),+ + } + } + + pub fn param_by_idx(&self, idx: usize) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match idx { + $($in_idx => Some(ParamId { + node: *self, + name: stringify!($para), + idx: $in_idx, + }),)* + $($in_at_idx => Some(ParamId { + node: *self, + name: stringify!($atom), + idx: $in_at_idx, + }),)* + _ => None, + } + }),+ + } + } + + pub fn inp_param(&self, name: &str) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match name { + $(stringify!($para) => Some(ParamId { + node: *self, + name: stringify!($para), + idx: $in_idx, + }),)* + $(stringify!($atom) => Some(ParamId { + node: *self, + name: stringify!($atom), + idx: $in_at_idx, + }),)* + _ => None, + } + }),+ + } + } + + pub fn inp(&self, name: &str) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match name { + $(stringify!($para) => Some($in_idx),)* + _ => None, + } + }),+ + } + } + + pub fn out_name_by_idx(&self, idx: u8) -> Option<&'static str> { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match idx { + $($out_idx => Some(stringify!($out)),)* + _ => None, + } + }),+ + } + } + + pub fn out(&self, name: &str) -> Option { + match self { + NodeId::$v1 => None, + $(NodeId::$variant(_) => { + match name { + $(stringify!($out) => Some($out_idx),)* + _ => None, + } + }),+ + } + } + + pub fn instance(&self) -> usize { + match self { + NodeId::$v1 => 0, + $(NodeId::$variant(i) => *i as usize),+ + } + } + } + + #[allow(non_snake_case)] + pub mod denorm_v { + $(pub mod $variant { + $(#[inline] pub fn $para(x: f32) -> f32 { $d_fun!(x) })* + })+ + } + + #[allow(non_snake_case)] + pub mod norm_def { + $(pub mod $variant { + $(#[inline] pub fn $para() -> f32 { $n_fun!($def) })* + })+ + } + + #[allow(non_snake_case)] + pub mod norm_v { + $(pub mod $variant { + $(#[inline] pub fn $para(v: f32) -> f32 { $n_fun!(v) })* + })+ + } + + #[allow(non_snake_case)] + pub mod denorm { + $(pub mod $variant { + $(#[inline] pub fn $para(buf: &crate::dsp::ProcBuf, frame: usize) -> f32 { + $d_fun!(buf.read(frame)) + })* + })+ + } + + #[allow(non_snake_case)] + pub mod inp_dir { + $(pub mod $variant { + $(#[inline] pub fn $para(buf: &crate::dsp::ProcBuf, frame: usize) -> f32 { + buf.read(frame) + })* + })+ + } + + #[allow(non_snake_case)] + pub mod inp { + $(pub mod $variant { + $(#[inline] pub fn $para(inputs: &[crate::dsp::ProcBuf]) -> &crate::dsp::ProcBuf { + &inputs[$in_idx] + })* + })+ + } + + #[allow(non_snake_case)] + pub mod at { + $(pub mod $variant { + $(#[inline] pub fn $atom(atoms: &[crate::dsp::SAtom]) -> &crate::dsp::SAtom { + &atoms[$at_idx] + })* + })+ + } + + #[allow(non_snake_case)] + pub mod out_dir { + $(pub mod $variant { + $(#[inline] pub fn $out(outputs: &mut [crate::dsp::ProcBuf], frame: usize, v: f32) { + outputs[$out_idx].write(frame, v); + })* + })+ + } + + #[allow(non_snake_case)] + pub mod out { + $(pub mod $variant { + $(#[inline] pub fn $out(outputs: &mut [crate::dsp::ProcBuf]) -> &mut crate::dsp::ProcBuf { + &mut outputs[$out_idx] + })* + })+ + } + + mod ni { + $( + #[derive(Debug, Clone)] + pub struct $variant { + inputs: Vec<&'static str>, + atoms: Vec<&'static str>, + outputs: Vec<&'static str>, + input_help: Vec<&'static str>, + output_help: Vec<&'static str>, + } + + impl $variant { + #[allow(unused_mut)] + pub fn new() -> Self { + let mut input_help = vec![$(crate::dsp::$variant::$para,)*]; + $(input_help.push(crate::dsp::$variant::$atom);)* + + Self { + inputs: vec![$(stringify!($para),)*], + atoms: vec![$(stringify!($atom),)*], + outputs: vec![$(stringify!($out),)*], + + input_help, + output_help: vec![$(crate::dsp::$variant::$out,)*], + } + } + + pub fn in_name(&self, in_idx: usize) -> Option<&'static str> { + if let Some(s) = self.inputs.get(in_idx) { + Some(*s) + } else { + Some(*(self.atoms.get(in_idx)?)) + } + } + + pub fn at_name(&self, in_idx: usize) -> Option<&'static str> { + Some(*(self.atoms.get(in_idx)?)) + } + + pub fn out_name(&self, out_idx: usize) -> Option<&'static str> { + Some(*(self.outputs.get(out_idx)?)) + } + + pub fn in_help(&self, in_idx: usize) -> Option<&'static str> { + Some(*self.input_help.get(in_idx)?) + } + + pub fn out_help(&self, out_idx: usize) -> Option<&'static str> { + Some(*(self.output_help.get(out_idx)?)) + } + + pub fn norm(&self, in_idx: usize, x: f32) -> f32 { + match in_idx { + $($in_idx => crate::dsp::norm_v::$variant::$para(x),)+ + _ => 0.0, + } + } + + pub fn denorm(&self, in_idx: usize, x: f32) -> f32 { + match in_idx { + $($in_idx => crate::dsp::denorm_v::$variant::$para(x),)+ + _ => 0.0, + } + } + + pub fn out_count(&self) -> usize { self.outputs.len() } + pub fn in_count(&self) -> usize { self.inputs.len() } + pub fn at_count(&self) -> usize { self.atoms.len() } + } + )+ + } + + impl NodeInfo { + pub fn from(s: &str) -> Self { + match s { + stringify!($s1) => NodeInfo::$v1, + $(stringify!($str) => + NodeInfo::$variant( + (NodeId::$variant(0), + crate::dsp::ni::$variant::new()))),+, + _ => NodeInfo::Nop, + } + } + + pub fn in_name(&self, idx: usize) -> Option<&'static str> { + match self { + NodeInfo::$v1 => None, + $(NodeInfo::$variant((_, ni)) => ni.in_name(idx)),+ + } + } + + pub fn out_name(&self, idx: usize) -> Option<&'static str> { + match self { + NodeInfo::$v1 => None, + $(NodeInfo::$variant((_, ni)) => ni.out_name(idx)),+ + } + } + + pub fn in_help(&self, idx: usize) -> Option<&'static str> { + match self { + NodeInfo::$v1 => None, + $(NodeInfo::$variant((_, ni)) => ni.in_help(idx)),+ + } + } + + pub fn out_help(&self, idx: usize) -> Option<&'static str> { + match self { + NodeInfo::$v1 => None, + $(NodeInfo::$variant((_, ni)) => ni.out_help(idx)),+ + } + } + + pub fn to_id(&self) -> NodeId { + match self { + NodeInfo::$v1 => NodeId::$v1, + $(NodeInfo::$variant((id, _)) => *id),+ + } + } + + pub fn at_count(&self) -> usize { + match self { + NodeInfo::$v1 => 0, + $(NodeInfo::$variant(n) => n.1.at_count()),+ + } + } + + pub fn in_count(&self) -> usize { + match self { + NodeInfo::$v1 => 0, + $(NodeInfo::$variant(n) => n.1.in_count()),+ + } + } + + pub fn out_count(&self) -> usize { + match self { + NodeInfo::$v1 => 0, + $(NodeInfo::$variant(n) => n.1.out_count()),+ + } + } + } + } +} + +macro_rules! make_node_enum { + ($s1: ident => $v1: ident, + $($str: ident => $variant: ident + UIType:: $gui_type: ident + UICategory:: $ui_cat: ident + $(($in_idx: literal $para: ident + $n_fun: ident $d_fun: ident + $min: expr, $max: expr, $def: expr))* + $({$in_at_idx: literal $at_idx: literal $atom: ident + $at_fun: ident ($at_init: expr) + $amin: literal $amax: literal})* + $([$out_idx: literal $out: ident])* + ,)+ + ) => { + /// Represents the actually by the DSP thread ([crate::NodeExecutor]) + /// executed [Node]. You don't construct this directly, but let the + /// [crate::NodeConfigurator] or more abstract types like + /// [crate::Matrix] do this for you. See also [NodeId] for a way to + /// refer to these. + /// + /// The method [Node::process] is called by [crate::NodeExecutor] + /// and comes with the overhead of a big `match` statement. + /// + /// This is the only point of primitive polymorphism inside + /// the DSP graph. Dynamic polymorphism via the trait object + /// is not done, as I hope the `match` dispatch is a slight bit faster + /// because it's more static. + /// + /// The size of a [Node] is also limited and protected by a test + /// in the test suite. The size should not be needlessly increased + /// by implementations, in the hope to achieve better + /// cache locality. All allocated [Node]s are held in a big + /// continuous vector inside the [crate::NodeExecutor]. + /// + /// The function [node_factory] is responsible for actually creating + /// the [Node]. + #[derive(Debug, Clone)] + pub enum Node { + /// An empty node that does nothing. It's a placeholder + /// for non allocated nodes. + $v1, + $($variant { node: $variant },)+ + } + + impl Node { + /// Returns the [NodeId] that can be used to refer to this node. + /// The node does not store it's instance index, so you have to + /// provide it. If the instance is of no meaning for the + /// use case pass 0 to `instance`. + pub fn to_id(&self, instance: usize) -> NodeId { + match self { + Node::$v1 => NodeId::$v1, + $(Node::$variant { .. } => NodeId::$variant(instance as u8)),+ + } + } + + /// Resets any state of this [Node], such as + /// any internal state variables or counters or whatever. + /// The [Node] should just behave as if it was freshly returned + /// from [node_factory]. + pub fn reset(&mut self) { + match self { + Node::$v1 => {}, + $(Node::$variant { node } => { + node.reset(); + }),+ + } + } + + /// Sets the current sample rate this [Node] should operate at. + pub fn set_sample_rate(&mut self, sample_rate: f32) { + match self { + Node::$v1 => {}, + $(Node::$variant { node } => { + node.set_sample_rate(sample_rate); + }),+ + } + } + + } + } +} + +node_list!{make_node_info_enum} +node_list!{make_node_enum} + +pub fn node_factory(node_id: NodeId) -> Option<(Node, NodeInfo)> { + macro_rules! make_node_factory_match { + ($s1: expr => $v1: ident, + $($str: ident => $variant: ident + UIType:: $gui_type: ident + UICategory:: $ui_cat: ident + $(($in_idx: literal $para: ident + $n_fun: ident $d_fun: ident + $min: expr, $max: expr, $def: expr))* + $({$in_at_idx: literal $at_idx: literal $atom: ident + $at_fun: ident ($at_init: expr) + $amin: literal $amax: literal})* + $([$out_idx: literal $out: ident])* + ,)+ + ) => { + match node_id { + $(NodeId::$variant(_) => Some(( + Node::$variant { node: $variant::new() }, + NodeInfo::from_node_id(node_id), + )),)+ + _ => None, + } + } + } + + node_list!{make_node_factory_match} +} + +impl Node { + /// This function is the heart of any DSP. + /// It dispatches this call to the corresponding [Node] implementation. + /// + /// You don't want to call this directly, but let [crate::NodeConfigurator] and + /// [crate::NodeExecutor] do their magic for you. + /// + /// The slices get passed a [ProcBuf] which is a super _unsafe_ + /// buffer, that requires special care and invariants to work safely. + /// + /// Arguments: + /// * `ctx`: The [NodeAudioContext] usually provides global context information + /// such as access to the actual buffers of the audio driver or access to + /// MIDI events. + /// * `atoms`: The [SAtom] settings the user can set in the UI or via + /// other means. These are usually non interpolated/smoothed settings. + /// * `params`: The smoothed input parameters as set by the user (eg. in the UI). + /// There is usually no reason to use these, because any parameter can be + /// overridden by assigning an output port to the corresponding input. + /// This is provided for the rare case that you still want to use the + /// value the user set in the interface, and not the input CV signal. + /// * `inputs`: For each `params` parameter there is a input port. + /// This slice will contain either a buffer from `params` or some output + /// buffer from some other (previously executed) [Node]s output. + /// * `outputs`: The output buffers this node will write it's signal/CV + /// results to. + /// * `led`: Contains the feedback [LedPhaseVals], which are used + /// to communicate the current value (set once per `process()` call, usually at the end) + /// of the most important internal signal. Usually stuff like the output + /// value of an oscillator, envelope or the current sequencer output + /// value. It also provides a second value, a so called _phase_ + /// which is usually used by graphical frontends to determine + /// the phase of the oscillator, envelope or the sequencer to + /// display some kind of position indicator. + #[inline] + pub fn process( + &mut self, ctx: &mut T, atoms: &[SAtom], params: &[ProcBuf], + inputs: &[ProcBuf], outputs: &mut [ProcBuf], led: LedPhaseVals) + { + macro_rules! make_node_process { + ($s1: ident => $v1: ident, + $($str: ident => $variant: ident + UIType:: $gui_type: ident + UICategory:: $ui_cat: ident + $(($in_idx: literal $para: ident + $n_fun: ident $d_fun: ident + $min: expr, $max: expr, $def: expr))* + $({$in_at_idx: literal $at_idx: literal $atom: ident + $at_fun: ident ($at_init: expr) + $amin: literal $amax: literal})* + $([$out_idx: literal $out: ident])* + ,)+ + ) => { + match self { + Node::$v1 => {}, + $(Node::$variant { node } => node.process(ctx, atoms, params, inputs, outputs, led),)+ + } + } + } + + node_list!{make_node_process} + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_node_size_staying_small() { + assert_eq!(std::mem::size_of::(), 48); + assert_eq!(std::mem::size_of::(), 2); + assert_eq!(std::mem::size_of::(), 24); + } + + #[test] + fn check_pitch() { + assert_eq!(d_pit!(-0.2).round() as i32, 110_i32); + assert_eq!((n_pit!(110.0) * 100.0).round() as i32, -20_i32); + assert_eq!(d_pit!(0.0).round() as i32, 440_i32); + assert_eq!((n_pit!(440.0) * 100.0).round() as i32, 0_i32); + assert_eq!(d_pit!(0.3).round() as i32, 3520_i32); + assert_eq!((n_pit!(3520.0) * 100.0).round() as i32, 30_i32); + + for i in 1..999 { + let x = (((i as f32) / 1000.0) - 0.5) * 2.0; + let r = d_pit!(x); + println!("x={:8.5} => {:8.5}", x, r); + assert_eq!( + (n_pit!(r) * 10000.0).round() as i32, + (x * 10000.0).round() as i32); + } + } +} diff --git a/src/dsp/node_amp.rs b/src/dsp/node_amp.rs new file mode 100644 index 0000000..bdb8813 --- /dev/null +++ b/src/dsp/node_amp.rs @@ -0,0 +1,86 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::NodeAudioContext; +use crate::dsp::{SAtom, ProcBuf, DspNode, LedPhaseVals}; + +/// A simple amplifier +#[derive(Debug, Clone)] +pub struct Amp { +} + +impl Amp { + pub fn new() -> Self { + Self { + } + } + pub const inp : &'static str = + "Amp inp\nSignal input\nRange: (-1..1)\n"; + pub const att : &'static str = + "Amp att\nAttenuate input. Does only attenuate the signal, not amplify it.\n\ + Use this for envelope input.\nRange: (0..1)\n"; + pub const gain : &'static str = + "Amp gain\nGain input. This control can actually amplify the signal.\nRange: (0..1)\n"; + pub const neg_att : &'static str = + "Amp neg\nIf this is set to 'Clip', only positive inputs to 'att' are used.\nRange: (0..1)\n"; + pub const sig : &'static str = + "Amp sig\nAmplified signal output\nRange: (-1..1)\n"; +} + +impl DspNode for Amp { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, _srate: f32) { } + fn reset(&mut self) { } + + #[inline] + fn process( + &mut self, ctx: &mut T, atoms: &[SAtom], _params: &[ProcBuf], + inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + use crate::dsp::{out, inp, denorm, denorm_v, inp_dir, at}; + + let gain = inp::Amp::gain(inputs); + let att = inp::Amp::att(inputs); + let inp = inp::Amp::inp(inputs); + let out = out::Amp::sig(outputs); + let neg = at::Amp::neg_att(atoms); + + let last_frame = ctx.nframes() - 1; + + let last_val = + if neg.i() > 0 { + for frame in 0..ctx.nframes() { + out.write(frame, + inp.read(frame) + * denorm_v::Amp::att( + inp_dir::Amp::att(att, frame) + .max(0.0)) + * denorm::Amp::gain(gain, frame)); + } + + inp.read(last_frame) + * denorm_v::Amp::att( + inp_dir::Amp::att(att, last_frame) + .max(0.0)) + * denorm::Amp::gain(gain, last_frame) + + } else { + for frame in 0..ctx.nframes() { + out.write(frame, + inp.read(frame) + * denorm_v::Amp::att( + inp_dir::Amp::att(att, frame).abs()) + * denorm::Amp::gain(gain, frame)); + } + + inp.read(last_frame) + * denorm_v::Amp::att( + inp_dir::Amp::att(att, last_frame).abs()) + * denorm::Amp::gain(gain, last_frame) + }; + + ctx_vals[0].set(last_val); + } +} diff --git a/src/dsp/node_out.rs b/src/dsp/node_out.rs new file mode 100644 index 0000000..6c2852d --- /dev/null +++ b/src/dsp/node_out.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::NodeAudioContext; +use crate::dsp::{SAtom, ProcBuf, inp, at, DspNode, LedPhaseVals}; + +/// The (stereo) output port of the plugin +#[derive(Debug, Clone)] +pub struct Out { + /// - 0: signal channel 1 + /// - 1: signal channel 2 + input: [f32; 2], +} + +impl Out { + pub fn new() -> Self { + Self { + input: [0.0; 2], + } + } + + pub const mono : &'static str = + "Out mono\nIf enabled, ch1 will be sent to both output channels\n(UI only)"; + pub const ch1 : &'static str = + "Out ch1\nAudio channel 1 (left)\nRange: (-1..1)"; + pub const ch2 : &'static str = + "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + + pub const ch3 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch4 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch5 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch6 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch7 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch8 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch9 : &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch10: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch11: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch12: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch13: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch14: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch15: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch16: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; + pub const ch17: &'static str = "Out ch2\nAudio channel 2 (right)\nRange: (-1..1)"; +} + +impl DspNode for Out { + fn outputs() -> usize { 0 } + + fn set_sample_rate(&mut self, _srate: f32) { } + fn reset(&mut self) { } + + #[inline] + fn process( + &mut self, ctx: &mut T, atoms: &[SAtom], _params: &[ProcBuf], + inputs: &[ProcBuf], _outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + let in1 = inp::Out::ch1(inputs); + + if at::Out::mono(atoms).i() > 0 { + for frame in 0..ctx.nframes() { + ctx.output(0, frame, in1.read(frame)); + ctx.output(1, frame, in1.read(frame)); + } + } else { + let in2 = inp::Out::ch2(inputs); + + for frame in 0..ctx.nframes() { + ctx.output(0, frame, in1.read(frame)); + ctx.output(1, frame, in2.read(frame)); + } + } + + let last_val = in1.read(ctx.nframes() - 1); + ctx_vals[0].set(last_val); + } +} diff --git a/src/dsp/node_sin.rs b/src/dsp/node_sin.rs new file mode 100644 index 0000000..f47ab6c --- /dev/null +++ b/src/dsp/node_sin.rs @@ -0,0 +1,67 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::NodeAudioContext; +use crate::dsp::{SAtom, ProcBuf, denorm, out, inp, DspNode, LedPhaseVals}; +use crate::dsp::helpers::fast_sin; + + +/// A sine oscillator +#[derive(Debug, Clone)] +pub struct Sin { + /// Sample rate + srate: f32, + /// Oscillator phase + phase: f32, +} + +const TWOPI : f32 = 2.0 * std::f32::consts::PI; + +impl Sin { + pub fn new() -> Self { + Self { + srate: 44100.0, + phase: 0.0, + } + } + pub const freq : &'static str = + "Sin freq\nFrequency of the oscillator.\n\nRange: (-1..1)\n"; + pub const sig : &'static str = + "Sin sig\nOscillator signal output.\n\nRange: (-1..1)\n"; +} + +impl DspNode for Sin { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, srate: f32) { + self.srate = srate; + } + + fn reset(&mut self) { + self.phase = 0.0; + } + + #[inline] + fn process( + &mut self, ctx: &mut T, _atoms: &[SAtom], _params: &[ProcBuf], + inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + let o = out::Sin::sig(outputs); + let freq = inp::Sin::freq(inputs); + let isr = 1.0 / self.srate; + + let mut last_val = 0.0; + for frame in 0..ctx.nframes() { + let freq = denorm::Sin::freq(freq, frame); + + last_val = fast_sin(self.phase * TWOPI); + o.write(frame, last_val); + + self.phase += freq * isr; + self.phase = self.phase.fract(); + } + + ctx_vals[0].set(last_val); + } +} diff --git a/src/dsp/node_test.rs b/src/dsp/node_test.rs new file mode 100644 index 0000000..94cd33b --- /dev/null +++ b/src/dsp/node_test.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::NodeAudioContext; +use crate::dsp::{SAtom, ProcBuf, GraphFun, GraphAtomData, DspNode, LedPhaseVals}; + +/// A simple amplifier +#[derive(Debug, Clone)] +pub struct Test { +} + +impl Test { + pub fn new() -> Self { + Self { + } + } + pub const f : &'static str = "F Test"; + pub const s : &'static str = "S Test"; +// pub const gain : &'static str = +// "Amp gain\nGain input\nRange: (0..1)\n"; +// pub const sig : &'static str = +// "Amp sig\nAmplified signal output\nRange: (-1..1)\n"; +} + +impl DspNode for Test { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, _srate: f32) { } + fn reset(&mut self) { } + + #[inline] + fn process( + &mut self, _ctx: &mut T, _atoms: &[SAtom], _params: &[ProcBuf], + _inputs: &[ProcBuf], _outputs: &mut [ProcBuf], _led: LedPhaseVals) + { +// use crate::dsp::out; +// use crate::dsp::inp; +// use crate::dsp::denorm; +// +// let gain = inp::Test::gain(inputs); +// let inp = inp::Test::inp(inputs); +// let out = out::Test::sig(outputs); +// for frame in 0..ctx.nframes() { +// out.write(frame, inp.read(frame) * denorm::Test::gain(gain, frame)); +// } + } + + fn graph_fun() -> Option { + Some(Box::new(|_gd: &dyn GraphAtomData, _init: bool, x: f32| -> f32 { + x + })) + } +} diff --git a/src/dsp/node_tseq.rs b/src/dsp/node_tseq.rs new file mode 100644 index 0000000..a003b58 --- /dev/null +++ b/src/dsp/node_tseq.rs @@ -0,0 +1,162 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::NodeAudioContext; +use crate::dsp::helpers::TriggerClock; +use crate::dsp::{SAtom, ProcBuf, DspNode, LedPhaseVals}; +use crate::dsp::tracker::TrackerBackend; + +use crate::dsp::MAX_BLOCK_SIZE; + +/// A tracker based sequencer +#[derive(Debug)] +pub struct TSeq { + backend: Option>, + clock: TriggerClock, + srate: f64, +} + +impl Clone for TSeq { + fn clone(&self) -> Self { Self::new() } +} + +impl TSeq { + pub fn new() -> Self { + Self { + backend: None, + srate: 48000.0, + clock: TriggerClock::new(), + } + } + + pub fn set_backend(&mut self, backend: TrackerBackend) { + self.backend = Some(Box::new(backend)); + } + + pub const clock : &'static str = + "TSeq clock\nClock input\nRange: (0..1)\n"; + pub const cmode : &'static str = + "TSeq cmode\n'clock' input signal mode:\n\ + - RowT: Trigger = advance row\n\ + - PatT: Trigger = pattern rate\n\ + - Phase: Phase to pattern index\n\ + \n"; + pub const trk1 : &'static str = + "TSeq trk1\nTrack 1 signal output\nRange: (-1..1)\n"; + pub const trk2 : &'static str = + "TSeq trk2\nTrack 2 signal output\nRange: (-1..1)\n"; + pub const trk3 : &'static str = + "TSeq trk3\nTrack 3 signal output\nRange: (-1..1)\n"; + pub const trk4 : &'static str = + "TSeq trk4\nTrack 4 signal output\nRange: (-1..1)\n"; + pub const trk5 : &'static str = + "TSeq trk5\nTrack 5 signal output\nRange: (-1..1)\n"; + pub const trk6 : &'static str = + "TSeq trk6\nTrack 6 signal output\nRange: (-1..1)\n"; +} + +impl DspNode for TSeq { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, srate: f32) { + self.srate = srate as f64; + } + + fn reset(&mut self) { + self.backend = None; + self.clock.reset(); + } + + #[inline] + fn process( + &mut self, ctx: &mut T, atoms: &[SAtom], _params: &[ProcBuf], + inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + use crate::dsp::{out, inp, at}; + let clock = inp::TSeq::clock(inputs); + let cmode = at::TSeq::cmode(atoms); + + let backend = + if let Some(backend) = &mut self.backend { + backend + } else { return; }; + + backend.check_updates(); + + let mut phase_out : [f32; MAX_BLOCK_SIZE] = + [0.0; MAX_BLOCK_SIZE]; + + let cmode = cmode.i(); + + for frame in 0..ctx.nframes() { + let mut clock_phase = + if cmode < 2 { + self.clock.next_phase(clock.read(frame)) + } else { + clock.read(frame).abs() as f64 + }; + + let phase = + match cmode { + // RowT + 0 => { + let plen = backend.pattern_len() as f64; + while clock_phase >= plen { + clock_phase -= plen; + } + + clock_phase / plen + }, + // 1 | 2 PatT, Phase + _ => { + clock_phase = clock_phase.fract(); + clock_phase + }, + }; + + phase_out[frame] = phase as f32; + } + +// println!("PHASE {}", phase_out[0]); + + let mut col_out : [f32; MAX_BLOCK_SIZE] = + [0.0; MAX_BLOCK_SIZE]; + let col_out_slice = &mut col_out[0..ctx.nframes()]; + let phase_out_slice = &phase_out[0..ctx.nframes()]; + + let out_t1 = out::TSeq::trk1(outputs); + backend.get_col_at_phase( + 0, phase_out_slice, col_out_slice); + out_t1.write_from(col_out_slice); + + ctx_vals[0].set(col_out_slice[col_out_slice.len() - 1]); + + let out_t2 = out::TSeq::trk2(outputs); + backend.get_col_at_phase( + 1, phase_out_slice, col_out_slice); + out_t2.write_from(col_out_slice); + + let out_t3 = out::TSeq::trk3(outputs); + backend.get_col_at_phase( + 2, phase_out_slice, col_out_slice); + out_t3.write_from(col_out_slice); + + let out_t4 = out::TSeq::trk4(outputs); + backend.get_col_at_phase( + 3, phase_out_slice, col_out_slice); + out_t4.write_from(col_out_slice); + + let out_t5 = out::TSeq::trk5(outputs); + backend.get_col_at_phase( + 4, phase_out_slice, col_out_slice); + out_t5.write_from(col_out_slice); + + let out_t6 = out::TSeq::trk6(outputs); + backend.get_col_at_phase( + 5, phase_out_slice, col_out_slice); + out_t6.write_from(col_out_slice); + + ctx_vals[1].set(phase_out_slice[phase_out_slice.len() - 1]); + } +} diff --git a/src/dsp/satom.rs b/src/dsp/satom.rs new file mode 100644 index 0000000..1fcc6fb --- /dev/null +++ b/src/dsp/satom.rs @@ -0,0 +1,99 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +#[derive(Debug, Clone)] +pub enum SAtom { + Str(String), + MicroSample(Vec), + AudioSample((String, Option>>)), + Setting(i64), + Param(f32), +} + +impl SAtom { + pub fn str(s: &str) -> Self { SAtom::Str(s.to_string()) } + pub fn setting(s: i64) -> Self { SAtom::Setting(s) } + pub fn param(p: f32) -> Self { SAtom::Param(p) } + pub fn micro(m: &[f32]) -> Self { SAtom::MicroSample(m.to_vec()) } + pub fn audio(s: &str, m: std::sync::Arc>) -> Self { + SAtom::AudioSample((s.to_string(), Some(m))) + } + + pub fn audio_unloaded(s: &str) -> Self { + SAtom::AudioSample((s.to_string(), None)) + } + + pub fn default_of(&self) -> Self { + match self { + SAtom::Str(_) => SAtom::Str("".to_string()), + SAtom::MicroSample(_) => SAtom::MicroSample(vec![]), + SAtom::AudioSample(_) => SAtom::AudioSample(("".to_string(), None)), + SAtom::Setting(_) => SAtom::Setting(0), + SAtom::Param(_) => SAtom::Param(0.0), + } + } + + pub fn is_continous(&self) -> bool { + if let SAtom::Param(_) = self { true } + else { false } + } + + pub fn i(&self) -> i64 { + match self { + SAtom::Setting(i) => *i, + SAtom::Param(i) => *i as i64, + _ => 0, + } + } + + pub fn f(&self) -> f32 { + match self { + SAtom::Setting(i) => *i as f32, + SAtom::Param(i) => *i, + _ => 0.0, + } + } + + pub fn v_ref(&self) -> Option<&[f32]> { + match self { + SAtom::MicroSample(v) => Some(&v[..]), + SAtom::AudioSample((_, Some(v))) => Some(&v[..]), + _ => None, + } + } +} + +impl From for SAtom { + fn from(n: f32) -> Self { SAtom::Param(n) } +} + + +#[cfg(feature="hexotk")] +use hexotk::Atom; + +#[cfg(feature="hexotk")] +impl From for SAtom { + fn from(n: Atom) -> Self { + match n { + Atom::Str(s) => SAtom::Str(s), + Atom::MicroSample(s) => SAtom::MicroSample(s), + Atom::AudioSample(s) => SAtom::AudioSample(s), + Atom::Setting(s) => SAtom::Setting(s), + Atom::Param(s) => SAtom::Param(s), + } + } +} + +#[cfg(feature="hexotk")] +impl From for Atom { + fn from(n: SAtom) -> Atom { + match n { + SAtom::Str(s) => Atom::Str(s), + SAtom::MicroSample(s) => Atom::MicroSample(s), + SAtom::AudioSample(s) => Atom::AudioSample(s), + SAtom::Setting(s) => Atom::Setting(s), + SAtom::Param(s) => Atom::Param(s), + } + } +} diff --git a/src/dsp/tracker/mod.rs b/src/dsp/tracker/mod.rs new file mode 100644 index 0000000..07b130e --- /dev/null +++ b/src/dsp/tracker/mod.rs @@ -0,0 +1,254 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +mod pattern; +mod sequencer; + +use ringbuf::{RingBuffer, Producer, Consumer}; + +use std::rc::Rc; +use std::cell::RefCell; + +pub const MAX_COLS : usize = 6; +pub const MAX_PATTERN_LEN : usize = 256; +pub const MAX_RINGBUF_SIZE : usize = 64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum PatternColType { + Note, + Step, + Value, + Gate, +} + +pub use pattern::{PatternData, UIPatternModel}; +pub use sequencer::PatternSequencer; + +#[derive(Debug, Clone, Copy)] +pub enum PatternUpdateMsg { + UpdateColumn { + col: usize, + col_type: PatternColType, + pattern_len: usize, + data: [f32; MAX_PATTERN_LEN] + }, +} + +pub struct Tracker { + data: Rc>, + data_prod: Producer, + seq: Option, + seq_cons: Option>, +} + +impl Clone for Tracker { + fn clone(&self) -> Self { Tracker::new() } +} + + +pub struct TrackerBackend { + seq: PatternSequencer, + seq_cons: Consumer, + col_types: [PatternColType; MAX_COLS], +} + +impl std::fmt::Debug for TrackerBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tracker") + .field("col_types", &self.col_types) + .field("seq", &"PatternSequencer") + .field("seq_cons", &"RingbufConsumer") + .finish() + } +} + +impl Tracker { + pub fn new() -> Self { + let rb = RingBuffer::new(MAX_RINGBUF_SIZE); + let (prod, con) = rb.split(); + + Self { + data: Rc::new(RefCell::new(PatternData::new(MAX_PATTERN_LEN))), + data_prod: prod, + seq: Some(PatternSequencer::new(MAX_PATTERN_LEN)), + seq_cons: Some(con), + } + } + + pub fn data(&self) -> Rc> { self.data.clone() } + + pub fn send_one_update(&mut self) -> bool { + let mut data = self.data.borrow_mut(); + + for col in 0..MAX_COLS { + if data.col_is_modified_reset(col) { + data.sync_out_data(col); + let out_data = data.get_out_data(); + let msg = + PatternUpdateMsg::UpdateColumn { + col_type: data.col_type(col), + pattern_len: data.rows(), + data: out_data[col], + col, + }; + + let _ = self.data_prod.push(msg); + + return true; + } + } + + false + } + + pub fn get_backend(&mut self) -> TrackerBackend { + if self.seq.is_none() { + let rb = RingBuffer::new(MAX_RINGBUF_SIZE); + let (prod, con) = rb.split(); + + self.seq = Some(PatternSequencer::new(MAX_PATTERN_LEN)); + self.data_prod = prod; + self.seq_cons = Some(con); + } + + let seq = self.seq.take().unwrap(); + let seq_cons = self.seq_cons.take().unwrap(); + + TrackerBackend { + seq, + seq_cons, + col_types: [PatternColType::Value; MAX_COLS], + } + } +} + +impl TrackerBackend { + pub fn check_updates(&mut self) -> bool { + if let Some(msg) = self.seq_cons.pop() { + match msg { + PatternUpdateMsg::UpdateColumn { col, col_type, pattern_len, data } => { + self.col_types[col] = col_type; + self.seq.set_rows(pattern_len); + self.seq.set_col(col, &data); + }, + } + + true + + } else { + false + } + } + + pub fn pattern_len(&self) -> usize { self.seq.rows() } + + pub fn get_col_at_phase(&mut self, col: usize, phase: &[f32], out: &mut [f32]) { + if self.seq.rows() == 0 { + return; + } + + match self.col_types[col] { + PatternColType::Note | PatternColType::Step => { + self.seq.col_get_at_phase(col, phase, out) + }, + PatternColType::Value => self.seq.col_interpolate_at_phase(col, phase, out), + PatternColType::Gate => self.seq.col_gate_at_phase(col, phase, out), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_float_eq { + ($a:expr, $b:expr) => { + if ($a - $b).abs() > 0.0001 { + panic!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, $a, $b) + } + } + } + + #[test] + fn check_tracker_com_step() { + let mut t = Tracker::new(); + let mut backend = t.get_backend(); + + t.data().borrow_mut().set_rows(16); + t.data().borrow_mut().set_col_step_type(0); + t.data().borrow_mut().set_cell_value(0, 0, 0xFFF); + t.data().borrow_mut().set_cell_value(7, 0, 0x777); + t.data().borrow_mut().set_cell_value(15, 0, 0x000); + + while t.send_one_update() { } + while backend.check_updates() { } + + let mut out = [0.0; 16]; + + backend.get_col_at_phase(0, &[0.2, 0.5, 0.99], &mut out[..]); + assert_float_eq!(out[0], 1.0); + assert_float_eq!(out[1], 0.46666666); + assert_float_eq!(out[2], 0.0); + } + + #[test] + fn check_tracker_com_interp() { + let mut t = Tracker::new(); + let mut backend = t.get_backend(); + + t.data().borrow_mut().set_rows(16); + t.data().borrow_mut().set_col_value_type(0); + t.data().borrow_mut().set_cell_value(0, 0, 0xFFF); + t.data().borrow_mut().set_cell_value(7, 0, 0x777); + t.data().borrow_mut().set_cell_value(15, 0, 0x000); + + while t.send_one_update() { } + while backend.check_updates() { } + + let mut out = [0.0; 16]; + + backend.get_col_at_phase(0, &[0.2, 0.5, 0.999999], &mut out[..]); + assert_float_eq!(out[0], 0.83238); + assert_float_eq!(out[1], 0.46666666); + assert_float_eq!(out[2], 0.0); + } + + #[test] + fn check_tracker_com_gate() { + let mut t = Tracker::new(); + let mut backend = t.get_backend(); + + t.data().borrow_mut().set_rows(4); + t.data().borrow_mut().set_col_gate_type(0); + t.data().borrow_mut().set_cell_value(0, 0, 0xFF7); + t.data().borrow_mut().clear_cell(1, 0); + t.data().borrow_mut().set_cell_value(2, 0, 0xFF0); + t.data().borrow_mut().set_cell_value(3, 0, 0xFFF); + + while t.send_one_update() { } + while backend.check_updates() { } + + let mut out = [0.0; 64]; + + let mut phase = [0.0; 64]; + for (i, p) in phase.iter_mut().enumerate() { + *p = i as f32 / 63.0; + } + + //d// println!("----"); + backend.get_col_at_phase(0, &phase[..], &mut out[..]); + //d// println!("out: {:?}", &out[16..32]); + + assert_eq!(out[0..8], [1.0; 8]); + assert_eq!(out[8..16], [0.0; 8]); + assert_eq!(out[16..32],[0.0; 16]); + + assert_float_eq!(out[32], 1.0); + assert_eq!(out[33..48],[0.0; 15]); + + assert_eq!(out[48..64],[1.0; 16]); + } +} diff --git a/src/dsp/tracker/pattern.rs b/src/dsp/tracker/pattern.rs new file mode 100644 index 0000000..5010dd0 --- /dev/null +++ b/src/dsp/tracker/pattern.rs @@ -0,0 +1,639 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use super::PatternColType; +use super::MAX_PATTERN_LEN; +use super::MAX_COLS; +use crate::matrix_repr::PatternRepr; + +#[derive(Debug)] +pub struct PatternData { + col_types: [PatternColType; MAX_COLS], + data: Vec>>, + out_data: Vec<[f32; MAX_PATTERN_LEN]>, + strings: Vec>>, + cursor: (usize, usize), + rows: usize, + edit_step: usize, + dirty_col: [bool; MAX_COLS], +} + +impl PatternData { + pub fn new(rows: usize) -> Self { + Self { + col_types: [PatternColType::Value; MAX_COLS], + data: vec![vec![None; MAX_COLS]; MAX_PATTERN_LEN], + out_data: vec![[0.0; MAX_PATTERN_LEN]; MAX_COLS], + strings: vec![vec![None; MAX_COLS]; MAX_PATTERN_LEN], + cursor: (2, 2), + edit_step: 4, + dirty_col: [true; MAX_COLS], + rows, + } + } +} + +impl PatternData { + pub fn is_unset(&self) -> bool { + for ct in self.col_types.iter() { + if *ct != PatternColType::Value { return false; } + } + + for rows in self.data.iter() { + for col in rows.iter() { + if col.is_some() { return false; } + } + } + + true + } + + pub fn to_repr(&self) -> PatternRepr { + let mut col_types = [0; MAX_COLS]; + for (i, ct) in self.col_types.iter().enumerate() { + col_types[i] = + match ct { + PatternColType::Value => 0, + PatternColType::Note => 1, + PatternColType::Step => 2, + PatternColType::Gate => 3, + }; + } + + let mut data = vec![vec![-1; MAX_COLS]; MAX_PATTERN_LEN]; + for (row_idx, row) in self.data.iter().enumerate() { + for (col_idx, cell) in row.iter().enumerate() { + data[row_idx][col_idx] = + if let Some(c) = cell { *c as i32 } + else { -1 }; + } + } + + PatternRepr { + col_types, + data, + rows: self.rows, + edit_step: self.edit_step, + cursor: self.cursor, + } + } + + pub fn from_repr(&mut self, repr: &PatternRepr) { + for (i, ct) in repr.col_types.iter().enumerate() { + self.col_types[i] = + match *ct { + 0 => PatternColType::Value, + 1 => PatternColType::Note, + 2 => PatternColType::Step, + 3 => PatternColType::Gate, + _ => PatternColType::Value, + }; + + self.modified_col(i); + } + + for (row_idx, row) in repr.data.iter().enumerate() { + for (col_idx, cell) in row.iter().enumerate() { + self.data[row_idx][col_idx] = + if *cell < 0 { None } + else { Some(*cell as u16) }; + } + } + + self.rows = repr.rows; + self.edit_step = repr.edit_step; + self.cursor = repr.cursor; + } + + pub fn get_out_data(&self) -> &[[f32; MAX_PATTERN_LEN]] { + &self.out_data + } + + fn modified_col(&mut self, col: usize) { + if let Some(bit) = self.dirty_col.get_mut(col) { + *bit = true; + } + } + + pub fn col_is_modified_reset(&mut self, col: usize) -> bool { + if self.dirty_col.get(col).copied().unwrap_or(false) { + self.dirty_col[col] = false; + + true + } else { + false + } + } + + + + pub fn col_type(&self, col: usize) -> PatternColType { + self.col_types.get(col).copied().unwrap_or(PatternColType::Step) + } + + pub fn sync_out_data(&mut self, col: usize) { + let out_col = &mut self.out_data[col]; + + if self.rows == 0 { + return; + } + + match self.col_types[col] { + PatternColType::Value => { + let mut start_value = 0.0; + let mut start_idx = 0; + let mut end_idx = 0; + + while end_idx <= self.rows { + let mut break_after_write = false; + let cur_value = + if end_idx == self.rows { + end_idx -= 1; + break_after_write = true; + Some(self.data[end_idx][col] + .map(|v| (v as f32) / (0xFFF as f32)) + .unwrap_or(0.0)) + } else { + self.data[end_idx][col].map(|v| + (v as f32) / (0xFFF as f32)) + }; + + if let Some(end_value) = cur_value { + out_col[start_idx] = start_value; + out_col[end_idx] = end_value; + + let delta_rows = end_idx - start_idx; + + if delta_rows > 1 { + for idx in (start_idx + 1)..end_idx { + let x = + (idx - start_idx) as f32 + / (delta_rows as f32); + out_col[idx] = + start_value * (1.0 - x) + end_value * x; + } + } + + start_value = end_value; + start_idx = end_idx; + end_idx = end_idx + 1; + + if break_after_write { + break; + } + + } else { + end_idx += 1; + } + } + }, + PatternColType::Note => { + let mut cur_value = 0.0; + + for row in 0..self.rows { + if let Some(new_value) = self.data[row][col] { + cur_value = + ((new_value as i32 - 69) as f32 * 0.1) / 12.0; + } + + out_col[row] = cur_value; + } + }, + PatternColType::Step => { + let mut cur_value = 0.0; + + for row in 0..self.rows { + if let Some(new_value) = self.data[row][col] { + cur_value = (new_value as f32) / (0xFFF as f32); + } + + out_col[row] = cur_value; + } + }, + PatternColType::Gate => { + for row in 0..self.rows { + out_col[row] = + if let Some(new_value) = self.data[row][col] { + f32::from_bits(new_value as u32) + } else { + f32::from_bits(0xF000 as u32) + }; + } + }, + } + } +} + +#[cfg(feature="hexotk")] +pub use hexotk::widgets::UIPatternModel; +#[cfg(not(feature="hexotk"))] +pub trait UIPatternModel: std::fmt::Debug { + fn get_cell(&mut self, row: usize, col: usize) -> Option<&str>; + fn is_col_note(&self, col: usize) -> bool; + fn is_col_step(&self, col: usize) -> bool; + fn is_col_gate(&self, col: usize) -> bool; + + fn rows(&self) -> usize; + fn cols(&self) -> usize; + fn set_rows(&mut self, rows: usize); + + fn clear_cell(&mut self, row: usize, col: usize); + fn set_col_note_type(&mut self, col: usize); + fn set_col_step_type(&mut self, col: usize); + fn set_col_value_type(&mut self, col: usize); + fn set_col_gate_type(&mut self, col: usize); + + fn set_cell_value(&mut self, row: usize, col: usize, val: u16); + fn get_cell_value(&mut self, row: usize, col: usize) -> u16; + + fn set_cursor(&mut self, row: usize, col: usize); + fn get_cursor(&self) -> (usize, usize); + fn set_edit_step(&mut self, es: usize); + fn get_edit_step(&mut self) -> usize; +} + +#[cfg(not(feature="hexotk"))] +impl UIPatternModel for PatternData { + fn get_cell(&mut self, row: usize, col: usize) -> Option<&str> { + if row >= self.data.len() { return None; } + if col >= self.data[0].len() { return None; } + + if self.strings[row][col].is_none() { + if let Some(v) = self.data[row][col] { + self.strings[row][col] = Some(format!("{:03x}", v)); + } else { + return None; + } + } + + Some(self.strings[row][col].as_ref().unwrap()) + } + + fn clear_cell(&mut self, row: usize, col: usize) { + if row >= self.data.len() { return; } + if col >= self.data[0].len() { return; } + + self.data[row][col] = None; + self.strings[row][col] = None; + self.modified_col(col); + } + + fn get_cell_value(&mut self, row: usize, col: usize) -> u16 { + if row >= self.data.len() { return 0; } + if col >= self.data[0].len() { return 0; } + + self.data[row][col].unwrap_or(0) + } + + fn set_cell_value(&mut self, row: usize, col: usize, val: u16) { + if row >= self.data.len() { return; } + if col >= self.data[0].len() { return; } + + self.data[row][col] = Some(val); + self.strings[row][col] = None; + self.modified_col(col); + } + + fn is_col_note(&self, col: usize) -> bool { + if let Some(ct) = self.col_types.get(col) { + *ct == PatternColType::Note + } else { + false + } + } + + fn is_col_step(&self, col: usize) -> bool { + if let Some(ct) = self.col_types.get(col) { + *ct == PatternColType::Step + } else { + false + } + } + + fn is_col_gate(&self, col: usize) -> bool { + if let Some(ct) = self.col_types.get(col) { + *ct == PatternColType::Gate + } else { + false + } + } + + fn cols(&self) -> usize { self.data[0].len() } + + fn rows(&self) -> usize { self.rows } + + fn set_rows(&mut self, rows: usize) { + self.rows = rows.min(self.data.len()); + self.modified_col(0); // modify any col, so we send an update. + } + + fn set_col_note_type(&mut self, col: usize) { + if col >= self.col_types.len() { return; } + self.col_types[col] = PatternColType::Note; + self.modified_col(col); + } + + fn set_col_step_type(&mut self, col: usize) { + if col >= self.col_types.len() { return; } + self.col_types[col] = PatternColType::Step; + self.modified_col(col); + } + + fn set_col_value_type(&mut self, col: usize) { + if col >= self.col_types.len() { return; } + self.col_types[col] = PatternColType::Value; + self.modified_col(col); + } + + fn set_col_gate_type(&mut self, col: usize) { + if col >= self.col_types.len() { return; } + self.col_types[col] = PatternColType::Gate; + self.modified_col(col); + } + + fn set_cursor(&mut self, row: usize, col: usize) { + self.cursor = (row, col); + } + fn get_cursor(&self) -> (usize, usize) { self.cursor } + fn set_edit_step(&mut self, es: usize) { self.edit_step = es; } + fn get_edit_step(&mut self) -> usize { self.edit_step } +} + + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_float_eq { + ($a:expr, $b:expr) => { + if ($a - $b).abs() > 0.0001 { + panic!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, $a, $b) + } + } + } + + #[test] + fn check_linear_value_corner_case1_0_to_1() { + let mut pats = PatternData::new(3); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(0, col, 0); + pats.set_cell_value(2, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + let inc = 1.0 / 2.0; + for i in 1..2 { + let delta = + out_data[col][i] + - out_data[col][i - 1]; + assert_float_eq!(delta, inc); + } + } + } + + #[test] + fn check_linear_value_corner_case2_0_to_1() { + let mut pats = PatternData::new(4); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(0, col, 0); + pats.set_cell_value(3, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + let inc = 1.0 / 3.0; + for i in 1..3 { + let delta = + out_data[col][i] + - out_data[col][i - 1]; + assert_float_eq!(delta, inc); + } + } + } + + #[test] + fn check_linear_value_out_0_to_1() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(0, col, 0); + pats.set_cell_value(15, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + let inc = 1.0 / 15.0; + + //d// println!("out: {:?}", &out_data[col][0..16]); + for i in 1..16 { + let delta = + out_data[col][i] + - out_data[col][i - 1]; + assert_float_eq!(delta, inc); + } + } + } + + #[test] + fn check_linear_value_out_1_to_0() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(0, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + let inc = 1.0 / 15.0; + + for i in 1..16 { + let delta = + out_data[col][i] + - out_data[col][i - 1]; + assert_float_eq!(delta.abs(), inc); + } + } + } + + #[test] + fn check_linear_value_out_cast1_1_to_1() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(7, col, 0xFFF); + pats.set_cell_value(8, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + //d// println!("out: {:?}", &out_data[col][0..16]); + for i in 0..8 { + assert_float_eq!( + out_data[col][i], + out_data[col][15 - i]); + } + } + } + + #[test] + fn check_linear_value_out_case2_1_to_1() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(6, col, 0xFFF); + pats.set_cell_value(9, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + //d// println!("out: {:?}", &out_data[col][0..16]); + for i in 0..8 { + assert_float_eq!( + out_data[col][i], + out_data[col][15 - i]); + } + } + } + + #[test] + fn check_linear_value_out_case3_1_to_1() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(6, col, 0xFFF); + pats.set_cell_value(7, col, 0x0); + pats.set_cell_value(8, col, 0x0); + pats.set_cell_value(9, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + //d// println!("out: {:?}", &out_data[col][0..16]); + for i in 0..8 { + assert_float_eq!( + out_data[col][i], + out_data[col][15 - i]); + } + } + } + + #[test] + fn check_linear_value_out_case4_1_to_1() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_value_type(col); + pats.set_cell_value(5, col, 0xFFF); + pats.set_cell_value(7, col, 0x0); + pats.set_cell_value(8, col, 0x0); + pats.set_cell_value(10, col, 0xFFF); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + + //d// println!("out: {:?}", &out_data[col][0..16]); + + assert_float_eq!(0.5, out_data[col][6]); + assert_float_eq!(0.5, out_data[col][9]); + + for i in 0..8 { + assert_float_eq!( + out_data[col][i], + out_data[col][15 - i]); + } + } + } + + #[test] + fn check_pattern_step_out() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_step_type(col); + pats.set_cell_value(4, col, 0x450); + pats.set_cell_value(5, col, 0x0); + pats.set_cell_value(7, col, 0x7ff); + pats.set_cell_value(9, col, 0x800); + pats.set_cell_value(10, col, 0xfff); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + assert_float_eq!(out_data[col][0], 0.0); + assert_float_eq!(out_data[col][4], 0.26959708); + assert_float_eq!(out_data[col][5], 0.0); + assert_float_eq!(out_data[col][7], 0.4998779); + assert_float_eq!(out_data[col][8], 0.4998779); + assert_float_eq!(out_data[col][9], 0.50012213); + assert_float_eq!(out_data[col][10], 1.0); + assert_float_eq!(out_data[col][15], 1.0); + } + } + + #[test] + fn check_pattern_note_out() { + let mut pats = PatternData::new(16); + + for col in 0..MAX_COLS { + pats.set_col_note_type(col); + pats.set_cell_value(4, col, 0x45); + pats.set_cell_value(5, col, 0x0); + pats.set_cell_value(7, col, 0x45 - 12); + pats.set_cell_value(10, col, 0x45 + 12); + pats.sync_out_data(col); + + let out_data = pats.get_out_data(); + assert_float_eq!(out_data[col][0], 0.0); + assert_float_eq!(out_data[col][4], 0.0); + assert_float_eq!(out_data[col][5], -0.575); + assert_float_eq!(out_data[col][7], -0.1); + assert_float_eq!(out_data[col][9], -0.1); + assert_float_eq!(out_data[col][10], 0.1); + assert_float_eq!(out_data[col][15], 0.1); + } + } + + #[test] + fn check_pattern_repr() { + let mut pat = PatternData::new(MAX_PATTERN_LEN); + + for col in 0..MAX_COLS { + pat.set_col_note_type(col); + for v in 1..(MAX_PATTERN_LEN + 1) { + pat.set_cell_value(v - 1, col, v as u16); + } + + pat.set_cursor(16, 3); + pat.set_edit_step(5); + pat.set_rows(133); + } + + let repr = pat.to_repr(); + + let mut pat2 = PatternData::new(MAX_PATTERN_LEN); + pat2.from_repr(&repr); + + for col in 0..MAX_COLS { + assert!(pat.is_col_note(col)); + for v in 1..(MAX_PATTERN_LEN + 1) { + assert_eq!(pat.get_cell_value(v - 1, col), v as u16); + } + + assert_eq!(pat.get_cursor(), (16, 3)); + assert_eq!(pat.get_edit_step(), 5); + assert_eq!(pat.rows(), 133); + } + } +} diff --git a/src/dsp/tracker/sequencer.rs b/src/dsp/tracker/sequencer.rs new file mode 100644 index 0000000..ed2548a --- /dev/null +++ b/src/dsp/tracker/sequencer.rs @@ -0,0 +1,471 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use super::MAX_PATTERN_LEN; +use super::MAX_COLS; +use crate::dsp::helpers::SplitMix64; + +pub struct PatternSequencer { + rows: usize, + data: Vec>, + rng: SplitMix64, + rand_vals: [(usize, f64); MAX_COLS], +} + +const FRACT_16THS : [f32; 16] = [ + 1.0 / 16.0, + 2.0 / 16.0, + 3.0 / 16.0, + 4.0 / 16.0, + 5.0 / 16.0, + 6.0 / 16.0, + 7.0 / 16.0, + 8.0 / 16.0, + 9.0 / 16.0, + 10.0 / 16.0, + 11.0 / 16.0, + 12.0 / 16.0, + 13.0 / 16.0, + 14.0 / 16.0, + 15.0 / 16.0, + 1.0 +]; + +impl PatternSequencer { + pub fn new_default_seed(rows: usize) -> Self { + Self { + rows, + data: vec![vec![0.0; MAX_PATTERN_LEN]; MAX_COLS], + rng: SplitMix64::new(0x91234), + rand_vals: [(99999, 0.0); MAX_COLS], + } + } + + pub fn new(rows: usize) -> Self { + use std::time::SystemTime; + let seed = + match SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + { + Ok(n) => n.as_nanos() as i64, + Err(_) => 1_234_567_890, + }; + Self { + rows, + data: vec![vec![0.0; MAX_PATTERN_LEN]; MAX_COLS], + rng: SplitMix64::new_from_i64(seed), + rand_vals: [(99999, 0.0); MAX_COLS], + } + } + + pub fn set_rows(&mut self, rows: usize) { + self.rows = rows; + } + + pub fn rows(&self) -> usize { self.rows } + + pub fn set_col(&mut self, col: usize, col_data: &[f32]) { + for (out_cell, in_cell) in + self.data[col] + .iter_mut() + .zip(col_data.iter()) + { + *out_cell = *in_cell; + } + } + + pub fn col_interpolate_at_phase( + &self, col: usize, phase: &[f32], out: &mut [f32]) + { + let col = &self.data[col][..]; + + let last_row_idx : f32 = (self.rows as f32) - 0.000001; + let rows = self.rows; + + for (phase, out) in phase.iter().zip(out.iter_mut()) { + let row_phase = phase * last_row_idx; + let phase_frac = row_phase.fract(); + let line = row_phase.floor() as usize % rows; + let prev_line = if line == 0 { self.rows - 1 } else { line - 1 }; + + let prev = col[prev_line]; + let next = col[line]; + + // println!("INTERP: {}={:9.7}, {}={:9.7} | {:9.7}", + // prev_line, prev, + // line, next, + // phase_frac); + + *out = prev * (1.0 - phase_frac) + next * phase_frac; + } + } + + pub fn col_get_at_phase( + &self, col: usize, phase: &[f32], out: &mut [f32]) + { + let col = &self.data[col][..]; + + let last_row_idx : f32 = (self.rows as f32) - 0.000001; + let rows = self.rows; + + for (phase, out) in phase.iter().zip(out.iter_mut()) { + let row_phase = phase * last_row_idx; + let line = row_phase.floor() as usize % rows; + + *out = col[line]; + } + } + + pub fn col_gate_at_phase( + &mut self, col_idx: usize, phase: &[f32], out: &mut [f32]) + { + let col = &self.data[col_idx][..]; + + let last_row_idx : f32 = (self.rows as f32) - 0.000001; + let rows = self.rows; + + for (phase, out) in phase.iter().zip(out.iter_mut()) { + let row_phase = phase.clamp(0.0, 1.0) * last_row_idx; + let line = row_phase.floor() as usize % rows; + let phase_frac = row_phase.fract(); + + let gate : u32 = col[line].to_bits(); + + // pulse_width: + // 0xF - Gate is on for full row + // 0x0 - Gate is on for a very short burst + let pulse_width : f32 = FRACT_16THS[(gate & 0x00F) as usize]; + // row_div: + // 0xF - Row has 1 Gate + // 0x0 - Row is divided up into 16 Gates + let row_div : f32 = (16 - ((gate & 0x0F0) >> 4)) as f32; + // probability: + // 0xF - Row is always triggered + // 0x7 - Row fires only in 50% of the cases + // 0x0 - Row fires only in ~6% of the cases + let probability : u8 = ((gate & 0xF00) >> 8) as u8; + + let sub_frac = (phase_frac * row_div).fract(); + //d// println!( + //d// "row_div={}, pw={}, phase={} / {}", + //d// row_div, pulse_width, sub_frac, phase_frac); + + if probability < 0xF { + let rand_val = + if self.rand_vals[col_idx].0 != line { + let new_rand_val = self.rng.next_open01(); + self.rand_vals[col_idx] = (line, new_rand_val); + new_rand_val + } else { + self.rand_vals[col_idx].1 + }; + //d// println!("RANDVAL: {:?} | {:9.7}", self.rand_vals[col_idx], FRACT_16THS[probability as usize]); + + if rand_val > (FRACT_16THS[probability as usize] as f64) { + *out = 0.0; + continue; + } + } + + println!("GATE: {:0X}", gate); + + if (gate & 0xF000) > 0 { + *out = 0.0; + } else { + *out = if sub_frac <= pulse_width { 1.0 } else { 0.0 }; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_float_eq { + ($a:expr, $b:expr) => { + if ($a - $b).abs() > 0.0001 { + panic!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, $a, $b) + } + } + } + + #[test] + fn check_seq_interpolate_1() { + let mut ps = PatternSequencer::new(2); + ps.set_col(0, &[0.0, 1.0]); + + let mut out = [0.0; 6]; + ps.col_interpolate_at_phase(0, &[0.0, 0.1, 0.50, 0.51, 0.9, 0.99999], &mut out[..]); + assert_float_eq!(out[0], 1.0); + assert_float_eq!(out[1], 0.8); + assert_float_eq!(out[2], 0.0); + assert_float_eq!(out[3], 0.02); + assert_float_eq!(out[4], 0.8); + assert_float_eq!(out[5], 0.99999); + } + + #[test] + fn check_seq_interpolate_buffer_end() { + let mut ps = PatternSequencer::new(256); + ps.set_col(0, &[f32::from_bits(0xF000); 256]); + + let mut out = [0.0; 1]; + ps.col_gate_at_phase(0, &[0.9999999999], &mut out[..]); + assert_float_eq!(out[0], 0.0); + + let mut ps = PatternSequencer::new(256); + ps.set_col(0, &[0.0; 256]); + + ps.col_get_at_phase(0, &[0.9999999999], &mut out[..]); + assert_float_eq!(out[0], 0.0); + + ps.col_interpolate_at_phase(0, &[0.9999999999], &mut out[..]); + assert_float_eq!(out[0], 0.0); + } + + #[test] + fn check_seq_step_1() { + let mut ps = PatternSequencer::new(2); + ps.set_col(0, &[0.0, 1.0]); + + let mut out = [0.0; 3]; + ps.col_get_at_phase(0, &[0.1, 0.51, 0.9], &mut out[..]); + assert_float_eq!(out[0], 0.0); + assert_float_eq!(out[1], 1.0); + assert_float_eq!(out[2], 1.0); + } + + #[test] + fn check_seq_step_2() { + let mut ps = PatternSequencer::new(3); + ps.set_col(0, &[0.0, 0.3, 1.0]); + + let mut out = [0.0; 6]; + ps.col_get_at_phase(0, &[0.1, 0.5, 0.51, 0.6, 0.9, 0.99], &mut out[..]); + assert_float_eq!(out[0], 0.0); + assert_float_eq!(out[1], 0.3); + assert_float_eq!(out[2], 0.3); + assert_float_eq!(out[3], 0.3); + assert_float_eq!(out[4], 1.0); + assert_float_eq!(out[5], 1.0); + } + + #[test] + fn check_seq_gate_1() { + let mut ps = PatternSequencer::new(2); + ps.set_col(0, &[ + f32::from_bits(0x0FFF), + f32::from_bits(0xF000), + ]); + + let mut out = [0.0; 6]; + ps.col_gate_at_phase(0, &[0.1, 0.5, 0.5001, 0.6, 0.9, 0.99], &mut out[..]); + //d// println!("out: {:?}", out); + + assert_float_eq!(out[0], 1.0); + assert_float_eq!(out[1], 1.0); + assert_float_eq!(out[2], 0.0); + assert_float_eq!(out[3], 0.0); + assert_float_eq!(out[4], 0.0); + assert_float_eq!(out[5], 0.0); + } + + fn count_high(slice: &[f32]) -> usize { + let mut sum = 0; + for p in slice.iter() { + if *p > 0.5 { sum += 1; } + } + sum + } + + fn count_up(slice: &[f32]) -> usize { + let mut sum = 0; + let mut cur = 0.0; + for p in slice.iter() { + if cur < 0.1 && *p > 0.5 { + sum += 1; + } + cur = *p; + } + sum + } + + #[test] + fn check_seq_gate_2() { + let mut ps = PatternSequencer::new(3); + ps.set_col(0, &[ + f32::from_bits(0x0FF0), + f32::from_bits(0x0FF7), + f32::from_bits(0x0FFF), + ]); + + let mut phase = vec![0.0; 96]; + let inc = 1.0 / (96.0 - 1.0); + let mut phase_run = 0.0; + for p in phase.iter_mut() { + *p = phase_run; + phase_run += inc; + } + + //d// println!("PHASE: {:?}", phase); + + let mut out = [0.0; 96]; + ps.col_gate_at_phase(0, &phase[..], &mut out[..]); + //d// println!("out: {:?}", &out[0..32]); + + assert_eq!(count_high(&out[0..32]), 2); + assert_eq!(count_high(&out[32..64]), 16); + assert_eq!(count_high(&out[64..96]), 32); + + assert_eq!(count_up(&out[0..32]), 1); + assert_eq!(count_up(&out[32..64]), 1); + assert_eq!(count_up(&out[64..96]), 1); + } + + #[test] + fn check_seq_gate_div_1() { + let mut ps = PatternSequencer::new(3); + ps.set_col(0, &[ + f32::from_bits(0x0F80), + f32::from_bits(0x0F87), + f32::from_bits(0x0F8F), + ]); + + let mut phase = vec![0.0; 3 * 64]; + let inc = 1.0 / ((3.0 * 64.0) - 1.0); + let mut phase_run = 0.0; + for p in phase.iter_mut() { + *p = phase_run; + phase_run += inc; + } + + //d// println!("PHASE: {:?}", phase); + + let mut out = [0.0; 3 * 64]; + ps.col_gate_at_phase(0, &phase[..], &mut out[..]); + + assert_eq!(count_high(&out[0..64]), 8); + assert_eq!(count_up( &out[0..64]), 8); + + assert_eq!(count_high(&out[64..128]), 32); + assert_eq!(count_up( &out[64..128]), 8); + + assert_eq!(count_high(&out[128..192]), 64); + assert_eq!(count_up( &out[128..192]), 1); + } + + #[test] + fn check_seq_gate_div_2() { + let mut ps = PatternSequencer::new(3); + ps.set_col(0, &[ + f32::from_bits(0x0F00), + f32::from_bits(0x0F07), + f32::from_bits(0x0F0F), + ]); + + let mut phase = vec![0.0; 6 * 64]; + let inc = 1.0 / ((6.0 * 64.0) - 1.0); + let mut phase_run = 0.0; + for p in phase.iter_mut() { + *p = phase_run; + phase_run += inc; + } + + //d// println!("PHASE: {:?}", phase); + + let mut out = [0.0; 6 * 64]; + ps.col_gate_at_phase(0, &phase[..], &mut out[..]); + + assert_eq!(count_high(&out[0..128]), 16); + assert_eq!(count_up( &out[0..128]), 16); + + assert_eq!(count_high(&out[128..256]), 64); + assert_eq!(count_up( &out[128..256]), 16); + + assert_eq!(count_high(&out[256..384]), 128); + assert_eq!(count_up( &out[256..384]), 1); + } + + #[test] + fn check_seq_gate_div_3() { + let mut ps = PatternSequencer::new(3); + ps.set_col(0, &[ + f32::from_bits(0x0FE0), + f32::from_bits(0x0FE7), + f32::from_bits(0x0FEF), + ]); + + let mut phase = vec![0.0; 6 * 64]; + let inc = 1.0 / ((6.0 * 64.0) - 1.0); + let mut phase_run = 0.0; + for p in phase.iter_mut() { + *p = phase_run; + phase_run += inc; + } + + //d// println!("PHASE: {:?}", phase); + + let mut out = [0.0; 6 * 64]; + ps.col_gate_at_phase(0, &phase[..], &mut out[..]); + + assert_eq!(count_high(&out[0..128]), 8); + assert_eq!(count_up( &out[0..128]), 2); + + assert_eq!(count_high(&out[128..256]), 64); + assert_eq!(count_up( &out[128..256]), 2); + + assert_eq!(count_high(&out[256..384]), 128); + assert_eq!(count_up( &out[256..384]), 1); + } + + fn run_probability_test_for(prob: u32) -> (usize, usize) { + let rows = 100; + + let mut ps = PatternSequencer::new_default_seed(rows); + let mut coldata = vec![0.0; rows]; + for i in 0..coldata.len() { + coldata[i] = f32::from_bits(0x00FF | prob); + } + ps.set_col(0, &coldata[..]); + + let samples = rows; + let mut phase = vec![0.0; samples]; + let inc = 1.0 / ((samples as f32) - 1.0); + let mut phase_run = 0.0; + for p in phase.iter_mut() { + *p = phase_run; + phase_run += inc; + } + + let mut out = vec![0.0; samples]; + ps.col_gate_at_phase(0, &phase[..], &mut out[..]); + + (count_high(&out[..]), count_up(&out[..])) + } + + #[test] + fn check_seq_gate_div_rng() { + // XXX: The result numbers are highly dependent on the + // sampling rate inside run_probability_test_for(). + assert_eq!(run_probability_test_for(0x000), (5, 5)); + assert_eq!(run_probability_test_for(0x100), (12, 11)); + assert_eq!(run_probability_test_for(0x200), (20, 18)); + assert_eq!(run_probability_test_for(0x300), (26, 23)); + assert_eq!(run_probability_test_for(0x400), (32, 26)); + assert_eq!(run_probability_test_for(0x500), (38, 29)); + assert_eq!(run_probability_test_for(0x600), (47, 29)); + assert_eq!(run_probability_test_for(0x700), (56, 26)); + assert_eq!(run_probability_test_for(0x800), (60, 25)); + assert_eq!(run_probability_test_for(0x900), (66, 24)); + assert_eq!(run_probability_test_for(0xA00), (70, 22)); + assert_eq!(run_probability_test_for(0xB00), (79, 18)); + assert_eq!(run_probability_test_for(0xC00), (84, 13)); + assert_eq!(run_probability_test_for(0xD00), (93, 7)); + assert_eq!(run_probability_test_for(0xE00), (96, 5)); + assert_eq!(run_probability_test_for(0xF00), (100, 1)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1d943b1..a455b25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,45 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +pub mod nodes; +#[allow(unused_macros)] +pub mod dsp; +pub mod matrix; +pub mod cell_dir; +pub mod monitor; +pub mod matrix_repr; +mod util; + +pub use nodes::{new_node_engine, NodeConfigurator, NodeExecutor}; +pub use cell_dir::CellDir; +pub use matrix::{Matrix, Cell}; +pub use dsp::{NodeId, SAtom}; +pub use matrix_repr::load_patch_from_file; +pub use matrix_repr::save_patch_to_file; + +pub struct Context<'a, 'b, 'c, 'd> { + pub nframes: usize, + pub output: &'a mut [&'b mut [f32]], + pub input: &'c [&'d [f32]], +} + +impl<'a, 'b, 'c, 'd> nodes::NodeAudioContext for Context<'a, 'b, 'c, 'd> { + #[inline] + fn nframes(&self) -> usize { self.nframes } + + #[inline] + fn output(&mut self, channel: usize, frame: usize, v: f32) { + self.output[channel][frame] = v; + } + + #[inline] + fn input(&mut self, channel: usize, frame: usize) -> f32 { + self.input[channel][frame] + } +} + + pub fn test() -> bool { true } diff --git a/src/matrix.rs b/src/matrix.rs new file mode 100644 index 0000000..1be5469 --- /dev/null +++ b/src/matrix.rs @@ -0,0 +1,1158 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::{ + NodeConfigurator, + NodeGraphOrdering, + NodeProg, + MAX_ALLOCATED_NODES +}; +use crate::dsp::{NodeInfo, NodeId, ParamId, SAtom}; +pub use crate::CellDir; +pub use crate::nodes::MinMaxMonitorSamples; +pub use crate::monitor::MON_SIG_CNT; +use crate::matrix_repr::*; +use crate::dsp::tracker::PatternData; + +/// This is a cell/tile of the hexagonal [Matrix]. +/// +/// The [Matrix] stores it to keep track of the graphical representation +/// of the hexagonal tilemap. Using [Matrix::place] you can place new cells. +/// +///``` +/// use hexodsp::*; +/// +/// let (node_conf, mut node_exec) = new_node_engine(); +/// let mut matrix = Matrix::new(node_conf, 3, 3); +/// +/// matrix.place( +/// 2, 2, +/// Cell::empty(NodeId::Sin(0)) +/// .input(Some(0), None, None) +/// .out(None, None, Some(0))); +/// +/// matrix.sync().unwrap(); +///``` +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Cell { + node_id: NodeId, + x: u8, + y: u8, + /// Top-Right output + out1: Option, + /// Bottom-Right output + out2: Option, + /// Bottom output + out3: Option, + /// Top input + in1: Option, + /// Top-Left input + in2: Option, + /// Bottom-Left input + in3: Option, +} + +impl Cell { + /// This is the main contructor of a [Cell]. + /// Empty means that there is no associated position of this cell + /// and no inputs/outputs have been assigned. Use the methods [Cell::input] and [Cell::out] + /// to assign inputs / outputs. + /// + ///``` + /// use hexodsp::*; + /// + /// let some_cell = + /// Cell::empty(NodeId::Sin(0)) + /// .input(None, Some(0), Some(0)) + /// .out(None, Some(0), Some(0)); + ///``` + pub fn empty(node_id: NodeId) -> Self { + Self { + node_id, + x: 0, + y: 0, + out1: None, + out2: None, + out3: None, + in1: None, + in2: None, + in3: None, + } + } + + /// Returns a serializable representation of this [Matrix] [Cell]. + /// + /// See also [CellRepr]. + /// + ///``` + /// use hexodsp::*; + /// + /// let some_cell = + /// Cell::empty(NodeId::Sin(0)) + /// .input(None, Some(0), Some(0)) + /// .out(None, Some(0), Some(0)); + /// + /// let repr = some_cell.to_repr(); + /// assert_eq!( + /// repr.serialize().to_string(), + /// "[\"sin\",0,0,0,[-1,0,0],[-1,0,0]]"); + ///``` + pub fn to_repr(&self) -> CellRepr { + CellRepr { + node_id: self.node_id, + x: self.x as usize, + y: self.y as usize, + out: [ + self.out1.map(|v| v as i16).unwrap_or(-1), + self.out2.map(|v| v as i16).unwrap_or(-1), + self.out3.map(|v| v as i16).unwrap_or(-1) + ], + inp: [ + self.in1.map(|v| v as i16).unwrap_or(-1), + self.in2.map(|v| v as i16).unwrap_or(-1), + self.in3.map(|v| v as i16).unwrap_or(-1) + ], + } + } + + pub fn from_repr(repr: &CellRepr) -> Self { + Self { + node_id: repr.node_id, + x: repr.x as u8, + y: repr.y as u8, + out1: if repr.out[0] < 0 { None } + else { Some(repr.out[0] as u8) }, + out2: if repr.out[1] < 0 { None } + else { Some(repr.out[1] as u8) }, + out3: if repr.out[2] < 0 { None } + else { Some(repr.out[2] as u8) }, + in1: if repr.inp[0] < 0 { None } + else { Some(repr.inp[0] as u8) }, + in2: if repr.inp[1] < 0 { None } + else { Some(repr.inp[1] as u8) }, + in3: if repr.inp[2] < 0 { None } + else { Some(repr.inp[2] as u8) }, + } + } + + pub fn with_pos_of(&self, other: Cell) -> Self { + let mut new = *self; + new.x = other.x; + new.y = other.y; + new + } + + pub fn is_empty(&self) -> bool { self.node_id == NodeId::Nop } + + pub fn node_id(&self) -> NodeId { self.node_id } + + pub fn set_node_id(&mut self, new_id: NodeId) { + self.node_id = new_id; + } + + pub fn label<'a>(&self, buf: &'a mut [u8]) -> Option<&'a str> { + use std::io::Write; + let mut cur = std::io::Cursor::new(buf); + + if self.node_id == NodeId::Nop { + return None; + } + +// let node_info = infoh.from_node_id(self.node_id); + + match write!(cur, "{}", self.node_id) { + Ok(_) => { + let len = cur.position() as usize; + Some( + std::str::from_utf8(&(cur.into_inner())[0..len]) + .unwrap()) + }, + Err(_) => None, + } + } + + pub fn pos(&self) -> (usize, usize) { + (self.x as usize, self.y as usize) + } + + pub fn has_dir_set(&self, dir: CellDir) -> bool { + match dir { + CellDir::TR => self.out1.is_some(), + CellDir::BR => self.out2.is_some(), + CellDir::B => self.out3.is_some(), + CellDir::BL => self.in3.is_some(), + CellDir::TL => self.in2.is_some(), + CellDir::T => self.in1.is_some(), + CellDir::C => false, + } + } + + pub fn local_port_idx(&self, dir: CellDir) -> Option { + match dir { + CellDir::TR => { self.out1 }, + CellDir::BR => { self.out2 }, + CellDir::B => { self.out3 }, + CellDir::BL => { self.in3 }, + CellDir::TL => { self.in2 }, + CellDir::T => { self.in1 }, + CellDir::C => None, + } + } + + pub fn clear_io_dir(&mut self, dir: CellDir) { + match dir { + CellDir::TR => { self.out1 = None; }, + CellDir::BR => { self.out2 = None; }, + CellDir::B => { self.out3 = None; }, + CellDir::BL => { self.in3 = None; }, + CellDir::TL => { self.in2 = None; }, + CellDir::T => { self.in1 = None; }, + CellDir::C => {}, + } + } + + pub fn set_io_dir(&mut self, dir: CellDir, idx: usize) { + match dir { + CellDir::TR => { self.out1 = Some(idx as u8); }, + CellDir::BR => { self.out2 = Some(idx as u8); }, + CellDir::B => { self.out3 = Some(idx as u8); }, + CellDir::BL => { self.in3 = Some(idx as u8); }, + CellDir::TL => { self.in2 = Some(idx as u8); }, + CellDir::T => { self.in1 = Some(idx as u8); }, + CellDir::C => {}, + } + } + + pub fn input(mut self, i1: Option, i2: Option, i3: Option) -> Self { + self.in1 = i1; + self.in2 = i2; + self.in3 = i3; + self + } + + pub fn out(mut self, o1: Option, o2: Option, o3: Option) -> Self { + self.out1 = o1; + self.out2 = o2; + self.out3 = o3; + self + } +} + +use std::rc::Rc; +use std::cell::RefCell; + +/// To report back cycle errors from [Matrix::check] and [Matrix::sync]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum MatrixError { + CycleDetected, + DuplicatedInput { + output1: (NodeId, u8), + output2: (NodeId, u8), + }, +} + +/// An intermediate data structure to store a single edge in the [Matrix]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Edge { + from: NodeId, + from_out: u8, + to: NodeId, + to_input: u8, +} + +pub struct Matrix { + /// The node configurator to control the backend. + config: NodeConfigurator, + /// Holds the actual 2 dimensional matrix cells in one big vector. + matrix: Vec, + /// Width of the matrix. + w: usize, + /// Height of the matrix. + h: usize, + + /// The retained data structure of the graph topology. + /// This is used by `sync()` and `check()` to determine the + /// order and cycle freeness of the graph. + /// We store it in this field, so we don't have to reallocate it + /// all the time. + graph_ordering: NodeGraphOrdering, + + /// Holds a saved version of the `matrix` field + /// to roll back changes that might introduce cycles or + /// other invalid topology. + saved_matrix: Option>, + + /// Stores the edges which are extracted from the `matrix` field + /// by [Matrix::update_graph_ordering_and_edges], which is used + /// by [Matrix::sync] and [Matrix::check]. + edges: Vec, + + /// Holds the currently monitored cell. + monitored_cell: Cell, + + /// A counter that increases for each sync(), it can be used + /// by other components of the application to detect changes in + /// the matrix to resync their own data. + gen_counter: usize, +} + +unsafe impl Send for Matrix {} + +impl Matrix { + pub fn new(config: NodeConfigurator, w: usize, h: usize) -> Self { + let mut matrix : Vec = Vec::new(); + matrix.resize(w * h, Cell::empty(NodeId::Nop)); + + Self { + monitored_cell: Cell::empty(NodeId::Nop), + gen_counter: 0, + saved_matrix: None, + graph_ordering: NodeGraphOrdering::new(), + edges: Vec::with_capacity(MAX_ALLOCATED_NODES * 2), + config, + w, + h, + matrix, + } + } + + pub fn size(&self) -> (usize, usize) { (self.w, self.h) } + + pub fn unique_index_for(&self, node_id: &NodeId) -> Option { + self.config.unique_index_for(node_id) + } + + pub fn info_for(&self, node_id: &NodeId) -> Option { + Some(self.config.node_by_id(&node_id)?.0.clone()) + } + + pub fn phase_value_for(&self, node_id: &NodeId) -> f32 { + self.config.phase_value_for(node_id) + } + + pub fn led_value_for(&self, node_id: &NodeId) -> f32 { + self.config.led_value_for(node_id) + } + + pub fn update_filters(&mut self) { + self.config.update_filters(); + } + + pub fn filtered_led_for(&mut self, ni: &NodeId) -> (f32, f32) { + self.config.filtered_led_for(ni) + } + + pub fn filtered_out_fb_for(&mut self, ni: &NodeId, out: u8) -> (f32, f32) { + self.config.filtered_out_fb_for(ni, out) + } + + pub fn get_pattern_data(&self, tracker_id: usize) + -> Option>> + { + self.config.get_pattern_data(tracker_id) + } + + /// Checks if pattern data updates need to be sent to the + /// DSP thread. + pub fn check_pattern_data(&mut self, tracker_id: usize) { + self.config.check_pattern_data(tracker_id) + } + + /// Saves the state of the hexagonal grid layout. + /// This is usually used together with [Matrix::check] + /// and [Matrix::restore_matrix] to try if changes on + /// the matrix using [Matrix::place] (or other grid changing + /// functions). + /// + /// It is advised to use convenience functions such as [Matrix::change_matrix]. + /// + /// See also [Matrix::change_matrix], [Matrix::check] and [Matrix::sync]. + pub fn save_matrix(&mut self) { + let matrix = self.matrix.clone(); + self.saved_matrix = Some(matrix); + } + + /// Restores the previously via [Matrix::save_matrix] saved matrix. + /// + /// It is advised to use convenience functions such as [Matrix::change_matrix]. + /// + /// See also [Matrix::change_matrix], [Matrix::check]. + pub fn restore_matrix(&mut self) { + if let Some(matrix) = self.saved_matrix.take() { + self.matrix = matrix; + } + } + + /// Helps encapsulating changes of the matrix and wraps them into + /// a [Matrix::save_matrix], [Matrix::check] and [Matrix::restore_matrix]. + /// + ///``` + /// use hexodsp::*; + /// + /// let (node_conf, mut node_exec) = new_node_engine(); + /// let mut matrix = Matrix::new(node_conf, 3, 3); + /// + /// let res = matrix.change_matrix(|matrix| { + /// matrix.place(0, 1, + /// Cell::empty(NodeId::Sin(1)) + /// .input(Some(0), None, None)); + /// matrix.place(0, 0, + /// Cell::empty(NodeId::Sin(1)) + /// .out(None, None, Some(0))); + /// }); + /// + /// // In this examples case there is an error, as we created + /// // a cycle: + /// assert!(res.is_err()); + ///``` + pub fn change_matrix(&mut self, mut f: F) + -> Result<(), MatrixError> + where F: FnMut(&mut Self) + { + self.save_matrix(); + + f(self); + + if let Err(e) = self.check() { + self.restore_matrix(); + Err(e) + } else { + Ok(()) + } + } + + /// Inserts a cell into the hexagonal grid of the matrix. + /// You have to make sure that the resulting DSP graph topology + /// does not have cycles, otherwise an upload to the DSP thread via + /// [Matrix::sync] will fail. + /// + /// You can safely check the DSP topology of changes using + /// the convenience function [Matrix::change_matrix] + /// or alternatively: [Matrix::save_matrix], [Matrix::restore_matrix] + /// and [Matrix::check]. + /// + /// See also the example in [Matrix::change_matrix] and [Matrix::check]. + pub fn place(&mut self, x: usize, y: usize, mut cell: Cell) { + cell.x = x as u8; + cell.y = y as u8; + self.matrix[x * self.h + y] = cell; + } + + pub fn clear(&mut self) { + for cell in self.matrix.iter_mut() { + *cell = Cell::empty(NodeId::Nop); + } + + self.graph_ordering.clear(); + self.edges.clear(); + self.saved_matrix = None; + + self.config.delete_nodes(); + self.monitor_cell(Cell::empty(NodeId::Nop)); + let _ = self.sync(); + } + + pub fn for_each_atom(&self, f: F) { + self.config.for_each_param(f); + } + + /// Returns the DSP graph generation, which is increased + /// after each call to [Matrix::sync]. + /// + /// This can be used by external components to track if they + /// should update their knowledge of the nodes in the DSP + /// graph. Such as parameter values. + /// + /// HexoSynth for instance updates the UI parameters + /// by tracking this value and calling [Matrix::for_each_atom] + /// to retrieve the most current set of parameter values. + /// In case new nodes were created and their default + /// parameter/atom values were added. + pub fn get_generation(&self) -> usize { self.gen_counter } + + /// Returns a serializable representation of the matrix. + /// This representation contains all parameters, + /// created nodes, connections and the tracker's pattern data. + /// + ///``` + /// use hexodsp::*; + /// + /// let (node_conf, mut _node_exec) = new_node_engine(); + /// let mut matrix = Matrix::new(node_conf, 3, 3); + /// + /// let sin = NodeId::Sin(2); + /// + /// matrix.place(0, 0, + /// Cell::empty(sin) + /// .out(None, Some(0), None)); + /// + /// let freq_param = sin.inp_param("freq").unwrap(); + /// matrix.set_param(freq_param, SAtom::param(-0.1)); + /// + /// let mut serialized = matrix.to_repr().serialize().to_string(); + /// + /// assert!(serialized.find("\"sin\",2,0,0,[-1,-1,-1],[-1,0,-1]").is_some()); + /// assert!(serialized.find("\"freq\",-0.100").is_some()); + ///``` + /// + /// See also [MatrixRepr::serialize]. + pub fn to_repr(&self) -> MatrixRepr { + let (params, atoms) = self.config.dump_param_values(); + + let mut cells : Vec = vec![]; + self.for_each(|_x, _y, cell| + if cell.node_id() != NodeId::Nop { + cells.push(cell.to_repr()) + }); + + let mut patterns : Vec> = vec![]; + let mut tracker_id = 0; + while let Some(pdata) = self.get_pattern_data(tracker_id) { + patterns.push( + if pdata.borrow().is_unset() { None } + else { Some(pdata.borrow().to_repr()) }); + + tracker_id += 1; + } + + MatrixRepr { + cells, + params, + atoms, + patterns, + } + } + + /// Loads the matrix from a previously my [Matrix::to_repr] + /// generated matrix representation. + /// + /// This function will call [Matrix::sync] after loading and + /// overwriting the current matrix contents. + pub fn from_repr(&mut self, repr: &MatrixRepr) -> Result<(), MatrixError> { + self.clear(); + + self.config.load_dumped_param_values( + &repr.params[..], + &repr.atoms[..]); + + for cell_repr in repr.cells.iter() { + let cell = Cell::from_repr(cell_repr); + self.place(cell.x as usize, cell.y as usize, cell); + } + + for (tracker_id, pat) in repr.patterns.iter().enumerate() { + if let Some(pat) = pat { + if let Some(pd) = self.get_pattern_data(tracker_id) { + pd.borrow_mut().from_repr(pat); + } + } + } + + self.sync() + } + + /// Receives the most recent data for the monitored signal at index `idx`. + /// Might introduce a short wait, because internally a mutex is still locked. + /// If this leads to stuttering in the UI, we need to change the internal + /// handling to a triple buffer. + pub fn get_minmax_monitor_samples(&mut self, idx: usize) -> &MinMaxMonitorSamples { + self.config.get_minmax_monitor_samples(idx) + } + + /// Returns the currently monitored cell. + pub fn monitored_cell(&self) -> &Cell { &self.monitored_cell } + + /// Sets the cell to monitor next. Please bear in mind, that you need to + /// call `sync` before retrieving the cell from the matrix, otherwise + /// the node instance might not have been created in the backend yet and + /// we can not start monitoring the cell. + pub fn monitor_cell(&mut self, cell: Cell) { + self.monitored_cell = cell; + + let inputs = [ + cell.in1, + cell.in2, + cell.in3, + ]; + let outputs = [ + cell.out1, + cell.out2, + cell.out3, + ]; + + self.config.monitor(&cell.node_id, &inputs, &outputs); + } + + /// Is called by [Matrix::sync] to refresh the monitored cell. + /// In case the matrix has changed (inputs/outputs of a cell) + /// we show the current state. + /// + /// Note, that if the UI actually moved a cell, it needs to + /// monitor the newly moved cell anyways. + fn remonitor_cell(&mut self) { + let m = self.monitored_cell(); + if let Some(cell) = self.get(m.x as usize, m.y as usize).copied() { + self.monitor_cell(cell); + } + } + + /// Assign [SAtom] values to input parameters and atoms. + pub fn set_param(&mut self, param: ParamId, at: SAtom) { + self.config.set_param(param, at); + } + + pub fn get_adjacent_output(&self, x: usize, y: usize, dir: CellDir) + -> Option<(NodeId, u8)> + { + if dir.is_output() { + return None; + } + + let cell = self.get_adjacent(x, y, dir)?; + + if cell.node_id == NodeId::Nop { + return None; + } + + let cell_out = + match dir { + CellDir::T => cell.out3?, + CellDir::TL => cell.out2?, + CellDir::BL => cell.out1?, + _ => { return None; } + }; + + Some((cell.node_id, cell_out)) + } + + pub fn get_adjacent(&self, x: usize, y: usize, dir: CellDir) -> Option<&Cell> { + let offs : (i32, i32) = dir.to_offs(x); + let x = x as i32 + offs.0; + let y = y as i32 + offs.1; + + if x < 0 || y < 0 || (x as usize) >= self.w || (y as usize) >= self.h { + return None; + } + + Some(&self.matrix[(x as usize) * self.h + (y as usize)]) + } + + pub fn adjacent_edge_has_input(&self, x: usize, y: usize, edge: CellDir) -> bool { + if let Some(cell) = self.get_adjacent(x, y, edge) { + //d// println!(" ADJ CELL: {},{} ({})", cell.x, cell.y, cell.node_id()); + match edge { + CellDir::TR => cell.in3.is_some(), + CellDir::BR => cell.in2.is_some(), + CellDir::B => cell.in1.is_some(), + _ => false, + } + } else { + false + } + } + + pub fn for_each(&self, mut f: F) { + for x in 0..self.w { + for y in 0..self.h { + let cell = &self.matrix[x * self.h + y]; + f(x, y, cell); + } + } + } + + pub fn edge_label<'a>(&self, cell: &Cell, edge: CellDir, buf: &'a mut [u8]) -> Option<(&'a str, bool)> { + use std::io::Write; + let mut cur = std::io::Cursor::new(buf); + + if cell.node_id == NodeId::Nop { + return None; + } + + let out_idx = + match edge { + CellDir::TR => Some(cell.out1), + CellDir::BR => Some(cell.out2), + CellDir::B => Some(cell.out3), + _ => None, + }; + let in_idx = + match edge { + CellDir::BL => Some(cell.in3), + CellDir::TL => Some(cell.in2), + CellDir::T => Some(cell.in1), + _ => None, + }; + + let info = self.info_for(&cell.node_id)?; + + let mut is_connected_edge = false; + + let edge_str = + if let Some(out_idx) = out_idx { + //d// println!(" CHECK ADJ EDGE {},{} @ {:?}", cell.x, cell.y, edge); + is_connected_edge = + self.adjacent_edge_has_input( + cell.x as usize, cell.y as usize, edge); + + info.out_name(out_idx? as usize) + + } else if let Some(in_idx) = in_idx { + info.in_name(in_idx? as usize) + + } else { + None + }; + + let edge_str = edge_str?; + + match write!(cur, "{}", edge_str) { + Ok(_) => { + let len = cur.position() as usize; + Some(( + std::str::from_utf8(&(cur.into_inner())[0..len]) + .unwrap(), + is_connected_edge)) + }, + Err(_) => None, + } + } + + pub fn get_copy(&self, x: usize, y: usize) -> Option { + if x >= self.w || y >= self.h { + return None; + } + + let mut cell = self.matrix[x * self.h + y]; + cell.x = x as u8; + cell.y = y as u8; + Some(cell) + } + + pub fn get(&self, x: usize, y: usize) -> Option<&Cell> { + if x >= self.w || y >= self.h { + return None; + } + + Some(&self.matrix[x * self.h + y]) + } + + pub fn get_unused_instance_node_id(&self, id: NodeId) -> NodeId { + self.config.unused_instance_node_id(id) + } + + fn create_intermediate_nodes(&mut self) { + // Scan through the matrix and check if (backend) nodes need to be created + // for new unknown nodes: + for x in 0..self.w { + for y in 0..self.h { + let cell = &mut self.matrix[x * self.h + y]; + + if cell.node_id == NodeId::Nop { + continue; + } + + // - check if each NodeId has a corresponding entry in NodeConfigurator + // - if not, create a new one on the fly + if self.config.unique_index_for(&cell.node_id).is_none() { + // - check if the previous node exist, if not, + // create them on the fly now: + for inst in 0..cell.node_id.instance() { + let new_hole_filler_node_id = + cell.node_id.to_instance(inst); + + if self.config + .unique_index_for(&new_hole_filler_node_id) + .is_none() + { + self.config.create_node(new_hole_filler_node_id) + .expect("NodeInfo existent in Matrix"); + } + } + + self.config.create_node(cell.node_id) + .expect("NodeInfo existent in Matrix"); + } + } + } + } + + fn update_graph_ordering_and_edges(&mut self) { + self.graph_ordering.clear(); + self.edges.clear(); + + for x in 0..self.w { + for y in 0..self.h { + let cell = self.matrix[x * self.h + y]; + if cell.node_id == NodeId::Nop { + continue; + } + + self.graph_ordering.add_node(cell.node_id); + + let in1_output = self.get_adjacent_output(x, y, CellDir::T); + let in2_output = self.get_adjacent_output(x, y, CellDir::TL); + let in3_output = self.get_adjacent_output(x, y, CellDir::BL); + + match (cell.in1, in1_output) { + (Some(in1_idx), Some(in1_output)) => { + self.edges.push(Edge { + to: cell.node_id, + to_input: in1_idx, + from: in1_output.0, + from_out: in1_output.1, + }); + self.graph_ordering.add_edge( + in1_output.0, cell.node_id); + }, + _ => {}, + } + + match (cell.in2, in2_output) { + (Some(in2_idx), Some(in2_output)) => { + self.edges.push(Edge { + to: cell.node_id, + to_input: in2_idx, + from: in2_output.0, + from_out: in2_output.1, + }); + self.graph_ordering.add_edge( + in2_output.0, cell.node_id); + }, + _ => {}, + } + + match (cell.in3, in3_output) { + (Some(in3_idx), Some(in3_output)) => { + self.edges.push(Edge { + to: cell.node_id, + to_input: in3_idx, + from: in3_output.0, + from_out: in3_output.1, + }); + self.graph_ordering.add_edge( + in3_output.0, cell.node_id); + }, + _ => {}, + } + } + } + } + + /// Compiles a [NodeProg] from the data collected by the previous + /// call to [Matrix::update_graph_ordering_and_edges]. + /// + /// May return an error if the graph topology is invalid (cycles) + /// or something else happened. + fn build_prog(&mut self) -> Result { + let mut ordered_nodes = vec![]; + if !self.graph_ordering.calculate_order(&mut ordered_nodes) { + return Err(MatrixError::CycleDetected); + } + + let mut prog = self.config.rebuild_node_ports(); + + for node_id in ordered_nodes.iter() { + self.config.add_prog_node(&mut prog, node_id); + } + + for edge in self.edges.iter() { + self.config.set_prog_node_exec_connection( + &mut prog, + (edge.to, edge.to_input), + (edge.from, edge.from_out)); + } + + Ok(prog) + } + + /// Checks the topology of the DSP graph represented by the + /// hexagonal matrix. + /// + /// Use [Matrix::save_matrix] and [Matrix::restore_matrix] + /// for trying out changes before committing them to the + /// DSP thread using [Matrix::sync]. + /// + /// Note that there is a convenience function with [Matrix::change_matrix] + /// to make it easier to test and rollback changes if they are faulty. + /// + ///``` + /// use hexodsp::*; + /// + /// let (node_conf, mut node_exec) = new_node_engine(); + /// let mut matrix = Matrix::new(node_conf, 3, 3); + /// + /// matrix.save_matrix(); + /// + /// // ... + /// matrix.place(0, 1, + /// Cell::empty(NodeId::Sin(1)) + /// .input(Some(0), None, None)); + /// matrix.place(0, 0, + /// Cell::empty(NodeId::Sin(1)) + /// .out(None, None, Some(0))); + /// // ... + /// + /// let error = + /// if let Err(_) = matrix.check() { + /// matrix.restore_matrix(); + /// true + /// } else { + /// matrix.sync().unwrap(); + /// false + /// }; + /// + /// // In this examples case there is an error, as we created + /// // a cycle: + /// assert!(error); + ///``` + pub fn check(&mut self) -> Result<(), MatrixError> { + self.update_graph_ordering_and_edges(); + + let mut edge_map = std::collections::HashMap::new(); + for edge in self.edges.iter() { + if let Some((out1_node_id, out1_idx)) = edge_map.get(&(edge.to, edge.to_input)) { + return Err(MatrixError::DuplicatedInput { + output1: (*out1_node_id, *out1_idx), + output2: (edge.from, edge.from_out), + }); + } else { + edge_map.insert( + (edge.to, edge.to_input), + (edge.from, edge.from_out)); + } + } + + let mut ordered_nodes = vec![]; + if !self.graph_ordering.calculate_order(&mut ordered_nodes) { + return Err(MatrixError::CycleDetected); + } + + Ok(()) + } + + /// Synchronizes the matrix with the DSP thread. + /// Call this everytime you changed any of the matrix [Cell]s + /// eg. with [Matrix::place] and want to publish the + /// changes to the DSP thread. + /// + /// This method might return an error, for instance if the + /// DSP graph topology contains cycles or has other errors. + /// + /// You can check any changes and roll them back + /// using the method [Matrix::change_matrix]. + pub fn sync(&mut self) -> Result<(), MatrixError> { + self.create_intermediate_nodes(); + + self.update_graph_ordering_and_edges(); + let prog = self.build_prog()?; + + self.config.upload_prog(prog, true); // true => copy_old_out + + // Update the generation counter which is used + // by external data structures to sync their state with + // the Matrix. + self.gen_counter += 1; + + // Refresh the input/outputs of the monitored cell, + // just in case something has changed with that monitored cell. + self.remonitor_cell(); + + Ok(()) + } + + /// Retrieves the output port feedback for a specific output + /// of the given [NodeId]. + /// + /// See also [NodeConfigurator::out_fb_for]. + pub fn out_fb_for(&self, node_id: &NodeId, out: u8) -> Option { + self.config.out_fb_for(node_id, out) + } + + /// Updates the output port feedback. Call this every UI frame + /// or whenever you want to get the most recent values from + /// [Matrix::out_fb_for]. + /// + /// See also [NodeConfigurator::update_output_feedback]. + pub fn update_output_feedback(&mut self) { + self.config.update_output_feedback(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_matrix_3_sine() { + use crate::nodes::new_node_engine; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + matrix.place(0, 0, + Cell::empty(NodeId::Sin(0)) + .out(None, Some(0), None)); + matrix.place(1, 0, + Cell::empty(NodeId::Sin(1)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + matrix.place(1, 1, + Cell::empty(NodeId::Sin(2)) + .input(Some(0), None, None)); + matrix.sync().unwrap(); + + node_exec.process_graph_updates(); + + let nodes = node_exec.get_nodes(); + assert!(nodes[0].to_id(0) == NodeId::Sin(0)); + assert!(nodes[1].to_id(1) == NodeId::Sin(1)); + assert!(nodes[2].to_id(2) == NodeId::Sin(2)); + + let prog = node_exec.get_prog(); + assert_eq!(prog.prog[0].to_string(), "Op(i=0 out=(0-1) in=(0-1) at=(0-0))"); + assert_eq!(prog.prog[1].to_string(), "Op(i=1 out=(1-2) in=(1-2) at=(0-0) cpy=(o0 => i1))"); + assert_eq!(prog.prog[2].to_string(), "Op(i=2 out=(2-3) in=(2-3) at=(0-0) cpy=(o1 => i2))"); + } + + #[test] + fn check_matrix_filled() { + use crate::nodes::new_node_engine; + use crate::dsp::{NodeId, Node}; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 9, 9); + + let mut i = 1; + for x in 0..9 { + for y in 0..9 { + matrix.place(x, y, Cell::empty(NodeId::Sin(i))); + i += 1; + } + } + matrix.sync().unwrap(); + + node_exec.process_graph_updates(); + + let nodes = node_exec.get_nodes(); + let ex_nodes : Vec<&Node> = + nodes.iter().filter(|n| n.to_id(0) != NodeId::Nop).collect(); + assert_eq!(ex_nodes.len(), 9 * 9 + 1); + } + + #[test] + fn check_matrix_into_output() { + use crate::nodes::new_node_engine; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + matrix.place(0, 0, + Cell::empty(NodeId::Sin(0)) + .out(None, Some(0), None)); + matrix.place(1, 0, + Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + matrix.sync().unwrap(); + + node_exec.set_sample_rate(44100.0); + node_exec.process_graph_updates(); + + let nodes = node_exec.get_nodes(); + assert!(nodes[0].to_id(0) == NodeId::Sin(0)); + assert!(nodes[1].to_id(0) == NodeId::Out(0)); + + let prog = node_exec.get_prog(); + assert_eq!(prog.prog.len(), 2); + assert_eq!(prog.prog[0].to_string(), "Op(i=0 out=(0-1) in=(0-1) at=(0-0))"); + assert_eq!(prog.prog[1].to_string(), "Op(i=1 out=(1-1) in=(1-3) at=(0-1) cpy=(o0 => i1))"); + } + + #[test] + fn check_matrix_skip_instance() { + use crate::nodes::new_node_engine; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + matrix.place(0, 0, + Cell::empty(NodeId::Sin(2)) + .out(None, Some(0), None)); + matrix.place(1, 0, + Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + matrix.sync().unwrap(); + + node_exec.set_sample_rate(44100.0); + node_exec.process_graph_updates(); + + let nodes = node_exec.get_nodes(); + assert!(nodes[0].to_id(0) == NodeId::Sin(0)); + assert!(nodes[1].to_id(0) == NodeId::Sin(0)); + assert!(nodes[2].to_id(0) == NodeId::Sin(0)); + assert!(nodes[3].to_id(0) == NodeId::Out(0)); + + let prog = node_exec.get_prog(); + assert_eq!(prog.prog.len(), 2); + assert_eq!(prog.prog[0].to_string(), "Op(i=2 out=(2-3) in=(2-3) at=(0-0))"); + assert_eq!(prog.prog[1].to_string(), "Op(i=3 out=(3-3) in=(3-5) at=(0-1) cpy=(o2 => i3))"); + } + + #[test] + fn check_matrix_check_cycle() { + use crate::nodes::new_node_engine; + + let (node_conf, _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + matrix.save_matrix(); + matrix.place(0, 1, + Cell::empty(NodeId::Sin(1)) + .input(Some(0), None, None)); + matrix.place(0, 0, + Cell::empty(NodeId::Sin(1)) + .out(None, None, Some(0))); + let error = + if let Err(_) = matrix.check() { + matrix.restore_matrix(); + true + } else { + matrix.sync().unwrap(); + false + }; + + // In this examples case there is an error, as we created + // a cycle: + assert!(error); + } + + #[test] + fn check_matrix_check_duplicate_input() { + use crate::nodes::new_node_engine; + + let (node_conf, _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 5, 5); + + matrix.save_matrix(); + matrix.place(0, 1, + Cell::empty(NodeId::Sin(0)) + .input(Some(0), None, None)); + matrix.place(0, 0, + Cell::empty(NodeId::Sin(1)) + .out(None, None, Some(0))); + + matrix.place(0, 3, + Cell::empty(NodeId::Sin(0)) + .input(Some(0), None, None)); + matrix.place(0, 2, + Cell::empty(NodeId::Sin(2)) + .out(None, None, Some(0))); + + assert_eq!(matrix.check(), Err(MatrixError::DuplicatedInput { + output1: (NodeId::Sin(1), 0), + output2: (NodeId::Sin(2), 0), + })); + } +} diff --git a/src/matrix_repr.rs b/src/matrix_repr.rs new file mode 100644 index 0000000..10e04c6 --- /dev/null +++ b/src/matrix_repr.rs @@ -0,0 +1,650 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::dsp::{NodeId, ParamId, SAtom}; +use serde_json::{Value, json}; + +#[derive(Debug, Clone, Copy)] +pub struct CellRepr { + pub node_id: NodeId, + pub x: usize, + pub y: usize, + pub inp: [i16; 3], + pub out: [i16; 3], +} + +fn deserialize_node_id(v: &Value, i1: usize, i2: usize) + -> Result +{ + let nid = NodeId::from_str(v[i1].as_str().unwrap_or("???")); + if nid == NodeId::Nop { + return Err( + MatrixDeserError::UnknownNode( + v[i1].as_str().unwrap_or("???").to_string())); + } + + Ok(nid.to_instance(v[i2].as_i64().unwrap_or(0) as usize)) +} + +impl CellRepr { + pub fn serialize(&self) -> Value { + json!([ + self.node_id.name(), + self.node_id.instance(), + self.x, + self.y, + [self.inp[0], self.inp[1], self.inp[2]], + [self.out[0], self.out[1], self.out[2]], + ]) + } + + pub fn deserialize(v: &Value) -> Result { + Ok(Self { + node_id: deserialize_node_id(v, 0, 1)?, + x: v[2].as_i64().unwrap_or(0) as usize, + y: v[3].as_i64().unwrap_or(0) as usize, + inp: [ + v[4][0].as_i64().unwrap_or(-1) as i16, + v[4][1].as_i64().unwrap_or(-1) as i16, + v[4][2].as_i64().unwrap_or(-1) as i16, + ], + out: [ + v[5][0].as_i64().unwrap_or(-1) as i16, + v[5][1].as_i64().unwrap_or(-1) as i16, + v[5][2].as_i64().unwrap_or(-1) as i16, + ], + }) + } +} + +use crate::dsp::tracker::{MAX_PATTERN_LEN, MAX_COLS}; + +#[derive(Debug, Clone)] +pub struct PatternRepr { + pub col_types: [u8; MAX_COLS], + pub data: Vec>, + pub rows: usize, + pub edit_step: usize, + pub cursor: (usize, usize), +} + +impl PatternRepr { + fn serialize(&self) -> Value { + let mut ret = json!({ + "rows": self.rows, + "edit_step": self.edit_step, + "cursor_row": self.cursor.0, + "cursor_col": self.cursor.1, + }); + + let mut cts = json!([]); + if let Value::Array(cts) = &mut cts { + for ct in self.col_types.iter() { + cts.push(json!(*ct as i64)); + } + } + ret["col_types"] = cts; + + let mut data = json!([]); + if let Value::Array(data) = &mut data { + for row in self.data.iter() { + let mut out_col = json!([]); + if let Value::Array(out_col) = &mut out_col { + for col in row.iter() { + out_col.push(json!(*col as i64)); + } + } + data.push(out_col); + } + } + ret["data"] = data; + + ret + } + + fn deserialize(v: &Value) -> Result { + let mut col_types = [0; MAX_COLS]; + + let cts = &v["col_types"]; + if let Value::Array(cts) = cts { + for (i, ct) in cts.iter().enumerate() { + col_types[i] = ct.as_i64().unwrap_or(0) as u8; + } + } + + let mut data = vec![vec![-1; MAX_COLS]; MAX_PATTERN_LEN]; + let dt = &v["data"]; + if let Value::Array(dt) = dt { + for (row_idx, row) in dt.iter().enumerate() { + if let Value::Array(row) = row { + for (col_idx, c) in row.iter().enumerate() { + data[row_idx][col_idx] = c.as_i64().unwrap_or(-1) as i32; + } + } + } + } + + Ok(Self { + col_types, + data, + rows: v["rows"] .as_i64().unwrap_or(0) as usize, + edit_step: v["edit_step"].as_i64().unwrap_or(0) as usize, + cursor: ( + v["cursor_row"].as_i64().unwrap_or(0) as usize, + v["cursor_col"].as_i64().unwrap_or(0) as usize + ), + }) + } +} + + +#[derive(Debug, Clone)] +pub struct MatrixRepr { + pub cells: Vec, + pub params: Vec<(ParamId, f32)>, + pub atoms: Vec<(ParamId, SAtom)>, + pub patterns: Vec>, +} + +#[derive(Debug, Clone)] +pub enum MatrixDeserError { + BadVersion, + UnknownNode(String), + UnknownParamId(String), + Deserialization(String), + IO(String), + InvalidAtom(String), + MatrixError(crate::matrix::MatrixError), +} + +impl From for MatrixDeserError { + fn from(err: crate::matrix::MatrixError) -> Self { + MatrixDeserError::MatrixError(err) + } +} + +impl From for MatrixDeserError { + fn from(err: serde_json::Error) -> MatrixDeserError { + MatrixDeserError::Deserialization(format!("{}", err)) + } +} + +impl From for MatrixDeserError { + fn from(err: std::str::Utf8Error) -> MatrixDeserError { + MatrixDeserError::Deserialization(format!("{}", err)) + } +} + +impl From for MatrixDeserError { + fn from(err: std::io::Error) -> MatrixDeserError { + MatrixDeserError::IO(format!("{}", err)) + } +} + +fn deserialize_atom(v: &Value) -> Result { + match v[0].as_str().unwrap_or("?") { + "i" => { + if let Some(v) = v[1].as_i64() { Ok(SAtom::setting(v)) } + else { Err(MatrixDeserError::InvalidAtom(v.to_string())) } + }, + "p" => { + if let Some(v) = v[1].as_f64() { Ok(SAtom::param(v as f32)) } + else { Err(MatrixDeserError::InvalidAtom(v.to_string())) } + }, + "s" => { + if let Some(v) = v[1].as_str() { Ok(SAtom::str(v)) } + else { Err(MatrixDeserError::InvalidAtom(v.to_string())) } + }, + "as" => { + if let Some(v) = v[1].as_str() { Ok(SAtom::audio_unloaded(v)) } + else { Err(MatrixDeserError::InvalidAtom(v.to_string())) } + }, + "ms" => { + let mut buf : [f32; 8] = [0.0; 8]; + + for i in 0..8 { + if let Some(v) = v[i + 1].as_f64() { + buf[i] = v as f32; + } else { + return Err(MatrixDeserError::InvalidAtom(v.to_string())); + } + } + + Ok(SAtom::micro(&buf)) + }, + _ => Err(MatrixDeserError::InvalidAtom(v.to_string())), + } +} + +fn serialize_atom(atom: &SAtom) -> Value { + match atom { + SAtom::MicroSample(s) => json!(["ms", + s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], + ]), + SAtom::Str(s) => json!(["s", s]), + SAtom::AudioSample((s, _)) => json!(["as", s]), + SAtom::Setting(i) => json!(["i", i]), + SAtom::Param(p) => json!(["p", p]), + } +} + +impl MatrixRepr { + pub fn empty() -> Self { + let cells = vec![]; + let params = vec![]; + let atoms = vec![]; + let patterns = vec![]; + + Self { + cells, + params, + atoms, + patterns, + } + } + + pub fn write_to_file(&mut self, filepath: &str) -> std::io::Result<()> { + use std::io::prelude::*; + use std::fs::OpenOptions; + + let tmp_filepath = format!("{}~", filepath); + + let mut ser = self.serialize(); + ser.push('\n'); + + let mut file = + OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&tmp_filepath)?; + file.write_all(ser.as_bytes())?; + std::fs::rename(&tmp_filepath, &filepath)?; + + Ok(()) + } + + pub fn read_from_file(filepath: &str) -> Result { + use std::io::prelude::*; + use std::fs::OpenOptions; + + let mut file = + OpenOptions::new() + .write(false) + .create(false) + .read(true) + .open(&filepath)?; + + let mut contents : Vec = Vec::new(); + file.read_to_end(&mut contents)?; + + let s = std::str::from_utf8(&contents)?; + + MatrixRepr::deserialize(s) + } + + pub fn deserialize(s: &str) -> Result { + let v : Value = serde_json::from_str(s)?; + + if let Some(version) = v.get("VERSION") { + let version : i64 = version.as_i64().unwrap_or(0); + + if version != 1 { + return Err(MatrixDeserError::BadVersion); + } + } + + let mut m = MatrixRepr::empty(); + + let cells = &v["cells"]; + if let Value::Array(cells) = cells { + for c in cells.iter() { + m.cells.push(CellRepr::deserialize(c)?); + } + } + + let params = &v["params"]; + if let Value::Array(params) = params { + for v in params.iter() { + let node_id = deserialize_node_id(&v, 0, 1)?; + let param_id = node_id.inp_param(v[2].as_str().unwrap_or("")); + + if let Some(param_id) = param_id { + m.params.push( + (param_id, v[3].as_f64().unwrap_or(0.0) as f32)); + } else { + return Err( + MatrixDeserError::UnknownParamId(v.to_string())); + } + } + } + + let atoms = &v["atoms"]; + if let Value::Array(atoms) = atoms { + for v in atoms.iter() { + let node_id = deserialize_node_id(&v, 0, 1)?; + let param_id = node_id.inp_param(v[2].as_str().unwrap_or("")); + + if let Some(param_id) = param_id { + m.atoms.push((param_id, deserialize_atom(&v[3])?)) + } else { + return Err( + MatrixDeserError::UnknownParamId(v.to_string())); + } + } + } + + let patterns = &v["patterns"]; + if let Value::Array(patterns) = patterns { + for p in patterns.iter() { + m.patterns.push( + if p.is_object() { + Some(PatternRepr::deserialize(&p)?) + } else { None }); + } + } + + Ok(m) + } + + pub fn serialize(&mut self) -> String { + let mut v = json!({ + "VERSION": 1, + }); + + self.params.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + self.atoms.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + + let mut params = json!([]); + if let Value::Array(params) = &mut params { + for (p, v) in self.params.iter() { + params.push( + json!([ + p.node_id().name(), + p.node_id().instance(), + p.name(), + v + ])); + } + } + + v["params"] = params; + + let mut atoms = json!([]); + if let Value::Array(atoms) = &mut atoms { + for (p, v) in self.atoms.iter() { + atoms.push( + json!([ + p.node_id().name(), + p.node_id().instance(), + p.name(), + serialize_atom(v), + ])); + } + } + + v["atoms"] = atoms; + + let mut cells = json!([]); + if let Value::Array(cells) = &mut cells { + for cell in self.cells.iter() { + cells.push(cell.serialize()); + } + } + + v["cells"] = cells; + + let mut patterns = json!([]); + if let Value::Array(patterns) = &mut patterns { + for p in self.patterns.iter() { + patterns.push( + if let Some(p) = p { p.serialize() } + else { Value::Null }); + } + } + + v["patterns"] = patterns; + + v.to_string() + } +} + +pub fn load_patch_from_file(matrix: &mut crate::matrix::Matrix, filepath: &str) + -> Result<(), MatrixDeserError> +{ + let mr = MatrixRepr::read_from_file(filepath)?; + matrix.from_repr(&mr)?; + Ok(()) +} + +pub fn save_patch_to_file(matrix: &mut crate::matrix::Matrix, filepath: &str) + -> std::io::Result<()> +{ + let mut mr = matrix.to_repr(); + mr.write_to_file(filepath) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::matrix::{Matrix, Cell}; + + #[test] + fn check_empty_repr_serialization() { + let mut matrix_repr = MatrixRepr::empty(); + + let s = matrix_repr.serialize(); + + assert_eq!(s, + "{\"VERSION\":1,\"atoms\":[],\"cells\":[],\"params\":[],\"patterns\":[]}"); + assert!(MatrixRepr::deserialize(&s).is_ok()); + } + + + #[test] + fn check_repr_serialization() { + use crate::nodes::new_node_engine; + + let (node_conf, mut _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(2); + + matrix.place(0, 0, + Cell::empty(sin) + .out(None, Some(0), None)); + matrix.place(1, 0, + Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.1)); + + let mut mr = matrix.to_repr(); + + let s = mr.serialize(); + + assert_eq!(s, + "{\"VERSION\":1,\"atoms\":[[\"out\",0,\"mono\",[\"i\",0]]],\"cells\":[[\"sin\",2,0,0,[-1,-1,-1],[-1,0,-1]],[\"out\",0,1,0,[-1,0,-1],[-1,-1,0]]],\"params\":[[\"out\",0,\"ch1\",0.0],[\"out\",0,\"ch2\",0.0],[\"sin\",0,\"freq\",0.0],[\"sin\",1,\"freq\",0.0],[\"sin\",2,\"freq\",-0.10000000149011612]],\"patterns\":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]}"); + + let mut mr2 = MatrixRepr::deserialize(&s).unwrap(); + + let s2 = mr2.serialize(); + + assert_eq!(s, s2); + } + + #[test] + fn check_atom_repr() { + let v = serialize_atom(&SAtom::str("foo")); + assert_eq!(v.to_string(), "[\"s\",\"foo\"]"); + let s = serialize_atom(&deserialize_atom(&v).unwrap()).to_string(); + assert_eq!(s, v.to_string()); + + let v = serialize_atom(&SAtom::setting(1337)); + assert_eq!(v.to_string(), "[\"i\",1337]"); + let s = serialize_atom(&deserialize_atom(&v).unwrap()).to_string(); + assert_eq!(s, v.to_string()); + + let v = serialize_atom(&SAtom::param(1.0)); + assert_eq!(v.to_string(), "[\"p\",1.0]"); + let s = serialize_atom(&deserialize_atom(&v).unwrap()).to_string(); + assert_eq!(s, v.to_string()); + + let v = + serialize_atom( + &SAtom::micro(&[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0])); + assert_eq!(v.to_string(), "[\"ms\",1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0]"); + let s = serialize_atom(&deserialize_atom(&v).unwrap()).to_string(); + assert_eq!(s, v.to_string()); + + let v = + serialize_atom( + &SAtom::audio( + "lol.wav", + std::sync::Arc::new(vec![1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0]))); + assert_eq!(v.to_string(), "[\"as\",\"lol.wav\"]"); + let s = serialize_atom(&deserialize_atom(&v).unwrap()).to_string(); + assert_eq!(s, v.to_string()); + } + + #[test] + fn check_cell_repr() { + let cell = + Cell::empty(NodeId::Out(2)) + .input(Some(2), Some(0), Some(3)) + .out(Some(11), Some(4), Some(1)); + let cr = cell.to_repr(); + + let s = cr.serialize().to_string(); + + let v : Value = serde_json::from_str(&s).unwrap(); + let cr2 = CellRepr::deserialize(&v).unwrap(); + + let s2 = cr2.serialize().to_string(); + assert_eq!(s, s2); + } + + #[test] + fn check_file_repr() { + let orig_serial = { + use crate::nodes::new_node_engine; + + let (node_conf, mut _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(2); + + matrix.place(0, 0, + Cell::empty(sin) + .out(None, Some(0), None)); + matrix.place(1, 0, + Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.1)); + + let mut mr = matrix.to_repr(); + let s2 = mr.serialize().to_string(); + + save_patch_to_file( + &mut matrix, "hexosynth_test_patch.hxy").unwrap(); + + s2 + }; + + { + use crate::nodes::new_node_engine; + + let (node_conf, mut _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + load_patch_from_file( + &mut matrix, "hexosynth_test_patch.hxy").unwrap(); + + let mut mr = matrix.to_repr(); + let s = mr.serialize().to_string(); + + assert_eq!(s, orig_serial); + } + } + + #[test] + fn check_matrix_track_repr() { + use crate::dsp::tracker::UIPatternModel; + + let orig_serial = { + use crate::nodes::new_node_engine; + + let (node_conf, mut _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let ts = NodeId::TSeq(0); + + matrix.place(0, 0, + Cell::empty(ts) + .out(None, Some(0), None)); + matrix.sync().unwrap(); + + { + let pat_ref = matrix.get_pattern_data(0).unwrap(); + let mut pat = pat_ref.borrow_mut(); + + for col in 0..MAX_COLS { + pat.set_col_note_type(col); + for v in 1..(MAX_PATTERN_LEN + 1) { + pat.set_cell_value(v - 1, col, v as u16); + } + + pat.set_cursor(16, 3); + pat.set_edit_step(5); + pat.set_rows(133); + } + } + + let mut mr = matrix.to_repr(); + let s2 = mr.serialize().to_string(); + + save_patch_to_file( + &mut matrix, "hexosynth_test_patch_2.hxy").unwrap(); + + s2 + }; + + { + use crate::nodes::new_node_engine; + + let (node_conf, mut _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + load_patch_from_file( + &mut matrix, "hexosynth_test_patch_2.hxy").unwrap(); + + let mut mr = matrix.to_repr(); + let s = mr.serialize().to_string(); + + assert_eq!(s, orig_serial); + + let pat_ref = matrix.get_pattern_data(0).unwrap(); + let mut pat = pat_ref.borrow_mut(); + + for col in 0..MAX_COLS { + assert!(pat.is_col_note(col)); + for v in 1..(MAX_PATTERN_LEN + 1) { + assert_eq!(pat.get_cell_value(v - 1, col), v as u16); + } + + assert_eq!(pat.get_cursor(), (16, 3)); + assert_eq!(pat.get_edit_step(), 5); + assert_eq!(pat.rows(), 133); + } + } + } +} diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..faddafd --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,621 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::dsp::MAX_BLOCK_SIZE; +use ringbuf::{RingBuffer, Producer, Consumer}; + +use std::sync::{Arc, Mutex}; +use std::sync::atomic::AtomicBool; +use std::thread::JoinHandle; + +use crate::util::PerfTimer; + +/// 3 inputs, 3 outputs of signal monitors. +pub const MON_SIG_CNT : usize = 6; + +/// Just some base to determine the monitor buffer sizes. +const IMAGINARY_MAX_SAMPLE_RATE : usize = 48000; + +/// The number of minmax samples to hold. +pub const MONITOR_MINMAX_SAMPLES : usize = 128; + +/// The length in seconds of the MONITOR_MINMAX_SAMPLES +const MONITOR_MINMAX_LEN_S : usize = 2; + +/// The sleep time of the thread that receives monitoring data +/// from the backend/audio thread. +/// It should be within the time of a frame of the UI thread for +/// smooth updates. The maximum is thus about 16ms. +/// The processing of the audio buffer is somewhere in the us +/// area. +const MONITOR_PROC_THREAD_INTERVAL_MS : u64 = 10; + +// TODO / FIXME: We should recalculate this on the basis of the +// real actual sample rate, otherwise the monitor scope +// is going to be too fast. +/// The number of audio samples over which to calculate +/// one min/max sample. Typically something around 750. +const MONITOR_INPUT_LEN_PER_SAMPLE : usize = + (MONITOR_MINMAX_LEN_S * IMAGINARY_MAX_SAMPLE_RATE) + / MONITOR_MINMAX_SAMPLES; + +/// Maximum number of monitor buffers to hold in the backend. +/// Typically there are only 16-32ms of monitor content floating +/// around, as the monitor processing thread regularily +/// processes the monitor. +const MONITOR_BUF_COUNT : usize = +// 2 for safety margin + 2 * (IMAGINARY_MAX_SAMPLE_RATE / MAX_BLOCK_SIZE); + +pub struct MonitorBackend { + rb_mon_prod: Producer, + rb_recycle_con: Consumer, + + /// Holds enough monitor buffers to hold about 1-2 seconds + /// of data. The [MonitorBuf] boxes are written in the + /// backend and then sent via [MonitorBackend::rb_mon_prod] to the frontend. + /// The frontend then sends the used [MonitorBufPtr] back + /// via quick_update_con. + unused_monitor_buffers: Vec, +} + +impl MonitorBackend { + /// Checks if there are any used monitor buffers to be + /// collected. + pub fn check_recycle(&mut self) { + while let Some(buf) = self.rb_recycle_con.pop() { + self.unused_monitor_buffers.push(buf); + } + } + + /// Hands out an unused [MonitorBuf] for filling and + /// sending to the [MonitorProcessor] thread. + pub fn get_unused_mon_buf(&mut self) -> Option { + self.unused_monitor_buffers.pop() + } + + /// A helper function for writing tests. + /// Returns the number of [MonitorBuf] we can hand out + /// until there are none anymore. + pub fn count_unused_mon_bufs(&self) -> usize { + self.unused_monitor_buffers.len() + } + + /// Sends a [MonitorBuf] to the [MonitorProcessor]. + pub fn send_mon_buf(&mut self, buf: MonitorBufPtr) { + match self.rb_mon_prod.push(buf) { + Ok(_) => (), + Err(buf) => self.unused_monitor_buffers.push(buf), + } + } +} + +/// Implements the logic for min/maxing a single signal channel/line. +pub struct MonitorMinMax { + /// Index of the signal in the [MonitorBuf] + sig_idx: usize, + + /// A ring buffer of min/max samples, written to by `buf_write_ptr`. + buf: [(f32, f32); MONITOR_MINMAX_SAMPLES], + + /// The pointer/index into `buf` to the next update to write. + buf_write_ptr: usize, + + /// Holds the currently accumulated min/max values and the length + /// of so far processed audio rate samples. Once MONITOR_INPUT_LEN_PER_SAMPLE + /// is reached, this will be written into `buf`. + cur_min_max: (f32, f32, usize), +} + +impl MonitorMinMax { + pub fn new(sig_idx: usize) -> Self { + Self { + sig_idx, + buf: [(0.0, 0.0); MONITOR_MINMAX_SAMPLES], + buf_write_ptr: 0, + cur_min_max: (100.0, -100.0, 0), + } + } + + /// Processes a monitoring buffer received from the Backend. + /// It returns `true` when a new data point was calculated. + pub fn process(&mut self, mon_buf: &mut MonitorBufPtr) -> bool { + let mut new_data = false; + + while let Some(sample) = + mon_buf.next_sample_for_signal(self.sig_idx) + { + self.cur_min_max.0 = self.cur_min_max.0.min(sample); + self.cur_min_max.1 = self.cur_min_max.1.max(sample); + self.cur_min_max.2 += 1; + + if self.cur_min_max.2 >= MONITOR_INPUT_LEN_PER_SAMPLE { + self.buf[self.buf_write_ptr] = ( + self.cur_min_max.0, + self.cur_min_max.1 + ); + new_data = true; + + self.buf_write_ptr = (self.buf_write_ptr + 1) % self.buf.len(); + + self.cur_min_max.0 = 100.0; + self.cur_min_max.1 = -100.0; + self.cur_min_max.2 = 0; + } + } + + new_data + } +} + +/// Represents a bunch of min/max samples. +/// Usually copied from the MonitorProcessor thread +/// to the frontend if required. +#[derive(Debug, Clone, Copy)] +pub struct MinMaxMonitorSamples { + samples: [(f32, f32); MONITOR_MINMAX_SAMPLES], + buf_ptr: usize, +} + +impl MinMaxMonitorSamples { + pub fn new() -> Self { + Self { + samples: [(0.0, 0.0); MONITOR_MINMAX_SAMPLES], + buf_ptr: 0, + } + } + + fn copy_from(&mut self, min_max_slice: (usize, &[(f32, f32)])) { + self.samples.copy_from_slice(min_max_slice.1); + self.buf_ptr = min_max_slice.0; + } + + fn copy_to(&self, sms: &mut MinMaxMonitorSamples) { + sms.buf_ptr = self.buf_ptr; + sms.samples.copy_from_slice(&self.samples[..]); + } + + /// Gets the sample at the offset relative to the start of the min_max_slice. + pub fn at(&self, offs: usize) -> &(f32, f32) { + let idx = (self.buf_ptr + offs) % self.samples.len(); + &self.samples[idx] + } + + pub fn len(&self) -> usize { MONITOR_MINMAX_SAMPLES } +} + +impl std::ops::Index for MinMaxMonitorSamples { + type Output = (f32, f32); + + fn index(&self, idx: usize) -> &Self::Output { + &self.at(idx) + } +} + +/// The actual frontend API for the MonitorProcessor. +/// We start an extra thread for handling monitored signals from the +/// MonitorBackend, because we can't guarantee that the UI thread +/// is actually started or working. Also because we want to be independent +/// of whether a UI is started at all. +/// +/// Just call [Monitor::get_minmax_monitor_samples] and you will always get +/// the most current data. +pub struct Monitor { + terminate_proc: Arc, + proc_thread: Option>, + + new_data: Arc, + monitor_samples: Arc>, + monitor_samples_copy: [MinMaxMonitorSamples; MON_SIG_CNT], +} + +impl Monitor { + pub fn new(rb_mon_con: Consumer, + rb_recycle_prod: Producer) + -> Self + { + let terminate_proc = Arc::new(AtomicBool::new(false)); + let th_terminate = terminate_proc.clone(); + + let monitor_samples = + Arc::new(Mutex::new( + [MinMaxMonitorSamples::new(); MON_SIG_CNT])); + let th_mon_samples = monitor_samples.clone(); + + let new_data = Arc::new(AtomicBool::new(false)); + let th_new_data = new_data.clone(); + + let th = std::thread::spawn(move || { + let mut proc = MonitorProcessor::new(rb_mon_con, rb_recycle_prod); + + loop { + if th_terminate.load(std::sync::atomic::Ordering::Relaxed) { + return; + } + + // let ta = std::time::Instant::now(); + proc.process(); + // let t0 = std::time::Instant::now().duration_since(ta); + + if proc.check_new_data() { + let mut ms = + th_mon_samples.lock() + .expect("Unpoisoned Lock for monitor_samples"); + for i in 0..MON_SIG_CNT { + ms[i].copy_from(proc.minmax_slice_for_signal(i)); + } + + th_new_data.store(true, std::sync::atomic::Ordering::Relaxed); + } + + // let ta = std::time::Instant::now().duration_since(ta); + // println!("txx Elapsed: {:?} | {:?}", t0, ta); + + std::thread::sleep( + std::time::Duration::from_millis( + MONITOR_PROC_THREAD_INTERVAL_MS)); + } + }); + + Self { + proc_thread: Some(th), + terminate_proc, + monitor_samples, + monitor_samples_copy: [MinMaxMonitorSamples::new(); MON_SIG_CNT], + new_data, + } + } + + pub fn get_minmax_monitor_samples(&mut self, idx: usize) -> &MinMaxMonitorSamples { + // TODO / FIXME: We should be using a triple buffer here + // for access to the set of MinMaxMonitorSamples. But I was + // too lazy and think we can bear with a slightly sluggish + // UI. Anyways, if we get a sluggish UI, we have to look here. + + let mut pt = PerfTimer::new("MMMSamp").off(); + if self.new_data.load(std::sync::atomic::Ordering::Relaxed) { + let ms = + self.monitor_samples.lock() + .expect("Unpoisoned Lock for monitor_samples"); + + pt.print("XXX"); + + for i in 0..MON_SIG_CNT { + ms[i].copy_to( + &mut self.monitor_samples_copy[i]); + } + + self.new_data.store(false, std::sync::atomic::Ordering::Relaxed); + pt.print("YYY"); + } + + &self.monitor_samples_copy[idx] + } +} + +impl Drop for Monitor { + fn drop(&mut self) { + self.terminate_proc.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = self.proc_thread.take().unwrap().join(); + } +} + +/// Coordinates the processing of incoming MonitorBufs. +pub struct MonitorProcessor { + rb_mon_con: Consumer, + rb_recycle_prod: Producer, + + new_data: bool, + + procs: Vec, +} + +impl MonitorProcessor { + pub fn new(rb_mon_con: Consumer, + rb_recycle_prod: Producer) + -> Self + { + let mut procs = vec![]; + for i in 0..MON_SIG_CNT { + procs.push(MonitorMinMax::new(i)); + } + + Self { + rb_mon_con, + rb_recycle_prod, + procs, + new_data: false, + } + } + + /// Helper function for tests, to access the current state of + /// the min/max buffers. + pub fn minmax_slice_for_signal(&self, idx: usize) -> (usize, &[(f32, f32)]) { + let buf_ptr = self.procs[idx].buf_write_ptr; + (buf_ptr, &self.procs[idx].buf[..]) + } + + /// Internal helper function for `process`. + fn process_mon_buf(&mut self, mon_buf: &mut MonitorBufPtr) { + for proc in self.procs.iter_mut() { + if proc.process(mon_buf) { + self.new_data = true; + } + } + } + + /// Processes all queued [MonitorBuf] instances and sends + /// then back to the [MonitorBackend] thread after + /// used for recycling. + pub fn process(&mut self) { + while let Some(mut buf) = self.rb_mon_con.pop() { + self.process_mon_buf(&mut buf); + buf.reset(); + let _ = self.rb_recycle_prod.push(buf); + } + } + + /// Returns true, when a new data point was received. + /// Resets the internal flag until the next time new data is received. + pub fn check_new_data(&mut self) -> bool { + let new_data = self.new_data; + self.new_data = false; + new_data + } +} + +/// Creates a pair of interconnected MonitorBackend and MonitorProcessor +/// instances, to be sent to different threads. +pub fn new_monitor_processor() -> (MonitorBackend, Monitor) { + let rb_monitor = RingBuffer::new(MONITOR_BUF_COUNT); + let rb_recycle = RingBuffer::new(MONITOR_BUF_COUNT); + + let (rb_mon_prod, rb_mon_con) = rb_monitor.split(); + let (rb_recycle_prod, rb_recycle_con) = rb_recycle.split(); + + let mut unused_monitor_buffers = Vec::with_capacity(MONITOR_BUF_COUNT); + + for _ in 0..MONITOR_BUF_COUNT { + unused_monitor_buffers.push(MonitorBuf::alloc()); + } + + let backend = MonitorBackend { + rb_mon_prod, + rb_recycle_con, + unused_monitor_buffers, + }; + + let frontend = Monitor::new(rb_mon_con, rb_recycle_prod); + + (backend, frontend) +} + +/// This structure holds the output of the 6 cell inputs and outputs +/// that is currently being monitored by the frontend. +pub struct MonitorBuf { + /// Holds the data of the signals. Each signal has it's + /// own length. The lengths of the individual elements is + /// reflected in the `len` attribute. + sig_blocks: [f32; MON_SIG_CNT * MAX_BLOCK_SIZE], + + /// Holds the lengths of the individual signal data blocks in `sig_blocks`. + len: [usize; MON_SIG_CNT], + + /// Holds the lengths of the individual signal data blocks in `sig_blocks`. + read_idx: [usize; MON_SIG_CNT], +} + +/// A trait that represents any kind of monitorable sources +/// that provides at least MAX_BLOCK_SIZE samples. +pub trait MonitorSource { + fn copy_to(&self, len: usize, slice: &mut [f32]); +} + +impl MonitorSource for &[f32] { + fn copy_to(&self, len: usize, slice: &mut [f32]) { + slice.copy_from_slice(&self[0..len]) + } +} + +impl MonitorBuf { + /// Allocates a monitor buffer that holds up to 6 signals. + pub fn alloc() -> MonitorBufPtr { + Box::new(Self { + sig_blocks: [0.0; MON_SIG_CNT * MAX_BLOCK_SIZE], + len: [0; MON_SIG_CNT], + read_idx: [0; MON_SIG_CNT], + }) + } + + pub fn reset(&mut self) { + self.len = [0; MON_SIG_CNT]; + self.read_idx = [0; MON_SIG_CNT]; + } + + #[inline(always)] + pub fn next_sample_for_signal(&mut self, idx: usize) -> Option { + let rd_idx = self.read_idx[idx]; + if rd_idx >= self.len[idx] { + return None; + } + + self.read_idx[idx] = rd_idx + 1; + let sb_idx = idx * MAX_BLOCK_SIZE; + + Some(self.sig_blocks[sb_idx + rd_idx]) + } + + pub fn feed(&mut self, idx: usize, len: usize, data: T) + where T: MonitorSource + { + let sb_idx = idx * MAX_BLOCK_SIZE; + data.copy_to(len, &mut self.sig_blocks[sb_idx..(sb_idx + len)]); + + self.len[idx] = len; + } +} + +/// Pointer type for the [MonitorBuf] +pub type MonitorBufPtr = Box; + +#[cfg(test)] +mod tests { + use super::*; + + fn send_n_monitor_bufs(backend: &mut MonitorBackend, + first: f32, last: f32, count: usize) + { + for _ in 0..count { + let mut mon = backend.get_unused_mon_buf().unwrap(); + + let mut samples : Vec = vec![]; + for _ in 0..MAX_BLOCK_SIZE { + samples.push(0.0); + } + samples[0] = first; + samples[MAX_BLOCK_SIZE - 1] = last; + + mon.feed(0, MAX_BLOCK_SIZE, &samples[..]); + + backend.send_mon_buf(mon); + } + } + + fn wait_for_monitor_process() { + // FIXME: This could in theory do some spin waiting for + // the new_data flag! + std::thread::sleep( + std::time::Duration::from_millis( + 3 * MONITOR_PROC_THREAD_INTERVAL_MS)); + } + + #[test] + fn check_monitor_proc() { + let (mut backend, mut frontend) = new_monitor_processor(); + + let count1 = + (MONITOR_INPUT_LEN_PER_SAMPLE / MAX_BLOCK_SIZE) + 1; + let count2 = + 2 * ((MONITOR_INPUT_LEN_PER_SAMPLE / MAX_BLOCK_SIZE) + 1); + + send_n_monitor_bufs(&mut backend, -0.9, 0.8, count1); + + send_n_monitor_bufs(&mut backend, -0.7, 0.6, count2); + + wait_for_monitor_process(); + + let sl = frontend.get_minmax_monitor_samples(0); + + println!("{:?}", sl); + + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 1], (-0.7, 0.6)); + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 2], (-0.7, 0.8)); + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 3], (-0.9, 0.8)); + + assert_eq!( + backend.count_unused_mon_bufs(), + MONITOR_BUF_COUNT - count1 - count2); + + backend.check_recycle(); + + assert_eq!( + backend.count_unused_mon_bufs(), + MONITOR_BUF_COUNT); + } + + #[test] + fn check_monitor_partial() { + let (mut backend, mut frontend) = new_monitor_processor(); + + let count1 = MONITOR_INPUT_LEN_PER_SAMPLE / MAX_BLOCK_SIZE; + + send_n_monitor_bufs(&mut backend, -0.9, 0.8, count1); + + wait_for_monitor_process(); + + let sl = frontend.get_minmax_monitor_samples(0); + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 1], (0.0, 0.0)); + + send_n_monitor_bufs(&mut backend, -0.9, 0.8, 1); + + wait_for_monitor_process(); + + let sl = frontend.get_minmax_monitor_samples(0); + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 1], (-0.9, 0.8)); + } + + #[test] + fn check_monitor_fragment() { + let (mut backend, mut frontend) = new_monitor_processor(); + + let count1 = MONITOR_INPUT_LEN_PER_SAMPLE / MAX_BLOCK_SIZE; + + let rest = MONITOR_INPUT_LEN_PER_SAMPLE - count1 * MAX_BLOCK_SIZE; + + send_n_monitor_bufs(&mut backend, -0.9, 0.8, count1); + + wait_for_monitor_process(); + + let sl = frontend.get_minmax_monitor_samples(0); + assert_eq!(sl[0], (0.0, 0.0)); + + let mut mon = backend.get_unused_mon_buf().unwrap(); + + let mut samples : Vec = vec![]; + let part1_len = rest - 1; + for _ in 0..part1_len { + samples.push(0.0); + } + samples[0] = -0.9; + samples[part1_len - 1] = -0.95; + + mon.feed(0, part1_len, &samples[..]); + backend.send_mon_buf(mon); + + wait_for_monitor_process(); + + let sl = frontend.get_minmax_monitor_samples(0); + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 1], (0.0, 0.0)); + + let mut mon = backend.get_unused_mon_buf().unwrap(); + mon.feed(0, 1, &[0.86][..]); + backend.send_mon_buf(mon); + + wait_for_monitor_process(); + + let sl = frontend.get_minmax_monitor_samples(0); + assert_eq!(sl[MONITOR_MINMAX_SAMPLES - 1], (-0.95, 0.86)); + } + + #[test] + fn check_monitor_wrap_buf() { + let (mut backend, mut frontend) = new_monitor_processor(); + + let count1 = + (MONITOR_INPUT_LEN_PER_SAMPLE / MAX_BLOCK_SIZE) + 1; + + for i in 0..MONITOR_MINMAX_SAMPLES { + let v = i as f32 / MONITOR_MINMAX_SAMPLES as f32; + send_n_monitor_bufs(&mut backend, -0.9, v, count1); + + // Give the MonitorProcessor some time to work on the buffers. + std::thread::sleep( + std::time::Duration::from_millis(5)); + backend.check_recycle(); + } + wait_for_monitor_process(); + backend.check_recycle(); + + let sl = frontend.get_minmax_monitor_samples(0); + println!("{:?}", sl); + + assert_eq!( + (sl[MONITOR_MINMAX_SAMPLES - 1].1 * 10000.0).floor() as u32, + 9921); + + assert_eq!( + backend.count_unused_mon_bufs(), + MONITOR_BUF_COUNT); + } +} diff --git a/src/nodes/drop_thread.rs b/src/nodes/drop_thread.rs new file mode 100644 index 0000000..9f714d6 --- /dev/null +++ b/src/nodes/drop_thread.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use super::DropMsg; + +use ringbuf::Consumer; + +/// For receiving deleted/overwritten nodes from the backend +/// thread and dropping them. +pub(crate) struct DropThread { + terminate: std::sync::Arc, + th: Option>, +} + +impl DropThread { + pub(crate) fn new(mut graph_drop_con: Consumer) -> Self { + let terminate = + std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let th_terminate = terminate.clone(); + + let th = std::thread::spawn(move || { + loop { + if th_terminate.load(std::sync::atomic::Ordering::Relaxed) { + return; + } + + while let Some(_node) = graph_drop_con.pop() { + // drop it ... + println!("Dropped some shit..."); + } + + std::thread::sleep(std::time::Duration::from_millis(250)); + } + }); + + Self { + th: Some(th), + terminate, + } + } +} + +impl Drop for DropThread { + fn drop(&mut self) { + self.terminate.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = self.th.take().unwrap().join(); + } +} diff --git a/src/nodes/feedback_filter.rs b/src/nodes/feedback_filter.rs new file mode 100644 index 0000000..8330c9e --- /dev/null +++ b/src/nodes/feedback_filter.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::dsp::NodeId; +use super::VisualSamplingFilter; + +use std::collections::HashMap; + +pub struct FeedbackFilter { + led_filters: HashMap, + out_filters: HashMap<(NodeId, u8), VisualSamplingFilter>, + recalc_state: bool, +} + +impl FeedbackFilter { + pub fn new() -> Self { + Self { + led_filters: HashMap::new(), + out_filters: HashMap::new(), + recalc_state: true, + } + } + + fn get_out_filter_for_node(&mut self, node_id: &NodeId, out: u8) + -> &mut VisualSamplingFilter + { + self.out_filters + .entry((*node_id, out)) + .or_insert_with(|| VisualSamplingFilter::new()) + } + + fn get_led_filter_for_node(&mut self, node_id: &NodeId) -> &mut VisualSamplingFilter { + self.led_filters + .entry(*node_id) + .or_insert_with(|| VisualSamplingFilter::new()) + } + + pub fn trigger_recalc(&mut self) { + self.recalc_state = !self.recalc_state; + } + + pub fn get_led(&mut self, node_id: &NodeId, sample: f32) -> (f32, f32) { + let recalc_state = self.recalc_state; + let filter = self.get_led_filter_for_node(node_id); + filter.get(recalc_state, sample) + } + + pub fn get_out(&mut self, node_id: &NodeId, out: u8, sample: f32) -> (f32, f32) { + let recalc_state = self.recalc_state; + let filter = self.get_out_filter_for_node(node_id, out); + filter.get(recalc_state, sample) + } +} diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs new file mode 100644 index 0000000..0dbe208 --- /dev/null +++ b/src/nodes/mod.rs @@ -0,0 +1,70 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +pub const MAX_ALLOCATED_NODES : usize = 256; +pub const MAX_SMOOTHERS : usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs +pub const MAX_AVAIL_TRACKERS : usize = 128; + +mod node_prog; +mod node_exec; +mod node_conf; +mod drop_thread; +mod node_graph_ordering; +pub mod visual_sampling_filter; +mod feedback_filter; + +pub(crate) use visual_sampling_filter::*; + +pub use node_exec::*; +pub use node_prog::*; +pub use node_conf::*; +pub use feedback_filter::*; +pub use node_graph_ordering::NodeGraphOrdering; + +pub use crate::monitor::MinMaxMonitorSamples; +use crate::monitor::MON_SIG_CNT; +use crate::dsp::{Node, SAtom}; + +#[derive(Debug)] +pub(crate) enum DropMsg { + Node { node: Node }, + Prog { prog: NodeProg }, + Atom { atom: SAtom }, +} + +/// Big messages for updating the NodeExecutor thread. +/// Usually used for shoveling NodeProg and Nodes to and from +/// the NodeExecutor thread. +#[derive(Debug)] +pub enum GraphMessage { + NewNode { index: u8, node: Node }, + NewProg { prog: NodeProg, copy_old_out: bool }, + Clear { prog: NodeProg }, +} + +/// Messages for small updates between the NodeExecutor thread +/// and the NodeConfigurator. +#[derive(Debug)] +pub enum QuickMessage { + AtomUpdate { at_idx: usize, value: SAtom }, + ParamUpdate { input_idx: usize, value: f32 }, + /// Sets the buffer indices to monitor with the FeedbackProcessor. + SetMonitor { bufs: [usize; MON_SIG_CNT], }, +} + +pub const UNUSED_MONITOR_IDX : usize = 99999; + +/// Creates a NodeConfigurator and a NodeExecutor which are interconnected +/// by ring buffers. +pub fn new_node_engine() -> (NodeConfigurator, NodeExecutor) { + let (nc, shared_exec) = NodeConfigurator::new(); + let ne = NodeExecutor::new(shared_exec); + + // XXX: This is one of the earliest and most consistent points + // in runtime to do this kind of initialization: + crate::dsp::helpers::init_cos_tab(); + + (nc, ne) +} + diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs new file mode 100644 index 0000000..c47465e --- /dev/null +++ b/src/nodes/node_conf.rs @@ -0,0 +1,875 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use super::{ + GraphMessage, QuickMessage, + NodeProg, NodeOp, + FeedbackFilter, + MAX_ALLOCATED_NODES, + MAX_AVAIL_TRACKERS, + UNUSED_MONITOR_IDX +}; +use crate::nodes::drop_thread::DropThread; +use crate::dsp::{NodeId, ParamId, NodeInfo, Node, SAtom, node_factory}; +use crate::util::AtomicFloat; +use crate::monitor::{ + Monitor, MON_SIG_CNT, new_monitor_processor, MinMaxMonitorSamples +}; +use crate::dsp::tracker::{Tracker, PatternData}; + +use ringbuf::{RingBuffer, Producer}; +use std::rc::Rc; +use std::cell::RefCell; +use std::sync::Arc; +use std::collections::HashMap; + +use triple_buffer::Output; + +/// A NodeInstance describes the input/output/atom ports of a Node +/// and holds other important house keeping information for the [NodeConfigurator]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct NodeInstance { + id: NodeId, + in_use: bool, + prog_idx: usize, + out_start: usize, + out_end: usize, + in_start: usize, + in_end: usize, + at_start: usize, + at_end: usize, +} + +impl NodeInstance { + pub fn new(id: NodeId) -> Self { + Self { + id, + in_use: false, + prog_idx: 0, + out_start: 0, + out_end: 0, + in_start: 0, + in_end: 0, + at_start: 0, + at_end: 0, + } + } + + pub fn mark_used(&mut self) { self.in_use = true; } + pub fn is_used(&self) -> bool { self.in_use } + + pub fn to_op(&self) -> NodeOp { + NodeOp { + idx: self.prog_idx as u8, + out_idxlen: (self.out_start, self.out_end), + in_idxlen: (self.in_start, self.in_end), + at_idxlen: (self.at_start, self.at_end), + inputs: vec![], + } + } + + pub fn in_local2global(&self, idx: u8) -> Option { + let idx = self.in_start + idx as usize; + if idx < self.in_end { Some(idx) } + else { None } + } + + pub fn out_local2global(&self, idx: u8) -> Option { + let idx = self.out_start + idx as usize; + if idx < self.out_end { Some(idx) } + else { None } + } + + pub fn set_index(mut self, idx: usize) -> Self { + self.prog_idx = idx; + self + } + + pub fn set_output(mut self, s: usize, e: usize) -> Self { + self.out_start = s; + self.out_end = e; + self + } + + pub fn set_input(mut self, s: usize, e: usize) -> Self { + self.in_start = s; + self.in_end = e; + self + } + + pub fn set_atom(mut self, s: usize, e: usize) -> Self { + self.at_start = s; + self.at_end = e; + self + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +struct NodeInputParam { + param_id: ParamId, + input_idx: usize, + value: f32, +} + +#[derive(Debug, Clone)] +struct NodeInputAtom { + param_id: ParamId, + at_idx: usize, + value: SAtom, +} + +/// This struct holds the frontend node configuration. +/// +/// It stores which nodes are allocated and where. +/// Allocation of new nodes is done here, and parameter management +/// and synchronization is also done by this. It generally acts +/// as facade for the executed node graph in the backend. +pub struct NodeConfigurator { + /// Holds all the nodes, their parameters and type. + pub(crate) nodes: Vec<(NodeInfo, Option)>, + /// An index of all nodes ever instanciated. + /// Be aware, that currently there is no cleanup implemented. + /// That means, any instanciated NodeId will persist throughout + /// the whole runtime. A garbage collector might be implemented + /// when saving presets. + pub(crate) node2idx: HashMap, + /// Holding the tracker sequencers + pub(crate) trackers: Vec, + /// The shared parts of the [NodeConfigurator] + /// and the [crate::nodes::NodeExecutor]. + pub(crate) shared: SharedNodeConf, + + feedback_filter: FeedbackFilter, + + /// Contains (automateable) parameters + params: std::collections::HashMap, + /// Stores the most recently set parameter values + param_values: std::collections::HashMap, + /// Contains non automateable atom data for the nodes + atoms: std::collections::HashMap, + /// Stores the most recently set atoms + atom_values: std::collections::HashMap, + + /// Holds a copy of the most recently updated output port feedback + /// values. Update this by calling [NodeConfigurator::update_output_feedback]. + output_fb_values: Vec, + + /// Holds the channel to the backend that sends output port feedback. + /// This is queried by [NodeConfigurator::update_output_feedback]. + output_fb_cons: Option>>, +} + +pub(crate) struct SharedNodeConf { + /// Holds the LED values of the nodes + pub(crate) node_ctx_values: Vec>, + /// For updating the NodeExecutor with graph updates. + pub(crate) graph_update_prod: Producer, + /// For quick updates like UI paramter changes. + pub(crate) quick_update_prod: Producer, + /// For receiving monitor data from the backend thread. + pub(crate) monitor: Monitor, + /// Handles deallocation of dead nodes from the backend. + #[allow(dead_code)] + pub(crate) drop_thread: DropThread, +} + +use super::node_exec::SharedNodeExec; + +impl SharedNodeConf { + pub(crate) fn new() -> (Self, SharedNodeExec) { + let rb_graph = RingBuffer::new(MAX_ALLOCATED_NODES * 2); + let rb_quick = RingBuffer::new(MAX_ALLOCATED_NODES * 8); + let rb_drop = RingBuffer::new(MAX_ALLOCATED_NODES * 2); + + let (rb_graph_prod, rb_graph_con) = rb_graph.split(); + let (rb_quick_prod, rb_quick_con) = rb_quick.split(); + let (rb_drop_prod, rb_drop_con) = rb_drop.split(); + + let drop_thread = DropThread::new(rb_drop_con); + + let (monitor_backend, monitor) = new_monitor_processor(); + + let mut node_ctx_values = Vec::new(); + node_ctx_values.resize_with( + 2 * MAX_ALLOCATED_NODES, + || Arc::new(AtomicFloat::new(0.0))); + + let mut exec_node_ctx_vals = Vec::new(); + for ctx_val in node_ctx_values.iter() { + exec_node_ctx_vals.push(ctx_val.clone()); + } + + (Self { + node_ctx_values, + graph_update_prod: rb_graph_prod, + quick_update_prod: rb_quick_prod, + monitor, + drop_thread, + }, SharedNodeExec { + node_ctx_values: exec_node_ctx_vals, + graph_update_con: rb_graph_con, + quick_update_con: rb_quick_con, + graph_drop_prod: rb_drop_prod, + monitor_backend, + }) + } +} + +impl NodeConfigurator { + pub(crate) fn new() -> (Self, SharedNodeExec) { + let mut nodes = Vec::new(); + nodes.resize_with(MAX_ALLOCATED_NODES, || (NodeInfo::Nop, None)); + + let (shared, shared_exec) = SharedNodeConf::new(); + + (NodeConfigurator { + nodes, + shared, + feedback_filter: FeedbackFilter::new(), + output_fb_values: vec![], + output_fb_cons: None, + params: std::collections::HashMap::new(), + param_values: std::collections::HashMap::new(), + atoms: std::collections::HashMap::new(), + atom_values: std::collections::HashMap::new(), + node2idx: HashMap::new(), + trackers: vec![Tracker::new(); MAX_AVAIL_TRACKERS], + }, shared_exec) + } +// FIXME: We can't drop nodes at runtime! +// We need to reinitialize the whole engine for this. +// There are too many things relying on the node index (UI). +// +// pub fn drop_node(&mut self, idx: usize) { +// if idx >= self.nodes.len() { +// return; +// } +// +// match self.nodes[idx] { +// NodeInfo::Nop => { return; }, +// _ => {}, +// } +// +// self.nodes[idx] = NodeInfo::Nop; +// let _ = +// self.graph_update_prod.push( +// GraphMessage::NewNode { +// index: idx as u8, +// node: Node::Nop, +// }); +// } + + pub fn for_each(&self, mut f: F) { + for (i, n) in self.nodes.iter().enumerate() { + let nid = n.0.to_id(); + if NodeId::Nop == nid { + break; + } + + f(&n.0, nid, i); + } + } + + pub fn unique_index_for(&self, ni: &NodeId) -> Option { + self.node2idx.get(&ni).copied() + } + + pub fn node_by_id(&self, ni: &NodeId) -> Option<&(NodeInfo, Option)> { + let idx = self.unique_index_for(ni)?; + self.nodes.get(idx) + } + + pub fn node_by_id_mut(&mut self, ni: &NodeId) -> Option<&mut (NodeInfo, Option)> { + let idx = self.unique_index_for(ni)?; + self.nodes.get_mut(idx) + } + + /// Assign [SAtom] values to input parameters and atoms. + /// + /// Only updates the DSP backend of [NodeConfigurator::rebuild_node_ports] was called + /// before calling this. + pub fn set_param(&mut self, param: ParamId, at: SAtom) { + if param.is_atom() { + self.atom_values.insert(param, at.clone()); + + if let Some(nparam) = self.atoms.get_mut(¶m) { + nparam.value = at.clone(); + + let at_idx = nparam.at_idx; + let _ = + self.shared.quick_update_prod.push( + QuickMessage::AtomUpdate { at_idx, value: at }); + } + } else { + self.param_values.insert(param, at.f()); + + if let Some(nparam) = self.params.get_mut(¶m) { + let value = at.f(); + nparam.value = value; + + let input_idx = nparam.input_idx; + let _ = + self.shared.quick_update_prod.push( + QuickMessage::ParamUpdate { input_idx, value }); + } + } + } + + /// Dumps all set parameters (inputs and atoms). + /// Most useful for serialization and saving patches. + pub fn dump_param_values(&self) + -> (Vec<(ParamId, f32)>, Vec<(ParamId, SAtom)>) + { + let params : Vec<(ParamId, f32)> = + self.param_values + .iter() + .map(|(param_id, value)| (*param_id, *value)) + .collect(); + + let atoms : Vec<(ParamId, SAtom)> = + self.atom_values + .iter() + .map(|(param_id, value)| (*param_id, value.clone())) + .collect(); + + (params, atoms) + } + + /// Loads parameter values from a dump. You will still need to upload + /// a new [NodeProg] which contains these values. + pub fn load_dumped_param_values( + &mut self, params: &[(ParamId, f32)], atoms: &[(ParamId, SAtom)]) + { + for (param_id, val) in params.iter() { + self.param_values.insert(*param_id, *val); + } + + for (param_id, val) in atoms.iter() { + self.atom_values.insert(*param_id, val.clone()); + } + } + + /// Iterates over every parameter and calls the given function with + /// it's current value. + pub fn for_each_param(&self, mut f: F) { + for (_, node_input) in self.atoms.iter() { + if let Some(unique_idx) = + self.unique_index_for(&node_input.param_id.node_id()) + { + f(unique_idx, node_input.param_id, &node_input.value); + } + } + + for (_, node_input) in self.params.iter() { + if let Some(unique_idx) = + self.unique_index_for(&node_input.param_id.node_id()) + { + f(unique_idx, node_input.param_id, + &SAtom::param(node_input.value)); + } + } + } + + /// Returns the current phase value of the given node. + /// + /// It usually returns something like the position of a sequencer + /// or the phase of an oscillator. + pub fn phase_value_for(&self, ni: &NodeId) -> f32 { + if let Some(idx) = self.unique_index_for(ni) { + self.shared.node_ctx_values[(idx * 2) + 1].get() + } else { + 0.0 + } + } + + /// Returns the current status LED value of the given node. + /// + /// A status LED might be anything a specific node deems the most + /// important value. Often it might be just the current value + /// of the primary signal output. + pub fn led_value_for(&self, ni: &NodeId) -> f32 { + if let Some(idx) = self.unique_index_for(ni) { + self.shared.node_ctx_values[idx * 2].get() + } else { + 0.0 + } + } + + /// Triggers recalculation of the filtered values from the + /// current LED values and output feedback. + /// + /// This function internally calls [NodeConfigurator::update_output_feedback] + /// for you, so you don't need to call it yourself. + /// + /// See also [NodeConfigurator::filtered_led_for] + /// and [NodeConfigurator::filtered_out_fb_for]. + pub fn update_filters(&mut self) { + self.update_output_feedback(); + self.feedback_filter.trigger_recalc(); + } + + /// Returns a filtered LED value that is smoothed a bit + /// and provides a min and max value. + /// + /// Make sure to call [NodeConfigurator::update_filters] + /// before calling this function, or the values won't be up to date. + /// + ///``` + /// use hexodsp::*; + /// + /// let (mut node_conf, mut node_exec) = new_node_engine(); + /// + /// node_conf.create_node(NodeId::Sin(0)); + /// node_conf.create_node(NodeId::Amp(0)); + /// + /// let mut prog = node_conf.rebuild_node_ports(); + /// + /// node_conf.add_prog_node(&mut prog, &NodeId::Sin(0)); + /// node_conf.add_prog_node(&mut prog, &NodeId::Amp(0)); + /// + /// node_conf.set_prog_node_exec_connection( + /// &mut prog, + /// (NodeId::Amp(0), NodeId::Amp(0).inp("inp").unwrap()), + /// (NodeId::Sin(0), NodeId::Sin(0).out("sig").unwrap())); + /// + /// node_conf.upload_prog(prog, true); + /// + /// node_exec.test_run(0.1, false); + /// assert!((node_conf.led_value_for(&NodeId::Sin(0)) - (-0.062522)).abs() < 0.001); + /// assert!((node_conf.led_value_for(&NodeId::Amp(0)) - (-0.062522)).abs() < 0.001); + /// + /// for _ in 0..10 { + /// node_exec.test_run(0.1, false); + /// node_conf.update_filters(); + /// node_conf.filtered_led_for(&NodeId::Sin(0)); + /// node_conf.filtered_led_for(&NodeId::Amp(0)); + /// } + /// + /// assert_eq!((node_conf.filtered_led_for(&NodeId::Sin(0)).0 * 1000.0).floor() as i64, 62); + /// assert_eq!((node_conf.filtered_led_for(&NodeId::Amp(0)).0 * 1000.0).floor() as i64, 62); + ///``` + pub fn filtered_led_for(&mut self, ni: &NodeId) -> (f32, f32) { + let led_value = self.led_value_for(ni); + self.feedback_filter.get_led(ni, led_value) + } + + /// Returns a filtered output port value that is smoothed + /// a bit and provides a min and max value. + /// + /// Make sure to call [NodeConfigurator::update_filters] + /// before calling this function, or the values won't be up to date. + /// That function also calls [NodeConfigurator::update_output_feedback] + /// for you conveniently. + /// + /// For an example on how to use see [NodeConfigurator::filtered_led_for] + /// which has the same semantics as this function. + pub fn filtered_out_fb_for(&mut self, node_id: &NodeId, out: u8) + -> (f32, f32) + { + let out_value = self.out_fb_for(node_id, out).unwrap_or(0.0); + self.feedback_filter.get_out(node_id, out, out_value) + } + + /// Monitor the given inputs and outputs of a specific node. + /// + /// The monitor data can be retrieved using + /// [NodeConfigurator::get_minmax_monitor_samples]. + pub fn monitor(&mut self, + node_id: &NodeId, inputs: &[Option], outputs: &[Option]) + { + let mut bufs = [UNUSED_MONITOR_IDX; MON_SIG_CNT]; + + if let Some((_node_info, node_instance)) = self.node_by_id(node_id) { + if let Some(node_instance) = node_instance { + + let mut i = 0; + for inp_idx in inputs.iter().take(MON_SIG_CNT / 2) { + if let Some(inp_idx) = inp_idx { + if let Some(global_idx) + = node_instance.in_local2global(*inp_idx) + { + bufs[i] = global_idx; + } + } + + i += 1; + } + + for out_idx in outputs.iter().take(MON_SIG_CNT / 2) { + if let Some(out_idx) = out_idx { + if let Some(global_idx) + = node_instance.out_local2global(*out_idx) + { + bufs[i] = global_idx; + } + } + + i += 1; + } + + let _ = + self.shared.quick_update_prod.push( + QuickMessage::SetMonitor { bufs }); + } + } + } + + pub fn get_pattern_data(&self, tracker_id: usize) + -> Option>> + { + if tracker_id >= self.trackers.len() { + return None; + } + + Some(self.trackers[tracker_id].data()) + } + + pub fn check_pattern_data(&mut self, tracker_id: usize) { + if tracker_id >= self.trackers.len() { + return; + } + + self.trackers[tracker_id].send_one_update(); + } + + pub fn delete_nodes(&mut self) { + self.node2idx.clear(); + self.nodes.fill_with(|| (NodeInfo::Nop, None)); + self.params .clear(); + self.param_values.clear(); + self.atoms .clear(); + self.atom_values .clear(); + + let _ = + self.shared.graph_update_prod.push( + GraphMessage::Clear { prog: NodeProg::empty() }); + } + + pub fn create_node(&mut self, ni: NodeId) -> Option<(&NodeInfo, u8)> { + println!("create_node: {}", ni); + + if let Some((mut node, info)) = node_factory(ni) { + let mut index : Option = None; + + if let Node::TSeq { node } = &mut node { + let tracker_idx = ni.instance(); + if let Some(trk) = self.trackers.get_mut(tracker_idx) { + node.set_backend(trk.get_backend()); + } + } + + for i in 0..self.nodes.len() { + if let NodeInfo::Nop = self.nodes[i].0 { + index = Some(i); + break; + + } else if ni == self.nodes[i].0.to_id() { + return Some((&self.nodes[i].0, i as u8)); + } + } + + if let Some(index) = index { + self.node2idx.insert(ni, index); + + self.nodes[index] = (info, None); + + let _ = + self.shared.graph_update_prod.push( + GraphMessage::NewNode { + index: index as u8, + node, + }); + + Some((&self.nodes[index].0, index as u8)) + + } else { + let index = self.nodes.len(); + self.node2idx.insert(ni, index); + + self.nodes.resize_with( + (self.nodes.len() + 1) * 2, + || (NodeInfo::Nop, None)); + self.nodes[index] = (info, None); + + let _ = + self.shared.graph_update_prod.push( + GraphMessage::NewNode { + index: index as u8, + node, + }); + + Some((&self.nodes[index].0, index as u8)) + } + } else { + None + } + } + + /// Returns the first instance of the given [NodeId] (starting with the + /// instance of the [NodeId]) that has not been used. + /// + /// Primarily used by the (G)UI when creating new nodes to be added to the + /// graph. + /// + /// Should be called after the [NodeProg] has been created + /// (and after [NodeConfigurator::rebuild_node_ports] was called). + /// + /// If new nodes were created/deleted/reordered in between this function + /// might not work properly and assign already used instances. + pub fn unused_instance_node_id(&self, mut id: NodeId) -> NodeId { + while let Some((_, Some(ni))) = self.node_by_id(&id) { + if !ni.is_used() { + return ni.id; + } + + id = id.to_instance(id.instance() + 1); + } + + id + } + + /// Rebuilds Input/Output/Atom indices for the nodes, which is necessary + /// if nodes were created/deleted or reordered. It also assigns + /// input parameter and atom values for new nodes. + /// + /// Returns a new NodeProg with space for all allocated nodes + /// inputs, outputs and atoms. + /// + /// Execute this after a [NodeConfigurator::create_node]. + pub fn rebuild_node_ports(&mut self) -> NodeProg { + // Regenerating the params and atoms in the next step: + self.params.clear(); + self.atoms.clear(); + + let mut out_len = 0; + let mut in_len = 0; + let mut at_len = 0; + + for (i, (node_info, node_instance)) + in self.nodes.iter_mut().enumerate() + { + let id = node_info.to_id(); + + // - calculate size of output vector. + let out_idx = out_len; + out_len += node_info.out_count(); + + // - calculate size of input vector. + let in_idx = in_len; + in_len += node_info.in_count(); + + // - calculate size of atom vector. + let at_idx = at_len; + at_len += node_info.at_count(); + + if id == NodeId::Nop { + break; + } + + // - save offset and length of each node's + // allocation in the output vector. + *node_instance = Some( + NodeInstance::new(id) + .set_index(i) + .set_output(out_idx, out_len) + .set_input(in_idx, in_len) + .set_atom(at_idx, at_len)); + + println!("INSERT[{}]: {:?} outidx: {},{} inidx: {},{} atidx: {},{}", + i, id, out_idx, out_len, in_idx, in_len, at_idx, at_len); + + // Create new parameters and initialize them if they did not + // already exist previously + for param_idx in in_idx..in_len { + if let Some(param_id) = id.inp_param_by_idx(param_idx - in_idx) { + let value = + if let Some(value) = self.param_values.get(¶m_id) { + *value + } else { + param_id.norm_def() + }; + + self.param_values.insert(param_id, value); + self.params.insert(param_id, NodeInputParam { + param_id, + value, + input_idx: param_idx, + }); + } + } + + // Create new atom data and initialize it if it did not + // already exist from a previous matrix instance. + for atom_idx in at_idx..at_len { + // XXX: See also the documentation of atom_param_by_idx about the + // little param_id for an Atom weirdness here. + if let Some(param_id) = id.atom_param_by_idx(atom_idx - at_idx) { + let value = + if let Some(atom) = + self.atom_values.get(¶m_id) + { + atom.clone() + } else { + param_id.as_atom_def() + }; + + self.atom_values.insert(param_id, value.clone()); + self.atoms.insert(param_id, NodeInputAtom { + param_id, + value, + at_idx: atom_idx, + }); + } + } + } + + NodeProg::new(out_len, in_len, at_len) + } + + /// Creates a new [NodeOp] and add it to the [NodeProg]. + /// + /// It will fail silently if the nodes have not been created yet or + /// [NodeConfigurator::rebuild_node_ports] was not called before. So make sure this is the + /// case or don't expect the node and input to be executed. + pub fn add_prog_node(&mut self, prog: &mut NodeProg, node_id: &NodeId) { + if let Some((_node_info, Some(node_instance))) + = self.node_by_id_mut(node_id) + { + node_instance.mark_used(); + let op = node_instance.to_op(); + prog.append_op(op); + } + } + + /// Adds an adjacent output connection to the given node input. + /// Will either create a new [NodeOp] in the [NodeProg] or append to an + /// existing one. This means the order you set the to be executed node + /// connections, is the order the [NodeProg] is going to be executed by the + /// DSP thread later. + /// + /// It will fail silently if the nodes have not been created yet or + /// [NodeConfigurator::rebuild_node_ports] was not called before. So make sure this is the + /// case or don't expect the node and input to be executed. + pub fn set_prog_node_exec_connection( + &mut self, prog: &mut NodeProg, + node_input: (NodeId, u8), + adjacent_output: (NodeId, u8)) + { + let output_index = + if let Some((_, Some(node_instance))) + = self.node_by_id(&adjacent_output.0) + { + node_instance.out_local2global(adjacent_output.1) + } else { + return; + }; + + if let Some((_node_info, Some(node_instance))) + = self.node_by_id_mut(&node_input.0) + { + node_instance.mark_used(); + let op = node_instance.to_op(); + + let input_index = node_instance.in_local2global(node_input.1); + match (input_index, output_index) { + (Some(input_index), Some(output_index)) => { + prog.append_edge(op, input_index, output_index); + }, + _ => {}, + } + } + } + + /// Uploads a new NodeProg instance. + /// + /// Create a new NodeProg instance with [NodeConfigurator::rebuild_node_ports] + /// for each call to this function. Otherwise things like the + /// [NodeConfigurator::out_fb_for] might not work properly! + /// + /// The `copy_old_out` parameter should be set if there are only + /// new nodes appended to the end of the node instances. + /// It helps to prevent clicks when there is a feedback path somewhere. + /// + /// It must not be set when a completely new set of node instances + /// was created, for instance when a completely new patch was loaded. + /// + /// Here is an example on how to use the [NodeConfigurator] + /// directly to setup and upload a [NodeProg]: + /// + ///``` + /// use hexodsp::*; + /// + /// let (mut node_conf, mut node_exec) = new_node_engine(); + /// + /// node_conf.create_node(NodeId::Sin(0)); + /// node_conf.create_node(NodeId::Amp(0)); + /// + /// let mut prog = node_conf.rebuild_node_ports(); + /// + /// node_conf.add_prog_node(&mut prog, &NodeId::Sin(0)); + /// node_conf.add_prog_node(&mut prog, &NodeId::Amp(0)); + /// + /// node_conf.set_prog_node_exec_connection( + /// &mut prog, + /// (NodeId::Amp(0), NodeId::Amp(0).inp("inp").unwrap()), + /// (NodeId::Sin(0), NodeId::Sin(0).out("sig").unwrap())); + /// + /// node_conf.upload_prog(prog, true); + ///``` + pub fn upload_prog(&mut self, mut prog: NodeProg, copy_old_out: bool) { + // Copy the parameter values and atom data into the program: + // They are extracted by process_graph_updates() later to + // reset the inp[] input value vector. + for (_param_id, param) in self.params.iter() { + prog.params_mut()[param.input_idx] = param.value; + } + + // The atoms are referred to directly on process() call. + for (_param_id, param) in self.atoms.iter() { + prog.atoms_mut()[param.at_idx] = param.value.clone(); + } + + self.output_fb_cons = prog.take_feedback_consumer(); + + let _ = + self.shared.graph_update_prod.push( + GraphMessage::NewProg { prog, copy_old_out }); + } + + /// Retrieves the feedback value for a specific output port of the + /// given [NodeId]. You need to call [NodeConfigurator::update_output_feedback] + /// before this, or otherwise your output values might be outdated + /// or not available at all. + /// + /// See also [NodeConfigurator::filtered_out_fb_for] for a + /// filtered variant suitable for UI usage. + pub fn out_fb_for(&self, node_id: &NodeId, out: u8) -> Option { + if let Some((_, Some(node_instance))) = self.node_by_id(node_id) { + self.output_fb_values.get( + node_instance.out_local2global(out)?).copied() + } else { + None + } + } + + /// Checks if the backend has new output feedback values. + /// Call this function for each frame of the UI to get the most + /// up to date output feedback values that are available. + /// + /// Retrieve the output value by calling [NodeConfigurator::out_fb_for]. + pub fn update_output_feedback(&mut self) { + if let Some(out_fb_output) = &mut self.output_fb_cons { + out_fb_output.update(); + let out_vec = out_fb_output.output_buffer(); + + self.output_fb_values.clear(); + self.output_fb_values.resize(out_vec.len(), 0.0); + self.output_fb_values.copy_from_slice(&out_vec[..]); + } + } + + pub fn get_minmax_monitor_samples(&mut self, idx: usize) -> &MinMaxMonitorSamples { + self.shared.monitor.get_minmax_monitor_samples(idx) + } +} diff --git a/src/nodes/node_exec.rs b/src/nodes/node_exec.rs new file mode 100644 index 0000000..2472a34 --- /dev/null +++ b/src/nodes/node_exec.rs @@ -0,0 +1,432 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use super::{ + GraphMessage, QuickMessage, DropMsg, NodeProg, + UNUSED_MONITOR_IDX, MAX_ALLOCATED_NODES, MAX_SMOOTHERS +}; +use crate::dsp::{NodeId, Node}; +use crate::util::{Smoother, AtomicFloat}; +use crate::monitor::{MonitorBackend, MON_SIG_CNT}; + +use ringbuf::{Producer, Consumer}; +use std::sync::Arc; + +/// Holds the complete allocation of nodes and +/// the program. New Nodes or the program is +/// not newly allocated in the audio backend, but it is +/// copied from the input ring buffer. +/// If this turns out to be too slow, we might +/// have to push buffers of the program around. +/// +pub struct NodeExecutor { + /// Contains the nodes and their state. + /// Is loaded from the input ring buffer when a corresponding + /// message arrives. + /// + /// In case the previous node contained something that needs + /// deallocation, the nodes are replaced and the contents + /// is sent back using the free-ringbuffer. + pub(crate) nodes: Vec, + + /// Contains the stand-by smoothing operators for incoming parameter changes. + pub(crate) smoothers: Vec<(usize, Smoother)>, + + /// Contains target parameter values after a smoother finished, + /// these will refresh the input buffers: + pub(crate) target_refresh: Vec<(usize, f32)>, + + /// Contains the to be executed nodes and output operations. + /// Is copied from the input ringbuffer when a corresponding + /// message arrives. + pub(crate) prog: NodeProg, + + /// Holds the input vector indices which are to be monitored by the frontend. + pub(crate) monitor_signal_cur_inp_indices: [usize; MON_SIG_CNT], + + /// The sample rate + pub(crate) sample_rate: f32, + + /// The connection with the [crate::nodes::NodeConfigurator]. + shared: SharedNodeExec, +} + +/// Contains anything that connects the [NodeExecutor] with the frontend part. +pub(crate) struct SharedNodeExec { + /// Holds two context values interleaved. + /// The first for each node is the LED value and the second is a + /// phase value. The LED will be displayed in the hex matrix, while the + /// phase might be used to display an envelope's play position. + pub(crate) node_ctx_values: Vec>, + /// For receiving Node and NodeProg updates + pub(crate) graph_update_con: Consumer, + /// For quick updates like UI paramter changes. + pub(crate) quick_update_con: Consumer, + /// For receiving deleted/overwritten nodes from the backend thread. + pub(crate) graph_drop_prod: Producer, + /// For sending feedback to the frontend thread. + pub(crate) monitor_backend: MonitorBackend, +} + +pub trait NodeAudioContext { + fn nframes(&self) -> usize; + fn output(&mut self, channel: usize, frame: usize, v: f32); + fn input(&mut self, channel: usize, frame: usize) -> f32; +} + +impl NodeExecutor { + pub(crate) fn new(shared: SharedNodeExec) -> Self { + let mut nodes = Vec::new(); + nodes.resize_with(MAX_ALLOCATED_NODES, || Node::Nop); + + let mut smoothers = Vec::new(); + smoothers.resize_with(MAX_SMOOTHERS, || (0, Smoother::new())); + + let target_refresh = Vec::with_capacity(MAX_SMOOTHERS); + + NodeExecutor { + nodes, + smoothers, + target_refresh, + sample_rate: 44100.0, + prog: NodeProg::empty(), + monitor_signal_cur_inp_indices: [UNUSED_MONITOR_IDX; MON_SIG_CNT], + shared, + } + } + + #[inline] + pub fn process_graph_updates(&mut self) { + while let Some(upd) = self.shared.graph_update_con.pop() { + match upd { + GraphMessage::NewNode { index, mut node } => { + node.set_sample_rate(self.sample_rate); + let prev_node = + std::mem::replace( + &mut self.nodes[index as usize], + node); + let _ = + self.shared.graph_drop_prod.push( + DropMsg::Node { node: prev_node }); + }, + GraphMessage::Clear { prog } => { + for n in self.nodes.iter_mut() { + if n.to_id(0) != NodeId::Nop { + let prev_node = std::mem::replace(n, Node::Nop); + let _ = + self.shared.graph_drop_prod.push( + DropMsg::Node { node: prev_node }); + } + } + + self.monitor_signal_cur_inp_indices = + [UNUSED_MONITOR_IDX; MON_SIG_CNT]; + + let prev_prog = std::mem::replace(&mut self.prog, prog); + let _ = + self.shared.graph_drop_prod.push( + DropMsg::Prog { prog: prev_prog }); + }, + GraphMessage::NewProg { prog, copy_old_out } => { + let mut prev_prog = std::mem::replace(&mut self.prog, prog); + + self.monitor_signal_cur_inp_indices = + [UNUSED_MONITOR_IDX; MON_SIG_CNT]; + + // XXX: Copying from the old vector works, because we only + // append nodes to the _end_ of the node instance vector. + // If we do a garbage collection, we can't do this. + // + // XXX: Also, we need to initialize the input parameter + // vector, because we don't know if they are updated from + // the new program outputs anymore. So we need to + // copy the old paramters to the inputs. + // + // => This does not apply to atom data, because that + // is always sent with the new program and "should" + // be up to date, even if we have a slight possible race + // condition between GraphMessage::NewProg + // and QuickMessage::AtomUpdate. + + // First overwrite by the current input parameters, + // to make sure _all_ inputs have a proper value + // (not just those that existed before). + // + // We preserve the modulation history in the next step. + // This is also to make sure that new input ports + // have a proper value too. + self.prog.initialize_input_buffers(); + + if copy_old_out { + // XXX: The following is commented out, because presisting + // the output proc buffers does not make sense anymore. + // Because we don't allow cycles, so there is no + // way that a node can read from the previous + // iteration anyways. + // + // // Swap the old out buffers into the new NodeProg + // // TODO: If we toss away most of the buffers anyways, + // // we could optimize this step with more + // // intelligence in the matrix compiler. + // for (old_pb, new_pb) in + // prev_prog.out.iter_mut().zip( + // self.prog.out.iter_mut()) + // { + // std::mem::swap(old_pb, new_pb); + // } + + // Then overwrite the inputs by the more current previous + // input processing buffers, so we keep any modulation + // (smoothed) history of the block too. + self.prog.swap_previous_outputs(&mut prev_prog); + } + + self.prog.assign_outputs(); + + let _ = + self.shared.graph_drop_prod.push( + DropMsg::Prog { prog: prev_prog }); + }, + } + } + } + + pub fn set_sample_rate(&mut self, sample_rate: f32) { + self.sample_rate = sample_rate; + for n in self.nodes.iter_mut() { + n.set_sample_rate(sample_rate); + } + + for sm in self.smoothers.iter_mut() { + sm.1.set_sample_rate(sample_rate); + } + } + + #[inline] + pub fn get_nodes(&self) -> &Vec { &self.nodes } + + #[inline] + pub fn get_prog(&self) -> &NodeProg { &self.prog } + + #[inline] + fn set_param(&mut self, input_idx: usize, value: f32) { + let prog = &mut self.prog; + + if input_idx >= prog.params.len() { + return; + } + + // First check if we already have a running smoother for this param: + for (sm_inp_idx, smoother) in + self.smoothers + .iter_mut() + .filter(|s| !s.1.is_done()) + { + if *sm_inp_idx == input_idx { + smoother.set(prog.params[input_idx], value); + //d// println!("RE-SET SMOOTHER {} {:6.3} (old = {:6.3})", + //d// input_idx, value, prog.params[input_idx]); + return; + } + } + + // Find unused smoother and set it: + if let Some(sm) = + self.smoothers + .iter_mut() + .filter(|s| s.1.is_done()) + .next() + { + sm.0 = input_idx; + sm.1.set(prog.params[input_idx], value); + //d// println!("SET SMOOTHER {} {:6.3} (old = {:6.3})", + //d// input_idx, value, prog.params[input_idx]); + } + } + + #[inline] + fn process_smoothers(&mut self, nframes: usize) { + let prog = &mut self.prog; + + while let Some((idx, v)) = self.target_refresh.pop() { + prog.inp[idx].fill(v); + } + + for (idx, smoother) in + self.smoothers + .iter_mut() + .filter(|s| + !s.1.is_done()) + { + + let inp = &mut prog.inp[*idx]; + let mut last_v = 0.0; + + for frame in 0..nframes { + let v = smoother.next(); + + inp.write(frame, v); + last_v = v; + } + + prog.params[*idx] = last_v; + self.target_refresh.push((*idx, last_v)); + } + + } + + #[inline] + pub fn process_param_updates(&mut self, nframes: usize) { + while let Some(upd) = self.shared.quick_update_con.pop() { + match upd { + QuickMessage::AtomUpdate { at_idx, value } => { + let prog = &mut self.prog; + let garbage = + std::mem::replace( + &mut prog.atoms[at_idx], + value); + + let _ = + self.shared.graph_drop_prod.push( + DropMsg::Atom { atom: garbage }); + }, + QuickMessage::ParamUpdate { input_idx, value } => { + self.set_param(input_idx, value); + }, + QuickMessage::SetMonitor { bufs } => { + self.monitor_signal_cur_inp_indices = bufs; + }, + } + } + + self.process_smoothers(nframes); + } + + #[inline] + pub fn process(&mut self, ctx: &mut T) { + // let tb = std::time::Instant::now(); + + self.process_param_updates(ctx.nframes()); + + let nodes = &mut self.nodes; + let ctx_vals = &mut self.shared.node_ctx_values; + let prog = &mut self.prog; + + let prog_out_fb = prog.out_feedback.input_buffer(); + + for op in prog.prog.iter() { + let out = op.out_idxlen; + let inp = op.in_idxlen; + let at = op.at_idxlen; + + let ctx_idx = op.idx as usize * 2; + + nodes[op.idx as usize] + .process( + ctx, + &prog.atoms[at.0..at.1], + &prog.inp[inp.0..inp.1], + &prog.cur_inp[inp.0..inp.1], + &mut prog.out[out.0..out.1], + &ctx_vals[ctx_idx..ctx_idx + 2]); + + let last_frame_idx = ctx.nframes() - 1; + for (pb, out_buf_idx) in + prog.out[out.0..out.1].iter() + .zip(out.0..out.1) + { + prog_out_fb[out_buf_idx] = pb.read(last_frame_idx); + } + } + + prog.out_feedback.publish(); + + self.shared.monitor_backend.check_recycle(); + + // let ta = std::time::Instant::now(); + + for (i, idx) in self.monitor_signal_cur_inp_indices.iter().enumerate() { + if *idx == UNUSED_MONITOR_IDX { + continue; + } + + if let Some(mut mon) = self.shared.monitor_backend.get_unused_mon_buf() { + if i > 2 { + mon.feed(i, ctx.nframes(), &prog.out[*idx]); + } else { + mon.feed(i, ctx.nframes(), &prog.cur_inp[*idx]); + } + + self.shared.monitor_backend.send_mon_buf(mon); + } + } + + // let ta = std::time::Instant::now().duration_since(ta); + // let tb = std::time::Instant::now().duration_since(tb); + // println!("ta Elapsed: {:?}", ta); + // println!("tb Elapsed: {:?}", tb); + } + + /// This is a convenience function used for testing + /// the DSP graph output in automated tests for this crate. + /// + /// The sample rate that is used to run the DSP code is 44100 Hz. + /// + /// Relying on the behvaiour of this function for production code + /// is not it's intended usecase and changes might break your code. + /// + /// * `seconds`: The number of seconds to run the DSP thread for. + /// * `realtime`: If this is set, the function will sleep. + /// + /// You can use it's source as reference for your own audio + /// DSP thread processing function. + pub fn test_run(&mut self, seconds: f32, realtime: bool) -> (Vec, Vec) { + const SAMPLE_RATE : f32 = 44100.0; + self.set_sample_rate(SAMPLE_RATE); + self.process_graph_updates(); + + let mut nframes = (seconds * SAMPLE_RATE) as usize; + + let input = vec![0.0; nframes]; + let mut output_l = vec![0.0; nframes]; + let mut output_r = vec![0.0; nframes]; + + for i in 0..nframes { + output_l[i] = 0.0; + output_r[i] = 0.0; + } + let mut offs = 0; + while nframes > 0 { + let cur_nframes = + if nframes >= crate::dsp::MAX_BLOCK_SIZE { + crate::dsp::MAX_BLOCK_SIZE + } else { + nframes + }; + nframes -= cur_nframes; + + let mut context = crate::Context { + nframes: cur_nframes, + output: &mut [&mut output_l[offs..(offs + cur_nframes)], + &mut output_r[offs..(offs + cur_nframes)]], + input: &[&input[offs..(offs + cur_nframes)]], + }; + + self.process(&mut context); + + if realtime { + let micros = + ((crate::dsp::MAX_BLOCK_SIZE as u64) * 1000000) + / (SAMPLE_RATE as u64); + std::thread::sleep( + std::time::Duration::from_micros(micros)); + } + + offs += cur_nframes; + } + + (output_l, output_r) + } + +} diff --git a/src/nodes/node_graph_ordering.rs b/src/nodes/node_graph_ordering.rs new file mode 100644 index 0000000..4d10a99 --- /dev/null +++ b/src/nodes/node_graph_ordering.rs @@ -0,0 +1,353 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. +use crate::dsp::NodeId; +use crate::nodes::MAX_ALLOCATED_NODES; +use std::collections::HashMap; +use std::collections::HashSet; + +pub const MAX_NODE_EDGES : usize = 64; +pub const UNUSED_NODE_EDGE : usize = 999999; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Node { + /// The [NodeId] of this node. + node_id: NodeId, + /// The output edges of this node. + edges: [usize; MAX_NODE_EDGES], + /// The first unused index in the `edges` array. + unused_idx: usize, +} + +impl Node { + pub fn new() -> Self { + Self { + node_id: NodeId::Nop, + edges: [UNUSED_NODE_EDGE; MAX_NODE_EDGES], + unused_idx: 0, + } + } + + pub fn clear(&mut self) { + self.node_id = NodeId::Nop; + self.edges = [UNUSED_NODE_EDGE; MAX_NODE_EDGES]; + self.unused_idx = 0; + } + + pub fn add_edge(&mut self, node_index: usize) { + for ni in self.edges.iter().take(self.unused_idx) { + if *ni == node_index { + return; + } + } + + self.edges[self.unused_idx] = node_index; + self.unused_idx += 1; + } +} + +#[derive(Debug, Clone)] +pub struct NodeGraphOrdering { + node2idx: HashMap, + node_count: usize, + nodes: [Node; MAX_ALLOCATED_NODES], + + in_degree: [usize; MAX_ALLOCATED_NODES], +} + +impl NodeGraphOrdering { + pub fn new() -> Self { + Self { + node2idx: HashMap::new(), + node_count: 0, + nodes: [Node::new(); MAX_ALLOCATED_NODES], + in_degree: [0; MAX_ALLOCATED_NODES], + } + } + + pub fn clear(&mut self) { + self.node2idx.clear(); + self.node_count = 0; + } + + pub fn add_node(&mut self, node_id: NodeId) -> usize { + if let Some(idx) = self.node2idx.get(&node_id) { + *idx + + } else { + let idx = self.node_count; + self.node_count += 1; + + self.nodes[idx].clear(); + self.nodes[idx].node_id = node_id; + self.node2idx.insert(node_id, idx); + + idx + } + } + + fn get_node(&self, node_id: NodeId) -> Option<&Node> { + let idx = *self.node2idx.get(&node_id)?; + Some(&self.nodes[idx]) + } + + fn get_node_mut(&mut self, node_id: NodeId) -> Option<&mut Node> { + let idx = *self.node2idx.get(&node_id)?; + Some(&mut self.nodes[idx]) + } + + pub fn add_edge(&mut self, from_node_id: NodeId, to_node_id: NodeId) { + let to_idx = self.add_node(to_node_id); + + if let Some(from_node) = self.get_node_mut(from_node_id) { + from_node.add_edge(to_idx); + } + } + + pub fn has_path(&self, from_node_id: NodeId, to_node_id: NodeId) -> Option { + let mut visited_set : HashSet = + HashSet::with_capacity(MAX_ALLOCATED_NODES); + + let mut node_stack = Vec::with_capacity(MAX_ALLOCATED_NODES); + node_stack.push(from_node_id); + + while let Some(node_id) = node_stack.pop() { + if visited_set.contains(&node_id) { + return None; + } else { + visited_set.insert(node_id); + } + + if node_id == to_node_id { + return Some(true); + } + + if let Some(node) = self.get_node(node_id) { + for node_idx in node.edges.iter().take(node.unused_idx) { + node_stack.push(self.nodes[*node_idx].node_id); + } + } + } + + return Some(false); + } + + /// Run Kahn's Algorithm to find the node order for the directed + /// graph. `out` will contain the order the nodes should be + /// executed in. If `false` is returned, the graph contains cycles + /// and no proper order can be computed. `out` will be cleared + /// in this case. + pub fn calculate_order(&mut self, out: &mut Vec) -> bool { + let mut deq = + std::collections::VecDeque::with_capacity(MAX_ALLOCATED_NODES); + + for indeg in self.in_degree.iter_mut() { + *indeg = 0; + } + + for node in self.nodes.iter().take(self.node_count) { + for out_node_idx in node.edges.iter().take(node.unused_idx) { + self.in_degree[*out_node_idx] += 1; + } + } + + for idx in 0..self.node_count { + if self.in_degree[idx] == 0 { + deq.push_back(idx); + } + } + + let mut visited_count = 0; + + while let Some(node_idx) = deq.pop_front() { + visited_count += 1; + + let node = &self.nodes[node_idx]; + + out.push(node.node_id); + + for neigh_node_idx in node.edges.iter().take(node.unused_idx) { + self.in_degree[*neigh_node_idx] -= 1; + + if self.in_degree[*neigh_node_idx] == 0 { + deq.push_back(*neigh_node_idx); + } + } + } + + if visited_count != self.node_count { + out.clear(); + false + } else { + true + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_ngraph_dfs_1() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + ng.add_node(NodeId::Sin(0)); + + ng.add_edge(NodeId::Sin(2), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(0), NodeId::Sin(1)); + + assert!(ng.has_path(NodeId::Sin(2), NodeId::Sin(1)).unwrap()); + assert!(ng.has_path(NodeId::Sin(2), NodeId::Sin(0)).unwrap()); + assert!(ng.has_path(NodeId::Sin(0), NodeId::Sin(1)).unwrap()); + assert!(!ng.has_path(NodeId::Sin(1), NodeId::Sin(0)).unwrap()); + assert!(!ng.has_path(NodeId::Sin(0), NodeId::Sin(2)).unwrap()); + assert!(!ng.has_path(NodeId::Amp(0), NodeId::Out(2)).unwrap()); + } + + #[test] + fn check_ngraph_order_1() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + + ng.add_edge(NodeId::Sin(2), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(0), NodeId::Sin(1)); + + let mut out = vec![]; + assert!(ng.calculate_order(&mut out)); + assert_eq!(out[..], [NodeId::Sin(2), NodeId::Sin(0), NodeId::Sin(1)]); + } + + #[test] + fn check_ngraph_order_2() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + ng.add_node(NodeId::Out(0)); + ng.add_node(NodeId::Amp(0)); + ng.add_node(NodeId::Amp(1)); + + ng.add_edge(NodeId::Sin(2), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(0), NodeId::Sin(1)); + + let mut out = vec![]; + assert!(ng.calculate_order(&mut out)); + assert_eq!(out[..], [ + NodeId::Sin(2), + NodeId::Out(0), + NodeId::Amp(0), + NodeId::Amp(1), + NodeId::Sin(0), + NodeId::Sin(1) + ]); + } + + #[test] + fn check_ngraph_order_3() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + ng.add_node(NodeId::Out(0)); + ng.add_node(NodeId::Amp(0)); + ng.add_node(NodeId::Amp(1)); + + /* + amp0 => sin0 + sin2 => sin0 => sin1 => out0 + => amp1 => sin0 + */ + + ng.add_edge(NodeId::Sin(2), NodeId::Sin(0)); + ng.add_edge(NodeId::Amp(0), NodeId::Sin(0)); + ng.add_edge(NodeId::Amp(1), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(2), NodeId::Amp(1)); + + ng.add_edge(NodeId::Sin(0), NodeId::Sin(1)); + ng.add_edge(NodeId::Sin(1), NodeId::Out(0)); + + let mut out = vec![]; + assert!(ng.calculate_order(&mut out)); + assert_eq!(out[..], [ + NodeId::Sin(2), + NodeId::Amp(0), + NodeId::Amp(1), + NodeId::Sin(0), + NodeId::Sin(1), + NodeId::Out(0), + ]); + } + + #[test] + fn check_ngraph_order_4() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + ng.add_node(NodeId::Out(0)); + ng.add_node(NodeId::Amp(0)); + ng.add_node(NodeId::Amp(1)); + + /* + amp1 => amp0 => sin0 + sin2 => sin1 => out0 + */ + + ng.add_edge(NodeId::Amp(1), NodeId::Amp(0)); + ng.add_edge(NodeId::Amp(0), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(2), NodeId::Sin(1)); + ng.add_edge(NodeId::Sin(1), NodeId::Out(0)); + + let mut out = vec![]; + assert!(ng.calculate_order(&mut out)); + assert_eq!(out[..], [ + NodeId::Sin(2), + NodeId::Amp(1), + NodeId::Sin(1), + NodeId::Amp(0), + NodeId::Out(0), + NodeId::Sin(0), + ]); + } + + #[test] + fn check_ngraph_dfs_cycle_2() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + + ng.add_edge(NodeId::Sin(2), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(0), NodeId::Sin(1)); + ng.add_edge(NodeId::Sin(0), NodeId::Sin(2)); + + assert!( + ng.has_path(NodeId::Sin(2), NodeId::Sin(1)) + .is_none()); + + let mut out = vec![]; + assert!(!ng.calculate_order(&mut out)); + } + + #[test] + fn check_ngraph_clear() { + let mut ng = NodeGraphOrdering::new(); + ng.add_node(NodeId::Sin(2)); + ng.add_node(NodeId::Sin(1)); + ng.add_node(NodeId::Sin(0)); + + ng.add_edge(NodeId::Sin(2), NodeId::Sin(0)); + ng.add_edge(NodeId::Sin(0), NodeId::Sin(1)); + + assert!(ng.has_path(NodeId::Sin(2), NodeId::Sin(1)).unwrap()); + + ng.clear(); + + assert!(!ng.has_path(NodeId::Sin(2), NodeId::Sin(1)).unwrap()); + } +} diff --git a/src/nodes/node_prog.rs b/src/nodes/node_prog.rs new file mode 100644 index 0000000..f4c4631 --- /dev/null +++ b/src/nodes/node_prog.rs @@ -0,0 +1,274 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::dsp::{ProcBuf, SAtom}; +use triple_buffer::{Input, Output, TripleBuffer}; + +/// Step in a `NodeProg` that stores the to be +/// executed node and output operations. +#[derive(Debug, Clone)] +pub struct NodeOp { + /// Stores the index of the node + pub idx: u8, + /// Output index and length of the node: + pub out_idxlen: (usize, usize), + /// Input index and length of the node: + pub in_idxlen: (usize, usize), + /// Atom data index and length of the node: + pub at_idxlen: (usize, usize), + /// Input indices, (, ) + pub inputs: Vec<(usize, usize)>, +} + +impl std::fmt::Display for NodeOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Op(i={} out=({}-{}) in=({}-{}) at=({}-{})", + self.idx, + self.out_idxlen.0, + self.out_idxlen.1, + self.in_idxlen.0, + self.in_idxlen.1, + self.at_idxlen.0, + self.at_idxlen.1)?; + + for i in self.inputs.iter() { + write!(f, " cpy=(o{} => i{})", i.0, i.1)?; + } + + write!(f, ")") + } +} + +/// A node graph execution program. It comes with buffers +/// for the inputs, outputs and node parameters (knob values). +#[derive(Debug)] +pub struct NodeProg { + /// The input vector stores the smoothed values of the params. + /// It is not used directly, but will be merged into the `cur_inp` + /// field together with the assigned outputs. + pub inp: Vec, + + /// The temporary input vector that is initialized from `inp` + /// and is then merged with the associated outputs. + pub cur_inp: Vec, + + /// The output vector, holding all the node outputs. + pub out: Vec, + + /// The param vector, holding all parameter inputs of the + /// nodes, such as knob settings. + pub params: Vec, + + /// The atom vector, holding all non automatable parameter inputs + /// of the nodes, such as samples or integer settings. + pub atoms: Vec, + + /// The node operations that are executed in the order they appear in this + /// vector. + pub prog: Vec, + + /// A marker, that checks if we can still swap buffers with + /// with other NodeProg instances. This is usally set if the ProcBuf pointers + /// have been copied into `cur_inp`. You can call `unlock_buffers` to + /// clear `locked_buffers`: + pub locked_buffers: bool, + + /// Holds the input end of a triple buffer that is used + /// to publish the most recent output values to the frontend. + pub out_feedback: Input>, + + /// Temporary hold for the producer for the `out_feedback`: + pub out_fb_cons: Option>>, +} + +impl Drop for NodeProg { + fn drop(&mut self) { + for buf in self.inp.iter_mut() { + buf.free(); + } + + for buf in self.out.iter_mut() { + buf.free(); + } + } +} + + +impl NodeProg { + pub fn empty() -> Self { + let out_fb = vec![]; + let tb = TripleBuffer::new(out_fb); + let (input_fb, output_fb) = tb.split(); + Self { + out: vec![], + inp: vec![], + cur_inp: vec![], + params: vec![], + atoms: vec![], + prog: vec![], + out_feedback: input_fb, + out_fb_cons: Some(output_fb), + locked_buffers: false, + } + } + + pub fn new(out_len: usize, inp_len: usize, at_len: usize) -> Self { + let mut out = vec![]; + out.resize_with(out_len, || ProcBuf::new()); + + let out_fb = vec![0.0; out_len]; + let tb = TripleBuffer::new(out_fb); + let (input_fb, output_fb) = tb.split(); + + let mut inp = vec![]; + inp.resize_with(inp_len, || ProcBuf::new()); + let mut cur_inp = vec![]; + cur_inp.resize_with(inp_len, || ProcBuf::null()); + + let mut params = vec![]; + params.resize(inp_len, 0.0); + let mut atoms = vec![]; + atoms.resize(at_len, SAtom::setting(0)); + + Self { + out, + inp, + cur_inp, + params, + atoms, + prog: vec![], + out_feedback: input_fb, + out_fb_cons: Some(output_fb), + locked_buffers: false, + } + } + + pub fn take_feedback_consumer(&mut self) -> Option>> { + self.out_fb_cons.take() + } + + pub fn params_mut(&mut self) -> &mut [f32] { + &mut self.params + } + + pub fn atoms_mut(&mut self) -> &mut [SAtom] { + &mut self.atoms + } + + pub fn append_op(&mut self, node_op: NodeOp) { + for n_op in self.prog.iter_mut() { + if n_op.idx == node_op.idx { + return; + } + } + + self.prog.push(node_op); + } + + pub fn append_edge( + &mut self, + node_op: NodeOp, + inp_index: usize, + out_index: usize) + { + for n_op in self.prog.iter_mut() { + if n_op.idx == node_op.idx { + n_op.inputs.push((out_index, inp_index)); + return; + } + } + } + + pub fn append_with_inputs( + &mut self, + mut node_op: NodeOp, + inp1: Option<(usize, usize)>, + inp2: Option<(usize, usize)>, + inp3: Option<(usize, usize)>) + { + for n_op in self.prog.iter_mut() { + if n_op.idx == node_op.idx { + if let Some(inp1) = inp1 { n_op.inputs.push(inp1); } + if let Some(inp2) = inp2 { n_op.inputs.push(inp2); } + if let Some(inp3) = inp3 { n_op.inputs.push(inp3); } + return; + } + } + + if let Some(inp1) = inp1 { node_op.inputs.push(inp1); } + if let Some(inp2) = inp2 { node_op.inputs.push(inp2); } + if let Some(inp3) = inp3 { node_op.inputs.push(inp3); } + self.prog.push(node_op); + } + + pub fn initialize_input_buffers(&mut self) { + for param_idx in 0..self.params.len() { + let param_val = self.params[param_idx]; + self.inp[param_idx].fill(param_val); + } + } + + pub fn swap_previous_outputs(&mut self, prev_prog: &mut NodeProg) { + if self.locked_buffers { + self.unlock_buffers(); + } + + if prev_prog.locked_buffers { + prev_prog.unlock_buffers(); + } + + // XXX: Swapping is now safe, because the `cur_inp` field + // no longer references to the buffers in `inp` or `out`. + for (old_inp_pb, new_inp_pb) in + prev_prog.inp.iter_mut().zip( + self.inp.iter_mut()) + { + std::mem::swap(old_inp_pb, new_inp_pb); + } + } + + pub fn unlock_buffers(&mut self) { + for buf in self.cur_inp.iter_mut() { + *buf = ProcBuf::null(); + } + self.locked_buffers = false; + } + + pub fn assign_outputs(&mut self) { + for op in self.prog.iter() { + + // First step is copying the ProcBufs to the `cur_inp` current + // input buffer vector. It holds the data for smoothed paramter + // inputs or just constant values since the last smoothing. + // + // Next we overwrite the input ProcBufs which have an + // assigned output buffer. + // + // ProcBuf has a raw pointer inside, and this copying + // is therefor very fast. + // + // XXX: This requires, that the graph is not cyclic, + // because otherwise we would write output buffers which + // are already accessed in the current iteration. + // This might lead to unexpected effects inside the process() + // call of the nodes. + let input_bufs = &mut self.cur_inp; + let out_bufs = &mut self.out; + + let inp = op.in_idxlen; + + // First step (refresh inputs): + input_bufs[inp.0..inp.1] + .copy_from_slice(&self.inp[inp.0..inp.1]); + + // Second step (assign outputs): + for io in op.inputs.iter() { + input_bufs[io.1] = out_bufs[io.0]; + } + } + + self.locked_buffers = true; + } +} + diff --git a/src/nodes/visual_sampling_filter.rs b/src/nodes/visual_sampling_filter.rs new file mode 100644 index 0000000..5c656fe --- /dev/null +++ b/src/nodes/visual_sampling_filter.rs @@ -0,0 +1,95 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +const VALUE_SAMPLING_FILTER_SIZE : usize = 10; + +/// Accumulates the values for a single visible feedback value, +/// like an LED ([crate::Matrix::led_value_for]) or the +/// output feedbacks [crate::Matrix::out_fb_for] from the [crate::Matrix]. +#[derive(Debug, Clone, Copy)] +pub struct VisualSamplingFilter { + /// Holds a state bit, that is used to check if this + /// filter needs to recalculate or not. + recalc_state: bool, + + /// Current write head into the sample buffer. + write_ptr: usize, + + /// Holds a set of the most recent samples to calculate + /// the output. + sample_buffer: [f32; VALUE_SAMPLING_FILTER_SIZE], + + /// Holds the last output, will only be recalculated + /// when necessary. + last_output: (f32, f32), +} + +impl VisualSamplingFilter { + pub fn new() -> Self { + Self { + recalc_state: false, + write_ptr: 0, + sample_buffer: [0.0; VALUE_SAMPLING_FILTER_SIZE], + last_output: (0.0, 0.0), + } + } + + /// Used to check if we need to update this filter. + #[inline] + fn needs_recalc(&mut self, recalc_value: bool) -> bool { + if self.recalc_state != recalc_value { + self.recalc_state = recalc_value; + true + } else { + false + } + } + + /// Retrieves the current output value of the filter. + /// Negate the input for `recalc_value` one each frame, + /// to reduce access to the `retrieve_fn` to be done only + /// once per frame and per [VisualSamplingFilter]. + /// + ///``` + /// use hexodsp::nodes::visual_sampling_filter::*; + /// + /// let mut vsf = VisualSamplingFilter::new(); + /// + /// let inputs = [-0.87, -0.8, 0.2, 0.75, 0.5, 0.0, 0.22]; + /// let mut recalc = true; + /// + /// let mut last_output = (0.0, 0.0); + /// for ip in inputs { + /// last_output = vsf.get(recalc, ip); + /// recalc = !recalc; + /// } + /// + /// assert_eq!(last_output, (0.87, 0.75)); + ///``` + pub fn get(&mut self, recalc_value: bool, sample: f32) -> (f32, f32) { + if self.needs_recalc(recalc_value) { + let write_ptr = + (self.write_ptr + 1) % self.sample_buffer.len(); + self.write_ptr = write_ptr; + + self.sample_buffer[write_ptr] = sample; + + let mut neg_max : f32 = 0.0; + let mut pos_max : f32 = 0.0; + + for v in self.sample_buffer.iter() { + if *v >= 0.0 { + pos_max = pos_max.max((*v).abs()); + } else { + neg_max = neg_max.max((*v).abs()); + } + } + + self.last_output = (neg_max, pos_max); + } + + self.last_output + } +} + diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6c56f1c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,162 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use std::sync::atomic::{AtomicU32, Ordering}; + +const SMOOTHING_TIME_MS : f32 = 10.0; + +pub struct Smoother { + slope_samples: usize, + value: f32, + inc: f32, + target: f32, + count: usize, + done: bool, +} + +impl Smoother { + pub fn new() -> Self { + Self { + slope_samples: 0, + value: 0.0, + inc: 0.0, + count: 0, + target: 0.0, + done: true, + } + } + + pub fn set_sample_rate(&mut self, sr: f32) { + self.slope_samples = ((sr * SMOOTHING_TIME_MS) / 1000.0).ceil() as usize; + } + + #[inline] + pub fn is_done(&self) -> bool { self.done } + + #[inline] + #[allow(dead_code)] + pub fn stop(&mut self) { self.done = true; } + + #[inline] + pub fn set(&mut self, current: f32, target: f32) { + self.value = current; + self.count = self.slope_samples; + self.inc = (target - current) / (self.count as f32); + self.target = target; + self.done = false; + } + + #[inline] + pub fn next(&mut self) -> f32 { + //d// println!("NEXT: count={}, value={:6.3} inc={:6.4}", + //d// self.count, + //d// self.value, + //d// self.inc); + if self.count == 0 { + self.done = true; + + self.target + } else { + self.value += self.inc; + self.count -= 1; + self.value + } + } +} + +pub struct PerfTimer { + lbl: &'static str, + i: std::time::Instant, + off: bool, + // let tb = std::time::Instant::now(); + // let ta = std::time::Instant::now().duration_since(ta); + // let tb = std::time::Instant::now().duration_since(tb); + // println!("ta Elapsed: {:?}", ta); +} + +impl PerfTimer { + #[inline] + pub fn off(mut self) -> Self { + self.off = true; + self + } + + + #[inline] + pub fn new(lbl: &'static str) -> Self { + Self { + lbl, + i: std::time::Instant::now(), + off: false, + } + } + + #[inline] + pub fn print(&mut self, lbl2: &str) { + if self.off { return; } + + let t = std::time::Instant::now().duration_since(self.i); + println!("*** PERF[{}/{}] {:?}", self.lbl, lbl2, t); + self.i = std::time::Instant::now(); + } +} + +// Implementation from vst-rs +// https://github.com/RustAudio/vst-rs/blob/master/src/util/atomic_float.rs +// Under MIT License +// Copyright (c) 2015 Marko Mijalkovic +pub struct AtomicFloat { + atomic: AtomicU32, +} + +impl AtomicFloat { + /// New atomic float with initial value `value`. + pub fn new(value: f32) -> AtomicFloat { + AtomicFloat { + atomic: AtomicU32::new(value.to_bits()), + } + } + + /// Get the current value of the atomic float. + #[inline] + pub fn get(&self) -> f32 { + f32::from_bits(self.atomic.load(Ordering::Relaxed)) + } + + /// Set the value of the atomic float to `value`. + #[inline] + pub fn set(&self, value: f32) { + self.atomic.store(value.to_bits(), Ordering::Relaxed) + } +} + +impl Default for AtomicFloat { + fn default() -> Self { + AtomicFloat::new(0.0) + } +} + +impl std::fmt::Debug for AtomicFloat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.get(), f) + } +} + +impl std::fmt::Display for AtomicFloat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.get(), f) + } +} + +impl From for AtomicFloat { + fn from(value: f32) -> Self { + AtomicFloat::new(value) + } +} + +impl From for f32 { + fn from(value: AtomicFloat) -> Self { + value.get() + } +} diff --git a/tests/basics.rs b/tests/basics.rs new file mode 100644 index 0000000..9cbfc32 --- /dev/null +++ b/tests/basics.rs @@ -0,0 +1,1107 @@ +use hexodsp::matrix::*; +use hexodsp::nodes::new_node_engine; +use hexodsp::dsp::*; + +use hound; +//use num_complex::Complex; +use microfft; + +macro_rules! assert_float_eq { + ($a:expr, $b:expr) => { + if ($a - $b).abs() > 0.0001 { + panic!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, $a, $b) + } + } +} + +const SAMPLE_RATE : f32 = 44100.0; + +fn save_wav(name: &str, buf: &[f32]) { + let spec = hound::WavSpec { + channels: 1, + sample_rate: SAMPLE_RATE as u32, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + let mut writer = hound::WavWriter::create(name, spec).unwrap(); + for s in buf.iter() { + let amp = i16::MAX as f32; + writer.write_sample((amp * s) as i16).unwrap(); + } +} + +fn run_no_input(node_exec: &mut hexodsp::nodes::NodeExecutor, seconds: f32) -> (Vec, Vec) { + run_realtime_no_input(node_exec, seconds, false) +} + +fn run_realtime_no_input(node_exec: &mut hexodsp::nodes::NodeExecutor, seconds: f32, sleep_a_bit: bool) -> (Vec, Vec) { + node_exec.test_run(seconds, sleep_a_bit) +} + +fn calc_rms_mimax_each_ms(buf: &[f32], ms: f32) -> Vec<(f32, f32, f32)> { + let ms_samples = ms * SAMPLE_RATE / 1000.0; + let len_ms = ms_samples as usize; + + let mut idx = 0; + let mut res = vec![]; + loop { + if (idx + len_ms) > buf.len() { + break; + } + + let mut max = -1000.0; + let mut min = 1000.0; + for s in buf[idx..(idx + len_ms)].iter() { + max = s.max(max); + min = s.min(min); + } + + let rms : f32 = + buf[idx..(idx + len_ms)] + .iter() + .map(|s: &f32| s * s).sum::() + / ms_samples; + + res.push((rms, min, max)); + + idx += len_ms; + } + + res +} + +fn run_and_undersample( + node_exec: &mut hexodsp::nodes::NodeExecutor, + run_len_ms: f32, samples: usize) -> Vec +{ + let (out_l, _out_r) = run_no_input(node_exec, run_len_ms / 1000.0); + + let sample_interval = out_l.len() / samples; + let mut out_samples = vec![]; + + for i in 0..samples { + let idx = i * sample_interval; + out_samples.push(out_l[idx]); + } + + out_samples +} + +fn run_and_get_l_rms_mimax( + node_exec: &mut hexodsp::nodes::NodeExecutor, + len_ms: f32) -> (f32, f32, f32) +{ + let (out_l, _out_r) = run_no_input(node_exec, (len_ms * 3.0) / 1000.0); + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], len_ms); + rms_mimax[1] +} + +fn run_and_get_fft4096( + node_exec: &mut hexodsp::nodes::NodeExecutor, + thres: u32, + offs_ms: f32) -> Vec<(u16, u32)> +{ + let min_samples_for_fft = 4096.0; + let offs_samples = (offs_ms * (SAMPLE_RATE / 1000.0)).ceil(); + let min_len_samples = + offs_samples + // 2.0 * for safety margin + + 2.0 * min_samples_for_fft; + let run_len_s = min_len_samples / SAMPLE_RATE; + let (mut out_l, _out_r) = run_no_input(node_exec, run_len_s); + fft_thres_at_ms(&mut out_l[..], FFT::F4096, thres, offs_ms) +} + +#[allow(unused)] +enum FFT { + F16, + F32, + F64, + F128, + F512, + F1024, + F2048, + F4096, +} + +fn fft_thres_at_ms(buf: &mut [f32], size: FFT, amp_thres: u32, ms_idx: f32) -> Vec<(u16, u32)> { + let ms_sample_offs = ms_idx * (SAMPLE_RATE / 1000.0); + let fft_nbins = match size { + FFT::F16 => 16, + FFT::F32 => 32, + FFT::F64 => 64, + FFT::F128 => 128, + FFT::F512 => 512, + FFT::F1024 => 1024, + FFT::F2048 => 2048, + FFT::F4096 => 4096, + }; + let len = fft_nbins; + + let idx = ms_sample_offs as usize; + let mut res = vec![]; + + if (idx + len) > buf.len() { + return res; + } + + // Hann window: + for (i, s) in buf[idx..(idx + len)].iter_mut().enumerate() { + let w = + 0.5 + * (1.0 + - ((2.0 * std::f32::consts::PI * i as f32) + / (fft_nbins as f32 - 1.0)) + .cos()); + *s *= w; + } + + let spec = + match size { + FFT::F16 => + microfft::real::rfft_16(&mut buf[idx..(idx + len)]), + FFT::F32 => + microfft::real::rfft_32(&mut buf[idx..(idx + len)]), + FFT::F64 => + microfft::real::rfft_64(&mut buf[idx..(idx + len)]), + FFT::F128 => + microfft::real::rfft_128(&mut buf[idx..(idx + len)]), + FFT::F512 => + microfft::real::rfft_512(&mut buf[idx..(idx + len)]), + FFT::F1024 => + microfft::real::rfft_1024(&mut buf[idx..(idx + len)]), + FFT::F2048 => + microfft::real::rfft_2048(&mut buf[idx..(idx + len)]), + FFT::F4096 => + microfft::real::rfft_4096(&mut buf[idx..(idx + len)]), + }; + let amplitudes: Vec<_> = spec.iter().map(|c| c.norm() as u32).collect(); + + for (i, amp) in amplitudes.iter().enumerate() { + if *amp >= amp_thres { + let freq = (i as f32 * SAMPLE_RATE) / fft_nbins as f32; + res.push((freq.round() as u16, *amp)); + } + } + + res +} + +#[test] +fn check_matrix_sine() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(2); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, sin.out("sig"), None)); + matrix.place(1, 0, Cell::empty(out) + .input(None, out.inp("ch1"), None)); + matrix.sync().unwrap(); + + let (mut out_l, out_r) = run_no_input(&mut node_exec, 4.0); + + let sum_l : f32 = out_l.iter().map(|v| v.abs()).sum(); + let sum_r : f32 = out_r.iter().map(|v| v.abs()).sum(); + assert_float_eq!(sum_l.floor(), 112301.0); + assert_float_eq!(sum_r, 0.0); + + save_wav("check_matrix_sine.wav", &out_l); + + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], 1000.0); + for i in 0..4 { + assert_float_eq!(rms_mimax[i].0, 0.5); + assert_float_eq!(rms_mimax[i].1, -0.9999999); + assert_float_eq!(rms_mimax[i].2, 0.9999999); + } + + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F1024, 100, 0.0); + assert_eq!(fft_res[0], (431, 248)); + assert_eq!(fft_res[1], (474, 169)); + + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F1024, 100, 1000.0); + assert_eq!(fft_res[0], (431, 248)); + assert_eq!(fft_res[1], (474, 169)); + + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F1024, 100, 1500.0); + assert_eq!(fft_res[0], (431, 248)); + assert_eq!(fft_res[1], (474, 169)); + + let sin_led_val = matrix.led_value_for(&sin); + let out_led_val = matrix.led_value_for(&out); + + assert_float_eq!(sin_led_val, -0.057622954); + assert_float_eq!(out_led_val, -0.057622954); +} + +#[test] +fn check_matrix_atom_set() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(2); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, sin.out("sig"), None)); + matrix.place(1, 0, Cell::empty(out) + .input(None, out.inp("ch1"), None)); + matrix.sync().unwrap(); + + let mono_param = out.inp_param("mono").unwrap(); + + matrix.set_param(mono_param, SAtom::setting(1)); + + let (out_l, out_r) = run_no_input(&mut node_exec, 4.0); + + let sum_l : f32 = out_l.iter().map(|v| v.abs()).sum(); + let sum_r : f32 = out_r.iter().map(|v| v.abs()).sum(); + assert_float_eq!(sum_l.floor(), 112301.0); + assert_float_eq!(sum_r.floor(), 112301.0); + + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], 1000.0); + for i in 0..4 { + assert_float_eq!(rms_mimax[i].0, 0.5); + assert_float_eq!(rms_mimax[i].1, -0.9999999); + assert_float_eq!(rms_mimax[i].2, 0.9999999); + } + + let rms_mimax = calc_rms_mimax_each_ms(&out_r[..], 1000.0); + for i in 0..4 { + assert_float_eq!(rms_mimax[i].0, 0.5); + assert_float_eq!(rms_mimax[i].1, -0.9999999); + assert_float_eq!(rms_mimax[i].2, 0.9999999); + } +} + + +#[test] +fn check_sine_pitch_change() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(2); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, sin.out("sig"), None)); + matrix.place(1, 0, Cell::empty(out) + .input(None, out.inp("ch1"), None)); + matrix.sync().unwrap(); + + let (mut out_l, _out_r) = run_no_input(&mut node_exec, 0.2); + + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F1024, 200, 0.0); + assert_eq!(fft_res[0], (431, 248)); + + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F64, 20, 100.0); + assert_eq!(fft_res[0], (0, 22)); + + let freq_param = sin.inp_param("freq").unwrap(); + + matrix.set_param( + freq_param, + SAtom::param(freq_param.norm(4400.0))); + + let (mut out_l, _out_r) = run_no_input(&mut node_exec, 1.0); + + // Test at the start of the slope (~ 690 Hz): + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F64, 15, 0.0); + assert_eq!(fft_res[0], (0, 18)); + assert_eq!(fft_res[1], (689, 15)); + + // In the middle (~ 2067 Hz): + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F64, 10, 5.0); + assert_eq!(fft_res[0], (1378, 14)); + assert_eq!(fft_res[1], (2067, 12)); + + // Goal (~ 4134 Hz) + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F64, 14, 10.0); + assert_eq!(fft_res[0], (4134, 14)); + + // Test the freq after the slope in high res (closer to 4400 Hz): + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F1024, 200, 400.0); + assert_eq!(fft_res[0], (4393, 251)); +} + +#[test] +fn check_matrix_monitor() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(2); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .input(sin.inp("freq"), sin.inp("freq"), sin.inp("freq")) + .out(sin.out("sig"), sin.out("sig"), sin.out("sig"))); + matrix.place(1, 0, Cell::empty(out) + .input(None, out.inp("ch1"), None)); + matrix.sync().unwrap(); + + // Go to 220Hz + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.1)); + + matrix.monitor_cell(*matrix.get(0, 0).unwrap()); + + let (mut out_l, _out_r) = + run_realtime_no_input(&mut node_exec, 0.2, true); + + // Give the MonitorProcessor some time to work on the buffers. + std::thread::sleep(std::time::Duration::from_millis(100)); + +//assert!(false); + for i in 0..3 { + let sl = matrix.get_minmax_monitor_samples(i); + //d// println!("SL={:?}", sl); + //d// println!("=> {}", i); + + assert_eq!((sl[sl.len() - 1].0 * 10000.0) as i64, -1000); + assert_eq!((sl[sl.len() - 1].1 * 10000.0) as i64, -1000); + assert_eq!((sl[sl.len() - 11].0 * 10000.0) as i64, -1000); + // Here we see that the paramter is smoothed in: + assert_eq!((sl[sl.len() - 11].1 * 10000.0) as i64, -2); + assert_eq!((sl[sl.len() - 12].0 * 10000.0) as i64, 0); + assert_eq!((sl[sl.len() - 12].1 * 10000.0) as i64, 0); + } + + for i in 3..6 { + let sl = matrix.get_minmax_monitor_samples(i); + //d// println!("SL={:?}", sl); + //d// println!("=> {}", i); + + assert_eq!((sl[sl.len() - 1].0 * 10000.0) as i64, -9999); + assert_eq!((sl[sl.len() - 1].1 * 10000.0) as i64, 9999); + assert_eq!((sl[sl.len() - 11].0 * 10000.0) as i64, -9999); + assert_eq!((sl[sl.len() - 11].1 * 10000.0) as i64, 9999); + assert_eq!((sl[sl.len() - 12].0 * 10000.0) as i64, 0); + assert_eq!((sl[sl.len() - 12].1 * 10000.0) as i64, 0); + } + + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], 50.0); + assert_float_eq!(rms_mimax[0].0, 0.5013241); + + // let ta = std::time::Instant::now(); + + // Test the freq after the slope in high res (closer to 4400 Hz): + let fft_res = fft_thres_at_ms(&mut out_l[..], FFT::F1024, 200, 50.0); + + // let ta = std::time::Instant::now().duration_since(ta); + // println!("ta Elapsed: {:?}", ta); + // assert!(false); + + // 220Hz is one Octave below 440Hz + assert_eq!(fft_res[0], (215, 253)); + +} + +#[test] +fn check_matrix_monitor_bug_1() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let amp = NodeId::Amp(1); + matrix.place(0, 0, Cell::empty(sin) + .out(None, sin.out("sig"), None)); + matrix.place(1, 0, Cell::empty(amp) + .out(None, None, amp.out("sig")) + .input(None, amp.inp("inp"), None)); + matrix.sync().unwrap(); + + matrix.monitor_cell(*matrix.get(1, 0).unwrap()); + + let (_out_l, _out_r) = + run_realtime_no_input(&mut node_exec, 0.2, true); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + for i in [0, 2, 3, 4].iter() { + let sl = matrix.get_minmax_monitor_samples(*i); + assert_eq!((sl[sl.len() - 1].0 * 10000.0) as i64, 0); + assert_eq!((sl[sl.len() - 1].1 * 10000.0) as i64, 0); + } + + for i in [1, 5].iter() { + let sl = matrix.get_minmax_monitor_samples(*i); + assert_eq!((sl[sl.len() - 1].0 * 10000.0) as i64, -9999); + assert_eq!((sl[sl.len() - 1].1 * 10000.0) as i64, 9999); + } +} + +#[test] +fn check_matrix_out_config_bug1() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + matrix.place(0, 0, Cell::empty(NodeId::Sin(0)) + .out(None, Some(0), None)); + matrix.place(1, 0, Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + + matrix.place(0, 1, Cell::empty(NodeId::Sin(1)) + .out(None, Some(0), None)); + matrix.place(1, 2, Cell::empty(NodeId::Sin(0)) + .input(None, Some(0), None) + .out(None, None, Some(0))); + matrix.place(1, 1, Cell::empty(NodeId::Out(0)) + .input(Some(1), Some(0), None) + .out(None, None, Some(0))); + + assert!(matrix.sync().is_err()); + + let (_out_l, _out_r) = run_no_input(&mut node_exec, 0.2); +} + +#[test] +fn check_matrix_out_config_bug1_reduced() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + matrix.place(1, 0, Cell::empty(NodeId::Out(0)) + .input(Some(0), None, None) + .out(None, None, Some(0))); + matrix.place(1, 2, Cell::empty(NodeId::Out(0)) + .input(Some(0), None, None) + .out(None, None, None)); + + matrix.sync().unwrap(); + + let (_out_l, _out_r) = run_no_input(&mut node_exec, 0.2); +} + +#[test] +fn check_matrix_out_config_bug1b_reduced() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + matrix.place(1, 0, Cell::empty(NodeId::Out(0)) + .out(None, None, Some(0))); + matrix.place(1, 1, Cell::empty(NodeId::Out(0)) + .input(Some(0), None, None)); + + assert!(matrix.sync().is_err()); + + let (_out_l, _out_r) = run_no_input(&mut node_exec, 0.2); +} + +#[test] +fn check_matrix_out_config_bug1c_reduced() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + matrix.place(1, 0, Cell::empty(NodeId::Sin(0)) + .out(None, None, Some(0))); + matrix.place(1, 1, Cell::empty(NodeId::Out(0)) + .input(Some(9), None, None)); + + matrix.sync().unwrap(); + + let (_out_l, _out_r) = run_no_input(&mut node_exec, 0.2); +} + +macro_rules! simple_sine_output_test { + ($matrix: ident, $block: tt) => { + let (node_conf, mut node_exec) = new_node_engine(); + let mut $matrix = Matrix::new(node_conf, 7, 7); + + $block; + + $matrix.sync().unwrap(); + + let (out_l, _out_r) = run_no_input(&mut node_exec, 0.2); + + let rms_mimax = calc_rms_mimax_each_ms(&out_l[..], 50.0); + assert_float_eq!(rms_mimax[0].0, 0.5); + assert_float_eq!(rms_mimax[0].1, -0.9999999); + assert_float_eq!(rms_mimax[0].2, 0.9999999); + } +} + +#[test] +fn check_matrix_connect_even_top_left() { + simple_sine_output_test!(matrix, { + matrix.place(1, 0, Cell::empty(NodeId::Sin(0)) + .out(None, Some(0), None)); + matrix.place(2, 1, Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None)); + }); +} + + +#[test] +fn check_matrix_connect_even_bottom_left() { + simple_sine_output_test!(matrix, { + matrix.place(1, 1, Cell::empty(NodeId::Sin(0)) + .out(Some(0), None, None)); + matrix.place(2, 1, Cell::empty(NodeId::Out(0)) + .input(None, None, Some(0))); + }); +} + +#[test] +fn check_matrix_connect_even_top() { + simple_sine_output_test!(matrix, { + matrix.place(0, 0, Cell::empty(NodeId::Sin(0)) + .out(None, None, Some(0))); + matrix.place(0, 1, Cell::empty(NodeId::Out(0)) + .input(Some(0), None, None)); + }); +} + +#[test] +fn check_matrix_connect_odd_top_left() { + simple_sine_output_test!(matrix, { + matrix.place(0, 0, Cell::empty(NodeId::Sin(0)) + .out(None, Some(0), None)); + matrix.place(1, 0, Cell::empty(NodeId::Out(0)) + .input(None, Some(0), None)); + }); +} + +#[test] +fn check_matrix_connect_odd_bottom_left() { + simple_sine_output_test!(matrix, { + matrix.place(0, 1, Cell::empty(NodeId::Sin(0)) + .out(Some(0), None, None)); + matrix.place(1, 0, Cell::empty(NodeId::Out(0)) + .input(None, None, Some(0))); + }); +} + +#[test] +fn check_matrix_connect_odd_top() { + simple_sine_output_test!(matrix, { + matrix.place(1, 0, Cell::empty(NodeId::Sin(0)) + .out(None, None, Some(0))); + matrix.place(1, 1, Cell::empty(NodeId::Out(0)) + .input(Some(0), None, None)); + }); +} + + +#[test] +fn check_matrix_adj_odd() { + let (node_conf, _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + /* + _____ + I2 / I1 \ O1 + / \ + \ / + I3 \_____/ O2 + O3 + + 0 1 2 3 + ___ ___ + 0/ \ ___ 0/ \ ___ + \___/0/S2 \ \___/0/ \ + ___ \___/ \___/ + 1/S1 \ ___ + \___/ ___ 1/S3 \ ___ + ___ 1/S0 \ \___/1/ \ + 2/S6 \ \___/ \___/ + \___/ ___ + ___ 2/S4 \ ___ + 2/S5 \ \___/2/ \ + \___/ \___/ + */ + + matrix.place(1, 1, Cell::empty(NodeId::Sin(0)) + .out(Some(0), Some(0), Some(0)) + .input(Some(0), Some(0), Some(0))); + + matrix.place(0, 1, Cell::empty(NodeId::Sin(1)) + .out(None, Some(0), None)); + matrix.place(1, 0, Cell::empty(NodeId::Sin(2)) + .out(None, None, Some(0))); + matrix.place(2, 1, Cell::empty(NodeId::Sin(3)) + .input(None, None, Some(0))); + matrix.place(2, 2, Cell::empty(NodeId::Sin(4)) + .input(None, Some(0), None)); + matrix.place(1, 2, Cell::empty(NodeId::Sin(5)) + .input(Some(0), None, None)); + matrix.place(0, 2, Cell::empty(NodeId::Sin(6)) + .out(Some(0), None, None)); + matrix.sync().unwrap(); + + assert_eq!( + matrix.get_adjacent(1, 1, CellDir::B).unwrap().node_id(), + NodeId::Sin(5)); + assert_eq!( + matrix.get_adjacent(1, 1, CellDir::BR).unwrap().node_id(), + NodeId::Sin(4)); + assert_eq!( + matrix.get_adjacent(1, 1, CellDir::TR).unwrap().node_id(), + NodeId::Sin(3)); + + assert_eq!( + matrix.get_adjacent(1, 1, CellDir::T).unwrap().node_id(), + NodeId::Sin(2)); + assert_eq!( + matrix.get_adjacent(1, 1, CellDir::TL).unwrap().node_id(), + NodeId::Sin(1)); + assert_eq!( + matrix.get_adjacent(1, 1, CellDir::BL).unwrap().node_id(), + NodeId::Sin(6)); +} + + +#[test] +fn check_matrix_adj_even() { + let (node_conf, _node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + /* + _____ + I2 / I1 \ O1 + / \ + \ / + I3 \_____/ O2 + O3 + + 0 1 2 3 + ___ ___ + 0/ \ ___ 0/S2 \ ___ + \___/0/S1 \ \___/0/S3 \ + ___ \___/ \___/ + 1/ \ ___ + \___/ ___ 1/S0 \ ___ + ___ 1/S6 \ \___/1/S4 \ + 2/ \ \___/ \___/ + \___/ ___ + ___ 2/S5 \ ___ + 2/ \ \___/2/ \ + \___/ \___/ + */ + + matrix.place(2, 1, Cell::empty(NodeId::Sin(0)) + .out(Some(0), Some(0), Some(0)) + .input(Some(0), Some(0), Some(0))); + + matrix.place(1, 0, Cell::empty(NodeId::Sin(1)) + .out(None, Some(0), None)); + matrix.place(2, 0, Cell::empty(NodeId::Sin(2)) + .out(None, None, Some(0))); + matrix.place(3, 0, Cell::empty(NodeId::Sin(3)) + .input(None, None, Some(0))); + matrix.place(3, 1, Cell::empty(NodeId::Sin(4)) + .input(None, Some(0), None)); + matrix.place(2, 2, Cell::empty(NodeId::Sin(5)) + .input(Some(0), None, None)); + matrix.place(1, 1, Cell::empty(NodeId::Sin(6)) + .out(Some(0), None, None)); + matrix.sync().unwrap(); + + assert_eq!( + matrix.get_adjacent(2, 1, CellDir::B).unwrap().node_id(), + NodeId::Sin(5)); + assert_eq!( + matrix.get_adjacent(2, 1, CellDir::BR).unwrap().node_id(), + NodeId::Sin(4)); + assert_eq!( + matrix.get_adjacent(2, 1, CellDir::TR).unwrap().node_id(), + NodeId::Sin(3)); + + assert_eq!( + matrix.get_adjacent(2, 1, CellDir::T).unwrap().node_id(), + NodeId::Sin(2)); + assert_eq!( + matrix.get_adjacent(2, 1, CellDir::TL).unwrap().node_id(), + NodeId::Sin(1)); + assert_eq!( + matrix.get_adjacent(2, 1, CellDir::BL).unwrap().node_id(), + NodeId::Sin(6)); +} + +#[test] +fn check_matrix_out_twice_assignment() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 7, 7); + + matrix.place(0, 0, Cell::empty(NodeId::Sin(0)) + .out(None, Some(0), None)); + matrix.place(0, 1, Cell::empty(NodeId::Sin(0)) + .out(Some(0), None, None)); + matrix.place(1, 0, Cell::empty(NodeId::Out(0)) + .input(None, Some(0), Some(0)) + .out(None, None, None)); + + matrix.sync().unwrap(); + + let (_out_l, _out_r) = run_no_input(&mut node_exec, 0.2); + +} + +#[test] +fn check_matrix_amp() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let amp = NodeId::Amp(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(amp) + .input(out.inp("ch1"), None, None) + .out(None, None, sin.out("sig"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let att_param = amp.inp_param("att").unwrap(); + matrix.set_param(att_param, SAtom::param(0.5)); + + let (rms, _, _) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); + assert_float_eq!(rms, 0.031249225); + + matrix.set_param(att_param, SAtom::param(1.0)); + let (rms, _, _) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); + assert_float_eq!(rms, 0.49998704); + + matrix.set_param(att_param, SAtom::param(0.0)); + let (rms, _, _) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); + assert_float_eq!(rms, 0.0); + + let gain_param = amp.inp_param("gain").unwrap(); + + matrix.set_param(att_param, SAtom::param(1.0)); + matrix.set_param(gain_param, SAtom::param(0.5)); + let (rms, min, max) = run_and_get_l_rms_mimax(&mut node_exec, 50.0); + assert_float_eq!(rms, 0.12499); + assert_float_eq!(min, -0.5); + assert_float_eq!(max, 0.5); +} + +#[test] +fn check_matrix_clear() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.2)); + + let fft = run_and_get_fft4096(&mut node_exec, 800, 0.0); + // slightly lower counts than later, because we have a slight + // frequency slope after setting the frequency to 110Hz + assert_eq!(fft[0], (108, 989)); + + let fft = run_and_get_fft4096(&mut node_exec, 800, 10.0); + assert_eq!(fft[0], (108, 993)); + + matrix.clear(); + + let fft = run_and_get_fft4096(&mut node_exec, 1, 50.0); + assert_eq!(fft.len(), 0); + + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let fft = run_and_get_fft4096(&mut node_exec, 800, 50.0); + assert_eq!(fft[0], (441, 1012)); +} + + +#[test] +fn check_matrix_serialize() { + { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.2)); + + let fft = run_and_get_fft4096(&mut node_exec, 800, 10.0); + assert_eq!(fft[0], (108, 993)); + + hexodsp::save_patch_to_file(&mut matrix, "check_matrix_serialize.hxy") + .unwrap(); + } + + { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + hexodsp::load_patch_from_file( + &mut matrix, "check_matrix_serialize.hxy").unwrap(); + + let fft = run_and_get_fft4096(&mut node_exec, 800, 10.0); + assert_eq!(fft[0], (108, 993)); + } +} + +#[test] +fn check_matrix_tseq() { + use hexodsp::dsp::tracker::UIPatternModel; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let tsq = NodeId::TSeq(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(tsq) + .input(tsq.inp("clock"), None, None) + .out(None, None, tsq.out("trk1"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.978)); + let cmode_param = tsq.inp_param("cmode").unwrap(); + matrix.set_param(cmode_param, SAtom::setting(1)); + + let pat = matrix.get_pattern_data(0).unwrap(); + { + let mut pr = pat.borrow_mut(); + pr.set_rows(16); + pr.set_cell_value(0, 0, 0xFFF); + pr.set_cell_value(15, 0, 0x000); + } + + for _ in 0..10 { + matrix.check_pattern_data(0); + } + + // We let the clock mode tune in: + run_and_undersample(&mut node_exec, 10000.0, 1); + + // Take some real samples: + let samples = run_and_undersample(&mut node_exec, 2000.0, 10); + + assert_float_eq!(samples[0], 0.3157); + assert_float_eq!(samples[1], 0.209); + assert_float_eq!(samples[2], 0.1024); + assert_float_eq!(samples[3], 0.0648); + assert_float_eq!(samples[4], 0.95566); + assert_float_eq!(samples[5], 0.84899); + assert_float_eq!(samples[6], 0.74231); + assert_float_eq!(samples[7], 0.6356); + assert_float_eq!(samples[8], 0.5289); + assert_float_eq!(samples[9], 0.42228); + + // switch to row trigger: + matrix.set_param(cmode_param, SAtom::setting(0)); + let samples = run_and_undersample(&mut node_exec, 2000.0, 5); + + assert_float_eq!(samples[0], 0.4863); + assert_float_eq!(samples[1], 0.4731); + assert_float_eq!(samples[2], 0.4597); + assert_float_eq!(samples[3], 0.4463); + assert_float_eq!(samples[4], 0.4331); + + // set to phase mode: + matrix.set_param(cmode_param, SAtom::setting(2)); + let samples = run_and_undersample(&mut node_exec, 1000.0, 5); + + assert_float_eq!(samples[0], 0.2491); + assert_float_eq!(samples[1], 0.0026); + assert_float_eq!(samples[2], 0.1616); + assert_float_eq!(samples[3], 0.6655); + assert_float_eq!(samples[4], 0.8104); +} + +#[test] +fn check_matrix_tseq_gate() { + use hexodsp::dsp::tracker::UIPatternModel; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let tsq = NodeId::TSeq(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(tsq) + .input(tsq.inp("clock"), None, None) + .out(None, None, tsq.out("trk1"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(-0.978)); + let cmode_param = tsq.inp_param("cmode").unwrap(); + matrix.set_param(cmode_param, SAtom::setting(1)); + + let pat = matrix.get_pattern_data(0).unwrap(); + { + let mut pr = pat.borrow_mut(); + pr.set_rows(16); + pr.set_col_gate_type(0); + // pulse_width: + // 0xF - Gate is on for full row + // 0x0 - Gate is on for a very short burst + // row_div: + // 0xF - Row has 1 Gate + // 0x0 - Row is divided up into 16 Gates + // probability: + // 0xF - Row is always triggered + // 0x7 - Row fires only in 50% of the cases + // 0x0 - Row fires only in ~6% of the cases + pr.set_cell_value(5, 0, 0xFFF); + pr.set_cell_value(7, 0, 0xFF0); + pr.set_cell_value(9, 0, 0xF00); + } + + for _ in 0..10 { + matrix.check_pattern_data(0); + } + + // We let the clock mode tune in: + run_and_undersample(&mut node_exec, 11100.0, 1); + + // Take some real samples: + let samples = run_and_undersample(&mut node_exec, 2000.0, 2000); + + assert_float_eq!(samples[117], 0.0); + for i in 118..243 { + assert_float_eq!(samples[i], 1.0); + } + assert_float_eq!(samples[243], 0.0); + + assert_float_eq!(samples[367], 0.0); + for i in 368..376 { + assert_float_eq!(samples[i], 1.0); + } + assert_float_eq!(samples[376], 0.0); + + assert_float_eq!(samples[680], 0.0); + assert_float_eq!(samples[681], 1.0); + assert_float_eq!(samples[682], 0.0); + + assert_float_eq!(samples[688], 0.0); + assert_float_eq!(samples[689], 1.0); + assert_float_eq!(samples[690], 0.0); +} + + +#[test] +fn check_matrix_tseq_2col_gate_bug() { + use hexodsp::dsp::tracker::UIPatternModel; + + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let tsq = NodeId::TSeq(0); + let out = NodeId::Out(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(tsq) + .input(tsq.inp("clock"), None, None) + .out(None, None, tsq.out("trk2"))); + matrix.place(0, 2, Cell::empty(out) + .input(out.inp("ch1"), None, None)); + matrix.sync().unwrap(); + + let freq_param = sin.inp_param("freq").unwrap(); + matrix.set_param(freq_param, SAtom::param(0.0)); + + let cmode_param = tsq.inp_param("cmode").unwrap(); + matrix.set_param(cmode_param, SAtom::setting(1)); + + let pat = matrix.get_pattern_data(0).unwrap(); + { + let mut pr = pat.borrow_mut(); + pr.set_rows(2); + pr.set_col_value_type(0); + pr.set_col_gate_type(1); + + // pulse_width: + // 0xF - Gate is on for full row + // 0x0 - Gate is on for a very short burst + // row_div: + // 0xF - Row has 1 Gate + // 0x0 - Row is divided up into 16 Gates + // probability: + // 0xF - Row is always triggered + // 0x7 - Row fires only in 50% of the cases + // 0x0 - Row fires only in ~6% of the cases + pr.set_cell_value(0, 0, 0xFFF); + pr.set_cell_value(1, 0, 0x000); + + pr.set_cell_value(0, 1, 0x0FF); + pr.set_cell_value(1, 1, 0x000); + } + + for _ in 0..10 { + matrix.check_pattern_data(0); + } + + let samples = run_and_undersample(&mut node_exec, 10000.0, 100000); + + let mut any_non_zero = false; + for s in samples.iter() { + if *s > 0.0 { any_non_zero = true; } + } + + assert!(any_non_zero); +} + + +#[test] +fn check_matrix_output_feedback() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + + let sin = NodeId::Sin(0); + let amp = NodeId::Amp(0); + matrix.place(0, 0, Cell::empty(sin) + .out(None, None, sin.out("sig"))); + matrix.place(0, 1, Cell::empty(amp) + .input(amp.inp("inp"), None, None)); + matrix.sync().unwrap(); + + let gain_p = amp.inp_param("gain").unwrap(); + matrix.set_param(gain_p, SAtom::param(0.25)); + + for _ in 0..10 { + node_exec.test_run(0.11, true); + matrix.update_filters(); + matrix.filtered_out_fb_for(&sin, sin.out("sig").unwrap()); + matrix.filtered_out_fb_for(&, amp.out("sig").unwrap()); + } + + let o_sin = matrix.out_fb_for(&sin, sin.out("sig").unwrap()).unwrap(); + let o_amp = matrix.out_fb_for(&, amp.out("sig").unwrap()).unwrap(); + let fo_sin = matrix.filtered_out_fb_for(&sin, sin.out("sig").unwrap()); + let fo_amp = matrix.filtered_out_fb_for(&, amp.out("sig").unwrap()); + + assert_float_eq!(o_sin, -0.061266); + assert_float_eq!(o_amp, -0.007658); + + assert_float_eq!(fo_sin.0, 0.96846); + assert_float_eq!(fo_sin.1, 0.9302191); + assert_float_eq!(fo_amp.0, 0.12105); + assert_float_eq!(fo_amp.1, 0.11627); +} +