copied code from hexosynth and made all tests pass
This commit is contained in:
parent
f20ff94692
commit
c3a7701416
28 changed files with 9566 additions and 12 deletions
|
@ -8,11 +8,15 @@ description = "Comprehensive DSP graph and synthesis library for developing a mo
|
|||
keywords = ["audio", "music", "real-time", "synthesis", "synthesizer", "dsp", "sound"]
|
||||
categories = ["multimedia::audio", "multimedia", "algorithms", "mathematics"]
|
||||
|
||||
#[features]
|
||||
#default = [ "hexotk" ]
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
|
|
|
@ -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
99
src/cell_dir.rs
Normal 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
420
src/dsp/helpers.rs
Normal 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
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
86
src/dsp/node_amp.rs
Normal 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
77
src/dsp/node_out.rs
Normal 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
67
src/dsp/node_sin.rs
Normal 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
54
src/dsp/node_test.rs
Normal 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
162
src/dsp/node_tseq.rs
Normal 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
99
src/dsp/satom.rs
Normal 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
254
src/dsp/tracker/mod.rs
Normal 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
639
src/dsp/tracker/pattern.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
471
src/dsp/tracker/sequencer.rs
Normal file
471
src/dsp/tracker/sequencer.rs
Normal 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));
|
||||
}
|
||||
}
|
42
src/lib.rs
42
src/lib.rs
|
@ -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
1158
src/matrix.rs
Normal file
File diff suppressed because it is too large
Load diff
650
src/matrix_repr.rs
Normal file
650
src/matrix_repr.rs
Normal 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
621
src/monitor.rs
Normal 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
49
src/nodes/drop_thread.rs
Normal 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();
|
||||
}
|
||||
}
|
54
src/nodes/feedback_filter.rs
Normal file
54
src/nodes/feedback_filter.rs
Normal 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
70
src/nodes/mod.rs
Normal 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
875
src/nodes/node_conf.rs
Normal 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(¶m) {
|
||||
nparam.value = at.clone();
|
||||
|
||||
let at_idx = nparam.at_idx;
|
||||
let _ =
|
||||
self.shared.quick_update_prod.push(
|
||||
QuickMessage::AtomUpdate { at_idx, value: at });
|
||||
}
|
||||
} else {
|
||||
self.param_values.insert(param, at.f());
|
||||
|
||||
if let Some(nparam) = self.params.get_mut(¶m) {
|
||||
let value = at.f();
|
||||
nparam.value = value;
|
||||
|
||||
let input_idx = nparam.input_idx;
|
||||
let _ =
|
||||
self.shared.quick_update_prod.push(
|
||||
QuickMessage::ParamUpdate { input_idx, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dumps all set parameters (inputs and atoms).
|
||||
/// Most useful for serialization and saving patches.
|
||||
pub fn dump_param_values(&self)
|
||||
-> (Vec<(ParamId, f32)>, Vec<(ParamId, SAtom)>)
|
||||
{
|
||||
let params : Vec<(ParamId, f32)> =
|
||||
self.param_values
|
||||
.iter()
|
||||
.map(|(param_id, value)| (*param_id, *value))
|
||||
.collect();
|
||||
|
||||
let atoms : Vec<(ParamId, SAtom)> =
|
||||
self.atom_values
|
||||
.iter()
|
||||
.map(|(param_id, value)| (*param_id, value.clone()))
|
||||
.collect();
|
||||
|
||||
(params, atoms)
|
||||
}
|
||||
|
||||
/// Loads parameter values from a dump. You will still need to upload
|
||||
/// a new [NodeProg] which contains these values.
|
||||
pub fn load_dumped_param_values(
|
||||
&mut self, params: &[(ParamId, f32)], atoms: &[(ParamId, SAtom)])
|
||||
{
|
||||
for (param_id, val) in params.iter() {
|
||||
self.param_values.insert(*param_id, *val);
|
||||
}
|
||||
|
||||
for (param_id, val) in atoms.iter() {
|
||||
self.atom_values.insert(*param_id, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over every parameter and calls the given function with
|
||||
/// it's current value.
|
||||
pub fn for_each_param<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(¶m_id) {
|
||||
*value
|
||||
} else {
|
||||
param_id.norm_def()
|
||||
};
|
||||
|
||||
self.param_values.insert(param_id, value);
|
||||
self.params.insert(param_id, NodeInputParam {
|
||||
param_id,
|
||||
value,
|
||||
input_idx: param_idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create new atom data and initialize it if it did not
|
||||
// already exist from a previous matrix instance.
|
||||
for atom_idx in at_idx..at_len {
|
||||
// XXX: See also the documentation of atom_param_by_idx about the
|
||||
// little param_id for an Atom weirdness here.
|
||||
if let Some(param_id) = id.atom_param_by_idx(atom_idx - at_idx) {
|
||||
let value =
|
||||
if let Some(atom) =
|
||||
self.atom_values.get(¶m_id)
|
||||
{
|
||||
atom.clone()
|
||||
} else {
|
||||
param_id.as_atom_def()
|
||||
};
|
||||
|
||||
self.atom_values.insert(param_id, value.clone());
|
||||
self.atoms.insert(param_id, NodeInputAtom {
|
||||
param_id,
|
||||
value,
|
||||
at_idx: atom_idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NodeProg::new(out_len, in_len, at_len)
|
||||
}
|
||||
|
||||
/// Creates a new [NodeOp] and add it to the [NodeProg].
|
||||
///
|
||||
/// It will fail silently if the nodes have not been created yet or
|
||||
/// [NodeConfigurator::rebuild_node_ports] was not called before. So make sure this is the
|
||||
/// case or don't expect the node and input to be executed.
|
||||
pub fn add_prog_node(&mut self, prog: &mut NodeProg, node_id: &NodeId) {
|
||||
if let Some((_node_info, Some(node_instance)))
|
||||
= self.node_by_id_mut(node_id)
|
||||
{
|
||||
node_instance.mark_used();
|
||||
let op = node_instance.to_op();
|
||||
prog.append_op(op);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an adjacent output connection to the given node input.
|
||||
/// Will either create a new [NodeOp] in the [NodeProg] or append to an
|
||||
/// existing one. This means the order you set the to be executed node
|
||||
/// connections, is the order the [NodeProg] is going to be executed by the
|
||||
/// DSP thread later.
|
||||
///
|
||||
/// It will fail silently if the nodes have not been created yet or
|
||||
/// [NodeConfigurator::rebuild_node_ports] was not called before. So make sure this is the
|
||||
/// case or don't expect the node and input to be executed.
|
||||
pub fn set_prog_node_exec_connection(
|
||||
&mut self, prog: &mut NodeProg,
|
||||
node_input: (NodeId, u8),
|
||||
adjacent_output: (NodeId, u8))
|
||||
{
|
||||
let output_index =
|
||||
if let Some((_, Some(node_instance)))
|
||||
= self.node_by_id(&adjacent_output.0)
|
||||
{
|
||||
node_instance.out_local2global(adjacent_output.1)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some((_node_info, Some(node_instance)))
|
||||
= self.node_by_id_mut(&node_input.0)
|
||||
{
|
||||
node_instance.mark_used();
|
||||
let op = node_instance.to_op();
|
||||
|
||||
let input_index = node_instance.in_local2global(node_input.1);
|
||||
match (input_index, output_index) {
|
||||
(Some(input_index), Some(output_index)) => {
|
||||
prog.append_edge(op, input_index, output_index);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads a new NodeProg instance.
|
||||
///
|
||||
/// Create a new NodeProg instance with [NodeConfigurator::rebuild_node_ports]
|
||||
/// for each call to this function. Otherwise things like the
|
||||
/// [NodeConfigurator::out_fb_for] might not work properly!
|
||||
///
|
||||
/// The `copy_old_out` parameter should be set if there are only
|
||||
/// new nodes appended to the end of the node instances.
|
||||
/// It helps to prevent clicks when there is a feedback path somewhere.
|
||||
///
|
||||
/// It must not be set when a completely new set of node instances
|
||||
/// was created, for instance when a completely new patch was loaded.
|
||||
///
|
||||
/// Here is an example on how to use the [NodeConfigurator]
|
||||
/// directly to setup and upload a [NodeProg]:
|
||||
///
|
||||
///```
|
||||
/// use hexodsp::*;
|
||||
///
|
||||
/// let (mut node_conf, mut node_exec) = new_node_engine();
|
||||
///
|
||||
/// node_conf.create_node(NodeId::Sin(0));
|
||||
/// node_conf.create_node(NodeId::Amp(0));
|
||||
///
|
||||
/// let mut prog = node_conf.rebuild_node_ports();
|
||||
///
|
||||
/// node_conf.add_prog_node(&mut prog, &NodeId::Sin(0));
|
||||
/// node_conf.add_prog_node(&mut prog, &NodeId::Amp(0));
|
||||
///
|
||||
/// node_conf.set_prog_node_exec_connection(
|
||||
/// &mut prog,
|
||||
/// (NodeId::Amp(0), NodeId::Amp(0).inp("inp").unwrap()),
|
||||
/// (NodeId::Sin(0), NodeId::Sin(0).out("sig").unwrap()));
|
||||
///
|
||||
/// node_conf.upload_prog(prog, true);
|
||||
///```
|
||||
pub fn upload_prog(&mut self, mut prog: NodeProg, copy_old_out: bool) {
|
||||
// Copy the parameter values and atom data into the program:
|
||||
// They are extracted by process_graph_updates() later to
|
||||
// reset the inp[] input value vector.
|
||||
for (_param_id, param) in self.params.iter() {
|
||||
prog.params_mut()[param.input_idx] = param.value;
|
||||
}
|
||||
|
||||
// The atoms are referred to directly on process() call.
|
||||
for (_param_id, param) in self.atoms.iter() {
|
||||
prog.atoms_mut()[param.at_idx] = param.value.clone();
|
||||
}
|
||||
|
||||
self.output_fb_cons = prog.take_feedback_consumer();
|
||||
|
||||
let _ =
|
||||
self.shared.graph_update_prod.push(
|
||||
GraphMessage::NewProg { prog, copy_old_out });
|
||||
}
|
||||
|
||||
/// Retrieves the feedback value for a specific output port of the
|
||||
/// given [NodeId]. You need to call [NodeConfigurator::update_output_feedback]
|
||||
/// before this, or otherwise your output values might be outdated
|
||||
/// or not available at all.
|
||||
///
|
||||
/// See also [NodeConfigurator::filtered_out_fb_for] for a
|
||||
/// filtered variant suitable for UI usage.
|
||||
pub fn out_fb_for(&self, node_id: &NodeId, out: u8) -> Option<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
432
src/nodes/node_exec.rs
Normal 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)
|
||||
}
|
||||
|
||||
}
|
353
src/nodes/node_graph_ordering.rs
Normal file
353
src/nodes/node_graph_ordering.rs
Normal 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
274
src/nodes/node_prog.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
95
src/nodes/visual_sampling_filter.rs
Normal file
95
src/nodes/visual_sampling_filter.rs
Normal 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
162
src/util.rs
Normal 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
1107
tests/basics.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue