AMEN AUDIO
AMEN AUDIO / MANUALDownload Markdown ↓

PRISM

An audio analyzer for the Daisy Pod and a Waveshare 2.42" OLED (SSD1309, 128×64, SPI), in the spirit of Excite Audio's Vision 4X. It includes a spectrum, spectrogram, oscilloscope, waveform, stereo field, EBU R128 loudness, level meters and a tuner. You can view them one at a time or four at once, and drive everything from the Pod's knobs, encoder and buttons. Audio passes straight through, so PRISM can sit inline in a chain.

Both images are rendered by the host simulator from the same code that runs on the Pod.

Wiring

The module ships in 4-wire SPI mode, which is what PRISM uses. Everything goes to the Pod's P2 header:

OLED Pod P2 pad Seed pin
VCC 3V3D 3.3 V digital (not 3V3A, which feeds the audio codec)
GND GND
DIN MO D10, SPI1 MOSI
CLK SCK D8, SPI1 SCK
CS SS D7, SPI1 NSS
DC MI D9, used as a plain output (the display never talks back)
RES A1 D16

SPI runs at 6.25 MHz, inside the SSD1309's 10 MHz limit. A full frame takes 1.3 ms, and the display refreshes at 60 fps.

Controls

Control On a page In the menu
Encoder turn previous / next page move; change the value while editing
Encoder click open the menu edit / toggle / confirm
Encoder hold – close the menu
Knob 1 / Knob 2 the page's two parameters (soft takeover, so nothing jumps) –
Button 1 freeze the display (HOLD) · hold: FPS/CPU readout back
Button 2 page mode (channel L/R·MID·SIDE·LEFT·RIGHT, or stereo draw mode) · hold: reset meters and integrated loudness –

Hold the encoder on the spectrum, spectrogram, scope, waveform or loudness page for a measurement cursor: the encoder then scrubs a vertical rule and reads out the exact value under it (frequency and dB on the spectrum, time and level elsewhere). Click to leave. It works while frozen, which is the point.

LED 1 shows level: green, then amber above −6 dBFS, then red on clip, and blue while frozen. The clip colour clears 1.5 s after the last clipped sample; the on-screen CLIP indicator latches until RESET METERS. LED 2 shows phase correlation: green above +0.3, amber from 0 to +0.3, and red when out of phase.

Pages

Page Knob 1 Knob 2 What it shows
SPECTRUM range tilt (0–6 dB/oct, default 4.5) log 20 Hz–20 kHz FFT (up to 4096) with glow fill, peak hold, and a peak frequency/level readout
SPECTROGRAM floor scroll speed scrolling waterfall, log frequency axis, dithered intensity
OSCILLOSCOPE time 0.5–100 ms gain (AUTO at 0) edge-triggered trace with phosphor persistence and a pitch readout
WAVEFORM span 1–20 s zoom scrolling min/max envelope with an RMS core (L up and R down in L/R mode)
STEREO FIELD zoom (AUTO at 0) persistence goniometer (lines/dots), polar field, or BANDS: width and correlation per octave, so you can see which frequencies are wide and which would collapse in mono
LOUDNESS target (−23…−9 LUFS) history span short-term LUFS; momentary, integrated, LRA and true peak; history graph against the target
LEVELS release peak hold LED-ladder peak/RMS bars, clip latch, and a peak / true peak / RMS / crest table
TUNER A4 reference 430–450 Hz damping note, cents needle, Hz, and a strobe band that stands still when in tune
TEMPO & KEY feel (70–190 BPM) – BPM from onset autocorrelation with a beat pulse, plus key from chroma (Krumhansl-Schmuckler) and 12 pitch-class bars. FEEL sets the tempo you expect to feel — a drum & bass break is 174 to one listener and 87 to another, and the half/double reading is always shown underneath
QUAD – – any four of the above at once; pick the slots in the menu

Menu: page settings · STYLE (per page) · SHADING · SOURCE (input, demo song, sweep, pink noise, 1 kHz −20 dBFS calibration tone, stereo test) · MONITOR (thru, mono, side, swap L/R, mute) · FFT size · averaging · peak hold · reset meters · brightness · invert · pixel shift (burn-in guard: the page is drawn two rows short and slides inside that margin, so nothing is ever clipped) · FPS/CPU · SAVE TO FLASH · FACTORY RESET.

With nothing plugged in, set SOURCE → DEMO SONG to show off every page.

