goofi v3.0.0

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.

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

SlotDirectionType
inputinputAUDIO
outoutputARRAY

Parameters

audio

NameTypeDefaultRangeDoc
unitstringsamplessamples | secondsSeconds uses the incoming audio sample rate.
modestringstreamstream | window | envelopeThe samples as the tap hands them over, cut into windows of `size`, or the level of every `size` of them.
sizefloat10240.001 … 1000000Window length, or length behind each level, in the selected unit.

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/AudioIn.rs.

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

← All nodes