goofi v3.0.0

BandFollow

How loud its input is in each band, one band per channel.

The half of a vocoder that listens: wire `out` to `BandFilter.gains` and the shape of this signal lands on that one. Every channel of the input is heard as one signal. In `harmonic` the bands stand on the partials of `pitch` and take its voices in turn, so what it measures is a chord rather than a spread.

BandFollow rs
input
out
Type name
audio:BandFollow
Plane
audio
Tags
analysis
Language
rust
Tier
native
Bundle
audio
Source
node-bundles/audio/BandFollow.rs
Availability
available

Slots

SlotDirectionType
inputinputAUDIOaccepts several cables
outoutputAUDIO

Parameters

band

NameTypeDefaultRangeDoc
pitchfloat0-6 … 6.5in `harmonic`, what the bands stand on, in volts per octave; an audio reference is one voice per channel
gatefloat10 … 1in `harmonic`, what one voice's partials are worth — a released note keeps its pitch, so this is what lets it go; `MidiIn.gate` drops it at once and an `Env` fades it
bandsint162 … 128how many bands leave, one per channel; `BandFilter` needs the same count
lowfloat-1.5-6 … 6.5in `spread`, the lowest band's centre in volts per octave, 0 at C4 — the same units as `Osc.pitch`
highfloat4.25-6 … 6.5in `spread`, the highest band's centre; the rest sit evenly between, so a band is a fixed interval
qfloat40.5 … 20how narrow one band is; near 4 the default sixteen meet without a gap
attackfloat0.0050 … 1seconds to follow a band that gets louder; short keeps the consonants
releasefloat0.050 … 1seconds to follow a band that gets quieter; long smears one word into the next
layoutstringspreadspread | harmonicwhere the bands sit: `spread` evenly from `low` to `high`, or `harmonic` on the partials of `pitch`

Source

The current source of this node, as it stands in the goofi repository at node-bundles/audio/BandFollow.rs.

node-bundles/audio/BandFollow.rs
use goofi_audio_sdk::goofi_core::SlotType;
use goofi_audio_sdk::{
    band_partial, band_volts, hz_of, AudioNode, Block, Lanes, Manifest, OutputDecl, ParamDecl, ParamSpec, SlotDecl, Tag,
    BLOCK,
};

goofi_audio_sdk::params! {
    PITCH = ParamDecl {
        group: "band",
        name: "pitch",
        spec: ParamSpec::Float { default: 0.0, min: -6.0, max: 6.5 },
        expression: None,
        doc: Some("in `harmonic`, what the bands stand on, in volts per octave; an audio reference is one voice per channel"),
        section: 0,
        show: None,
    },
    GATE = ParamDecl {
        group: "band",
        name: "gate",
        spec: ParamSpec::Float { default: 1.0, min: 0.0, max: 1.0 },
        expression: None,
        doc: Some("in `harmonic`, what one voice's partials are worth — a released note keeps its pitch, so this is what lets it go; `MidiIn.gate` drops it at once and an `Env` fades it"),
        section: 0,
        show: None,
    },
    BANDS = ParamDecl {
        group: "band",
        name: "bands",
        spec: ParamSpec::Int { default: 16, min: 2, max: 128, options: &[] },
        expression: None,
        doc: Some("how many bands leave, one per channel; `BandFilter` needs the same count"),
        section: 0,
        show: None,
    },
    LOW = ParamDecl {
        group: "band",
        name: "low",
        spec: ParamSpec::Float { default: -1.5, min: -6.0, max: 6.5 },
        expression: None,
        doc: Some("in `spread`, the lowest band's centre in volts per octave, 0 at C4 — the same units as `Osc.pitch`"),
        section: 0,
        show: None,
    },
    HIGH = ParamDecl {
        group: "band",
        name: "high",
        spec: ParamSpec::Float { default: 4.25, min: -6.0, max: 6.5 },
        expression: None,
        doc: Some("in `spread`, the highest band's centre; the rest sit evenly between, so a band is a fixed interval"),
        section: 0,
        show: None,
    },
    Q = ParamDecl {
        group: "band",
        name: "q",
        spec: ParamSpec::Float { default: 4.0, min: 0.5, max: 20.0 },
        expression: None,
        doc: Some("how narrow one band is; near 4 the default sixteen meet without a gap"),
        section: 0,
        show: None,
    },
    ATTACK = ParamDecl {
        group: "band",
        name: "attack",
        spec: ParamSpec::Float { default: 0.005, min: 0.0, max: 1.0 },
        expression: None,
        doc: Some("seconds to follow a band that gets louder; short keeps the consonants"),
        section: 0,
        show: None,
    },
    RELEASE = ParamDecl {
        group: "band",
        name: "release",
        spec: ParamSpec::Float { default: 0.05, min: 0.0, max: 1.0 },
        expression: None,
        doc: Some("seconds to follow a band that gets quieter; long smears one word into the next"),
        section: 0,
        show: None,
    },
    LAYOUT = ParamDecl {
        group: "band",
        name: "layout",
        spec: ParamSpec::Str { default: "spread", options: &["spread", "harmonic"], refresh: false },
        expression: None,
        doc: Some("where the bands sit: `spread` evenly from `low` to `high`, or `harmonic` on the partials of `pitch`"),
        section: 0,
        show: None,
    },
}

