Inspiration

I'm a Linux user, and cava is always running in a corner of my screen while I listen to music. I like it, but after a while its bars started to feel the same for every song. They show how loud each frequency band is, but they don't show what the music actually is.

Then one day I came across the Chladni sand effect: sand scattered on a metal plate, and when the plate vibrates at a certain pitch, the sand jumps away from the moving parts and gathers on the nodal lines, where the surface stays still. Every pitch draws its own geometric figure, and when the pitch changes, the sand rearranges itself into a new one. Ernst Chladni first showed this in 1787, and it was one of the first times anyone could see a sound.

That's when the idea for Grava came to me: what if my audio visualizer worked like Chladni's plate instead of a bar chart? A visualizer where every note has a shape, a melody becomes a sequence of shapes, and the physics of sound becomes art, right inside the terminal I already live in.

What it does

Grava listens to whatever the computer is playing and draws sand on a vibrating plate or drum head, in real time, using only text characters.

  • Every note has its own figure. An A always draws the same pattern. When the melody moves, the sand visibly flows from one figure into the next.
  • Musically meaningful, not decorative. Higher octaves give more intricate figures. Louder music shakes the sand harder. Beats make it jump. In silence the sand rests, exactly like a real plate.
  • Two instruments: a square plate (classic Chladni figures) and a circular drum head (Bessel-function modes) that slowly rotates.
  • Three mappings: by note (pitch class → figure, octave → complexity), by frequency, or by spectrum (the strongest peaks each pull their own figure, blended together).
  • Fine "sand" made of Braille dots (2×4 sub-pixels per character), at 60 fps with no flicker. It falls back to quadrant blocks, half blocks or ASCII, so it works even on the bare Linux console.
  • Fully customizable: 8 built-in themes, pywal support, 7 ways to color the sand (density, distance, amplitude, the note being played…), borders, a live status line, and a config that reloads while it runs.
  • Zero setup: it works out of the box on any PipeWire or PulseAudio desktop, records what you hear (never the microphone), and includes a built-in demo melody for machines without audio.

How I built it

Grava is written in Rust as a reusable core library (dsp, chladni, sim, render) plus a thin command-line app.

  1. Capture. An audio thread records the monitor of the default output through PulseAudio (a pure-Rust protocol client), native PipeWire, ALSA, a FIFO, or stdin, and pushes samples into a lock-free ring buffer. The audio callback never blocks and never allocates.
  2. Listen like a musician. Every frame, the last 4096 samples go through a Hann window and an FFT. From there Grava measures loudness with automatic gain, finds spectral peaks with sub-bin interpolation, folds them into a 12-note chroma to name the pitch and octave, and detects beats with spectral flux and an adaptive threshold.
  3. Physics of the plate. Square-plate modes follow the classic Chladni model: $$f(x,y) = X_n(x)\,X_m(y) \pm X_m(x)\,X_n(y),\quad X_k(t)=\cos(k\pi t)$$ Drum modes use Bessel functions: $$u(r,\theta) = J_n(j_{n,k}\,r)\cos\big(n(\theta-\theta_0)\big)$$ Grava computes the Bessel zeros at startup and sorts all modes by eigenfrequency into a fixed catalog. Then each note maps to a figure, and the figure stays the same when the window is resized.
  4. Sand simulation. Up to 150,000 grains follow a physical rule: they shake in proportion to how much the plate moves under them, drift down the gradient towards the nodal lines, and jump on every beat. Figures crossfade smoothly when the note changes.
  5. Rendering. I wrote a custom terminal renderer. It rasterizes grains into Braille sub-pixels, colors them with gradients blended in OKLab, and redraws only the cells that changed, in one synchronized write per frame.

I wrote a detailed design specification first (architecture, math, performance budgets and milestones) and then built the project milestone by milestone with an AI coding agent (OpenAI Codex). Every milestone had to pass formatting, linting and tests before the next one began. I tested real audio capture myself on my own Linux machines.

Challenges I ran into

  • Naming the note correctly. Below about 200 Hz, a single FFT bin is wider than a semitone, so naive pitch detection names the wrong note. I solved it by folding interpolated peaks rather than raw bins into the chroma.
  • Figures that looked like noise. High-order drum modes ("whispering gallery" modes) barely move in the center, so sand there never settled. I measured each mode's "dead radius" and removed the hollow ones from the catalog.
  • Stability vs. responsiveness. Real music is messy. A note has to win for a short hold time before the figure changes, and figures crossfade, so the sand neither flickers nor lags.
  • Smooth at 60 fps in a terminal. Terminals are slow, so I diff every frame cell by cell, track color state to avoid redundant escape codes, and wrap each frame in synchronized output.
  • Working on every Linux system. The default build needs no system libraries at all, so the same static binary runs on Fedora, Debian, Arch, Void and Alpine (glibc and musl).

Accomplishments that I'm proud of

  • The whole pipeline (audio analysis, physics, simulation and rendering) takes about 0.5 ms per frame on a single CPU core. A resting plate is not redrawn at all, so idle cost is far below 1% of one core.
  • 329 automated tests, plus a deterministic headless mode (same seed gives the exact same frame), so the art is reproducible and testable.
  • It is actually shipped: static binaries for x86_64 and ARM64, .deb and .rpm packages, a signed APT repository, Fedora COPR, a Nix flake and a Snap.
  • Watching a piece of music, such as Vivaldi's Summer, draw itself in sand for the first time.

What I learned

  • The physics of vibrating plates and membranes: eigenmodes, nodal lines and Bessel functions, and how closely they connect to musical pitch.
  • Practical digital signal processing: windowing, FFT resolution limits, chroma features and onset detection.
  • Real-time systems design in Rust: lock-free queues, zero-allocation hot loops, and performance budgets.
  • How to write a precise specification and steer an AI coding agent through a large project while keeping the quality high.

What's next for Grava

  • Learning mode: show the note name and the mode next to each figure, so students can see music theory and acoustics at the same time.
  • Live performance: MIDI input and a full-screen GPU mode for projecting at concerts and in galleries.
  • Image and video export, so a song can become a printable "sand portrait".
  • More instrument shapes (rectangular plates, violin-shaped plates) and publishing to the AUR and Void repositories.

Built With

  • alsa
  • arch
  • crossterm
  • debian
  • digital-signal-processing
  • fedora
  • fft
  • github-actions
  • linux
  • nix
  • pipewire
  • pulseaudio
  • realfft
  • rust
  • terminal
  • toml
Share this project:

Updates

Submission history