Skip to the content
4 layers160 × 90 mmLED Matrix, the printed circuit board in 3D

Real-time audio-reactive LED matrix powered by an ESP32, featuring custom PCB design, I2S microphone input, and dynamic visual effects through FFT signal processing.

Size
160 × 90 mm
Copper layers
4
Thickness
1.6 mm
Parts
141
Nets
131
Pads
534
Tracks, vias
815, 262
Pours
2
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
5 May 2026
Updated
17 May 2026

Overview

LED Matrix is an open-source printed circuit board by h2dakhil, from the h2dakhil/FFT-Audio-Spectrum-Analyzer repository on GitHub. It is a 4-layer board of 160 × 90 mm with 141 parts on 131 nets, routed with 815 tracks and 262 vias.

Its parts include 128 diodes, 5 capacitors and 1 connector. It belongs with the lEDs and displays and audio and music boards of this library.

README

README.md

🎵 Audio Spectrum Visualizer

A real-time audio spectrum analyzer built from the ground up — dual custom PCBs, embedded DSP firmware on an ESP32-S3, and a complete software pipeline validated on Raspberry Pi before committing to silicon.


Table of Contents

  1. Project Overview
  2. System Architecture
  3. Phase 1 — Software Proof of Concept (Raspberry Pi)
  4. Signal Processing — FFT Pipeline
  5. Custom PCB Design
  6. Power Architecture
  7. Design Constraints & Trade-offs
  8. Roadblocks & How They Were Solved
  9. Firmware Architecture
  10. Bill of Materials (Abridged)
  11. Roadmap

Project Overview

This device listens to ambient audio, performs real-time FFT-based frequency analysis on an ESP32-S3, and renders a live spectrum visualization on a custom-designed 8×16 WS2812B RGB LED matrix. Three physical buttons allow standalone control of display mode, brightness, and visual theme — no PC, no monitor, no SSH required after initial flash.

Key design goals:

  • Full hand-solderability — no reflow oven required (MEMS mic requires hot air; all other components are iron-solderable)
  • Software-first validation: complete pipeline proven on Raspberry Pi before any PCB was committed to fab
  • Justified component choices backed by real numbers: thermal dissipation analysis, power budget, signal integrity calculations
  • Two fully custom PCBs: a 2-layer controller board and a 4-layer 8×16 LED matrix board

System Architecture

text
┌──────────────────────────────────────────────────────────────────┐
│                     CONTROLLER BOARD (2-layer)                   │
│                                                                  │
│  USB-C ──► 5V Rail ──► Buck Converter ──► 3V3 Rail              │
│                                │                                 │
│                         ESP32-S3-WROOM-1-N8                      │
│                         (dual-core Xtensa LX7, 240 MHz)          │
│                                │                                 │
│   ICS-43434 MEMS Mic ──► I2S ──┤                                 │
│   (24-bit, 64 dBA SNR)         │                                 │
│                                │                                 │
│   Button ×3 ──────────► GPIO ──┤                                 │
│   (color theme / gain / viz mode)  │                             │
│                                │                                 │
│   Native USB (D+/D−) ──────────┤  (flash + debug, no UART chip) │
│                                │                                 │
│                         GPIO1 ──► 330 Ω ──► JST-3 Header        │
└────────────────────────────────────────────────────────────────┬─┘
                                                                 │ 3-wire JST
                                                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│                     LED MATRIX BOARD (4-layer)                   │
│                                                                  │
│  JST Header ──► DIN ──► WS2812B 8×16 Matrix (128 LEDs)          │

Phase 1 — Software Proof of Concept (Raspberry Pi)

Before committing to any PCB design, the complete audio-to-visuals pipeline was validated on a Raspberry Pi 4 using a USB microphone and an off-the-shelf WS2812B 8×32 matrix. This approach de-risked the DSP algorithm, the band-mapping logic, and the LED driving code as three independent concerns — completely decoupled from hardware design.

Software stack:

  • Python 3, pyaudio for mic capture
  • numpy.fft for FFT computation
  • pygame for real-time visualization on HDMI monitor (development aid)
  • rpi_ws281x for WS2812B LED matrix output

