AudioService
Source:
AudioService.h
AudioService¶
src/core/AudioService.h:89Inherits:
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::hasAudioInput: a pin-wired I2S mic on boards, an OS capture device on desktop (the device Select: the system default mic out of the box, and loopback devices such as BlackHole when installed, so effects can follow what the machine plays). A desktop in Local mode with "send audio" on is a WLED audio-sync SOURCE: one machine's capture drives a whole fleet of boards in Receive mode.
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 audio read and the FFT kernel are platform code (boards: platform_esp32_i2s.cpp, IDF's i2s_std driver + esp-dsp's radix-2 FFT; desktop: platform_desktop_audio.cpp, miniaudio capture into a lock-free ring + a radix-2 Cooley-Tukey in platform_desktop.cpp); everything else is plain domain math, host-tested in CI. 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¶
uint8_t prevBands_ = {}
: last block's raw bands, the flux's reference
BandConditioner cond_
: the per-band floor and peak tables, learned live
LevelConditioner levelCond_
: the same, for the overall level (VU) in automatic mode
OnsetDetector onset_
: the hit decision, with its running mean and refractory
uint8_t device = 0
: 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.
uint8_t micMode = 0
: Which kind of microphone is wired: 0 = I2S (three wires, an INMP441-class PCM part), 1 = PDM (two wires, the one-bit part boards solder on, such as the QuinLED Dig-Next-2's).
int8_t sckPin = -1
: bit clock / BCLK (-1 = unset). Changing it re-creates the I2S channel live (no reboot).
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 = 128
: sensitivity, HIGHER = more (a narrower dB window so a given sound fills more of the bar).
uint8_t levels = 1
: Per-band conditioning ([AudioBands.h], BandConditioner): the learner that levels the RIG, mic response and room, without touching the music's own balance.
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 = false
: 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¶
inline void finishBands()
: Block size = FFT size: a power of two.
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(const char * 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 analyzed frame, what effects read.
inline bool syncOpenForTest() const
inline const char * syncStatusForTest() const
: The sync state as the card shows it.
inline uint32_t syncSendCountForTest() 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¶
constexpr size_t kBlock = 512
constexpr size_t kMag = kBlock / 2
: real-FFT magnitude bins
constexpr uint8_t kRatio = 4
: Automatic levelling, fixed rather than exposed.
constexpr uint8_t kMaxGainDb = 24
constexpr uint8_t kSimMode = platform::hasNetwork ? 2 : 1
: The mode value that means Simulate.
constexpr uint16_t kSampleRates = {8000, 16000, 22050, 44100}
constexpr uint8_t kSampleRateCount = 4
Public Static Methods¶
constexpr static inline constexpr uint32_t syncSendIntervalMsForTest()
constexpr static inline constexpr uint32_t syncFallbackMsForTest()
constexpr static inline constexpr uint32_t syncOpenRetryMsForTest()
static inline const AudioFrame * latestFrame()
: Process-wide accessor for the consumers (audio effects).