The siraph
crate is a node-based digital signal processing crate.
nodes: Adds the nodes
module that contains a couple of basic useful nodes as Map
or Const
. This feature is enabled by default.
math: Adds the nodes::math
module that contains useful nodes related to mathematics as Add
or Oscillator
. This feature depends on the num-traits
crate and the nodes
feature.
random: Adds the nodes::random
module that contains useful nodes related to random number generators as SampleAndHold
. This feature depends on the rand
crate and the nodes
feature.
``rust
// Using the
nodes` feature
use siraph::Graph; use siraph::nodes::{Hold, Pulse, FromIter, Map};
// The ()
is a context that will be given to the nodes.
// The nodes that we are going to use do not need such a context.
let mut graph = Graph::<()>::new();
// First, we can insert nodes into the graph.
// This node just takes the values given by an iterator // and send them into its output. let from_iter = graph.insert(FromIter::new(std::iter::successors(Some(0u32), |&i| Some(i + 1))));
// This node infinitly outputs 4 false
then 1 true
.
let pulse = graph.insert(Pulse::new(4));
// This one wait for a pulse and holds the value its has in its input
// until a new pulse.
let hold = graph.insert(Hold::
// Simply uses the given function to maps its input to its output. let map = graph.insert(Map::new(|val: u32| val * val));
// Then, we can plug them together. graph.plug(from_iter, "output", hold, "input").unwrap(); graph.plug(pulse, "output", hold, "resample").unwrap(); graph.plug(hold, "output", map, "input").unwrap();
// Once our graph is done, we can retreive values from it using a sink. let mut sink = graph.sink(map, "output").unwrap();
// Here is a simple schem of what we have so far /* +------------------------+ | from_iter output i32 >----+ +------------------------------+ +------------------------+ +---> input | +----------------------+ | hold output i32 >--> input map output > sink +---> resample | +----------------------+ +---------------------+ | +------------------------------+ | pulse output bool >-------+ +---------------------+ */
// Values can be retreived with the next
function on the sink
// Once again, the ()
is the context provided to the nodes.
asserteq!(sink.next(&()), Some(0));
asserteq!(sink.next(&()), Some(0));
asserteq!(sink.next(&()), Some(0));
asserteq!(sink.next(&()), Some(0));
asserteq!(sink.next(&()), Some(0));
asserteq!(sink.next(&()), Some(25));
asserteq!(sink.next(&()), Some(25));
asserteq!(sink.next(&()), Some(25));
asserteq!(sink.next(&()), Some(25));
asserteq!(sink.next(&()), Some(25));
// The sink can be turned into an iterator using the with_ctx
function.
for (i, val) in sink.withctx(|| &()).take(1000).enumerate() {
let i = ((i/5)*5) as u32 + 10;
asserteq!(val, i*i)
}
```
You can create your own nodes using the Node
trait.
```rust
use siraph::{Node, Register, Input, Output};
// Our node will take an input and smooth it using a basic interpolation function.
pub struct Smooth {
input: Input
last_value: Option<f64>,
}
// The ctx must stay generic because even if this node does not need
// a global context, other nodes in the graph may need it.
impl
fn process(&mut self, _ctx: &Ctx) {
// It is in this function that all the processing will be done.
const X: f64 = 1.0/3.0;
// In our case, our computation is not very expensive but
// in other cases, things can get complicated.
// We can skip certain part of the processing by
// checking if our outputs are used.
if self.output.is_used() {
if let Some(cur) = self.input.get() {
if let Some(last) = self.last_value {
self.last_value = Some(last * X + (1.0 - X) * cur);
self.output.set(self.last_value);
} else {
self.output.set(cur);
self.last_value = Some(cur);
}
} else {
self.output.set(None);
}
}
}
fn reset(&mut self) {
// In this function, the inputs of the node should not be used even
// if they may return valid values.
self.last_value = None;
}
} ```