Validation milestones, achieved in sequence:

  1. USB mic → raw sample capture → FFT computed in Python → bars rendered via pygame on HDMI ✅
  2. Log-scaled FFT bins remapped to logarithmically spaced frequency bands → output to 8×32 matrix ✅
  3. Peak-as-bar decay (instant attack, linear fall) tuned empirically — flickering eliminated while preserving fast musical transients ✅
  4. Physical buttons wired to Pi GPIO with software debouncing — color theme, input gain, and visualization mode all selectable — fully standalone operation (no keyboard, no monitor, no SSH) ✅

Only after every milestone was confirmed working end-to-end was the custom PCB designed and sent to fab.


Signal Processing — FFT Pipeline

What the microphone actually outputs

The ICS-43434 MEMS microphone measures air pressure variations over time and digitizes them into a 24-bit I2S stream. This is a time-domain signal — a series of amplitude samples encoding when pressure changes occur. The microphone itself detects neither bass nor treble. Frequency extraction is entirely performed by the FFT in firmware.

The Fast Fourier Transform

The Fast Fourier Transform (FFT) is an O(N log N) algorithm for computing the Discrete Fourier Transform (DFT):

text
X[k] = Σ (n=0 to N-1)  x[n] · e^(−j2πkn/N)

Where:

  • x[n] = time-domain audio samples from the I2S DMA buffer
  • X[k] = complex-valued frequency-domain coefficient at bin k
  • N = FFT window size — 2048 samples in this firmware (2× the per-chunk capture, so each FFT frame overlaps the previous one by 50%)
  • k = bin index corresponding to physical frequency f = k · Fs / N

At Fs = 44.1 kHz, N = 2048 the bin spacing is Fs/N ≈ 21.5 Hz — fine enough to resolve kick-drum fundamentals around 40–60 Hz. Only the first N/2 bins are meaningful (Nyquist limit); the upper half is a conjugate mirror.

The implementation uses the ESP-DSP radix-2 single-precision routine dsps_fft2r_fc32 followed by dsps_bit_rev_fc32 — 2048 is 2^11 (not a power of 4), so radix-4 was not an option.

Pre-FFT conditioning — DC removal + Hanning window

MEMS microphones output samples with a DC bias that, if left untouched, would dump a huge spike into bin 0 and leak into its low-frequency neighbours. The firmware subtracts the per-window mean before applying a 2048-point Hanning window:

text
w[n] = 0.5 · (1 − cos(2π · n / (N − 1)))

Hanning suppresses spectral leakage from signal segments that aren't periodic in the window, at the cost of a known coherent-gain factor of 0.5 that is compensated for in the dBFS calibration below.

dBFS calibration

Every band level is expressed in dBFS — decibels relative to digital full scale — so the visualizer's dynamic range is deterministic regardless of input volume. With INMP441 24-bit samples right-shifted into int16 scale (INT16_FULL = 32768), a full-scale sine concentrated in a single bin produces:

text
|X[k]|_raw = (N / 2) · 0.5 · INT16_FULL = N · INT16_FULL / 4
              └─FFT scale─┘   └Hanning gain┘

Dividing each bin's magnitude by N · INT16_FULL / 4 therefore makes a full-scale single-bin tone read exactly 1.0 (= 0 dBFS) — a fixed reference that anchors every threshold downstream.

Logarithmic frequency band mapping + RMS-sum aggregation

24 bands are log-spaced from 30 Hz to 16 kHz so bass, mids, and treble each get roughly equal visual real estate, matching perceived pitch:

text
band_edge[i] = f_min · (f_max / f_min)^(i / N_bands)

Within each band the firmware sums power (|X[k]|²) across every FFT bin whose centre falls in [lo, hi), then takes a square root. This is energy-correct aggregation, the way professional RTAs do it: a full-scale tone in any single bin still reads 0 dBFS regardless of whether it lands in a narrow or wide band, while broadband content correctly accumulates the energy of all its bins instead of being washed out by averaging. For bands narrower than the bin spacing (which happens at the very bottom of the range — 21.5 Hz between bins), the firmware falls back to the bin nearest the band's geometric centre so the bar still registers.

