Skip to content

Plan: audio that answers the music

Approved 2026-10-30 and revised 2026-11-03 (the plan change), built in small steps judged on the bench, so the steps below follow what the music shows. The goal is the best visible answer to the music: silence is dark, a quiet passage stays quiet, a loud one is loud, the bars sit balanced across the spectrum, every band answers its own range and not its neighbor's, the bass separates note from note, a kick drum is found whatever song is playing, the beat lands on the beat, and the lights follow the harmony. The least to set follows from it, since a level that learns the song beats any knob set for one song. The analysis stays one pipeline on every board; calibration against a measured chain is not the goal and goes to the backlog.

The yardstick is twofold. For the bands it is the real-time analyzer, whose third-octave filters a standard defines (IEC 61260-1). For what is read from them it is music information retrieval, the field whose libraries (Essentia, librosa, aubio) describe music to a machine: loudness, harmony, spectral balance, onsets per band, and tempo with beat phase, in that order. Step 1 builds the bands, steps 2 to 6 one descriptor each, each step reading what the ones before it give: harmony comes third, gated by the level, so the bass note, the note onsets and the downbeat build on it. Two things the field settles that this plan follows: a band per instrument is not a thing (instruments overlap in frequency, and separating them is a neural net on a GPU), while the drum kit does separate by range; and genre presets lose to adaptation (a floor, a gate and a gain that learn the dynamics give a quiet classical passage and a loud rock chorus the same treatment with no switch).

Every step lands twice. AudioSpectrumEffect shows the core audio as it is, the developer's view of what the step changed, and one showcase effect shows it the way an end user picks it for a show. BeatRipplesEffect is the model: stones dropped on the music, readable at a glance by an audience. A signal nobody draws is not verified, so the pair is part of the step, not a follow-up.

The design basis

  • One pipeline, no engine select (CLAUDE.md, principle 3): one analysis path, the same on every chip: the filter bank, with every output derived from its bands.
  • The textbook. A third-octave band is the power through its band-pass filter, as IEC 61260-1 defines it and every digital analyzer implements it: a multirate filter bank, octave by octave, where one octave's filters run again at half the rate for the octave below. Band power makes pink noise read flat, the spectrum real music resembles and the signal an analyzer is calibrated with, so balanced bars follow by construction. A noise gate opens fast and closes slow with hysteresis, as every studio gate does. Automatic gain is one offset that holds the loudest band at the top, with a fast attack and a slow release, as a compressor's make-up gain does.
  • Study, credit, write fresh. MoonLight's audio path is its own implementation, from the standards and standard DSP; the people whose thinking it studies are credited by name below and in the header's @attribution.
  • Measured on synthesized signals on the desktop, then judged by eye on the bench: the product owner's eyes are the measurement for a light show.

What exists

  • AudioService (AudioService.h): a 512-sample block, a DC blocker, a Hann window, esp-dsp's FFT on the ESP32 and a DFT on the desktop, a frame every 23 ms at the default 22,050 Hz.
  • The bands (AudioBands.h): sixteen log-spaced bands, each the loudest bin of its slice. A bin is 43 Hz wide, so below about 130 Hz each band owns one bin, a tone leaks into the next band about 6 dB down, and two bass notes a semitone apart land in the same band. The learner in BandConditioner corrects each band's level, which lifts the leakage with it.
  • The level path: floor is a silence threshold and a window bottom at once; levels switches between a manual gain and a learned window; BandConditioner and LevelConditioner learn a floor and a peak each and apply a compressor's ratio. Three mechanisms share the one job of "show the music at the right size", from three places.
  • OnsetDetector: one onset for a hit anywhere in the spectrum, so a kick and a snare look alike.
  • The readers: thirteen effects read bands[16], the WLED sync packet carries sixteen, and MoonLive reads audioBand(i) and audioPeakHz.

Prior art

Audio-reactive lighting has a lineage (WLED-SR, WLED-MM, MoonLight), and four people's thinking is weighed here, their ideas and our analysis.

Frank (softhack007), main author of the WLED-MM audioreactive usermod, who the product owner worked alongside for years: the adaptive noise gate that step 2 builds. His concept, analyzed with his permission: a standard noise gate on the time-domain level, open fast and close slow with hysteresis, keyed to a learned floor so it self-calibrates to whatever source is connected, with thresholds as factors of that floor rather than absolute counts. His constraints bind step 2: the gate decides "is there sound" and leaves scaling to the gain; it reads the RMS the level path already computes, so it adds no delay; the whole chain stays under 30 ms, so every smoother is a one-pole.

LedFx, the host-side effect engine whose whole purpose is audio reactivity: its mel-spaced bands and per-band asymmetric smoothing, reached independently of the WLED lineage. Mel and log coincide above 1 kHz, and step 1 takes the standard's third octaves; the asymmetric smoothing is the attack and release step 2 gives the gain.

Damian (DedeHai), WLED: the fixed-point FFT for chips without an FPU. Every MoonLight audio target has one, so the bank is float; a fixed-point bank, on his reasoning, is the lever if a C3 ever analyzes audio.

Troy (troyhacks)

MoonModules: his AudioReactive rewrite, branch troy/hdmi-on-p4-experimental, commit 6c4615f4 of 2026-11-03, running on the bench's Olimex ESP32-P4-PC at 192.168.1.223 and checked against a hardware analyzer.

  • No FFT. 31 ISO 266 bands from two filter banks: the bottom 8 (20 to 100 Hz) on the input decimated by 16 to 3 kHz, behind a 4th-order Butterworth anti-alias filter, and the top 23 at the full 48 kHz. Each band is a 4th-order Butterworth band-pass, two second-order sections, solved per band so its equivalent noise bandwidth is 0.85 of the nominal width.
  • Measured on his design: the leakage into the neighboring band is −12.2 dB at the bottom and −7.8 dB at the top; the two banks cost 0.12 and 0.99 ms per 10.7 ms frame on the P4, about 2.1 ms of analysis per frame in all.
  • The display: sixteen bars as a subset of the 31 picked by a pattern that shifts them −3 to +3 bands, one-pole smoothing at 8 ms attack and 60 ms release, a dB window auto-ranged to the loudest band.
  • A tempo tracker (ar_tempo.h): onsets from the 40 to 160 Hz energy of the 3 kHz stream, a phase-locked comb that scores each candidate period by how tightly the onsets cluster in phase, a PLL grid and a weak prior at 125 BPM; median hit rate 0.817 on a hundred house tracks. Prior art for step 5.

Two decisions carry over as the plan's own:

Decision What it does Why it matters here
A unit contract Every band is the shaped power through its filter, so a tone at a band's center reads exactly its own power and pink noise reads flat A test asserts "balanced bars" and "separated bands" instead of a learner correcting for the estimator
Four quantities, kept apart The level is the RMS of the fresh audio in dB; the floor snaps down at once and creeps up with a 10 s time constant, only while the level sits within 3 dB of it; the gate opens at squelch dB above the floor and shuts 4 dB lower; the AGC is one dB offset holding the loudest band at 0 dB, 20 ms attack, 1,500 ms release, frozen while the gate is shut Nothing reads its own output, so the deadlocks of one number doing four jobs are impossible; our three mechanisms approximate this from three places

