copied code from hexosynth and made all tests pass

This commit is contained in:
Weird Constructor 2021-05-18 05:11:19 +02:00
parent f20ff94692
commit c3a7701416
28 changed files with 9566 additions and 12 deletions

View file

@ -1,18 +1,22 @@
[package]
name = "HexoDSP"
version = "0.1.0"
authors = ["Weird Constructor <weirdconstructor@m8geil.de>"]
license = "AGPL-3.0-or-later"
edition = "2018"
name = "HexoDSP"
version = "0.1.0"
authors = ["Weird Constructor <weirdconstructor@m8geil.de>"]
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"]

View file

@ -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();

99
src/cell_dir.rs Normal file
View file

@ -0,0 +1,99 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<HexDir> for CellDir {
fn from(h: HexDir) -> Self {
CellDir::from(h.to_edge())
}
}
#[cfg(feature="hexotk")]
impl From<CellDir> for HexDir {
fn from(c: CellDir) -> Self {
HexDir::from(c.to_edge())
}
}

420
src/dsp/helpers.rs Normal file
View file

@ -0,0 +1,420 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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::<f64>() 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 <http://creativecommons.org/publicdomain/zero/1.0/>.
//"""
//
// Written by Alexander Stocko <as@coder.gg>
//
// 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 <LICENSE or http://creativecommons.org/publicdomain/zero/1.0/>
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 <tobydox/at/users.sourceforge.net>
//
/// 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 <tobydox/at/users.sourceforge.net>
//
/// 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 <littleioaudio@gmail.com>
// Matt Tytel <matthewtytel@gmail.com>
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 <littleioaudio@gmail.com>
// Matt Tytel <matthewtytel@gmail.com>
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);
}
}

1180
src/dsp/mod.rs Normal file

File diff suppressed because it is too large Load diff

86
src/dsp/node_amp.rs Normal file
View file

@ -0,0 +1,86 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<T: NodeAudioContext>(
&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);
}
}

77
src/dsp/node_out.rs Normal file
View file

@ -0,0 +1,77 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<T: NodeAudioContext>(
&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);
}
}

67
src/dsp/node_sin.rs Normal file
View file

@ -0,0 +1,67 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<T: NodeAudioContext>(
&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);
}
}

54
src/dsp/node_test.rs Normal file
View file

@ -0,0 +1,54 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<T: NodeAudioContext>(
&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<GraphFun> {
Some(Box::new(|_gd: &dyn GraphAtomData, _init: bool, x: f32| -> f32 {
x
}))
}
}

162
src/dsp/node_tseq.rs Normal file
View file

@ -0,0 +1,162 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<Box<TrackerBackend>>,
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<T: NodeAudioContext>(
&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]);
}
}

99
src/dsp/satom.rs Normal file
View file

@ -0,0 +1,99 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<f32>),
AudioSample((String, Option<std::sync::Arc<Vec<f32>>>)),
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<Vec<f32>>) -> 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<f32> for SAtom {
fn from(n: f32) -> Self { SAtom::Param(n) }
}
#[cfg(feature="hexotk")]
use hexotk::Atom;
#[cfg(feature="hexotk")]
impl From<Atom> 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<SAtom> 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),
}
}
}

254
src/dsp/tracker/mod.rs Normal file
View file

@ -0,0 +1,254 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<RefCell<PatternData>>,
data_prod: Producer<PatternUpdateMsg>,
seq: Option<PatternSequencer>,
seq_cons: Option<Consumer<PatternUpdateMsg>>,
}
impl Clone for Tracker {
fn clone(&self) -> Self { Tracker::new() }
}
pub struct TrackerBackend {
seq: PatternSequencer,
seq_cons: Consumer<PatternUpdateMsg>,
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<RefCell<PatternData>> { 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]);
}
}

639
src/dsp/tracker/pattern.rs Normal file
View file

@ -0,0 +1,639 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<Vec<Option<u16>>>,
out_data: Vec<[f32; MAX_PATTERN_LEN]>,
strings: Vec<Vec<Option<String>>>,
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);
}
}
}

View file