The final per-band level is converted to dBFS with 20 · log10(magnitude), clamped from below to avoid log10(0).

Per-band EQ and display range mapping

A 24-element EQ curve in dB is added to each band's dBFS value to compensate for microphone frequency response — a mild bass lift to offset MEMS low-frequency roll-off, a mild presence lift in the upper mids. EQ in the log domain is mathematically clean: multiplicative gain becomes simple addition.

Final mapping clamps each band into the visual dynamic range:

text
display = clip( (bands_db + eq_db − DB_FLOOR) / (DB_CEIL − DB_FLOOR), 0, 1 )

With DB_CEIL = −18 dBFS (bar saturates) and DB_FLOOR = −45 dBFS (squelch — below this the bar reads zero). DB_FLOOR sits a few dB above typical mic self-noise so ambient room tone never flickers the bars at rest.

A coarse pre-FFT silence gate runs in parallel — if the raw int16 peak of the latest chunk is below SILENCE_PEAK_INT16 = 1500 (≈ −27 dBFS) the entire frame is treated as silence and skips the FFT entirely.

Peak-as-bar decay

Each band drives two animated quantities every frame:

text
bar_height -= BAR_FALL_RATE      // 0.04 / frame, linear fall
bar_height  = max(bar_height, current)

peak_mark  -= PEAK_FALL_RATE     // 0.015 / frame, slower
peak_mark   = max(peak_mark, current)

Both snap instantly upward whenever a band exceeds the current value. The bar falls fast enough to track musical dynamics; the peak marker — rendered as a dim white dot above the bar — falls slowly enough to leave a visible peak-hold trace. This is the same scheme used on the Raspberry Pi proof-of-concept, ported byte-for-byte.


Custom PCB Design

Controller Board (2-layer)

Hand-solderable 2-layer board hosting the ESP32-S3 module, MEMS microphone, power regulation, USB-C input, and user buttons.

Stackup: Standard 2-layer FR4, 1 oz copper.


MCU — ESP32-S3-WROOM-1-N8
PropertyValue
CoreDual Xtensa LX7
Max clock240 MHz
Flash8 MB (N8 variant)
USBNative Full Speed (12 Mbps), integrated PHY
WirelessWi-Fi 4, Bluetooth 5 LE
Form factorModule, castellated edge pads

Selection rationale:

  • Compute headroom: Real-time 2048-point floating-point FFT, log-band aggregation, dBFS conversion, peak-bar animation, and WS2812B single-wire serialization all run concurrently. The dual LX7 cores at 240 MHz provide comfortable headroom — capture and DSP/render are split across the two cores via FreeRTOS task pinning. An ATmega328P at 16 MHz cannot handle simultaneous I2S DMA, floating-point FFT, and the tight timing requirements of WS2812B bit-banging without frame drops.
  • Module form factor: The bare ESP32-S3 SoC is a QFN56 package requiring reflow. The WROOM-1 module exposes castellated edge pads, which are hand-solderable with a standard iron — consistent with the design's solderability requirement.
  • Native USB: Eliminates an external USB-to-UART bridge IC (CH340, CP2102, etc.). Flashing and serial debug use the same USB-C connector as power.
  • N8 variant specifically: Selected over R8/R16V to keep IO35–IO37 available as general-purpose GPIO.

Supporting circuitry follows Espressif's official ESP32-S3 Hardware Design Guidelines:

PinFunctionImplementation
EN (CHIP_PU)Chip enable / hard reset10 kΩ pull-up to 3V3; 1 µF RC delay cap to GND per Espressif design guide; pushbutton to GND for manual reset
GPIO0Boot mode strapping10 kΩ pull-up to 3V3 (default HIGH = normal boot); pushbutton to GND to enter download mode
D+/D−Native USB differential pair33 Ω series resistors placed close to MCU pins; traces matched in length, ~5 mm spacing, routed away from switching nodes and clocks
GPIO1WS2812B data out330 Ω series resistor (detail below)

Microphone — ICS-43434

A fully integrated I2S MEMS microphone combining the MEMS transducer, analog signal conditioning, ADC, decimation filter, anti-aliasing filter, and power management internally.

