Arnov Sharma
Published © MIT

WaveFORM : the Background Sound Visualizer

Waveform is a small wall-mounted display that turns sound into moving waves.

BeginnerFull instructions provided5 hours80
WaveFORM : the Background Sound Visualizer

Things used in this project

Hardware components

Raspberry Pi Pico 2
Raspberry Pi Pico 2
×1
NextPCB  Custom PCB Board
NextPCB Custom PCB Board
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

STEP FILE

Schematics

SCH

Code

code

C/C++
#include <Adafruit_Protomatter.h>
#include <math.h>
#define WIDTH 64
#define HEIGHT 32
// HUB75 pinout (use your wiring)
#define R1 2
#define G1 3
#define B1 4
#define R2 5
#define G2 8
#define B2 9
#define A 10
#define B 16
#define C 18
#define D 20
#define CLK 11
#define LAT 12
#define OE 13
uint8_t rgbPins[]  = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { A, B, C, D };
Adafruit_Protomatter matrix(
WIDTH, HEIGHT, 1,
rgbPins, 4, addrPins,
CLK, LAT, OE, false
);
// ---------- audio + waveform settings ----------
const int MIC_PIN   = 28;    // GP28 (your working mic pin)
const int FRAME_MS  = 16;    // ~60 FPS
const int OVERSAMP  = 128;   // samples per frame
// Horizontal spacing between points: 1 = packed, 2 = more spread, 3 = even more
const int X_STEP    = 2;
const int NUM_POINTS = WIDTH / X_STEP;  // how many points across the screen
// How much of the screen height the wave can use (leaves a small margin)
const float MAX_WAVE_FRACTION = 0.5f; // 0.5 -> up to half-height up and down
// Loudness tracking (visual auto-gain, not audio)
float rmsPeak    = 200.0f;   // starting guess, will adapt
float loudSmooth = 0.0f;
// Adaptive noise floor
bool  noiseInit   = false;
float noiseFloor  = 100.0f;   // will get overwritten quickly
// Gate: how strong the signal must be (0..1 after normalization) to draw
const float LOUD_GATE = 0.08f;   // raise this if still too sensitive
// For drawing
float waveY[NUM_POINTS];
void setup() {
analogReadResolution(12);  // 0..4095 on RP2040
matrix.begin();
matrix.fillScreen(0);
matrix.show();
// Initialise wave points to center
float mid = (HEIGHT - 1) * 0.5f;
for (int i = 0; i < NUM_POINTS; i++) {
waveY[i] = mid;
}
}
void loop() {
static unsigned long lastFrame = 0;
unsigned long now = millis();
if (now - lastFrame < (unsigned long)FRAME_MS) return;
lastFrame = now;
// --------- collect audio samples for this frame ---------
static float samples[OVERSAMP];
uint32_t sum = 0;
int vmin = 4095;
int vmax = 0;
for (int i = 0; i < OVERSAMP; i++) {
int v = analogRead(MIC_PIN);
samples[i] = (float)v;
sum += v;
if (v < vmin) vmin = v;
if (v > vmax) vmax = v;
// small delay between samples; tweak if needed
delayMicroseconds(150);
}
// DC offset = average of this frame
float dc = (float)sum / (float)OVERSAMP;
// Center samples and compute RMS + max absolute value
float rmsSum = 0.0f;
float maxAbs = 1.0f; // avoid divide by zero
for (int i = 0; i < OVERSAMP; i++) {
float centered = samples[i] - dc;
samples[i] = centered;
float a = fabsf(centered);
if (a > maxAbs) maxAbs = a;
rmsSum += centered * centered;
}
float rms = sqrtf(rmsSum / (float)OVERSAMP);
// --------- adaptive noise floor + effective RMS ---------
if (!noiseInit) {
noiseFloor = rms;   // first frame sets starting noise level
noiseInit = true;
}
// If current rms is lower than floor, follow quickly (room got quieter)
if (rms < noiseFloor) {
noiseFloor = 0.95f * noiseFloor + 0.05f * rms;
} else {
// If louder, creep up very slowly (avoid calling constant background "noise")
noiseFloor = 0.999f * noiseFloor + 0.001f * rms;
}
// Effective signal above noise floor
float rmsEffective = rms - noiseFloor;
if (rmsEffective < 0.0f) rmsEffective = 0.0f;
// --------- auto visual gain from effective RMS ---------
// Track a peak RMS (above noise) that decays slowly so visuals adapt
if (rmsEffective > rmsPeak) {
rmsPeak = rmsEffective;
} else {
rmsPeak *= 0.995f; // decay, tweak for slower/faster adaptation
if (rmsPeak < 50.0f) rmsPeak = 50.0f; // floor
}
float loud = (rmsPeak > 0.0f) ? (rmsEffective / rmsPeak) : 0.0f;   // 0..1-ish
if (loud > 1.5f) loud = 1.5f;    // clamp a bit above 1
if (loud < 0.0f) loud = 0.0f;
// Apply gate: ignore very small signals
if (loud < LOUD_GATE) {
loud = 0.0f;
}
// Smooth loudness so it doesn't jitter
const float LOUD_ALPHA = 0.2f;  // lower = smoother, higher = more responsive
loudSmooth = (1.0f - LOUD_ALPHA) * loudSmooth + LOUD_ALPHA * loud;
// If gated to zero, keep it exactly zero after smoothing when very small
if (loudSmooth < (LOUD_GATE * 0.5f)) {
loudSmooth = 0.0f;
}
// --------- build waveform points across the width ---------
float mid = (HEIGHT - 1) * 0.5f;
float maxAmp = (HEIGHT * MAX_WAVE_FRACTION); // how many pixels up/down
int stride = OVERSAMP / NUM_POINTS;
if (stride < 1) stride = 1;
for (int i = 0; i < NUM_POINTS; i++) {
int idx = i * stride;
if (idx >= OVERSAMP) idx = OVERSAMP - 1;
float s = samples[idx];  // centered sample
// Normalize this sample to -1..1 based on maxAbs in this frame
float sampleNorm = s / maxAbs;
if (sampleNorm > 1.0f)  sampleNorm = 1.0f;
if (sampleNorm < -1.0f) sampleNorm = -1.0f;
// Scale by loudness (bigger wave when effective RMS is higher)
float amp = loudSmooth * maxAmp;
float y = mid - sampleNorm * amp; // minus so positive = up
if (y < 0.0f) y = 0.0f;
if (y > (float)(HEIGHT - 1)) y = (float)(HEIGHT - 1);
waveY[i] = y;
}
// --------- choose color based on loudness (no yellow) ---------
// loudSmooth = 0.0  → bright green
// loudSmooth ~ mid  → dimmer green
// loudSmooth high   → pure red
float loudNorm = loudSmooth;
if (loudNorm < 0.0f) loudNorm = 0.0f;
if (loudNorm > 1.0f) loudNorm = 1.0f;
const float RED_THRESHOLD = 0.6f;  // when to switch from green → red
uint8_t r, g;
if (loudNorm < RED_THRESHOLD) {
// Green only, fade from 255 → ~100 as loudness grows
float t = loudNorm / RED_THRESHOLD;      // 0..1
g = (uint8_t)(255.0f - 155.0f * t);      // 255 → 100
r = 0;
} else {
// Red only, fade from 100 → 255 as loudness goes high
float t = (loudNorm - RED_THRESHOLD) / (1.0f - RED_THRESHOLD); // 0..1
r = (uint8_t)(100.0f + 155.0f * t);      // 100 → 255
g = 0;
}
uint16_t color = matrix.color565(r, g, 0);
// --------- draw waveform ---------
matrix.fillScreen(0);
for (int i = 1; i < NUM_POINTS; i++) {
int x0 = (i - 1) * X_STEP;
int x1 = i * X_STEP;
int y0 = (int)roundf(waveY[i - 1]);
int y1 = (int)roundf(waveY[i]);
matrix.drawLine(x0, y0, x1, y1, color);
}
matrix.show();
}

Credits

Arnov Sharma
362 projects • 369 followers
I'm Arnov. I build, design, and experiment with tech—3D printing, PCB design, and retro consoles are my jam.

Comments