Threshold
Turn a signal into a decision: one where it is past the level, zero where it is not.
input
out
- Type name
signal:Threshold- Plane
signal- Tags
control- Language
rust- Tier
native- Bundle
signal- Source
node-bundles/signal/Threshold.rs- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
input | input | ARRAY | |
out | output | ARRAY |
Parameters
threshold
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
mode | string | above | above | below | Whether the decision is true above the level or below it. |
level | float | 0 | -1000000000 … 1000000000 | The value the signal is compared against. |
hysteresis | float | 0 | 0 … 1000000000 | How far past the level the signal must go to switch back, so noise sitting on the level cannot rattle the decision. |
edge | bool | false | Emit one only on the update where the decision turns true, rather than while it holds. | |
hold | float | 0 | 0 … 3600 | How long in seconds a decision stays before it is allowed to switch again. |
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/Threshold.rs.
//! Threshold — a signal becomes a decision: one where it is past the level, zero where it is not.
//! Its output is what a `reset` or a `trigger` pulse elsewhere in the patch references.
use goofi_core::{Data, SlotType};
use goofi_signal_sdk::{Inputs, Manifest, Node, NodeCtx, NodeResult, OutputDecl, Outputs, ParamDecl, Params, ParamSpec, SlotDecl, Tag};
/// One element's decision and when it last changed.
#[derive(Clone, Copy, Default)]
struct State {
high: bool,
since: f64,
}
#[derive(Default)]
struct Threshold {
states: Vec<State>,
}
impl Node for Threshold {
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 level = p.f64("threshold", "level").unwrap_or(0.0) as f32;
let band = p.f64("threshold", "hysteresis").unwrap_or(0.0).abs() as f32;
let below = p.str("threshold", "mode").unwrap_or("above") == "below";
let edge_only = p.bool("threshold", "edge").unwrap_or(false);
let hold = p.f64("threshold", "hold").unwrap_or(0.0).max(0.0);
let count = a.as_bytes().len() / 4;
if self.states.len() != count {
self.states = vec![State::default(); count];
}
let mut buf = Vec::with_capacity(count * 4);
for (x, state) in a.as_bytes().chunks_exact(4).zip(&mut self.states) {
let v = f32::from_le_bytes(x.try_into().expect("four bytes"));
// The band is crossed from whichever side the state is on, so noise at the level
// cannot rattle the decision.
let past = if state.high { v > level - band } else { v > level + band };
let want = if below { !past } else { past };
let mut rose = false;
if want != state.high && c.now - state.since >= hold {
rose = want;
state.high = want;
state.since = c.now;
}
let emit = if edge_only { rose } else { state.high };
buf.extend_from_slice(&if emit { 1.0f32 } else { 0.0f32 }.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: "threshold",
name: "mode",
spec: ParamSpec::Str { default: "above", options: &["above", "below"], refresh: false },
expression: None,
doc: Some("Whether the decision is true above the level or below it."),
},
ParamDecl {
group: "threshold",
name: "level",
spec: ParamSpec::Float { default: 0.0, min: -1.0e9, max: 1.0e9 },
expression: None,
doc: Some("The value the signal is compared against."),
},
ParamDecl {
group: "threshold",
name: "hysteresis",
spec: ParamSpec::Float { default: 0.0, min: 0.0, max: 1.0e9 },
expression: None,
doc: Some(
"How far past the level the signal must go to switch back, so noise sitting on the \
level cannot rattle the decision.",
),
},
ParamDecl {
group: "threshold",
name: "edge",
spec: ParamSpec::Bool { default: false },
expression: None,
doc: Some("Emit one only on the update where the decision turns true, rather than while it holds."),
},
ParamDecl {
group: "threshold",
name: "hold",
spec: ParamSpec::Float { default: 0.0, min: 0.0, max: 3600.0 },
expression: None,
doc: Some("How long in seconds a decision stays before it is allowed to switch again."),
},
];
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::Control],
doc: "Turn a signal into a decision: one where it is past the level, zero where it is not.",
inputs: INPUTS,
outputs: OUTPUTS,
params: PARAMS,
producer: false,
};
goofi_signal_sdk::export!(Threshold, MANIFEST);This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.