ParameterValue
Supply voltage1.65 V – 3.63 V
InterfaceI2S, 24-bit
SNR64 dBA
Operating current~490 µA (high-performance mode)
PackageBottom-terminated LGA (hot air required for assembly)

No external ADC or audio codec is needed. All analog signal conditioning is internal, significantly simplifying the schematic and eliminating noise concerns around an external analog frontend.

Supply filtering: The buck converter on the same board switches at 500 kHz, injecting noise onto the 3.3 V rail. A 10 Ω / 10 µF RC low-pass filter on the ICS-43434 VDD pin attenuates this before it can corrupt the microphone's internal analog circuitry:

text
f_c = 1 / (2π × R × C)
    = 1 / (2π × 10 × 10×10⁻⁶)
    ≈ 1,592 Hz

Attenuation at 500 kHz switching frequency:
  f / f_c = 500,000 / 1,592 ≈ 314
  A = 1 / √(1 + 314²) ≈ 1/314 ≈ −50 dB

−50 dB attenuation at the switching frequency exceeds the accepted −40 dB threshold for sensitive mixed-signal supply filtering. The resistive voltage drop at 490 µA is 4.9 mV — well within the 1.65 V minimum supply margin.

SD pin: Pulled to GND through a 100 kΩ resistor to ensure the output line decays to a defined low state during the tristate period.


WS2812B Data Line — Signal Integrity

A 330 Ω series resistor is placed between GPIO1 and the first WS2812B DIN, as close to the MCU pin as practical. Purpose:

  • Damping: Reduces transmission-line ringing caused by PCB trace inductance/capacitance interacting with fast GPIO switching edges.
  • Transient current limiting:
text
I_transient ≈ V / R = 3.3 V / 330 Ω ≈ 10 mA  (instantaneous worst case)
  • Rise time check: The RC time constant with the WS2812B input capacitance (~10 pF):
text
t_rise ≈ 2.2 × R × C = 2.2 × 330 × 10×10⁻¹² ≈ 7.3 ns

7.3 ns is well within the WS2812B's 200 ns minimum pulse width specification — the resistor damps ringing without distorting signal timing.

Logic level shifting: No level shifter IC is used. The WS2812B HIGH threshold is >0.7 × VDD = 3.5 V at 5 V supply, which is marginally above the ESP32-S3's 3.3 V GPIO output. This was validated experimentally during the Raspberry Pi proof-of-concept (the Pi's GPIO is also 3.3 V) before being carried forward to the PCB. At the trace lengths involved, 3.3 V logic drives the WS2812B reliably, eliminating a component from the BOM.


User Buttons

Three 12 mm × 12 mm SMD tactile pushbuttons, each mapped to a distinct runtime function:

ButtonFunctionFirmware Behaviour
B1 — Color ThemeCycles through preset color palettesSteps an index through a palette table stored in flash (e.g. spectrum rainbow, monochrome green, fire gradient, cool blue)
B2 — Input GainIncreases microphone input gain in discrete steps, wrapping back to minimumScales the FFT magnitude output by a fixed multiplier before log conversion — effectively sets the display sensitivity for quiet or loud environments
B3 — Visualization ModeCycles through visualization stylesToggles between: (1) standard bar spectrum, (2) peak-hold bars where peaks decay slowly under gravity, (3) mirror mode where bars grow outward from the vertical center of the matrix

Internal ESP32-S3 pull-up resistors are enabled in firmware. A timer-based software debounce (~20 ms lockout window) eliminates contact bounce without external RC networks.


LED Matrix Board (4-layer)

A custom 4-layer PCB housing 128 WS2812B RGB LEDs in an 8-row × 16-column grid. Designed from scratch rather than using an off-the-shelf panel, allowing exact physical dimensions, LED pitch, and layer stackup to be optimized for this enclosure.

Why 4 layers:
A dedicated ground plane on layer 2 provides a low-impedance return path for WS2812B switching currents, which can be large and fast (each LED draws up to 60 mA and switches on the 800 kHz data clock). The ground plane also reduces EMI from the LED data lines running across the board.

