Skip to content

bin(x) — reaching the spectrum

bass/mid/treb are three numbers for the whole audible range. bin(x) reads the same analysis at whatever resolution you point it at: the engine’s log-spaced band array, sampled at a normalized position and interpolated between the two adjacent bands. A preset never names the band count, so nothing here breaks if the engine ever re-bands.

[params]
# Morph an attractor's shape from the low-mids rather than from a whole band.
a = "1.4 + bin(0.15) * 0.4"
# A treble-region shimmer, independent of what the bass is doing.
brightness = "0.6 + bin(0.85) * 0.8"

It is total: bin(0) is the lowest band, bin(1) the highest, anything outside 0..1 clamps, and a NaN argument reads the lowest band. It never errors and never rejects a preset at load.

[!WARNING] bin(x) is a narrow probe, not a region average. One call sees about two of the 64 bands, so a handful of calls spot-samples a region rather than integrating it. The axis underneath it is logarithmic end to end — see the table.

The band edges are laid out on a log curve from 35 Hz to 18 kHz (core/src/dsp/fft.rs, edges_hz[k] = 35 × (18000/35)^(k/64)), and a second, longer analysis window feeds every band below the 246 Hz crossover, so no band is starved and the curve is the truth end to end (ADR-0049). No part of the axis is linear: a single-window analysis would floor each band at one FFT bin (23.4 Hz at 48 kHz) and bind the bottom half of the axis that way.

Derived from fft.rs’s edge formula and expr.rs’s bin(), which places the probe at band-space position x × 63 and interpolates — so what it listens to is the centre of that interpolation, not of one band:

xlands onband width there
0.00~37 Hz4 Hz (1.8 semitones)
0.10~68 Hz7 Hz (1.8 semitones)
0.20~126 Hz13 Hz (1.8 semitones)
0.31~247 Hz25 Hz (1.8 semitones) — the crossover sits here
0.41~457 Hz47 Hz (1.8 semitones)
0.50~794 Hz81 Hz (1.8 semitones)
0.75~3.7 kHz378 Hz (1.8 semitones)
0.84~6.4 kHz657 Hz (1.8 semitones)
1.00~17.1 kHz1.8 kHz (1.8 semitones)
  • Every band is 1.8 semitones wide, everywhere. That uniformity is the axis being genuinely logarithmic, and it is what the second window bought. The old array’s bottom was its coarsest region — band 0 spanned a full octave in one number — and that is simply no longer the case.
  • 35 × 514.3^x is now accurate across the whole axis, to within a few per cent. The old warning that it is “up to 2.9× wrong below the crossover” was true of the old layout and is false of this one. The residual is the x × 63 step and the half-band interpolation offset; if you need better than ~5 %, read the table rather than the formula.
  • The mapping no longer moves with the sample rate. hi = min(18 kHz, sr × 0.45), so at 44.1, 48 and 96 kHz the edges are identical. (What does still vary with rate is the resolution behind the low bands, since both windows are sized in samples — design-backlog 0032.)
  • bin(0.02) is still not “the kick”. It is a ~1.7-semitone sliver near 40 Hz. Sweep the value while listening rather than assuming.

Averaging a few calls does not integrate a region — it spot-samples one. Measured against a 6.5 kHz tone: bin(0.84) reads 0.094 while bin(0.82) and bin(0.88) both read exactly zero. Samples a few hundredths apart in x step over bands rather than across them, so a narrow peak between two probes is invisible to all of them. There is no range-integrating companion (bin_range(lo, hi)) yet — until there is, use bin() when you want selectivity and bass/mid/treb when you want a region. Combining both is usually right: a band scalar for the body, a bin() term for the detail, so the parameter never goes still on material that misses the probe.

Values come off the same normalization as the bands: a full-scale sine reads near 1.0 in its band, and ordinary music reads small, so multiply up and clamp exactly as you would with bass.

Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.