Skip to content
/ crust Public

Real-time audio spectrum visualizer for Raspberry Pi Zero 2W + Adafruit OLED Bonnet.

License

Notifications You must be signed in to change notification settings

jnf/crust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

crust

Real-time audio spectrum visualizer for Raspberry Pi Zero 2W + Adafruit OLED Bonnet.

Hardware

Component Details
Pi Raspberry Pi Zero 2W, 64-bit Raspberry Pi OS Lite (aarch64)
Display Adafruit OLED Bonnet 4567 — 128×32px, SSD1305, I2C
I2C bus /dev/i2c-1, address 0x3C, 400kHz fast-mode
Audio signal chain micro-HDMI → HDMI audio extractor → S/PDIF → digital receiver

Signal path

MPD (playback)
  ├── ALSA → hw:vc4hdmi,0 → HDMI → extractor → S/PDIF → receiver
  └── FIFO output plugin → /tmp/mpd.fifo  (raw PCM, 44100/16bit/stereo)
                                  │
                               CAVA
                  (reads PCM FIFO, outputs spectrum bars at 30fps)
                                  │
                    /tmp/cava.fifo  (16 × u16 LE per frame)
                                  │
                               crust  (this binary)
                  (reads bar values, scales, renders rectangles)
                                  │
                            I2C @ 400kHz
                                  │
                         SSD1305 OLED (128×32)

Pi-side dependencies

  • mpd 0.24.4: audio playback daemon
  • mpc: MPD command-line client
  • cava 0.10.4: audio spectrum analyzer
  • i2c-tools: for i2cdetect (diagnostics)

Building

Cross-compile on macOS for aarch64 Linux using cargo-zigbuild:

# prerequisites (one-time)
rustup target add aarch64-unknown-linux-gnu
brew install zig
cargo install cargo-zigbuild

# build
cargo zigbuild --release

# deploy
scp target/aarch64-unknown-linux-gnu/release/crust <user>@<pi>:~/

The default target is set in .cargo/config.toml, so plain cargo zigbuild --release builds for the Pi.

See SETUP.md for first-time Pi setup: dependencies, config files, systemd services, and deployment steps.

Visualization — how the bars render

CAVA output format

Each frame from /tmp/cava.fifo is exactly NUM_BARS * 2 bytes: one little-endian u16 per bar, in frequency order (low → high). Values range from 0 to 65535.

frame = [ lo0, hi0, lo1, hi1, ..., lo15, hi15 ]   (32 bytes total)

Bar layout on the 128×32 display

The SSD1305 has 132 column drivers for a 128px panel; hardware columns 0–3 are off-screen on the left. X_OFFSET=2 distributes the resulting 3px deficit across both edge bars: bar 0 loses 2px (renders 5px, flush with left panel edge), bar 15 loses 1px (renders 6px). All 14 inner bars are 7px wide with 1px gaps.

const NUM_BARS: usize = 16;   // bars CAVA is configured to output
const BAR_WIDTH: u32  = 7;    // pixels wide per bar
const BAR_STEP: u32   = 8;    // pixels from one bar's left edge to the next
                               // = BAR_WIDTH (7) + 1px gap
const X_OFFSET: i32   = 2;    // splits SSD1305 3px deficit across edge bars
const DISPLAY_HEIGHT: u32 = 32; // pixel rows available

Layout (software x positions, bar index 0–15):

bar 0:  x = 2,   panel pixels 0–4    (5px visible — 2px off-screen left)
bar 1:  x = 10,  panel pixels 6–12   (7px)
bar 2:  x = 18,  panel pixels 14–20  (7px)
...
bar 15: x = 122, panel pixels 118–123 (6px — 1px clipped at software edge)

Height scaling

CAVA outputs u16 values (0–65535). These are scaled linearly to pixel height (0–32):

let raw    = u16::from_le_bytes([buf[i * 2], buf[i * 2 + 1]]);
let height = (raw as u32 * DISPLAY_HEIGHT) / 65535;

Bars grow from the bottom of the display. The top-left corner of each bar rectangle is computed as:

let x = (i as u32 * BAR_STEP) as i32;       // left edge of bar i
let y = (DISPLAY_HEIGHT - height) as i32;   // top edge (higher bar → smaller y)

A bar with height = 0 is skipped entirely (nothing drawn). A bar with height = 32 fills the full column from y=0 to y=31.

Render loop

Each iteration:

  1. read_exact — blocks until a full 32-byte frame arrives from the FIFO
  2. Clear the framebuffer by drawing a black rectangle over the full display
  3. For each bar: draw a white filled rectangle at (x, y) with size (BAR_WIDTH, height)
  4. flush() — push the framebuffer to the display over I2C

The loop rate is naturally paced by CAVA's output (30fps target). No explicit sleep or timer is needed.

About

Real-time audio spectrum visualizer for Raspberry Pi Zero 2W + Adafruit OLED Bonnet.

Resources

License

Stars

Watchers

Forks

Contributors