Decoupling strategy:

  • 100 nF ceramic capacitor placed as close as physically possible to the VDD/GND pins of each LED cluster — absorbs high-frequency switching transients local to each LED
  • 1000 µF bulk electrolytic at the board's power entry — handles large aggregate transient current when many LEDs update simultaneously

Connector: 3-position JST-PH SMD header (5V, GND, Data) on each board, connected via a short cable to the controller board.


Power Architecture

Input — USB-C

The device is powered from a standard USB-C 5 V supply. Per the USB-C specification, a UFP (device) must place 5.1 kΩ pull-down resistors on both CC1 and CC2 to signal its presence and request 5 V/900 mA from a host or charger. SBU1/SBU2 are left unconnected (only required for alternate modes such as DisplayPort). D+/D− are routed to the ESP32-S3 native USB PHY for flashing and debug.

5V Rail

The 5 V rail feeds the WS2812B matrix directly. LEDs are the dominant current consumer and are rated for 5 V operation.

3.3V Rail — Buck Converter

The 3.3 V rail (ESP32-S3, MEMS mic, logic) is stepped down from 5 V via a synchronous buck converter.

Why not an LDO — thermal analysis:

An LDO dissipates (V_in − V_out) × I_load as heat. At the ESP32-S3 peak current draw of 350 mA:

text
P_LDO = (5.0 − 3.3) V × 0.350 A = 595 mW

At a typical θ_JA of ~40 °C/W for a small SMD package, this raises junction temperature ~24 °C above ambient under sustained load with no heatsinking — unacceptable thermal stress for a hobby PCB.

A buck converter at ~88% efficiency dissipates:

text
P_out = 3.3 V × 0.350 A = 1.155 W
P_dissipated = P_out × (1/η − 1) = 1.155 × (1/0.88 − 1) ≈ 158 mW

Less than 27% of the LDO's dissipation. The buck converter was the clear choice.

Full System Power Budget

RailConsumerTypicalPeak
5 VWS2812B 8×16 (128 LEDs)~1.5 A~3.8 A (all-white full brightness)
3.3 VESP32-S3-WROOM-1~100 mA~350 mA
3.3 VICS-43434 MEMS mic0.49 mA0.49 mA
5 V total~1.6 A~4.2 A

The design targets a 5 V / 5 A USB-C adapter. System peak current sits below the 5 A ceiling with margin.

3.3V Decoupling Network

Following Espressif hardware design guidelines for the ESP32-S3:

CapacitorValueDielectricVoltage RatingPackage
Bulk22 µFX7R25 V1210
Mid-frequency1 µFX7R25 V0603
High-frequency bypass100 nFX7R50 V0603

X7R dielectric was specified throughout for stable capacitance over temperature and DC bias (±15% over −55 to +125 °C). Y5V/Z5U alternatives can lose >50% capacitance under DC bias and were explicitly avoided.

ESD protection: A PESD3V3S1UB unidirectional TVS diode is placed at the 3.3 V rail entry per Espressif's recommendation.


Design Constraints & Trade-offs

ConstraintDecisionRationale
Hand-solderabilityESP32-S3-WROOM-1 module over bare SoCBare QFN56 requires reflow; castellated module is iron-solderable
Hand-solderabilityMEMS mic requires hot air gunAll I2S MEMS microphones with adequate SNR use bottom-terminated packages — no through-hole alternative at this performance level
ThermalBuck converter over LDOLDO dissipates 595 mW at peak; buck dissipates ~158 mW
Signal integrity330 Ω series on WS2812B data lineDamps ringing; 7.3 ns rise time is well within 200 ns WS2812B spec
Supply noise10 Ω / 10 µF RC on microphone VDD−50 dB attenuation at 500 kHz switching frequency
USB implementationNative ESP32-S3 USB PHYEliminates external UART bridge IC, reduces BOM cost and board complexity
GPIO variantN8 over R8/R16VPreserves IO35–IO37 as general GPIO
LED matrixCustom 4-layer PCB over off-the-shelf panelExact dimensions, pitch, dedicated ground plane for LED switching currents
Logic level shiftingNo shifter IC3.3 V GPIO validated against WS2812B at relevant trace lengths during Pi phase

Roadblocks & How They Were Solved

