Hilbert
How big a signal's swing is, where in its cycle it stands, and how fast that is turning.
input
envelope
phase
frequency
- Type name
signal:Hilbert- Plane
signal- Tags
analysis- Language
rust- Tier
native- Bundle
signal- Source
node-bundles/signal/Hilbert.rs- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
input | input | ARRAY | |
envelope | output | ARRAY | |
phase | output | ARRAY | |
frequency | output | ARRAY |
Parameters
hilbert
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
axis | int | -1 | -8 … 7 | Which axis holds the samples. -1 is time. |
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/Hilbert.rs.
//! Hilbert — the analytic signal of a frame: how big the swing is, where in the cycle it is, and
//! how fast that is turning. Per frame, so it follows a Buffer.
use goofi_core::{resolve_axis, stream, Data, SlotType};
use goofi_signal_sdk::{Inputs, Manifest, Node, NodeCtx, NodeResult, OutputDecl, Outputs, ParamDecl, Params, ParamSpec, SlotDecl, Tag};
use rustfft::{num_complex::Complex32, FftPlanner};
struct Hilbert {
planner: FftPlanner<f32>,
}
impl Default for Hilbert {
fn default() -> Hilbert {
Hilbert { planner: FftPlanner::new() }
}
}
impl Node for Hilbert {
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.assert_ndims().at_least(1)?;
let dim = resolve_axis(p.i64("hilbert", "axis").unwrap_or(-1), a.shape().len())?;
let n = a.shape()[dim];
if n < 4 {
return Err(format!("needs at least 4 samples along the axis, got {n}").into());
}
let sfreq = d.meta().sfreq().ok_or("this node needs a frame that carries its sample rate")?;
let forward = self.planner.plan_fft_forward(n);
let inverse = self.planner.plan_fft_inverse(n);
let lanes = stream::lanes(a.shape(), dim, a.as_bytes());
let (mut env, mut ang, mut hz) = (Vec::new(), Vec::new(), Vec::new());
let mut scratch = vec![Complex32::default(); n];
for lane in &lanes {
for (c, x) in scratch.iter_mut().zip(lane) {
*c = Complex32::new(*x, 0.0);
}
forward.process(&mut scratch);
// The one-sided spectrum, doubled: the negative half of a real signal says nothing new.
let half = n / 2;
for (k, c) in scratch.iter_mut().enumerate() {
if k == 0 || (n % 2 == 0 && k == half) {
continue;
} else if k < half {
*c *= 2.0;
} else {
*c = Complex32::default();
}
}
inverse.process(&mut scratch);
let z: Vec<Complex32> = scratch.iter().map(|c| c / n as f32).collect();
env.push(z.iter().map(|c| c.norm()).collect::<Vec<f32>>());
let phase: Vec<f32> = z.iter().map(|c| c.arg()).collect();
// The turn between two samples, brought into one revolution, is the frequency.
let mut rate = Vec::with_capacity(n);
for k in 0..n {
let (a, b) = (phase[k.max(1) - 1], phase[(k + 1).min(n - 1)]);
let step = if k == 0 || k == n - 1 { 1.0 } else { 2.0 };
let mut turn = (b - a) as f64;
while turn > std::f64::consts::PI {
turn -= std::f64::consts::TAU;
}
while turn < -std::f64::consts::PI {
turn += std::f64::consts::TAU;
}
rate.push((turn / step * sfreq / std::f64::consts::TAU) as f32);
}
ang.push(phase);
hz.push(rate);
}
let shape = a.shape().to_vec();
for (name, lanes) in [("envelope", &env), ("phase", &ang), ("frequency", &hz)] {
let buf = stream::unlanes(&shape, dim, lanes);
out.set(name, Data::array_f32(shape.clone(), buf, d.meta().clone()).map_err(|e| e.to_string())?);
}
Ok(())
}
}
static PARAMS: &[ParamDecl] = &[ParamDecl {
group: "hilbert",
name: "axis",
spec: ParamSpec::Int { default: -1, min: -8, max: 7 },
expression: None,
doc: Some("Which axis holds the samples. -1 is time."),
}];
static INPUTS: &[SlotDecl] = &[SlotDecl {
name: "input",
kind: SlotType::Array,
trigger_process: true,
multi: false,
required: true,
}];
static OUTPUTS: &[OutputDecl] = &[
OutputDecl { name: "envelope", kind: SlotType::Array },
OutputDecl { name: "phase", kind: SlotType::Array },
OutputDecl { name: "frequency", kind: SlotType::Array },
];
static MANIFEST: Manifest = Manifest {
tags: &[Tag::Analysis],
doc: "How big a signal's swing is, where in its cycle it stands, and how fast that is turning.",
inputs: INPUTS,
outputs: OUTPUTS,
params: PARAMS,
producer: false,
};
goofi_signal_sdk::export!(Hilbert, MANIFEST);This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.