static INS: &[SlotDecl] =
    &[SlotDecl { name: "input", kind: SlotType::Audio, trigger_process: false, multi: true, required: false }];
static OUTS: &[OutputDecl] = &[OutputDecl { name: "out", kind: SlotType::Audio }];

static MANIFEST: Manifest = Manifest {
    tags: &[Tag::Analysis],
    doc: "How loud its input is in each band, one band per channel.\n\
          The half of a vocoder that listens: wire `out` to `BandFilter.gains` and the shape of \
          this signal lands on that one. Every channel of the input is heard as one signal. In \
          `harmonic` the bands stand on the partials of `pitch` and take its voices in turn, so \
          what it measures is a chord rather than a spread.",
    inputs: INS,
    outputs: OUTS,
    params: PARAMS,
};

#[derive(Default)]
struct BandFollow {
    rate: f32,
    ic1: Lanes<f32>,
    ic2: Lanes<f32>,
    level: Lanes<f32>,
}

/// The one-pole step for a time constant; a zero time is a wire.
fn coef(seconds: f32, rate: f32) -> f32 {
    match seconds > 0.0 {
        true => 1.0 - (-1.0 / (seconds * rate)).exp(),
        false => 1.0,
    }
}

impl AudioNode for BandFollow {
    fn channels(&self, _ins: &[u16], params: &[f64], outs: usize) -> Vec<u16> {
        vec![(params[P::BANDS] as u16).max(1); outs]
    }

    fn audio_params(&self, _declared: usize) -> usize {
        2
    }

    fn prepare(&mut self, rate: f64) {
        self.rate = rate as f32;
    }

    fn process(&mut self, b: &mut Block<'_>) {
        let rate = self.rate;
        let (low, high) = (b.scalars[P::LOW], b.scalars[P::HIGH]);
        let k = 1.0 / b.scalars[P::Q].max(0.5);
        let (attack, release) = (coef(b.scalars[P::ATTACK], rate), coef(b.scalars[P::RELEASE], rate));

        let input = &b.ins[0];
        let mut heard = [0.0f32; BLOCK];
        for c in 0..input.channels() as usize {
            let x = input.chan(c);
            for i in 0..BLOCK {
                heard[i] += x[i];
            }
        }

        let (pitch, gate) = (&b.params[P::PITCH], &b.params[P::GATE]);
        let harmonic = b.scalars[P::LAYOUT] as u8 == 1;
        let voices = pitch.channels() as usize;
        let out = &mut b.outs[0];
        let bands = out.channels() as usize;
        let (ic1s, ic2s, levels) = (self.ic1.fit(bands), self.ic2.fit(bands), self.level.fit(bands));
        for band in 0..bands {
            // The bank re-tunes once a block: a note lands on a block edge, and a tan per sample
            // per band buys nothing for it.
            let (volts, voiced) = match harmonic {
                true => {
                    let (voice, partial) = band_partial(band, voices);
                    (pitch.chan(voice)[0] + partial, gate.chan(voice)[0])
                }
                false => (band_volts(band, bands, low, high), 1.0),
            };
            let f = hz_of(volts).clamp(1.0, 0.45 * rate);
            let g = (std::f32::consts::PI * f / rate).tan();
            let a1 = 1.0 / (1.0 + g * (g + k));
            let (a2, a3) = (g * a1, g * g * a1);
            let (ic1, ic2, level) = (&mut ic1s[band], &mut ic2s[band], &mut levels[band]);
            let y = out.chan_mut(band);
            for i in 0..BLOCK {
                let v3 = heard[i] - *ic2;
                let v1 = a1 * *ic1 + a2 * v3;
                let v2 = *ic2 + a2 * *ic1 + a3 * v3;
                *ic1 = 2.0 * v1 - *ic1;
                *ic2 = 2.0 * v2 - *ic2;
                // `k * v1` is the band at unity, where `Filter`'s own band output peaks at `q`.
                let magnitude = (k * v1).abs();
                *level += (magnitude - *level) * if magnitude > *level { attack } else { release };
                y[i] = *level * voiced;
            }
        }
    }
}

goofi_audio_sdk::export!(BandFollow, MANIFEST);

← All nodes