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.
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
| Slot | Direction | Type | |
|---|---|---|---|
input | input | AUDIO | accepts several cables |
out | output | AUDIO |
Parameters
band
| Name | Type | Default | Range | Doc |
|---|---|---|---|---|
pitch | float | 0 | -6 … 6.5 | in `harmonic`, what the bands stand on, in volts per octave; an audio reference is one voice per channel |
gate | float | 1 | 0 … 1 | 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 |
bands | int | 16 | 2 … 128 | how many bands leave, one per channel; `BandFilter` needs the same count |
low | float | -1.5 | -6 … 6.5 | in `spread`, the lowest band's centre in volts per octave, 0 at C4 — the same units as `Osc.pitch` |
high | float | 4.25 | -6 … 6.5 | in `spread`, the highest band's centre; the rest sit evenly between, so a band is a fixed interval |
q | float | 4 | 0.5 … 20 | how narrow one band is; near 4 the default sixteen meet without a gap |
attack | float | 0.005 | 0 … 1 | seconds to follow a band that gets louder; short keeps the consonants |
release | float | 0.05 | 0 … 1 | seconds to follow a band that gets quieter; long smears one word into the next |
layout | string | spread | spread | harmonic | where 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.
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);