1. FFT output was visually noisy and impossible to read
Raw FFT magnitudes mapped linearly to LED height produced a constantly saturated, unreadable display. Resolution: applied 20·log10(|X[k]|) per bin for dB scaling, then grouped bins into logarithmically spaced frequency bands. This transformed the visualization into a clean, perceptually balanced spectrum with clear bass, mid, and treble separation.

2. Display flickering between frames
Frame-to-frame FFT variation was fast enough to cause severe LED flickering. Resolution: a peak-as-bar animation with instant attack and linear decay (bar -= BAR_FALL_RATE, snap up on any larger sample), plus a separate slower-decaying peak-hold marker. Decay rates were tuned empirically during the Pi phase to balance smooth fall against visual reactivity, and the same constants were carried over to the ESP32 port.

3. LDO thermal concern on 3.3V rail
Initial design considered an LDO for simplicity. Thermal analysis revealed 595 mW peak dissipation — unacceptable for an SMD package on a hobby board with no thermal management. Buck converter selected; RC filtering on the mic supply mitigates the switching noise introduced.

4. WS2812B logic level margin
The WS2812B HIGH threshold (3.5 V at 5 V supply) exceeds ESP32-S3 GPIO output (3.3 V). Rather than adding a level shifter IC, this was validated experimentally on the Raspberry Pi (also 3.3 V GPIO) before the PCB was designed. Validated reliable at relevant trace lengths — the level shifter was eliminated from the BOM.

5. Buck switching noise on microphone supply
Concern that 500 kHz switching noise would corrupt audio captures. Resolved with a 10 Ω / 10 µF RC low-pass filter on the ICS-43434 VDD pin, providing −50 dB attenuation at the switching frequency — exceeding the −40 dB threshold considered best practice for mixed-signal supplies.


Firmware Architecture

The firmware lives under Firmware/ESP32Firmware/ as a PlatformIO project targeting Arduino-ESP32 v3.x (ESP-IDF 5.x). FFT runs on the bundled ESP-DSP component; the WS2812B chain is driven by FastLED over the ESP32's RMT peripheral. Capture and DSP/render are split across the two cores using FreeRTOS task pinning:

text
Core 0  ── i2s_capture task                       Core 1  ── dsp_render task
──────────────────────────────────                ───────────────────────────────────────
i2s_std driver (INMP441, MONO, LEFT slot)
  44.1 kHz · 32-bit slot · DMA pool 8 × 256
        │
        │  blocking read of CHUNK_SIZE samples
        ▼
sample >>= 16   (24-bit → int16, preserves
                 the int16 dBFS reference)
        │
        ▼
xQueueSend(audioQueue, …, 0)   ─────────────────► xQueueReceive(audioQueue, …, portMAX_DELAY)
  length 2, drop-oldest on full                          │
  (keeps I2S DMA hard real-time)                         ▼
                                                  silence gate on raw int16 peak
                                                         │
                                                         ▼
                                                  dsp_push_chunk
                                                    rolling 2048-sample window
                                                    shifts left, newest chunk appended
                                                         │
                                                         ▼
                                                  dsp_compute_bands
                                                    DC removal

File map

