Math
Scale and shift every value, then remap the range.
`bound` says what happens to a value that lands outside it. Shape and metadata are untouched.
input
out
- Type name
signal:Math- Plane
signal- Tags
transform- Language
rust- Tier
native- Bundle
signal- Source
node-bundles/signal/Math.rs- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
input | input | ARRAY | |
out | output | ARRAY |
Parameters
math
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
pre_add | float | 0 | -1000000000 … 1000000000 | Added to every value before the multiply, which is how you centre a signal. |
multiply | float | 1 | -1000000000 … 1000000000 | Scales every value; a negative number turns the signal upside down. |
post_add | float | 0 | -1000000000 … 1000000000 | Added to every value after the multiply, which is how you set a baseline. |
range
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
from_low | float | 0 | -1000000000 … 1000000000 | The bottom of the range the values are expected to arrive in. |
from_high | float | 1 | -1000000000 … 1000000000 | The top of the range the values are expected to arrive in. |
to_low | float | 0 | -1000000000 … 1000000000 | The bottom of the range they are mapped onto. |
to_high | float | 1 | -1000000000 … 1000000000 | The top of the range they are mapped onto. |
bound | string | none | none | clamp | wrap | fold | What happens to a value outside the target range. `clamp` holds it at the edge; `wrap` carries it round to the other end; `fold` halves or doubles it until it lands inside, which is how a spectral peak becomes an audible pitch or an LFO rate. `fold` needs a positive range, and one narrower than an octave cannot hold every value. |
common
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
autotrigger | bool | false | Run on the node's own schedule, instead of waiting for an input frame. Turn this on for sources; leave it off for transforms driven by their input. | |
max_frequency | float | 0 | 0 … 100 | Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow. |
frequency_mode | string | updates_per_second | updates_per_second | seconds_per_update | How to read `max_frequency`: as a rate in Hz (updates per second), or as a period in seconds between updates — convenient for very slow nodes. |
Source
The current source of this node, as it stands in the goofi repository at node-bundles/signal/Math.rs.
//! Math — the arithmetic every patch needs on the way between two nodes: a scale and a shift,
//! then a range mapped onto another range, and how a value outside that range is brought back in.
//! Shape and meta are untouched.
use goofi_core::{Data, SlotType};
use goofi_signal_sdk::{Inputs, Manifest, Node, NodeCtx, NodeResult, OutputDecl, Outputs, ParamDecl, Params, ParamSpec, SlotDecl, Tag};
/// Bring `v` inside `[lo, hi]` the way `bound` says, or leave it where it is.
fn bind(v: f32, lo: f32, hi: f32, bound: &str) -> f32 {
match bound {
// A NaN bound has no inside; `clamp` would panic on it rather than pass the value on.
"clamp" if lo <= hi => v.clamp(lo, hi),
// Modulo, so the top of the range and the bottom are the same place.
"wrap" if hi > lo => lo + (v - lo).rem_euclid(hi - lo),
// The octave fold: the value keeps its pitch class and changes register. Only a positive
// range has octaves, and a range narrower than one cannot hold every value — what comes
// out is then the nearest octave of the input, which may still sit outside.
"fold" if v > 0.0 && v.is_finite() && lo > 0.0 && hi > lo => {
let mut v = v;
while v > hi {
v *= 0.5;
}
while v < lo {
v *= 2.0;
}
v
}
_ => v,
}
}
#[derive(Default)]
struct Math;
impl Node for Math {
fn process(
&mut self,
inp: &Inputs<'_>,
out: &mut Outputs<'_>,
_c: &mut NodeCtx,
p: &Params<'_>,
) -> NodeResult {
let d = inp.get("input").ok_or("`input` is required")?;
let a = d.as_array()?;
let pre = p.f64("math", "pre_add").unwrap_or(0.0) as f32;
let mul = p.f64("math", "multiply").unwrap_or(1.0) as f32;
let post = p.f64("math", "post_add").unwrap_or(0.0) as f32;
let from_low = p.f64("range", "from_low").unwrap_or(0.0) as f32;
let from_high = p.f64("range", "from_high").unwrap_or(1.0) as f32;
let to_low = p.f64("range", "to_low").unwrap_or(0.0) as f32;
let to_high = p.f64("range", "to_high").unwrap_or(1.0) as f32;
let bound = p.str("range", "bound").unwrap_or("none");
// A zero-width source range has no ratio to carry, so every value lands at `to_low`.
let span = from_high - from_low;
let scale = if span == 0.0 { 0.0 } else { (to_high - to_low) / span };
let (lo, hi) = if to_low <= to_high { (to_low, to_high) } else { (to_high, to_low) };
let mut buf = Vec::with_capacity(a.as_bytes().len());
for x in a.as_bytes().chunks_exact(4) {
let v = f32::from_le_bytes(x.try_into().expect("four bytes"));
let v = (v + pre) * mul + post;
let v = to_low + (v - from_low) * scale;
buf.extend_from_slice(&bind(v, lo, hi, bound).to_le_bytes());
}
out.set("out", Data::array_f32(a.shape().to_vec(), buf, d.meta().clone()).map_err(|e| e.to_string())?);
Ok(())
}
}
static PARAMS: &[ParamDecl] = &[
ParamDecl {
group: "math",
name: "pre_add",
spec: ParamSpec::Float { default: 0.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("Added to every value before the multiply, which is how you centre a signal."),
section: 0,
show: None,
},
ParamDecl {
group: "math",
name: "multiply",
spec: ParamSpec::Float { default: 1.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("Scales every value; a negative number turns the signal upside down."),
section: 0,
show: None,
},
ParamDecl {
group: "math",
name: "post_add",
spec: ParamSpec::Float { default: 0.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("Added to every value after the multiply, which is how you set a baseline."),
section: 0,
show: None,
},
ParamDecl {
group: "range",
name: "from_low",
spec: ParamSpec::Float { default: 0.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("The bottom of the range the values are expected to arrive in."),
section: 0,
show: None,
},
ParamDecl {
group: "range",
name: "from_high",
spec: ParamSpec::Float { default: 1.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("The top of the range the values are expected to arrive in."),
section: 0,
show: None,
},
ParamDecl {
group: "range",
name: "to_low",
spec: ParamSpec::Float { default: 0.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("The bottom of the range they are mapped onto."),
section: 0,
show: None,
},
ParamDecl {
group: "range",
name: "to_high",
spec: ParamSpec::Float { default: 1.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("The top of the range they are mapped onto."),
section: 0,
show: None,
},
ParamDecl {
group: "range",
name: "bound",
spec: ParamSpec::Str { default: "none", options: &["none", "clamp", "wrap", "fold"], refresh: false },
expression: None,
doc: Some(
"What happens to a value outside the target range. `clamp` holds it at the edge; \
`wrap` carries it round to the other end; `fold` halves or doubles it until it lands \
inside, which is how a spectral peak becomes an audible pitch or an LFO rate. `fold` \
needs a positive range, and one narrower than an octave cannot hold every value.",
),
section: 0,
show: None,
},
];
static INPUTS: &[SlotDecl] = &[SlotDecl {
name: "input",
kind: SlotType::Array,
trigger_process: true,
multi: false,
required: true,
}];
static OUTPUTS: &[OutputDecl] = &[OutputDecl { name: "out", kind: SlotType::Array }];
static MANIFEST: Manifest = Manifest {
tags: &[Tag::Transform],
doc: "Scale and shift every value, then remap the range.\n\
`bound` says what happens to a value that lands outside it. Shape and metadata are untouched.",
inputs: INPUTS,
outputs: OUTPUTS,
params: PARAMS,
producer: false,
};
goofi_signal_sdk::export!(Math, MANIFEST);