Measurement notes

  • Loudness follows ITU-R BS.1770-4 / EBU R128. It uses K-weighting at 48 kHz, 400 ms momentary and 3 s short-term windows, and integrated loudness gated at −70 LUFS absolute and −10 LU relative. LRA follows EBU Tech 3342, and true peak uses 4× oversampling. The host tests check these against the EBU 3341/3342 reference cases.
  • Onset detection uses flux in linear magnitude, not dB. In dB a hi-hat rising out of silence outweighs a kick, which flattens every subdivision to the same height and leaves the tracker nothing to lock to.
  • Numbers drawn as text are lightly smoothed and republished 8 times a second, so the last digit stays readable; graphs still update every frame.
  • Tempo tracking weights candidate periods by a log-normal preference around 120 BPM. Without it the half-tempo lag wins on any track whose period is not a whole number of frames (a 128 BPM click track scored 1.00 at half tempo against 0.82 at the true tempo), and averaging across that disagreement produced a reading that was never true.
  • Key detection picks spectral peaks and interpolates them parabolically rather than summing bins. At 110 Hz a semitone is 6.5 Hz while a 4096-point bin is 11.7 Hz, so a summed bass A lands in G# and A#.
  • The 1 kHz −20 dBFS test tone reads −20.0 LUFS, −20.0 dBFS peak, −23.0 dBFS RMS and B5 +21 cents on the tuner.
  • The main loop never sleeps. Idling the core between frames makes the Pod's audio IRQ current swing couple into the codec as a 1 kHz comb, and this analyzer would faithfully draw that comb.

Build, flash, test

make                          # firmware (BOOT_QSPI)
make program-dfu              # Daisy bootloader must be in DFU mode
make -C host/test test        # 176 host checks under ASan/UBSan
make -C host sheet            # render every page to host/build/sheet.png
make -C host tour             # scripted UI tour -> host/build/tour.mp4
host/build/sim --wav song.wav --page 4 --out stereo.png

Layout: core/ holds all analysis and drawing as headers with no libDaisy dependency. It is shared by Prism.cpp (firmware), host/sim.cpp (renders PNGs) and host/test/.

Rig (USB-MIDI)

tools/prism_rig.py needs pip install mido python-rtmidi:

tools/prism_rig.py boot            # CC119=127 -> bootloader, then make program-dfu
tools/prism_rig.py beat            # heartbeat: check t_ms is small after flashing
tools/prism_rig.py grab now.png    # screen grab of the live framebuffer
tools/prism_rig.py sheet all.png   # grab every page
tools/prism_rig.py check "EP-136"  # play test tones into the Pod input, compare readings

Settings persistence

SAVE TO FLASH in the menu stores everything (page parameters, styles, source, monitor, shading, brightness, current page) in the last 4 KB sector of the QSPI flash, and it is restored at the next power-up. Saving stops audio for about 100 ms; a rejected or blank sector simply leaves the defaults in place. tools/prism_rig.py save does the same over USB, and store reports the result.

This needs its own driver. The app runs XIP from QSPI, so the flash cannot serve instruction fetches while it is being erased: the entire write path lives in ITCM (qspi_store.h, prism_qspi.lds) and runs with audio stopped and interrupts masked. libDaisy's PersistentStorage cannot be used at all here — from a BOOT_QSPI app it takes a refusal path that stores a status byte through a null pimpl_, scribbling on absolute address 0x5C.

The part that took the debugging: libDaisy leaves the flash in continuous-read mode (0xEB quad I/O, alternate byte 0xA0, SIOO), where the chip expects no instruction byte at all, so the first write-enable is swallowed as address bits and the erase never starts. The fix is to toggle the QUADSPI enable bit and then send a four-line read whose mode byte is not 0xAx, in that order — verified by reading the JEDEC ID back (9D 60 17) after each candidate sequence. prism_rig.py can re-run that probe if it ever regresses.

If the Pod is running PT32-V, boot works for the very first flash too, since it uses the same CC119 hook. Otherwise, open the bootloader's DFU window by hand: press RESET, then tap BOOT while the LED pulses. Settings live in RAM and reset on power-up.

Blank display? Every boot starts with a 0.6 s all-white lamp test (SSD1309 command 0xA5, independent of the framebuffer). If even that stays dark, the problem is wiring or power, not firmware. SysEx 0x0A makes the Pod report whether SPI initialised, its transmit error count, and a read-back of the CS/DC/RES pins. On the first build a swapped GND/MO pair showed exactly this: every check passed and the panel stayed dark.