Threshold

Turn a signal into a decision: one where it is past the level, zero where it is not.

Threshold rs
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

SlotDirectionType
inputinputARRAY
outoutputARRAY

Parameters

threshold

NameTypeDefaultRangeDoc
modestringaboveabove | belowWhether the decision is true above the level or below it.
levelfloat0-1000000000 … 1000000000The value the signal is compared against.
hysteresisfloat00 … 1000000000How far past the level the signal must go to switch back, so noise sitting on the level cannot rattle the decision.
edgeboolfalseEmit one only on the update where the decision turns true, rather than while it holds.
holdfloat00 … 3600How long in seconds a decision stays before it is allowed to switch again.

common

NameTypeDefaultRangeDoc
autotriggerboolfalseRun 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_frequencyfloat00 … 100Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow.
frequency_modestringupdates_per_secondupdates_per_second | seconds_per_updateHow 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.

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);

← All nodes

This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.