FileResponsibility
src/config.hAll DSP constants (sample rate, FFT size, band count, EQ curve, dBFS range, decay rates), I2S pins, LED layout, FreeRTOS core/priority assignments. Single source of truth for every tunable.
src/audio_capture.cppI2S setup on I2S_NUM_0 via the ESP-IDF 5.x i2s_std driver. Capture task pinned to core 0. Non-blocking xQueueSend with drop-oldest fallback so the I2S DMA never stalls if the DSP side falls behind.
src/dsp.cppPrecomputed Hanning window + log-spaced band edges. Rolling 2048-sample buffer. ESP-DSP dsps_fft2r_fc32 (radix-2) + dsps_bit_rev_fc32. RMS-sum power aggregation per band with nearest-bin fallback for bands narrower than the bin spacing. dBFS conversion via the calibration constant N · INT16_FULL / 4.
src/themes.cppFive colour themes × five stops each, linearly interpolated by band index and dimmed by amplitude (bright = 0.4 + 0.6 · amp). Same five palettes as the Pi version.
src/led_matrix.cppFastLED init on LED_DATA_PIN. xy_to_led walks chained 8×8 panels in serpentine layout (even rows L→R, odd rows R→L). Fills rows below the bar height with the column's theme colour; draws a dim white marker at the peak-hold row above.
src/main.cppsetup() brings up LEDs / DSP / capture and spawns the DSP+render task on core 1. loop() watches USB serial for 1–5 to switch themes (stand-in for the Pi version's keyboard hotkeys).

Cross-core synchronisation

The capture and render tasks share state through a single FreeRTOS queue of AudioChunk items (length 2). The capture side uses non-blocking xQueueSend; on a full queue it discards the oldest pending chunk and re-sends. This keeps I2S DMA running at hard real-time even if the FFT/render side stalls briefly (e.g. during the WS2812B frame flush), at the cost of occasionally dropping a chunk under sustained backpressure — strictly preferable to letting the DMA descriptors overflow.

There is no shared mutable state between cores beyond that queue. The render task owns the rolling 2048-sample buffer, the per-band decay state, and the LED framebuffer; the capture task owns only the I2S DMA and the int16 shift.

DSP constants

All knobs live as constexpr in config.h and are carried over byte-for-byte from the Raspberry Pi proof of concept (visualizer.py):

ConstantValuePurpose
SAMPLE_RATE44 100 HzI2S clock, Nyquist ≈ 22.05 kHz
CHUNK_SIZE1024 samplesPer-DMA-read frame size
FFT_SIZE2048Rolling window — 2× chunk so frames overlap 50%; bin spacing ≈ 21.5 Hz
NUM_BANDS24Log-spaced display bands (one per matrix column)
FREQ_MIN / FREQ_MAX30 / 16 000 HzDisplay range
EQ_DB[24]mild bass + presence liftAdditive per-band EQ in dB
DB_CEIL / DB_FLOOR−18 / −45 dBFSDisplay dynamic range
SILENCE_PEAK_INT161500Silence gate (≈ −27 dBFS peak)
BAR_FALL_RATE0.04 / frameLinear bar decay
PEAK_FALL_RATE0.015 / frameSlower decay for the peak-hold marker

Build & flash

text
cd Firmware/ESP32Firmware
pio run -t upload
pio device monitor

The platform line in platformio.ini points at the pioarduino fork, which ships Arduino-ESP32 v3.x with the ESP-IDF 5.x i2s_std driver and the bundled esp-dsp component — both required by this firmware.


Bill of Materials (Abridged)

ComponentPartKey Specification
MCU moduleESP32-S3-WROOM-1-N8Dual LX7 240 MHz, 8 MB flash, native USB
MicrophoneICS-43434I2S 24-bit, 64 dBA SNR
RGB LEDsWS2812B5 V, single-wire addressable, ×128
USB-C connectorUSB4085 or equivalentUFP, CC1/CC2 5.1 kΩ termination
ESD protectionPESD3V3S1UB3.3 V rail TVS
Tactile buttons12 mm × 12 mm SMD×3 (color theme, input gain, viz mode)
JST connectorB3B-PH-SM4-TB3-pos SMD, ×2 (one per board)
WS2812B series R330 Ω 0603Data line signal integrity
EN pull-up R10 kΩ ±1% 0603 100 mWEN RC delay circuit
GPIO0 pull-up R10 kΩ 0603Boot strapping default HIGH
USB D+/D− R33 Ω 0603 ×2USB differential signal integrity
3V3 bulk cap22 µF X7R 25 V 1210ESP32-S3 decoupling
3V3 mid cap1 µF X7R 25 V 0603Decoupling + EN RC
3V3 bypass cap100 nF X7R 50 V 0603High-frequency bypass
Mic supply filter10 Ω resistor + 10 µF capBuck noise suppression on VDD
LED bulk cap1000 µF electrolytic5 V rail at matrix board entry
LED bypass caps100 nF ceramic per clusterWS2812B local transient decoupling

Roadmap

  • Software proof-of-concept on Raspberry Pi 4
  • FFT pipeline: I2S capture → Hanning → 2048-pt FFT → log bands (RMS-sum power) → dBFS + EQ → peak-bar decay → LED output
  • Standalone button control with software debouncing
  • Controller board schematic and layout (2-layer)
  • LED matrix board schematic and layout (4-layer)
  • PCB fabrication and bring-up
  • Firmware ported to ESP32 with dual-core task allocation (capture on core 0, DSP+render on core 1, ESP-DSP FFT, FastLED render)
  • Firmware tuned and validated on the ESP32-S3 controller board (re-pin I2S/LED data lines for the production layout)
  • Enclosure design (Fusion 360) and 3D print
  • Final integration, calibration, and enclosure fit

References

Parts

141
QtyValueFootprintReferences
128~LED_SMD:LED_WS2812B-V6D1, D10, D11, D100, D101, D102, D103, D104, D105, D106, D107, D108, D109, D110, D111, D112 +112
4100nFCapacitor_SMD:C_0603_1608Metric_Pad1.08x0.95mm_HandSolderC2, C3, C4, C5
3, ,
11000 uFCapacitor_SMD:CP_Elec_10x10.5C1
1B3B-PH-SM4-TBLFSNConnector_PinHeader_2.54mm:CONN_B3B-PH-SM4-TB_03P_JSTJ1

The 137 most common of 141 parts.

More from h2dakhil/FFT-Audio-Spectrum-Analyzer

1
  • 2 layers80 × 30 mmController PCB, the printed circuit board in 3D

    Controller PCB

    h2dakhil/FFT-Audio-Spectrum-Analyzer

    Real-time audio-reactive LED matrix powered by an ESP32, featuring custom PCB design, I2S microphone input, and dynamic visual effects through FFT signal processing.

    42 parts

Similar boards

  • 4 layersSwitch demod, the printed circuit board in 3D

    Switch demod

    loxodes/vna

    Under development open hardware microwave vector network analyzer

    141 parts205
  • 4 layers101.6 × 101.6 mmISASTM, the printed circuit board in 3D

    ISASTM

    Manawyrm/ISASTM

    STM32-based ISA-over-USB-adapter

    141 parts136GPL-3.0
  • 4 layers61.2 × 61.2 mmGanglion 01, the printed circuit board in 3D

    Ganglion 01

    OpenBCI/Ganglion_Hardware_Design_Files

    General repo for Ganglion hardware files

    141 parts91
  • 4 layers90 × 50 mmGreenmobi bms, the printed circuit board in 3D

    Greenmobi bms

    G-Pereira/greenmobi-bms

    A VESC compatible battery management system

    141 parts31CERN-OHL-S-2.0
  • 4 layers536.5 × 385.7 mmDC27-badge, the printed circuit board in 3D

    DC27-badge

    netik/dc27_badge

    Defcon 27 "DaBomb!" badge.

    141 parts30Apache-2.0
  • 4 layers144 × 83.5 mmEbrake1, the printed circuit board in 3D

    Ebrake1

    wayri/SPIKE

    PCB-focused multiphysics simulation, from board to complete assembly: PI, SI, circuits, thermal, EM/RF and mechanical workflows, with MCAD collaboration and a roadmap toward large-scale coupled assemblies.

    141 parts17
  • 4 layers100 × 100 mmSDR TRX, the printed circuit board in 3D

    SDR TRX

    frohro/SDR-TRX

    SDR-TRX is an experimental low-cost digital mode ham radio HF transceiver.

    141 parts14
  • 4 layers241.7 × 56.7 mmDaisy studio, the printed circuit board in 3D

    Daisy studio

    squeedee/daisy-studio

    Daisy Seed Carrier for Balanced Pro Audio

    141 parts14MIT

Questions

How big is the LED Matrix PCB?

It measures 160 × 90 mm and has 4 copper layers, 1.6 mm thick.

What parts does the LED Matrix use?

141 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the LED Matrix KiCad files?

From the h2dakhil/FFT-Audio-Spectrum-Analyzer repository on GitHub, at the commit this page was made from: the board is Hardware/PCB/LED Display/LED Matrix.kicad_pcb and its schematic Hardware/PCB/Controller PCB/Controller PCB.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the LED Matrix design in my own project?

Its repository states no licence, so its author keeps all rights: ask h2dakhil before reusing the design.