Skip to content

spectrum — the frequency-axis readout

A line system like the three above, but its figure is not a generator’s geometry: it is the engine’s log-spaced band array, divided into elements by the [spectrum] table. Element 0 is the bottom of the spectrum and the last is the top, so nothing in [params] maps audio to position — that mapping is the scene. What the params say is how the elements look and how far they reach.

  • base — the length every element has before any audio, in world units (the frame is 2 units tall). Deliberately not zero by default: at zero the readout vanishes completely in a silence, which reads as a broken preset. Bind it to time for a resting breath.
  • scale — multiplier on the element’s own band level. The bands read small on real music, the same caveat as bass/mid/treb, so useful values are well above 1.
  • curve — the level-shaping exponent, default 1.0 (exactly linear, the map this scene had before it existed). level^curve, applied to the element’s raw level before the easing; 0.5 is a square root and lower values compress harder, which is how you get a dB-like readout where the quiet elements are legible instead of stubbed. Per element, so it can be walked with index. The level is floored at 0 and the exponent clamped to [0.05, 4.0], so no expression can produce a NaN length.
  • radiusradial_ring only: the inner circle the spokes stand on. No effect on bars or polyline.
  • span — the figure’s half-width in world units, default 1.0. bars and polyline only (the ring is sized by radius).
  • baseline — the world y the elements stand on, default -0.85, so bars grow upward from near the bottom edge. bars and polyline only.

curve and scale are not independent, and the factor is large. Measured typical band levels are ~0.02–0.05. At curve = 0.5 a level of 0.03 becomes 0.173 — a 5.8x boost — so a preset adopting a curve has to bring scale down by roughly that factor or the readout leaves the top of the frame. That is why the default is exactly 1.0: a curve is opt-in, and opting in means retuning scale in the same edit. Say so in the preset’s header comment, with the factor.

What curve does not disturb is the timing. The easing runs on the curved value, which is the value you see, so a fall’s time constant is exactly the release you wrote — at curve = 1.0, at 0.5, at 0.25. The two knobs are independent in time; what a curve changes is amplitude. (Curving after the easing instead would have made the effective release release / curve, silently doubling every fall at curve = 0.5. That ordering was measured and rejected — ADR-0040.) Neither ordering produces an even fall: a one-pole is exponential, so it covers the first half of its travel in about 30 % of its settling time at any curve.

How wide the readout is, and why span is not a “fill the frame” switch. The line renderer divides world x by the target’s aspect — the same rule every line scene follows, and the reason a radial_ring comes out a circle rather than an ellipse. span is therefore a world quantity, not a fraction of the frame: the default 1.0 makes bars and polyline span the frame’s height in pixels, which is about 56 % of its width at 16:9. span ≈ 1.78 fills a 16:9 frame edge to edge — and leaves an ultrawide short, because 1.78 is still 1.78 there. That is correct behaviour for a world quantity and the reason there is deliberately no fit/auto mode: a scene that sized itself from its render target’s aspect is exactly the trap ADR-0037 exists to forbid, and it has shipped twice in this codebase. Pick the span that suits the frames you care about, or bind it and accept that the number means world units. Note zoom is still not a substitute for either lever — it scales y with x, so widening the comb also lifts it.

baseline = 0 is how you get a centre-mirrored readout. With the default -0.85, mirror_reflect produces two combs pinned against the top and bottom edges growing toward each other — which reads as a bug and is not. The geometry mirror reflects across the x-axis on every line scene, so a figure standing at -0.85 reflects to one hanging from +0.85. Move the feet onto the axis and the same mirror gives the symmetric “landscape and its reflection”: bars up, their copy down, sharing one foot line on the frame centre. pan_y cannot substitute — the mirror runs on world coordinates in update() while the view transform is applied later in the shader, so panning moves the mirrored pair together.

  • rotation — turns the whole figure about the frame centre, in radians, on every layout. On the ring it is the natural motion; on bars and the polyline it tilts the readout, which is what makes those two worth folding with mirror_*.
  • hue_spread — walks the palette across the elements: 0 (the default) colours the whole figure one hue, 1 spans the full palette from the lowest element to the highest, so you can see where a peak is without counting positions. On radial_ring a spread of exactly 1 makes the wrap continuous in colour as well as in position.

Per-element bindings. This is the one system where a single [params] expression can say something different about each element. A binding whose text names index — the element’s normalized 0..1 position along the frequency axis — is evaluated once per element instead of once per frame:

[params]
thickness = "0.01 + bin(index) * 0.05" # thick where that element's band is loud
base = "0.16 + index * 0.12" # a longer rest toward the quiet top end
hue = "index * 0.3 + time * 0.02" # colour walked by hand instead of hue_spread

The params that genuinely vary per element are the ones describing a single element — base, scale, curve, thickness, brightness, hue. The rest describe the whole figure (span, baseline, radius, rotation, glow, hue_spread, palette_mix, saturation, the view transform, the mirror), so a series aimed at one of those takes its index = 0 value rather than being dropped. index reads 0 on every other system, and a [smoothing] entry naming a per-element binding is a surfaced warning — ease the levels with [spectrum] smoothing instead. Full semantics in Expression language.

What this system honors, since a silent no-op would be worse than an absence: the shared view transform, the geometry mirror, the palette surface ([palette]/[palette_b]/palette_mix/saturation, sampled per element — this is the only line system that reads [palette]; the other three still colour from the built-in cosine), and all the engine stages (bg_*, trails, kaleido_*, bloom_*, exposure, ink_*). On radial_ring the geometry mirror is close to a no-op for the same reason it is on star_pattern — the figure is already rotationally symmetric about the frame centre, so the copies land on the original. On bars and polyline it is genuinely transformative.

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