Anjin's measurements, and the trackers they test against

Anjin, on the MoonModules Discord (2026-11-03), ran a faithful port of Troy's tracker and his own filter-bank front end through labelled sets, against Essentia, the QM beat tracker, Beat This!, madmom's TempoDetector and TCN:

  • Troy's tracker: 97% within 2% of the label on house, level with the best offline methods; about 70% on mixed EDM and 47% on short loops of eight genres. The 125 BPM prior holds it near 125, and the comb's chance score rises with tempo, so noise locks at about 130, as .223 showed: 127 to 128 BPM from the microphone's noise floor before any music arrived. On a microcontroller nothing he tested is as small or as fast.
  • His filter-bank front end: 8 bands with edges at 30, 100, 200, 400, 800, 1,600, 3,200, 6,400 and 16,000 Hz, one RBJ band-pass section each at the full rate, the rise in each band's level summed into a flux read 200 times a second, no FFT on the beat path: 88.4% on Beatport and 75.3% on the loops, with a fair periodicity stage. A sharper bank, he expects, gives cleaner input still, since its low bands separate the kick from the bass.
  • Agreement as confidence: where independent trackers agreed, they were right 99 to 100% of the time, so two trackers that agree are a lock that costs almost nothing.
  • Filter delay: a band-pass delays its band by about the inverse of its bandwidth, tens of milliseconds below 100 Hz against under one for the hats, so onsets compared across bands, a tom sweep and the downbeat need each band's delay measured with a click and subtracted.
  • What to test on: known tempos across 40 to 160 BPM, and drum and bass in particular, at 170 to 175 BPM with a half-time feel near 87, where a 125 prior invites the octave error. His harness and data are reusable, offered for MoonLight's desktop build.
  • More from the thread (2026-11-03): the comb's score for random onsets is 0.39 at 90 BPM and 0.58 at 175, since its ±70 ms window covers more of a short beat, and noise locks near 130 at about 0.45, so a raw score is no confidence until the comb is chance-corrected; 12 s of picked kicks is thin evidence, and a longer memory or the comb run over the continuous onset curve does better; Troy's tracker tends to fail at double the tempo and Anjin's at half, opposite directions that make them a strong pair. A reference tracker, BpmDj (an offline tool; not Troy's numbers), reads 89.8% within 2% on 774 Beatport previews, but drum and bass only 28%, with 55% double or half, mostly label errors (Beatport lists 116 to 119 where trackers answer 174 to 178); on 296 held-out Freesound loops Anjin's fold reads 75.3%, BpmDj 65.5%, QM 63.2%, madmom's comb 61.5%, Beat This! 57.4% and Troy's 47.0%. Anjin's Beatport figure reads 88.4% in one message and 90.6% in his table, possibly two variants, to be asked before it is cited further. The material: the Freesound Loop Dataset, with a 12 s warm-up per loop, and the Beatport EDM Key Dataset for key.

The textbook behind it: Scheirer (Tempo and beat analysis of acoustic musical signals, JASA 1998) is the filter-bank tracker this all descends from: a few bands, each envelope differentiated and half-wave rectified, summed, and a bank of comb resonators for the tempo. Goto's real-time beat tracker found the bar boundaries from chord changes, the downbeat of step 6. Faraldo's key profiles for electronic dance music beat the classical profiles on dance music in Anjin's tests, the profiles of step 3. Tempo is scored as MIREX scores it: accuracy 1 within 4% of the label, accuracy 2 also forgiving the double, half and triple.

Twelve to the octave: notes and harmony

Prior art for step 3, the same bank at twelve bands per octave, gathered 2026-11-03:

  • Müller and Ewert's Chroma Toolbox (ISMIR 2011, AudioLabs Erlangen), MATLAB and offline: 88 bands on the piano's pitches, A0 to C8, from a multirate filter bank of elliptic filters at 22,050, 4,410 and 882 Hz. The textbook precedent for the design. Six banks shifted by fractions of a semitone serve recordings not tuned to A440, which a twelve-band bank needs too, as a tuning estimate before the notes.
  • ColorChord (Charles Lohr), on an ESP32 with WS2812B LEDs: the notes heard, folded into one octave, as LED colors. The closest prior art for notes to color on a microcontroller.
  • The Chord Colourizer (2025), Python and librosa: chords and key from a chromagram, averaged over 4 s chunks, shown only above a confidence score, since transients misclassify. LEDs follow.
  • A patent fires lighting cues on a detected chord sequence and changes the stage lighting on a detected chorus.
  • WLED's Rocktaves maps the FFT's one peak to a note within the octave: one note, not a note spectrum.

What is new here is the combination: one bank, live on an ESP32-P4 or S3, giving the analyzer's bars, WLED's sixteen, the notes and the harmony at once, inside a lighting system with layers, drivers and audio sync; harmony-driven effects are new in the WLED lineage. Whether it stands out is decided by the effects, not the bank: an effect that visibly does what an FFT effect cannot, such as a palette that changes exactly on the chord change.

What a full mix allows, by the field's results:

Signal Realistic Why
The moment the harmony changes yes needs only that the twelve pitch classes shifted, not which chord; the standard harmonic-change detector is robust and cheap
A color from the harmony yes the same chord always gives the same color, and a misread gives a near color rather than a visible error
Major or minor, the key mostly needs 10 s or more of music: a slow mood color
The chord's name sometimes templates over the pitch classes get roughly 60 to 70% right on pop, neural nets roughly 80 to 85%: too often wrong to show a name
The bass line's note in sparse mixes the kick shares the range, and low notes need 0.3 to 0.6 s to separate
The melody, and its octave no every note also lights its overtones, and the octave is the classic failure of pitch detection

A solo instrument through line-in does much better than a full mix, and a MIDI instrument gives the notes exactly and at once.

Steps

Each step ends in a desktop test, its two effects, and a bench look. The bench's songs, for the product owner and for end users, are one playlist: MoonLight audio test songs.

