274 lines
8.2 KiB
Rust
274 lines
8.2 KiB
Rust
// 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;
|
|
}
|
|
}
|
|
|