AudioService
Source:
AudioService.h
AudioService¶
src/core/AudioService.h:85Inherits:
MoonModule
Acquires an audio source and publishes an AudioFrame — an overall sound level, a 16-band frequency spectrum, and the dominant peak — as the producer half of the audio-reactive pipeline.
The frame is available to consumers every render tick, but its analysed values are recomputed only when a full sample block has accumulated (a 512-sample block at 22 kHz takes ~23 ms, longer than one tick), so a tick that doesn't complete a block re-publishes the previous AudioFrame unchanged rather than re-analysing. AudioVolumeEffect and AudioSpectrumEffect are the consumers, reaching the live frame through the static latestFrame().
Named for what it does — audio acquisition plus analysis, not for one source. Today the source is a digital I2S MEMS microphone (INMP441-class, the only one wired); the same source-independent analysis pipeline is built to serve other sources (line-in, USB audio, PDM mics, I2C codecs) behind the platform read seam as they are added. Most of the module is the analysis (DC-blocker, RMS level, windowed FFT, band mapping), which is source-independent.
User-added Service. A child of the Services container, registered in the factory and added through the UI when wanted, not boot-wired — auto-wiring it forced an I2S init on every board, which on the classic ESP32 hung setup() and boot-looped a mic-less device. When added, its pins default to unset (−1, the standard Pin-control sentinel, so GPIO 0 stays a usable mic pin) and it stays idle with a status note until the user enters the real GPIOs. Chip-agnostic: gated on platform::hasI2sMic, inert with a status note on targets without I2S and on desktop.
The AudioFrame pipeline. Each tick() that completes a block: read a block of samples, DC-blocker high-pass, compute the level, window + FFT, map to bands. The high-pass conditions the raw block once, up front, so both the level and the spectrum see the same cleaned signal. The DSP choices are textbook defaults on purpose — a Hann window, RMS for level, a geometric band split, argmax for the peak — with deliberately no per-frequency correction table (the INMP441 is flat ±3 dB across the range that matters). The level is overall RMS loudness computed independently of the FFT, not derived from the bands.
Hardware: INMP441-class digital mic. A self-clocked I2S MEMS microphone: standard/Philips framing, 24-bit data left-justified in a 32-bit slot, mono. The part is self-clocked from the bit clock; there is no master-clock (MCLK) pin. The bench wiring is SCK=6 (bit clock), WS=4 (word-select/LRCLK), SD=5 (serial data out). It drives the one slot its L/R select pin chooses (tie L/R to GND for the left slot); if level stays at the floor with sound present, the mic is filling the other slot — one wire, not firmware.
Platform seams. Only the I2S read and the FFT kernel are platform code (platform_esp32_i2s.cpp: IDF's i2s_std driver + esp-dsp's float dsps_fft2r_fc32, the radix-2 real FFT); everything else is plain domain math that runs in CI on the desktop's reference DFT. The signal math is host-tested domain code (AudioLevel.h, AudioBands.h); this module owns the lifecycle, the controls, and the two seams.
Hot path: fixed member scratch buffers (sample block + window + magnitudes, ~6 KB DRAM-resident), one float FFT per loop, no per-loop heap. The mic read is non-blocking (the first ~250 ms of power-on settling garbage flows through the first few reads and self-corrects); a bad init leaves the module idle (zeroed frame), never crashing.
Prior art: audio-reactive lighting is a long-standing idea in the LED-controller world (WLED-MM and MoonLight are the closest lineage). This is projectMM's own implementation, designed from the INMP441 datasheet (https://invensense.tdk.com/wp-content/uploads/2015/02/INMP441.pdf) and standard DSP rather than traced from any one project — studying, with credit, the thinking of Frank (softhack007, WLED-MM audioreactive), Troy (troyhacks, the esp-dsp FFT + biquad pre-filters path we share), and Damian Schneider (DedeHai, the fixed-point FFT for FPU-less chips). The forward-looking analysis (source-seam extensions — line-in / PDM / analog / I²C codecs — and the adaptive-noise-gate design that would retire the borrowed floor squelch) is a design study in docs/backlog/audio-dsp-roadmap.md.

