hexodsp/
chain_builder.rs

1// Copyright (c) 2021-2022 Weird Constructor <weirdconstructor@gmail.com>
2// This file is a part of HexoDSP. Released under GPL-3.0-or-later.
3// See README.md and COPYING for details.
4/*!  Defines an API for easy DSP chain building with the hexagonal [crate::Matrix].
5
6The [crate::MatrixCellChain] abstractions allows very easy placement of DSP signal chains:
7
8```
9 use hexodsp::*;
10 let mut chain = MatrixCellChain::new(CellDir::BR);
11 chain.node_out("sin", "sig")
12     .set_denorm("freq", 220.0)
13     .node_io("amp", "inp", "sig")
14     .set_denorm("att", 0.5)
15     .node_inp("out", "ch1");
16
17 // use crate::nodes::new_node_engine;
18 let (node_conf, _node_exec) = new_node_engine();
19 let mut matrix = Matrix::new(node_conf, 7, 7);
20
21 chain.place(&mut matrix, 2, 2).expect("no error in this case");
22```
23*/
24
25use crate::{Cell, CellDir, Matrix, NodeId, ParamId, SAtom};
26use std::collections::HashMap;
27
28#[derive(Debug, Clone)]
29struct MatrixChainLink {
30    cell: Cell,
31    dir: CellDir,
32    params: Vec<(ParamId, SAtom)>,
33}
34
35/// A DSP chain builder for the [crate::Matrix].
36///
37/// This is an extremely easy API to create and place new DSP chains into the [crate::Matrix].
38/// It can be used by frontends to place DSP chains on user request or it can be used
39/// by test cases to quickly fill the hexagonal Matrix.
40///
41///```
42/// use hexodsp::*;
43/// let mut chain = MatrixCellChain::new(CellDir::BR);
44/// chain.node_out("sin", "sig")
45///     .set_denorm("freq", 220.0)
46///     .node_io("amp", "inp", "sig")
47///     .set_denorm("att", 0.5)
48///     .node_inp("out", "ch1");
49///
50/// // use crate::nodes::new_node_engine;
51/// let (node_conf, _node_exec) = new_node_engine();
52/// let mut matrix = Matrix::new(node_conf, 7, 7);
53///
54/// chain.place(&mut matrix, 2, 2).expect("no error in this case");
55///```
56#[derive(Debug, Clone)]
57pub struct MatrixCellChain {
58    chain: Vec<MatrixChainLink>,
59    error: Option<ChainError>,
60    dir: CellDir,
61    param_idx: usize,
62}
63
64/// Error type for the [crate::MatrixCellChain].
65#[derive(Debug, Clone)]
66pub enum ChainError {
67    UnknownNodeId(String),
68    UnknownOutput(NodeId, String),
69    UnknownInput(NodeId, String),
70}
71
72impl MatrixCellChain {
73    /// Create a new [MatrixCellChain] with the given placement direction.
74    ///
75    /// The direction is used to guide the placement of the cells.
76    pub fn new(dir: CellDir) -> Self {
77        Self { dir, chain: vec![], error: None, param_idx: 0 }
78    }
79
80    fn output_dir(&self) -> CellDir {
81        if self.dir.is_output() {
82            self.dir
83        } else {
84            self.dir.flip()
85        }
86    }
87
88    fn input_dir(&self) -> CellDir {
89        if self.dir.is_input() {
90            self.dir
91        } else {
92            self.dir.flip()
93        }
94    }
95
96    /// Sets the current parameter cell by chain index.
97    pub fn params_for_idx(&mut self, idx: usize) -> &mut Self {
98        self.param_idx = idx;
99        if self.param_idx >= self.chain.len() {
100            self.param_idx = self.chain.len();
101        }
102
103        self
104    }
105
106    /// Sets the denormalized value of the current parameter cell's parameter.
107    ///
108    /// The current parameter cell is set automatically when a new node is added.
109    /// Alternatively you can use [MatrixCellChain::params_for_idx] to set the current
110    /// parameter cell.
111    pub fn set_denorm(&mut self, param: &str, denorm: f32) -> &mut Self {
112        let link = self.chain.get_mut(self.param_idx).expect("Correct parameter idx");
113
114        if let Some(pid) = link.cell.node_id().inp_param(param) {
115            link.params.push((pid, SAtom::param(pid.norm(denorm as f32))));
116        } else {
117            self.error = Some(ChainError::UnknownInput(link.cell.node_id(), param.to_string()));
118        }
119
120        self
121    }
122
123    /// Sets the normalized value of the current parameter cell's parameter.
124    ///
125    /// The current parameter cell is set automatically when a new node is added.
126    /// Alternatively you can use [MatrixCellChain::params_for_idx] to set the current
127    /// parameter cell.
128    pub fn set_norm(&mut self, param: &str, norm: f32) -> &mut Self {
129        let link = self.chain.get_mut(self.param_idx).expect("Correct parameter idx");
130
131        if let Some(pid) = link.cell.node_id().inp_param(param) {
132            link.params.push((pid, SAtom::param(norm as f32)));
133        } else {
134            self.error = Some(ChainError::UnknownInput(link.cell.node_id(), param.to_string()));
135        }
136
137        self
138    }
139
140    /// Sets the atom value of the current parameter cell's parameter.
141    ///
142    /// The current parameter cell is set automatically when a new node is added.
143    /// Alternatively you can use [MatrixCellChain::params_for_idx] to set the current
144    /// parameter cell.
145    pub fn set_atom(&mut self, param: &str, at: SAtom) -> &mut Self {
146        let link = self.chain.get_mut(self.param_idx).expect("Correct parameter idx");
147
148        if let Some(pid) = link.cell.node_id().inp_param(param) {
149            link.params.push((pid, at));
150        } else {
151            self.error = Some(ChainError::UnknownInput(link.cell.node_id(), param.to_string()));
152        }
153
154        self
155    }
156
157    /// Utility function for creating [crate::Cell] for this chain.
158    pub fn spawn_cell_from_node_id_name(&mut self, node_id_name: &str) -> Option<Cell> {
159        let node_id = NodeId::from_str(node_id_name);
160        if node_id == NodeId::Nop && node_id_name != "nop" {
161            return None;
162        }
163
164        Some(Cell::empty(node_id))
165    }
166
167    /// Utility function to add a pre-built [crate::Cell] as next link.
168    ///
169    /// This also sets the current parameter cell.
170    pub fn add_link(&mut self, cell: Cell) {
171        self.chain.push(MatrixChainLink { dir: self.dir, cell, params: vec![] });
172        self.param_idx = self.chain.len() - 1;
173    }
174
175    /// Place a new node in the chain without any inputs or outputs. This is of limited
176    /// use in this API, but might makes a few corner cases easier in test cases.
177    pub fn node(&mut self, node_id: &str) -> &mut Self {
178        if let Some(cell) = self.spawn_cell_from_node_id_name(node_id) {
179            self.add_link(cell);
180        } else {
181            self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
182        }
183
184        self
185    }
186
187    /// Place a new node in the chain with the given output assigned.
188    pub fn node_out(&mut self, node_id: &str, out: &str) -> &mut Self {
189        if let Some(mut cell) = self.spawn_cell_from_node_id_name(node_id) {
190            if let Err(()) = cell.set_output_by_name(out, self.output_dir()) {
191                self.error = Some(ChainError::UnknownOutput(cell.node_id(), out.to_string()));
192            }
193
194            self.add_link(cell);
195        } else {
196            self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
197        }
198
199        self
200    }
201
202    /// Place a new node in the chain with the given input assigned.
203    pub fn node_inp(&mut self, node_id: &str, inp: &str) -> &mut Self {
204        if let Some(mut cell) = self.spawn_cell_from_node_id_name(node_id) {
205            if let Err(()) = cell.set_input_by_name(inp, self.input_dir()) {
206                self.error = Some(ChainError::UnknownInput(cell.node_id(), inp.to_string()));
207            }
208
209            self.add_link(cell);
210        } else {
211            self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
212        }
213
214        self
215    }
216
217    /// Place a new node in the chain with the given input and output assigned.
218    pub fn node_io(&mut self, node_id: &str, inp: &str, out: &str) -> &mut Self {
219        if let Some(mut cell) = self.spawn_cell_from_node_id_name(node_id) {
220            if let Err(()) = cell.set_input_by_name(inp, self.input_dir()) {
221                self.error = Some(ChainError::UnknownInput(cell.node_id(), inp.to_string()));
222            }
223
224            if let Err(()) = cell.set_output_by_name(out, self.output_dir()) {
225                self.error = Some(ChainError::UnknownOutput(cell.node_id(), out.to_string()));
226            }
227
228            self.add_link(cell);
229        } else {
230            self.error = Some(ChainError::UnknownNodeId(node_id.to_string()));
231        }
232
233        self
234    }
235
236    /// Places the chain into the matrix at the given position.
237    ///
238    /// If any error occured while building the chain (such as bad input/output names
239    /// or unknown parameters), it will be returned here.
240    pub fn place(
241        &mut self,
242        matrix: &mut Matrix,
243        at_x: usize,
244        at_y: usize,
245    ) -> Result<(), ChainError> {
246        if let Some(err) = self.error.take() {
247            return Err(err);
248        }
249
250        let mut last_unused = HashMap::new();
251
252        let mut pos = (at_x, at_y);
253
254        for link in self.chain.iter() {
255            let (x, y) = pos;
256
257            let mut cell = link.cell;
258
259            let node_id = cell.node_id();
260            let node_name = node_id.name();
261
262            let node_id = if let Some(i) = last_unused.get(node_name).cloned() {
263                last_unused.insert(node_name.to_string(), i + 1);
264                node_id.to_instance(i + 1)
265            } else {
266                let node_id = matrix.get_unused_instance_node_id(node_id);
267                last_unused.insert(node_name.to_string(), node_id.instance());
268                node_id
269            };
270
271            cell.set_node_id_keep_ios(node_id);
272
273            matrix.place(x, y, cell);
274
275            let offs = link.dir.as_offs(pos.0);
276            pos.0 = (pos.0 as i32 + offs.0) as usize;
277            pos.1 = (pos.1 as i32 + offs.1) as usize;
278        }
279
280        for link in self.chain.iter() {
281            for (pid, at) in link.params.iter() {
282                matrix.set_param(*pid, at.clone());
283            }
284        }
285
286        Ok(())
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn check_matrix_chain_builder_1() {
296        use crate::nodes::new_node_engine;
297
298        let (node_conf, _node_exec) = new_node_engine();
299        let mut matrix = Matrix::new(node_conf, 7, 7);
300
301        let mut chain = MatrixCellChain::new(CellDir::B);
302
303        chain
304            .node_out("sin", "sig")
305            .set_denorm("freq", 220.0)
306            .node_io("amp", "inp", "sig")
307            .set_denorm("att", 0.5)
308            .node_inp("out", "ch1");
309
310        chain.params_for_idx(0).set_atom("det", SAtom::param(0.1));
311
312        chain.place(&mut matrix, 2, 2).expect("no error in this case");
313
314        matrix.sync().expect("Sync ok");
315
316        let cell_sin = matrix.get(2, 2).unwrap();
317        assert_eq!(cell_sin.node_id(), NodeId::Sin(0));
318
319        let cell_amp = matrix.get(2, 3).unwrap();
320        assert_eq!(cell_amp.node_id(), NodeId::Amp(0));
321
322        let cell_out = matrix.get(2, 4).unwrap();
323        assert_eq!(cell_out.node_id(), NodeId::Out(0));
324
325        assert_eq!(
326            format!("{:?}", matrix.get_param(&NodeId::Sin(0).inp_param("freq").unwrap()).unwrap()),
327            "Param(-0.1)"
328        );
329        assert_eq!(
330            format!("{:?}", matrix.get_param(&NodeId::Sin(0).inp_param("det").unwrap()).unwrap()),
331            "Param(0.1)"
332        );
333        assert_eq!(
334            format!("{:?}", matrix.get_param(&NodeId::Amp(0).inp_param("att").unwrap()).unwrap()),
335            "Param(0.5)"
336        );
337    }
338}