@ -0,0 +1,471 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<Vec<f32>>,
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));
}
}

View file

@ -1,3 +1,45 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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
}

1158
src/matrix.rs Normal file

File diff suppressed because it is too large Load diff

650
src/matrix_repr.rs Normal file
View file

@ -0,0 +1,650 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<NodeId, MatrixDeserError>
{
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<Self, MatrixDeserError> {
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<Vec<i32>>,
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<Self, MatrixDeserError> {
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<CellRepr>,
pub params: Vec<(ParamId, f32)>,
pub atoms: Vec<(ParamId, SAtom)>,
pub patterns: Vec<Option<PatternRepr>>,
}
#[derive(Debug, Clone)]
pub enum MatrixDeserError {
BadVersion,
UnknownNode(String),
UnknownParamId(String),
Deserialization(String),
IO(String),
InvalidAtom(String),
MatrixError(crate::matrix::MatrixError),
}
impl From<crate::matrix::MatrixError> for MatrixDeserError {
fn from(err: crate::matrix::MatrixError) -> Self {
MatrixDeserError::MatrixError(err)
}
}
impl From<serde_json::Error> for MatrixDeserError {
fn from(err: serde_json::Error) -> MatrixDeserError {
MatrixDeserError::Deserialization(format!("{}", err))
}
}
impl From<std::str::Utf8Error> for MatrixDeserError {
fn from(err: std::str::Utf8Error) -> MatrixDeserError {
MatrixDeserError::Deserialization(format!("{}", err))
}
}
impl From<std::io::Error> for MatrixDeserError {
fn from(err: std::io::Error) -> MatrixDeserError {
MatrixDeserError::IO(format!("{}", err))
}
}
fn deserialize_atom(v: &Value) -> Result<SAtom, MatrixDeserError> {
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<MatrixRepr, MatrixDeserError> {
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<u8> = Vec::new();
file.read_to_end(&mut contents)?;
let s = std::str::from_utf8(&contents)?;
MatrixRepr::deserialize(s)
}
pub fn deserialize(s: &str) -> Result<MatrixRepr, MatrixDeserError> {
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);
}
}
}
}

621
src/monitor.rs Normal file
View file

@ -0,0 +1,621 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<MonitorBufPtr>,
rb_recycle_con: Consumer<MonitorBufPtr>,
/// 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<MonitorBufPtr>,
}
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<MonitorBufPtr> {
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<usize> 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<AtomicBool>,
proc_thread: Option<JoinHandle<()>>,
new_data: Arc<AtomicBool>,
monitor_samples: Arc<Mutex<[MinMaxMonitorSamples; MON_SIG_CNT]>>,
monitor_samples_copy: [MinMaxMonitorSamples; MON_SIG_CNT],
}
impl Monitor {
pub fn new(rb_mon_con: Consumer<MonitorBufPtr>,
rb_recycle_prod: Producer<MonitorBufPtr>)
-> 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<MonitorBufPtr>,
rb_recycle_prod: Producer<MonitorBufPtr>,
new_data: bool,
procs: Vec<MonitorMinMax>,
}
impl MonitorProcessor {
pub fn new(rb_mon_con: Consumer<MonitorBufPtr>,
rb_recycle_prod: Producer<MonitorBufPtr>)
-> 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<f32> {
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<T>(&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<MonitorBuf>;
#[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<f32> = 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<f32> = 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);
}
}

49
src/nodes/drop_thread.rs Normal file
View file

@ -0,0 +1,49 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<std::sync::atomic::AtomicBool>,
th: Option<std::thread::JoinHandle<()>>,
}
impl DropThread {
pub(crate) fn new(mut graph_drop_con: Consumer<DropMsg>) -> 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();
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<NodeId, VisualSamplingFilter>,
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)
}
}

70
src/nodes/mod.rs Normal file
View file

@ -0,0 +1,70 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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)
}

875
src/nodes/node_conf.rs Normal file
View file