1. The bands by the book ✅

  • The bank (FractionalOctaveBank.h, built 2026-11-03, B bands to the octave and used at three). A third-octave filter bank, multirate octave by octave: the top two octaves' band-pass filters, each three second-order sections, run at the full rate; a low-pass halves the rate, and the same three filters measure each octave below at the rate that puts it in the same place, ten octaves deep. 31 bands at 48 kHz, 20 Hz to 20 kHz, with base-2 octaves, which IEC 61260-1 allows beside base-10, so the centers sit within 2.5% of their ISO 266 labels. A band's value is the power through its filter, through a one-pole detector.
  • What derives from it. The sixteen bands are picked from the 31 as Troy's default pattern picks them, so a WLED receiver sees the bars a WLED-MM sender sends. Flux and onset are recomputed from the bands. The dominant frequency (peakHz, the sync packet's major peak) is the loudest band, its position interpolated from its two neighbors, whose ratio the known filter shape turns into an offset; the FFT goes.
  • The rate (decided 2026-11-03): 24 kHz on every board. 24 kHz carries the 28 bands from 20 Hz to 10 kHz, the 16 WLED bars among them, at half the bank's cost at 48 kHz; the three bands above 10 kHz carry cymbal sheen and the converter's own noise rather than beats, and WLED-MM's bars stop at 10 kHz too. 48 kHz adds them for an analyzer view; 22,050 Hz would lose the sixteenth bar at 10 kHz. The rate is stored by its name, 24000 sitting between 22050 and 44100.
  • AudioSpectrumEffect draws 31 bars while the 31 are live, sixteen otherwise.
  • Showcase: WaterfallEffect, a spectrogram scrolling down the matrix, one column per band, the newest frame on top. A sine sweep draws a clean diagonal, leakage shows as a smear beside it, two bass notes draw two lines, and a hit draws a sharp edge.
  • Desktop tests: synthesized pink noise reads every band within 1 dB of flat, white noise rises 3 dB an octave; a sine at each band's center reads its own power within 0.5 dB and the adjacent bands at least 16 dB lower, 11.5 in the top octave where the bilinear transform squeezes the filters toward Nyquist (measured 2026-11-03: 17 to 21 dB below 10 kHz, 12 dB between the 16 and 20 kHz bands); 50 Hz and 60 Hz land in different bands; a sweep lights the bands in order and reads back as peakHz within a set percentage of its frequency; a synthesized kick still fires onset while steady pink noise does not; the bank's cost per block is recorded.
  • Bench: a sine sweep and pink noise through the line input, side by side with .223 running Troy's bank on the same source, and the cost measured on a classic ESP32 before anything builds on it.
  • Each band's delay, measured and pinned (2026-11-03). A click through the bank reaches each band's peak after 118 ms at 31.5 Hz, 67 at 63 Hz, 42 at 100 Hz, 19 at 250 Hz, 6 at 1 kHz and 1 ms from 6.3 kHz up, the band-pass's own delay and, below 400 Hz, the halving low-passes'; a test pins it. The halving delay stays a design criterion beside the cost: a higher corner or a halving by four per stage shortens it.
  • Measured on the boards (2026-11-03). The bands' share of one core, on the card as cpu: 6.0% on the P4 at 48 kHz (470 FPS on .221), 16.3% on the S3 at 48 kHz (350 FPS, the FFT release ran 320), 12.3% on the classic ESP32 at 22,050 Hz (82 to 86 FPS on a Dig-Next-2, the October build ran 89). The bank is fed a block at a time, each octave's three bands side by side in plain C, and the analysis sits in internal RAM; fed a sample at a time it cost three to four times more, and esp-dsp's generic Xtensa biquad kernel made it slower still, so esp-dsp stays out.
  • The audio task (2026-11-03). The read and the analysis run on a task of their own with a blocking read, as Troy's does, the frames passing to the render tick through a single-producer single-consumer ring; a board without tasks analyzes in the tick. Read in the render tick, a slow frame lost samples: the P4 delivered 47,139 a second for 48,000 and the classic ESP32 about 44,600, measured as the input's real rate on the card. On the task the P4 delivers 48,048 at 500 to 545 FPS. Its priority is 6, over the frame encoder's 5: a lost sample is lost for good while a late frame is only late, and below the encoder a busy core starved it, the Waveshare's 64 by 64 panel to a third of its samples and the classic at 48 kHz to 39,000. At 24 kHz the Waveshare delivers 24,066 at 148 to 155 FPS with the bands at 7.5% of a core. The input's DMA ring holds 60 ms at any rate.
  • Built (2026-11-03): the bank and its tests, AudioService on it with the FFT and its platform seam gone, the 31 in AudioFrame and conditioned like the sixteen, one 10 ms detector for every band, AudioSpectrumEffect at 31 or sixteen bars, WaterfallEffect, and the cpu readout; on the desktop, the P4, the S3 and the classic ESP32.

2. One level path, no knobs ✅

  • Built (2026-11-04), in AudioDynamics.h, pure maths beside the bank: the level, each block's RMS in dBFS; the floor, the quietest block of the last 10 to 20 s, minimum statistics (Martin 2001), so it follows a room growing noisier by any step and forgets a dip, as the Waveshare's mic start and the pre-commit review showed the first design, creeping only within 3 dB, could not; the bands' window stops at a floor of their own, the quietest loudest band, since a band's power sits under the RMS; the gate, opening squelch dB over the floor and closing 4 dB lower (Frank's adaptive gate, under his constraints above), after 250 ms once the music has left the window, as stopped music on line-in does, else after 3 s, since a song without a pause lifts the floor into its quiet moments and a soft passage dips near it (.221 on Satie, 2026-11-04; holding the floor down instead opened the gate on the line's silence); the top, following the loudest band with a 20 ms attack and a 3 s release, only while the level is over the close threshold, so a pause leaves it where the music had it. The bands map through the 30 dB window under the top, Troy's RtaRange and a hardware analyzer's scale range (40 dB until 2026-11-04, when his settings showed the contrast a narrower window gives), and the level through a top of its own. The window's bottom never sinks under the floor: near the floor the window narrows rather than the top rising, so a quiet passage is lifted only as far as the room's noise allows. That bound answers the lift cap the design left open.
  • Removed: BandConditioner, LevelConditioner, the levels and gain controls, the conditioner's ratio and maximum gain, and the two learned tables. floor is renamed squelch, as WLED-MM and radios name it; MIGRATING carries both.
  • Desktop tests: silence keeps the gate shut and the bars dark; music opens it; music shorter than the window leaves the floor alone; the floor follows a 10 dB step and forgets a dip; the bands' own floor keeps a scale for quiet music; the gate's hysteresis and hold; a quiet song after a loud one reaches the top; music near the floor narrows the window, the noise staying dark; the top holds still while gated; the gate's place on the bands' scale.
  • The card's dynamics line shows the level, the floor, the top and the gate, so a gated room reads as gated rather than as a dead audio path.
  • AudioSpectrumEffect draws the gate as a mark at its right edge, white while open and red while gated, and the dominant frequency as a mark along its top, each a bar wide, behind its one readouts switch with the level meter; the floor is the bars' base by construction. Every coloring follows the selected palette, so a palette following the music reaches the spectrum too.
  • Measured on the boards (2026-11-04), this step's build, AudioSpectrum on live audio, each board at its own rate and then at 48 kHz; the bands' share of one core is the card's cpu:

    Board Lights FPS Largest cost in a tick Bands at its rate At 48 kHz
    P4, .221 16,200 on HDMI 369 the effect, 1.6 ms 3.6% at 24 kHz 6.8%, 310 FPS
    S3 Waveshare, .127 4,096 on HUB75 153 the Hub75 driver, 6.2 ms 7.2% at 24 kHz 14.4%, 135 FPS
    Classic Dig-Next-2, .122 256 WS2812 90 the RMT driver, 9.3 ms, the wire taking 7.7 8.4% at 22,050 Hz 19.0%, 82 FPS

    The cost follows the rate: every octave's filters run at the rate that octave needs, so the work per input sample is the same at any rate, about 33 sections, and half the rate is half the work. That is 15 to 30 cycles a section across the three cores, plain C. Open: the input's measured rate wanders, 20,700 to 22,300 Hz on the classic and 23,500 to 24,700 on the S3; at 48 kHz the P4 loses a fifth of its frames for 3% more of a core, the audio task waking the render core twice as often being a hypothesis; the Hub75 driver's 6.2 ms is some 360 cycles a pixel. - No showcase effect (2026-11-04): an audio modifier was dropped as adding no value; a MoonLive palette following audioSmooth() already reaches every palette effect. - Bench (2026-11-04): the floor and the gate read right on the Waveshare and the P4; the product owner closed the rest of the bench.

