AudioIn
The audio plane crossing into the signal plane.
A stream has no frame of its own — a tap hands over whatever the last blocks held — so `mode` is the framing the analysis asks for rather than the one the clock happened to give: the samples as they came, a window of a fixed length, or one level per fixed run of them.
input
out
- Type name
signal:AudioIn- Plane
signal- Tags
transform- Language
rust- Tier
native- Bundle
signal- Source
node-bundles/signal/AudioIn.rs- Availability
- available
Slots
| Slot | Direction | Type | |
|---|---|---|---|
input | input | AUDIO | |
out | output | ARRAY |
Parameters
audio
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
unit | string | samples | samples | seconds | Seconds uses the incoming audio sample rate. |
mode | string | stream | stream | window | envelope | The samples as the tap hands them over, cut into windows of `size`, or the level of every `size` of them. |
size | float | 1024 | 0.001 … 1000000 | Window length, or length behind each level, in the selected unit. |
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/AudioIn.rs.
//! AudioIn — the audio plane as frames. A tap hands over whatever the last blocks held, so a
//! frame's length is the CLOCK's rather than the analysis's; the modes here are the framing a
//! signal node wants, cut from the samples themselves.
use goofi_core::{Data, SlotType};
use goofi_signal_sdk::{Inputs, Manifest, Node, NodeCtx, NodeResult, OutputDecl, Outputs, ParamDecl, Params, ParamSpec, SlotDecl, Tag};
#[derive(Default)]
struct AudioIn {
/// The samples no frame has carried yet, one lane per channel.
held: Vec<Vec<f32>>,
}
fn read(bytes: &[u8], i: usize) -> f32 {
f32::from_le_bytes(bytes[i * 4..i * 4 + 4].try_into().expect("four bytes"))
}
impl Node for AudioIn {
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 shape = a.shape();
let t = *shape.last().expect("at least one axis");
let lanes: usize = shape[..shape.len() - 1].iter().product::<usize>().max(1);
let mode = p.str("audio", "mode").unwrap_or("stream");
if mode == "stream" {
self.held.clear();
out.set("out", d.clone());
return Ok(());
}
let size = goofi_core::stream::window_count(
p.f64("audio", "size").unwrap_or(1024.0),
p.str("audio", "unit").unwrap_or("samples"), d.meta(),
)?.clamp(1, 1_000_000);
if self.held.len() != lanes {
self.held = vec![Vec::new(); lanes];
}
let src = a.as_bytes();
for (l, lane) in self.held.iter_mut().enumerate() {
lane.extend((0..t).map(|i| read(src, l * t + i)));
}
let kept = self.held[0].len();
let mut shape = shape[..shape.len() - 1].to_vec();
let mut meta = d.meta().clone();
let mut buf = Vec::new();
if mode == "envelope" {
let blocks = kept / size;
if blocks == 0 {
return Ok(());
}
for lane in &mut self.held {
for b in 0..blocks {
let block = &lane[b * size..(b + 1) * size];
buf.extend((block.iter().map(|v| v * v).sum::<f32>() / size as f32).sqrt().to_le_bytes());
}
lane.drain(..blocks * size);
}
meta.set_sfreq(d.meta().sfreq().map(|sf| sf / size as f64));
shape.push(blocks);
} else {
if kept < size {
return Ok(());
}
// The newest window, whole. A window's worth is what a frame carries, so a stream
// running faster than the patch reads it leaves the samples between two windows
// behind rather than the newest ones — latest wins, as every crossing here is.
for lane in &mut self.held {
buf.extend(lane[lane.len() - size..].iter().flat_map(|v| v.to_le_bytes()));
lane.clear();
}
shape.push(size);
}
out.set("out", Data::array_f32(shape, buf, meta).map_err(|e| e.to_string())?);
Ok(())
}
}
static PARAMS: &[ParamDecl] = &[
ParamDecl {
group: "audio",
name: "unit",
spec: ParamSpec::Str { default: "samples", options: &["samples", "seconds"], refresh: false },
expression: None,
doc: Some("Seconds uses the incoming audio sample rate."),
section: 0,
show: None,
},
ParamDecl {
group: "audio",
name: "mode",
spec: ParamSpec::Str { default: "stream", options: &["stream", "window", "envelope"], refresh: false },
expression: None,
doc: Some(
"The samples as the tap hands them over, cut into windows of `size`, or the level of \
every `size` of them.",
),
section: 0,
show: None,
},
ParamDecl {
group: "audio",
name: "size",
spec: ParamSpec::Float { default: 1024.0, min: 0.001, max: 1_000_000.0 },
expression: None,
doc: Some("Window length, or length behind each level, in the selected unit."),
section: 0,
show: None,
},
];
static INPUTS: &[SlotDecl] =
&[SlotDecl { name: "input", kind: SlotType::Audio, trigger_process: true, multi: false, required: true }];
static OUTPUTS: &[OutputDecl] = &[OutputDecl { name: "out", kind: SlotType::Array }];
static MANIFEST: Manifest = Manifest {
tags: &[Tag::Transform],
doc: "The audio plane crossing into the signal plane.\n\
A stream has no frame of its own — a tap hands over whatever the last blocks held — so \
`mode` is the framing the analysis asks for rather than the one the clock happened to \
give: the samples as they came, a window of a fixed length, or one level per fixed run \
of them.",
inputs: INPUTS,
outputs: OUTPUTS,
params: PARAMS,
producer: false,
};
goofi_signal_sdk::export!(AudioIn, MANIFEST);