Public Attributes¶
int8_t sckPin = -1
: Unlike a zero-cost diagnostic peripheral, this module pays a real per-tick cost (the FFT) that IS the capability, not an optional extra, so it must not run when the user turns it off.
int8_t wsPin = -1
: word-select / LRCLK (-1 = unset). Changing it re-creates the I2S channel live.
int8_t sdPin = -1
: serial data in / DOUT (-1 = unset). Changing it re-creates the I2S channel live.
int8_t mclkPin = -1
: master clock out (-1 = none).
uint8_t sampleRateSel = 2
: Sample rate is a discrete choice (the standard audio rates), so it's a dropdown over a fixed set, not a free number.
uint8_t floor = 100
: noise floor (dB display floor) — bands/level below this read as silence.
uint8_t gain = 222
: sensitivity — HIGHER = more (a narrower dB window so a given sound fills more of the bar).
uint8_t simulate = 0
: Simulated-audio pattern (only shown, and only used, in Simulate mode — see mode).
uint8_t mode = 0
: The module's audio SOURCE, the first thing to pick (below status).
bool send =
: Broadcast this device's AudioFrame over UDP (WLED v2 wire format) for WLED / MoonLight receivers.
uint16_t syncPort = WLED_SYNC_PORT
: The sync UDP port — the Send destination and the Receive listen port.
Public Methods¶
virtual inline ModuleRole role() const override
: Role for type identification (no RTTI needed).
inline uint8_t sync() const
: The three source/sink states the sync machinery keys off, derived from mode + send so the socket/tick logic stays a single 0/1/2 switch (0 = no socket, 1 = broadcast, 2 = network sink): Local+send → send, Local alone → off (local-only, no socket), Receive → receive, Simulate → off.
inline uint32_t sampleRate() const
virtual inline void defineControls() override
: defineControls MUST be idempotent and pure: only controls_.clear() + controls_.addX().
virtual inline bool affectsPrepare(constchar * name) const override
: A pin or rate change rebuilds the I2S channel (live, no reboot); a mode / send / syncPort change re-binds/unbinds the UDP socket AND re-toggles which control rows show (all flow through prepare → rebuildControls).
virtual inline void prepare() override
: Pure build (see MoonModule::prepare): claim the frame election (this instance's frame_ drives the effects in EVERY mode — a live mic, a received peer frame, or a synthesized one), then acquire only the hardware the current mode needs.
virtual inline void setup() override
: One-time wiring only; the mic acquire + election live in [prepare()], the sole gate.
virtual inline void release() override
inline const AudioFrame * audioFrame() const
: The latest analysed frame — what effects read.
inline bool syncOpenForTest() const
inline uint8_t syncFrameCounterForTest() const
inline constchar * syncStatusForTest() const
virtual inline void tick() override
inline void synthesizeFrame(bool sweep)
: Fill frame_ with a synthesized signal.
virtual inline void tick1s() override
Public Static Attributes¶
constexprsize_t kBlock = 512
: Block size = FFT size: a power of two.
constexprsize_t kMag = / 2
: real-FFT magnitude bins
constexpruint8_t kSimMode = platform::hasNetwork ? 2 : 1
: The mode value that means Simulate.
constexpruint16_t kSampleRates = {8000, 16000, 22050, 44100}
constexpruint8_t kSampleRateCount = 4
Public Static Methods¶
constexpr static inline constexpruint32_t syncSendIntervalMsForTest()
constexpr static inline constexpruint32_t syncFallbackMsForTest()
constexpr static inline constexpruint32_t syncOpenRetryMsForTest()
static inline const AudioFrame * latestFrame()
: Process-wide accessor for the consumers (audio effects).