Skip to content

AudioService

Source: AudioService.h

AudioService

class AudioService
src/core/AudioService.h:85

Inherits: 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.

AudioService card

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).