3. The lights follow the harmony ✅

  • The bank at twelve bands per octave, FractionalOctaveBank<12> counted from A440, on the chips that carry it: one band per semitone, 20 Hz to 20 kHz, and the 31 third octaves as sums of four. A tuning estimate comes first, since a recording off A440 smears each note across two bands; Müller's tuning-shifted banks are the prior art.
  • What derives from it: the twelve pitch classes (chroma) by folding the octaves, a harmonic-change signal for the moment the harmony moves, and the key with its major or minor over 10 s or more, matched against Faraldo's profiles for electronic dance music beside the classical ones. The robust signals only, by what a full mix allows: no chord names, no melody.
  • One bank per board, decided here by measurement. Four layouts compared on fast response, CPU and harmony: the three-band bank alone; the twelve-band bank with the third octaves as sums of four semitones, whose bass bands respond about four times slower (step 1 measured 67 ms at 63 Hz for a third octave, so some 270 ms for semitones; Anjin estimates about 100 ms a section near 55 Hz) since a semitone filter is that much narrower; third-octave filters below about 200 Hz and semitones above; and both banks side by side. Desktop: a synthesized kick, a 60 Hz burst with its click, timing the bass bands' rise and the onset in each layout, and each layout's cost per sample. Board: the cost on a classic ESP32 and on a P4. Bench: a kick track on the waterfall, the layouts side by side, judged by eye. The fastest layout that holds the response and fits the board's budget wins, per board type. A board without twelve to the octave has no harmony of its own and reads it through audio sync from one that has.
  • Decided by measurement (2026-11-04): the third octaves as they are, beside a semitone bank limited to 2.5 kHz, on every board. Desktop, 24 kHz, the click's peak in the 63 Hz band and the cost against the third octaves alone:

    Layout Cost 63 Hz Harmony
    the third octaves alone 1.0× 67 ms none
    semitones alone, the thirds as sums of four 2.55× 187 ms full
    thirds below 200 Hz, semitones above about 2.5× 67 ms, the mids 3× slower no bass notes
    both banks, full range 3.55× 67 ms full
    the thirds and the semitones to 2.5 kHz 2.1× 67 ms, every bar as before 85 notes, 19 Hz to D#7

    Harmony needs nothing above 2.5 kHz, and the bank's cost sits in its top octaves, so the limited bank starts a halving down; a quarter of the rate is out of reach, the halving low-pass passing only to 0.24 of the halved rate. 2.07× at 22,050 Hz, 1.73× at 48 kHz; on the boards, scaled from step 2's measurement, about 15% of an S3 core, 17.5% of a classic's and 7.6% of a P4's, to be measured. The semitone bands answer as slowly as alone, 115 ms at 110 Hz and 34 at 440, which a chord's pace allows; the bars and onsets keep the fast bank. A harmony switch, on by default, turns the semitone bank off where nothing reads it: it saves its CPU and its memory: the analysis is allocated only while an input is open and the harmony's state only while harmony is on, so the Audio object shrinks from 14,712 bytes to 960 on the desktop, simulate and receive hold none of it, and harmony off frees 7,072 bytes (built 2026-11-04), 8,520 with step 5's note onsets. - Built (2026-11-04), parts 1 to 3: FractionalOctaveBank takes a band limit, halving while its top band fits where a lower octave sits; SemitoneBank from A440 to 2.5 kHz runs beside the 31 in AudioService. AudioHarmony.h folds the notes from C2 up into the twelve pitch classes, each an amplitude against the loudest over 200 ms, and finds the harmonic change by Harte, Sandler and Gasser (2006): the twelve as a point on the tonnetz's circles of fifths, minor and major thirds, a change being that point moving fast against itself 100 ms behind. A held chord moves it under 0.02, noise included, C major to A minor 0.13, C, G, A minor and F 0.24 to 0.27; a change fires at 0.06 and re-arms under 0.03, none in the 500 ms after the gate opens. AudioFrame carries chroma[12], harmonyChange and the piano's keys A0 to D#7 as notes[79]; the card's notes line names the three strongest classes and the changes a second; AudioSpectrumEffect's bands gains notes, one bar a key. Desktop tests: a C major triad lights C, E and G; a note reads its class in any octave; C major to A minor fires one change within 300 ms, a held chord none, C, G, A minor and F three; a shut gate forgets the harmony and reopening fires none; A4 lights its own key; harmony off empties the notes and on brings them back. - The gate holds the harmony. The twelve pitch classes are normalized by step 2's level and gate, so a silent room shows no harmony made of its noise. - AudioSpectrumEffect shows the twelve pitch classes as a ring or a row, and marks each harmonic change. - Built (2026-11-04), part 6: with readouts and harmony on, AudioSpectrumEffect draws the twelve pitch classes as a row over the bars, C at the left, each in its palette color as bright as it sounds, the row going dark for 100 ms on each harmonic change (a white flash read too heavy); the bars, the peak mark and the note names sit under it. noteNames writes the three strongest classes at the top left, on its own switch. - Built (2026-11-04), part 7: harmony.mlp, a MoonLive palette on two new builtins, audioChroma(i) and audioChordChange(): each pitch class a fixed hue on the circle of fifths, so C, G, F and A minor take neighboring colors; on each chord change the three strongest classes, ordered by their place on the circle, set the sixteen entries in three hues, crossfading over crossfade (150 ms); the chord already showing changes nothing, so frames faster than the audio and a returning chord leave it; before the first chord the whole circle. The frame hand-over carries a dropped block's change as it carries its onset. The StadBeest autopilot's fire palette moves to 66, the new palette sorting before it. - Built (2026-11-04), parts 4 and 5: the key by Krumhansl-Schmuckler key finding, the twelve averaged over 15 s against two profile pairs at every tonic, Krumhansl and Kessler's and Faraldo's from dance music (Essentia's edma), each key scoring the better of its two, named after 10 s of open gate and changing only when another key correlates 0.05 better; AudioFrame.key, audioKey(), the card's key line and AudioSpectrum's noteNames show it. The tuning by where each strong note's power sits between its neighbors, a parabola in dB averaged on the circle over 10 s, reading three quarters of the offset; once past 10 cents for 10 s the semitone bank retunes by it, so music 40 cents sharp settles within a few cents, shown on the key line. Desktop tests: C, F, G read C major after 10 s and not before; A minor, D minor and E read A minor; a pause and a borrowed chord keep the key; a track shaped like Faraldo's G minor reads G minor; a tone 40 cents sharp reads sharp; music 40 cents sharp retunes the bank by 30 to 50 cents. - Measured on the boards (2026-11-04), harmony off against on: the S3 Waveshare at 24 kHz 7.2% to 17.2% of a core, 153 to 132 FPS; the classic Dig-Next-2 at 22,050 Hz 8.2% to 19.2%, 62 to 54 FPS; the P4 3.6% to 8.1%. About 2.3 times the third octaves alone, a little over the 2.1 the host measured. - Showcase: a palette that changes on each chord change, its colors from the pitch classes, so the same chord always returns as the same colors. - Desktop tests: a synthesized C major triad lights C, E and G among the twelve; a step from C major to A minor fires one harmonic change and steady chords fire none; a recording tuned 20 cents sharp still lands each note on its own band after the tuning estimate. - Bench, by end users (2026-11-04): marked done by the product owner for end users to check in the release, with the songs below from the test playlist. A pop song and a piano piece through line-in, the palette turning on the chord changes. The piano pieces, each for what it shows: the intro of "Let It Be" (The Beatles), C, G, A minor and F a chord every two beats, the tests' own progression, so notes steps through the four triads at about a change a second; Satie's "Gymnopédie No. 1", two chords alternating a bar each, every change apart and nothing firing between; Pachelbel's Canon in D on piano, eight chords repeating, the same chords returning as the same names. The 0.06 threshold is judged here, on real songs.

4. The kick is found in any song ✅

  • Three sums beside the bands, each a uint8_t 0 to 255 on the bands' scale: the power of the third-octave bands from 20 to 125 Hz, 160 Hz to 2 kHz, and 2.5 kHz up, so an effect keyed on the bass finds the kick whichever band it lands in. A sum of band powers is exact, as an octave is the sum of its three thirds. MoonLive gains audioBass, audioMid and audioTreble.
  • The bass line's note beside the bass energy, from the semitone bands of step 3 where the board has them: in a sparse mix the bass follows its note, in a dense one the kick shares the range and the note reads only as often as it holds.
  • AudioSpectrumEffect draws the three sums beside its bars.
  • Showcase: BeatRipplesEffect keyed on the bass sum, stones dropped on the kick rather than on any hit.
  • Bench, by end users (2026-11-04): marked done for end users to check in the release, with the test playlist. A kick-drum track, the ripples on the kick and not on the vocal: Daft Punk's "Around the World", a steady kick under a bass line and a repeated vocal, and Jeff Mills' "The Bells", a 909 kick under fast bells and hats.
  • Built (2026-11-04): bankToRanges gives bass, mid and treble, each its range's power per band through the bands' window, so a range reads on the bands' scale and a bass hit over three thirds does not saturate it; bassNote is the loudest semitone from B0 to B3 as a MIDI number while it stands 6 dB over its range, else 0. MoonLive gains audioBass, audioMid, audioTreble and audioBassNote. AudioSpectrumEffect draws each range as a grey line across its own bars at its level, on the bars rather than beside them so it takes no room, and noteNames adds the tuning to the key, "Am +12". BeatRipplesEffect gains trigger: any hit, or the kick, a hit whose strongest range is the bass, until step 5 times an onset per range. Desktop tests: a 63 Hz, 1 kHz and 6.3 kHz tone each light their own range most; A2 alone reads 45 and none while gated; ten bass notes at once name none; the lines over their bars; on the kick trigger a treble hit drops no stone and a bass hit does.

5. A kick, a snare and each tom look different ✅

  • An onset per range (low, mid, high) from the sums of step 4, each with the flux-against-its-mean decision OnsetDetector already makes; onset stays as the any-hit signal. The bank updates every band every frame, so a mid or treble hit is timed within a frame rather than within the transform's 23 ms block. Moved here from the live-performance plan, step 5, which keeps the drum-trigger route.
  • The onset signal from the bands' powers, at 200 readings a second. Each band's log power, its rise kept and its fall dropped, summed over the bands every 5 ms: Scheirer's construction and Anjin's, read from the bank's detectors rather than from the sixteen display bytes once a block, which is the flux onset reads today.
  • Each band's delay subtracted before onsets are compared or shown together, step 1's measured delays, so a tom fill sweeps the rig in time.
  • An onset per band beside the three ranges, the same decision on each of the 31, with a refractory time per band so strokes up to 10 a second stay separate hits; a snare roll at 15 to 20 is in the backlog. The toms are pitched, from about 80 Hz for the floor tom to 400 Hz for the high one, so each lands in its own band and a tom fill reads as a sweep across the bands.
  • A new note starts where a semitone band's onset fires on a board with step 3's bank: the attack of a piano key or a plucked string, beside the drum hits, and the toms by their pitch.
  • The kick is timed by its click. A narrow bass band takes tens of milliseconds to rise, while the beater's click at 2 to 5 kHz rises within a few: the click's band times the hit, and the low bands confirm it was a kick and not a bass note.
  • AudioSpectrumEffect flashes each range's onset above its bars, and each band's onset on its own bar.
  • Showcase: a drum-kit effect: a kick splashes from the floor, a snare flashes the middle, a hi-hat sparkles the top, and each tom lights its own place on the rig, so a drum solo plays across it.
  • Desktop tests: a synthesized kick with a soft attack fires the low onset and not the high one, a synthesized hi-hat the reverse (a kick with its click reaches the treble first, the next point's case); three synthesized toms a fourth apart fire three different bands in order; a synthesized kick with its click is timed within 5 ms of its start.
  • Bench, by end users (2026-11-05): marked done by the product owner for end users to check in the release. A drum track and a drum solo, best through an aux send from the mixer giving the kit alone, each drum on its own layer: Neil Peart's "O Baterista", the toms sweeping the kit, and Daft Punk's "Around the World" from step 4, the kick and the hi-hat apart.
  • Measured on the boards (2026-11-05), harmony on, the bands and both onset readers together: the S3 Waveshare at 24 kHz 22% of a core, the P4 14%, the classic Dig-Next-2 at 21,600 Hz 26%; the S3's frame rate 131 to 121 FPS against the build before step 5. Against step 3's harmony-on readings, 17.2%, 8.1% and 19.2%, the onsets add 5 to 7 points of a core, more than the per-reading arithmetic predicts; a measurement with the onsets alone tells them from the boards' other load.
  • Built (2026-11-04), part 1: AudioOnsets.h, an onset per range read from the bank every 5 ms of stream time: each band's log power, its rise summed over its range, a band counting only within 40 dB of the loudest with both readings held at that floor (Klapuri 1999), so a hi-hat's faint bass rising out of the room's hiss does not fire the bass; a range fires on a rise of 10 dB in a reading, by OnsetDetector's decision, and the block's own gate passes it. onsetLow, onsetMid, onsetHigh in the frame, carried across dropped frames, audioOnset(range) in MoonLive; AudioSpectrum's range lines flash white on their hit; BeatRipples' kick is the low onset, the stopgap of step 4 gone. The analysis was released outside local mode in the same build: the Audio object 14,712 bytes to 960 on the desktop. Desktop tests: a soft-attack kick fires the low onset and not the high one, a hi-hat the reverse.
  • Built (2026-11-04), part 2: each band's delay is its filter's group delay at its center, the band-pass's and the halving low-passes' ahead of it, computed by the bank from its own design at any rate (47 ms at 80 Hz, under 1 ms from 4 kHz up, within 3 ms of a tone burst's rise); the product owner chose to subtract it from a hit's timestamp rather than hold the fast bands back, which would add the bass's delay to every hit. An onset per third-octave band, its rise against the band's own recent level, a one-pole over the band's delay, so a slow tom band's whole rise counts; 40 ms between a band's hits, which keeps strokes apart up to about 10 a second, the 15 to 20 of a snare roll in the backlog. thirdsHits, a bit per band in the frame; AudioSpectrum flashes each third's bar top on its hit. Desktop test: three toms a fourth apart fire their own bands in order, each timed within 15 ms. A stick's attack also fires the faster bands above a tom, as a real one does. The per-band state costs 400 bytes in the analysis.
  • Built (2026-11-04), part 3: BandOnsets, one per-band onset for any bank, the thirds' and the semitones'; a new note fires where a key's band rises and stands over both neighbors, as a sounding note does, so an attack's click spreading across the keys around it fires the note's own; a band waits 40 ms or its own delay between hits, whichever is longer, 105 ms at 110 Hz on the semitones. notesHits, a bit per key in the frame, the thirds' hits a bit array the same way; AudioSpectrum flashes each key's bar top as it is struck. Desktop test: E4 plucked over a held C4 fires E4 and its octave, not C4. The note onsets cost 1.4 KB in the harmony state, only with harmony on; a note struck out of silence still fires its key a second time one delay after the first.
  • Built (2026-11-04), part 4: a kick is a click in the beater's bands, 2 to 5 kHz, whose bass starts to rise, 5 dB summed in a reading, within the delay of the bass's fastest band, 27 ms; the click times it and the bass confirms it, so a hi-hat's click alone is no kick, and a kick without a click still fires on the bass's own onset. kickStartMs, for step 6. MoonLive's audioBandOnset(i) and audioNoteOnset(i). Desktop tests: a kick with its click fires the treble first, then the low onset, its start within 5 ms; a script reads a band's hit and a key's start.
  • Built (2026-11-04), part 5: DrumKitEffect: a kit seen from the audience, each drum lighting as it is played: the bass drum on the kick, the snare and the hi-hat at the left with stars over the top, three rack toms over the bass drum and the floor tom at the right, the toms two thirds each from 80 to 400 Hz; each hit glows for decay, and the kit rests at idle between hits. Drawn from the hits' ages at fixed places, so a few bytes of state and the same shape on any grid. Desktop tests: dim until a hit and dark at idle 0, each drum at its place, the floor tom right of the high one, a 1x1 and a 0x0 grid. Audio simulation carries no range or band onsets, so the effect rests dim there.

6. The beat lands on the beat ⏳

  • Tempo and beat phase from the onset curve. Two trackers, run side by side: the textbook (Ellis, Beat tracking by dynamic programming, 2007: autocorrelation of the onset curve over 12 s or more picks the tempo in 60 to 200 BPM, and the phase that lines up with the recent onsets picks the beat), and Troy's phase-locked comb, measured on real house music, its comb run over the continuous onset curve rather than picked kicks, corrected for the score a period earns by chance and given a lock confidence, so noise no longer locks near 130. The two fail in opposite directions, Troy's at double the tempo and the textbook's to be measured, which is what makes their agreement a lock. Where the two agree the tempo is locked, the confidence an effect reads; where they disagree the frame says so. The frame carries bpm, beatPhase in 0 to 1 and beat for the block the phase wraps. MoonLive's audioBeat, a level-above-its-average guess until now, returns the tracked beat, and audioOnset and audioBpm are added.
  • Harmonic changes find the downbeat. A beat tracker hears beats but not which is the bar's first; Troy's says so of its own. Chords mostly change on the bar line, so step 3's harmonic changes, lined up with the beat grid, place the one, and the frame carries downbeat and the beat in the bar. A board without harmony counts the bar from the lock, as Troy's does.
  • The beat phase is what the live-performance plan's MidiFrame takes from a MIDI clock when one runs; the audio estimate fills the same field when none does, so an effect reads one phase from either source.
  • Tap tempo, the escape route (robwomp, 2026-11-04). A third beat source beside the MIDI clock and the tracker, as lighting consoles, DJ mixers and pedals have it: the tempo is the mean of the last four to eight intervals between taps, a gap over about 2 s starts a new count, and the last tap sets the phase. The order is MIDI clock, then tap, then the tracker; a tap also re-phases a tracker whose tempo is right and whose beat is not. A button on the card, a MIDI pad or a GPIO through input mapping; four taps average the WiFi's 5 to 20 ms jitter well inside ±20 ms.
  • Beat-synchronous analysis once the beat locks (Anjin, 2026-11-04). Each feature averaged over a beat rather than a block (Bello and Pickens 2005; Ellis 2007), time-synchronous averaging in vibration analysis: step 3's pitch classes per beat, their change checked on the beat boundaries where chords change, so fewer false changes and each on the beat; step 5's band onsets folded at the beat period, so hats and rides gather at their places in the sixteenth grid and random noise spreads. It sharpens timing, not the spectrum, and needs a tempo within about 1%, a 2% error smearing the fold within a few bars; Troy's bass-led tracker guides it. The bank suits it, every band updating every block, so a beat boundary falls anywhere.
  • AudioSpectrumEffect shows the tempo and a marker on each beat.
  • Showcase: BeatRipplesEffect on beat instead of onset, the ripples on the pulse rather than on every hit, a beat sweep, a bar crossing the rig once per beat, its position from the phase, and a palette per bar turning on the downbeat.
  • Desktop tests: a synthesized click track at 120 BPM reads 120 within 1 BPM and beat fires within a block of each click; a track that steps to 140 BPM follows within four beats; silence reads no tempo; a synthesized progression changing chord every four beats puts downbeat on the change.
  • Corpus tests: Anjin's labelled sets and harness through the desktop build, the Freesound loops with a 12 s warm-up each, known tempos across 40 to 190 BPM, scored as MIREX does (accuracy 1 within 4%, accuracy 2 forgiving the double, half and triple) and reported per genre, with 3:2 as its own class rather than "wrong"; beside Troy's tracker's 97% on house, about 70% on mixed EDM and 47% on loops, and BpmDj's table.
  • Chance and silence tests: random onsets at 90 and 175 BPM lock at neither, after the chance correction; beatless music (ambient, implied beats) reads "no lock" rather than a tempo.
  • Bench material: drum and bass, hard techno with a 909 kick whose fast click must not read as a hat, Jeff Mills' unquantized swing, breakcore and sliced amen breaks, and a jam band for the non-dance end.

What an effect reads

AudioFrame is the whole contract: one flat struct per tick, every field a small integer, read through AudioService::latestFrame() by an effect and through builtins by a MoonLive script. Today it carries the level (raw and smoothed), the sixteen bands (raw and with meter ballistics), the dominant frequency with its magnitude, the flux and onset. The plan widens the struct and nothing else:

Step New fields MoonLive
1 the 31 third-octave bands, and whether they are live; bands and level mean what they did, measured better a read of the 31
2 none; level read better
3 the twelve pitch classes, harmonyChange, the key and its mode, the tuning, the piano's keys audioChroma(i), audioChordChange, audioKey
4 bass, mid, treble, and the bass note audioBass, audioMid, audioTreble, audioBassNote
5 onsetLow, onsetMid, onsetHigh, an onset per band, and a new note audioOnset(range), audioBandOnset(i), audioNoteOnset
6 bpm, beatPhase, beat, downbeat and the beat in the bar audioBpm, audioBeat, audioDownbeat

A field is 0 when its source is absent, so an effect on a board without a microphone draws its idle state, as today. The new fields are local: the WLED audio-sync packet carries the sixteen and none of the rest, so a board in receive mode leaves them at 0, AudioSpectrumEffect falls back to sixteen bars there, and a desktop test pins both.

What it costs, per chip

Analysis is not where the frame goes. AudioService's fixed I2S block read adds 13 to 22 µs to an average tick (performance.md, 2026-06-17). Each step keeps it that way, and its desktop test and bench run record the tick it adds.

Step What it adds Estimate, to be measured
1 the bank: the full rate carries two octaves and every octave below halves the samples, so about 33 second-order sections per input sample with the halving low-passes; the 512-point FFT, its window and its 4 KB go measured: 6.0% of a P4 core at 48 kHz, 16.3% of an S3's, 12.3% of a classic ESP32's at 22,050 Hz, read on the card's cpu
2 a few multiplies and two exponentials for the floor, gate and top negligible
3 the bank at twelve to the octave, per the layout the step chooses, plus the folding and the change detector per frame the twelve-band bank about four times step 1's; measured beside it on the classic ESP32 and the P4
4 three sums over 31 values, and the loudest bass semitone negligible
5 three flux decisions, and one per band negligible: a comparison a band a frame
6 an autocorrelation or comb over 12 s or more of the onset curve, a phase fit, and the harmonic changes lined up with the grid about 16,000 multiply-adds per block, well under a millisecond on a classic ESP32

Each chip measures what it can carry, and audio sync shares it. Every chip that analyzes audio carries the three-band signals, so those behave alike on each; the harmony runs where step 3 finds the twelve-band bank fits (decided 2026-11-03: differences between boards are accepted). A chip with spare cycles spends them on resolution: 48 kHz for the top three bands, three sections per band rather than two, twelve bands to the octave rather than three. Each is chosen per chip by the platform, a default rather than a user setting, so it adds no knob. A board that measures less reads more through audio sync: the packet says how many bands to the octave it carries, and a receiver sums finer bands into the coarser ones its effects read.

The classic ESP32 is the floor. Step 1 is measured there before anything builds on it. A bank that does not fit the render's headroom there runs at 32 kHz on that chip (29 bands, all sixteen) or on the second core, before it loses a section per band.

The showcase per step

Step AudioSpectrumEffect shows Showcase effect
1 31 bars while live, sixteen otherwise WaterfallEffect: the spectrogram scrolling down the matrix
2 the gate across the bars, the floor as their base none: a MoonLive palette on audioSmooth() reaches every palette effect
3 the twelve pitch classes and each harmonic change a palette that changes on each chord change
4 the bass, mid and treble sums, and the bass note BeatRipplesEffect on the bass
5 each range's and each band's onset a drum kit: kick, snare and hi-hat a sprite each, each tom its own place
6 the tempo, a marker per beat, and the downbeat BeatRipplesEffect on the beat, a beat sweep, and a palette per bar

More effects on the same signals, each a small change once its field exists: a harmony color, the rig tinted by the twelve pitch classes, from step 3; three zones (bass on the floor, mids between, treble at the top, breathing with their range) from step 4; PulseEffect and PaintBrushEffect on the beat, one shell or stroke per beat, from step 6.

Backlog

On the topic and not a step. What remains here when the plan ships returns to the Project as cards, item by item, then.

  • A sync packet for every band count. WLED receivers accept only the 44-byte 00002 packet and drop anything over 96 bytes, so the sixteen stay in it and the bands travel in a second packet type under its own header, which WLED ignores. It describes itself: bands to the octave, the reference frequency, the first band and the count, then the values (about 45 bytes for 31 bands, 135 for 121). A receiver sums finer bands into the coarser ones its effects read; a coarser packet cannot give finer bands, so an effect that needs them idles. MoonLight decides the format (2026-11-03).
  • Troy's display settings, for a like-for-like comparison. The pattern that shifts the sixteen −3 to +3 bands, the dB window span, auto or fixed range, the dB reference, attack and release. A run pins each on both boards.
  • Calibration, what a rig cannot show, taken when a user or an effect asks: a biquad pre-filter chain (rumble high-pass, anti-alias low-pass) from esp-dsp's dsps_biquad_f32; calibration against a measured chain and a self-test on the card. Troy's rewrite has both, checked against a hardware analyzer.
  • More audio effects on the bands, 2D and palette-driven frequency-reactive ones, beside the showcases.
  • Pin auto-scan: detect the microphone's sdPin with wsPin and sckPin fixed, a noise prompt and a confirm; explicit pin controls ship today.
  • Sources beyond the I²S microphone, each a variant behind the same platform read, the analysis untouched. The I²S peripheral already drives MCLK (mclkPin) for a line-in ADC, and codecGain sets an ES8311 or ES7210 between microphone and line level.
    • PDM microphones: the IDF i2s_pdm driver, a sub-mode of the same peripheral.
    • Analog line-in: DedeHai has it on the S3 and Troy in ParrotRadio, where he recorded and played back through it. An analog path is validated by listening to it, since one that only feeds a level meter "may not be as accurate as it looks" (Troy).
    • Multi-channel front-ends: a TDM codec (the ES7210 on the Waveshare RGB-Matrix board) or a DSP board giving one analyzed channel per instrument, the input the live-performance plan needs for acoustic drums.
  • A fast roll as separate hits (2026-11-05). A band's onset keeps strokes apart up to about 10 a second; at 20 a second it fires 2 of 10, since the band falls only about 14 dB between strokes through the bank's 10 ms detector and the flux-against-its-mean decision raises its own threshold. A rise measured from the band's recent low point, rather than against its mean flux, would keep a snare roll's strokes; a desktop test pins 20 a second.
  • An instrument tuner (2026-11-04). The semitone bands say which note sounds, not how far off it is, and a tuner needs 1 to 2 cents, a hundredth of a semitone. The bank picks the note; a period detector on the waveform itself, YIN (de Cheveigné and Kawahara 2002) or McLeod's normalized square difference (2005), the textbook pitch detectors, measures it to a tenth of a cent, for one instrument through line-in, never a full mix. The note's power against its two neighbors, as peakHz reads the third octaves, gives a few cents at best: enough for flat or sharp. Shown on AudioSpectrum's noteNames line as the note and its cents, and readable by an effect, a strip lit green when in tune. Not step 3's tuning estimate, which measures how far a recording sits from A440 over many notes.
  • Desktop audio capturing what the machine plays (2026-09-29). platform_desktop_audio.cpp enumerates capture devices only (ma_context_get_devices with null playback lists) and opens ma_device_type_capture, so on a Windows bench the picker offers default and the microphone and the speakers are absent, with no Stereo Mix to fall back on. miniaudio has ma_device_type_loopback, WASAPI-only, taking a playback device: enumerate playback devices too, mark them in the picker so a microphone and "what is playing" tell apart, and open loopback when one is chosen. macOS has no equivalent and needs a virtual device such as BlackHole, so the picker offers what the platform can do rather than a row that fails. A disconnected microphone jack reports a plausible level (RMS 20 to 30 at mains hum), which the status of step 2 tells from a working input.

Open questions

  • Which settings stay on the card: step 2 settles it per setting on the bench. Recommended (2026-10-28): the result decides, not the count; the expectation is floor alone, and a response-speed setting (attack and release) is the likeliest second if a bench case wins with it.
  • Whether step 2 keeps a manual mode for a show that wants deterministic levels. Recommended (2026-10-28): no; freezing the floor and the top is the deterministic mode, as a freeze toggle, if a show asks.
  • The classic ESP32's default rate: settled (2026-11-03). At 48 kHz its core 1 cannot carry the bank beside the LED encoder, so every board runs 24 kHz, the rate above.
  • Where the bank runs: settled (2026-11-03). On an audio task of its own, above.

What it removes

In code: the loudest-bin estimator, the band-edge and bin-ownership code in AudioBands.h with their technical notes, and the FFT with its window, the platform's audioFft seam on both platforms and its test (decided 2026-11-03: the readers of peakHz map it to a hue or a position on a log scale, which the interpolated band gives at a fraction of the cost; whether esp-dsp stays, for its assembly biquad kernels, is step 1's measurement on the classic ESP32); removed in step 2, levels, gain, the conditioner's ratio and maximum gain and the two learned tables. In the docs: the backlog's decimated FFT and filter-bank trial, settled by step 1; earlier, the audio DSP roadmap, the audio-reactive follow-ups and the desktop-capture and noise-floor items moved into this plan, and per-band onsets left the live-performance plan for step 5.

The plan change (2026-11-03)

What changed. The plan grew a sixth step: the lights follow the harmony, from the same bank at twelve bands per octave, as step 3, right after the level path that gates it, so the bass note, the note onsets and the downbeat of the later steps build on it. Which bank each board runs is step 3's measurement, fast response weighed with CPU and harmony, and boards may differ in what they measure, audio sync sharing it. Step 1 was "balanced bars": each band the power summed over its slice of the FFT, the sixteen bands kept. It is now "the bands by the book": a multirate third-octave filter bank measuring the 31 ISO bands, the sixteen picked from them, the dominant frequency interpolated from the bands, and the FFT gone. Every step now lands in AudioSpectrumEffect and in one showcase effect. The backlog's decimated FFT and filter-bank trial are settled by it, and the default rate rises to 48 kHz.

Why. The bench put the two engines side by side on the same line input: Troy's filter banks on .223, MoonLight's FFT on .221. The FFT's 43 Hz bins cannot separate the bass, whatever the bands are summed from: a third-octave band at 20 Hz is 4.6 Hz wide, and below about 400 Hz a band is narrower than two bins. Fixing that inside the transform takes several FFTs at falling rates, a filter bank built the hard way. For third-octave bands the standard's own construction is the filter bank, and the octave-by-octave form costs about twice its top octave, about half of Troy's two banks.

Pros.

  • The bass separates, down to 20 Hz, each band on its own filter.
  • Every band updates every frame: a mid or treble hit is timed within a frame, not within a 23 ms block, which the live-performance plan's latency asks for and steps 5 and 6 build on.
  • Pink noise reads flat by construction, so balanced bars need no learner correcting the estimator.
  • One measurement serves every resolution: 31 bars, the sixteen, ten octaves as sums of three, and bass, mid and treble as sums of ranges.
  • The bars compare with a hardware analyzer and with Troy's WLED bar for bar.
  • The cost stays near Troy's two banks while each band gets a third filter section, for 17 to 21 dB of separation where two sections give 12.

Cons.

  • Cost per sample, not per block: about 1.6 million filter sections a second at 48 kHz: 6% of a P4 core, 16% of an S3's, and 12% of a classic ESP32's at 22,050 Hz, where the FFT cost a few percent. A C3 without an FPU cannot carry it.
  • Delay grows toward the bass: the band-passes and the halving low-passes delay the low bands by tens of milliseconds, 118 ms at 31.5 Hz against 1 ms in the treble, so timing across bands needs each band's delay subtracted.
  • Separation is a filter's, not a brick wall's: two sections per band leak −7.8 dB into the next band at the top in Troy's design; three sections improve it at 50% more cost per band.
  • The lowest bands jitter and react slowly. Telling 20 Hz from 25 Hz takes about 0.2 s of signal whatever the method, and a detector at 20 Hz holds less than one cycle, so it needs smoothing.
  • The dominant frequency is coarser in the treble: interpolated within a band 2 kHz wide at 10 kHz, where the FFT's bins are 43 Hz; finer in the bass, where the bands are narrower than a bin. Its readers map it to a hue or a position on a log scale.
  • Design work the FFT does not need: the filters per rate, the halving low-passes, and float precision at the lowest bands.
  • A higher default rate: 48 kHz doubles the samples read per second against 22,050 Hz.