@ -0,0 +1,875 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<usize> {
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<usize> {
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<NodeInstance>)>,
/// 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<NodeId, usize>,
/// Holding the tracker sequencers
pub(crate) trackers: Vec<Tracker>,
/// 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<ParamId, NodeInputParam>,
/// Stores the most recently set parameter values
param_values: std::collections::HashMap<ParamId, f32>,
/// Contains non automateable atom data for the nodes
atoms: std::collections::HashMap<ParamId, NodeInputAtom>,
/// Stores the most recently set atoms
atom_values: std::collections::HashMap<ParamId, SAtom>,
/// Holds a copy of the most recently updated output port feedback
/// values. Update this by calling [NodeConfigurator::update_output_feedback].
output_fb_values: Vec<f32>,
/// Holds the channel to the backend that sends output port feedback.
/// This is queried by [NodeConfigurator::update_output_feedback].
output_fb_cons: Option<Output<Vec<f32>>>,
}
pub(crate) struct SharedNodeConf {
/// Holds the LED values of the nodes
pub(crate) node_ctx_values: Vec<Arc<AtomicFloat>>,
/// For updating the NodeExecutor with graph updates.
pub(crate) graph_update_prod: Producer<GraphMessage>,
/// For quick updates like UI paramter changes.
pub(crate) quick_update_prod: Producer<QuickMessage>,
/// 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<F: FnMut(&NodeInfo, NodeId, usize)>(&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<usize> {
self.node2idx.get(&ni).copied()
}
pub fn node_by_id(&self, ni: &NodeId) -> Option<&(NodeInfo, Option<NodeInstance>)> {
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<NodeInstance>)> {
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(&param) {
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(&param) {
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<F: FnMut(usize, ParamId, &SAtom)>(&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<u8>], outputs: &[Option<u8>])
{
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<Rc<RefCell<PatternData>>>
{
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<usize> = 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(&param_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(&param_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<f32> {
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)
}
}

432
src/nodes/node_exec.rs Normal file
View file

@ -0,0 +1,432 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<Node>,
/// 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<Arc<AtomicFloat>>,
/// For receiving Node and NodeProg updates
pub(crate) graph_update_con: Consumer<GraphMessage>,
/// For quick updates like UI paramter changes.
pub(crate) quick_update_con: Consumer<QuickMessage>,
/// For receiving deleted/overwritten nodes from the backend thread.
pub(crate) graph_drop_prod: Producer<DropMsg>,
/// 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<Node> { &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<T: NodeAudioContext>(&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<f32>, Vec<f32>) {
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)
}
}

View file

@ -0,0 +1,353 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<NodeId, usize>,
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<bool> {
let mut visited_set : HashSet<NodeId> =
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<NodeId>) -> 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());
}
}

274
src/nodes/node_prog.rs Normal file
View file

@ -0,0 +1,274 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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, (<out vec index>, <own node input index>)
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<ProcBuf>,
/// The temporary input vector that is initialized from `inp`
/// and is then merged with the associated outputs.
pub cur_inp: Vec<ProcBuf>,
/// The output vector, holding all the node outputs.
pub out: Vec<ProcBuf>,
/// The param vector, holding all parameter inputs of the
/// nodes, such as knob settings.
pub params: Vec<f32>,
/// The atom vector, holding all non automatable parameter inputs
/// of the nodes, such as samples or integer settings.
pub atoms: Vec<SAtom>,
/// The node operations that are executed in the order they appear in this
/// vector.
pub prog: Vec<NodeOp>,
/// 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<Vec<f32>>,
/// Temporary hold for the producer for the `out_feedback`:
pub out_fb_cons: Option<Output<Vec<f32>>>,
}
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<Output<Vec<f32>>> {
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;
}
}

View file

@ -0,0 +1,95 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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
}
}

162
src/util.rs Normal file
View file

@ -0,0 +1,162 @@
// Copyright (c) 2021 Weird Constructor <weirdconstructor@gmail.com>
// 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<f32> for AtomicFloat {
fn from(value: f32) -> Self {
AtomicFloat::new(value)
}
}
impl From<AtomicFloat> for f32 {
fn from(value: AtomicFloat) -> Self {
value.get()
}
}

1107
tests/basics.rs Normal file

File diff suppressed because it is too large Load diff