Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cymatic

MilkDrop-class music visualizer that renders real GPU pixels inside your Ghostty/Kitty pane. Play anything in Music.app — or Spotify, or a browser tab — and the pane becomes a beat-synced light show: ray-marched tunnel, hyperspace starfield, CQT waterfall, feedback-warp plasma. 60fps, true pixels, no Unicode bars, no half-block ASCII.

cargo run --release

Now playing

A dedicated top line shows what's playing — ♪ Title — Artist ▕████░░▏ 1:29 / 3:35 · Music — with a live progress bar; the playhead is polled from Music.app/Spotify every ~2s via Apple Events (public scripting API) and extrapolated between polls. Track progress is also fed to scenes as u.track_progress (0..1). Disable with --no-track to reclaim the row for pixels.

How it works

Music.app / Spotify / browser
        │  Core Audio process tap (global stereo mix — works with DRM'd
        │  Apple Music because we tap the process output, never the files)
        │  + Apple Events now-playing poll (title/artist/duration/position
        │    from Music.app and Spotify, extrapolated between polls)
        ▼
DSP @ render cadence — 2048-pt FFT, 128 log bands, bass/mid/treble
        envelopes, spectral-flux onsets, BPM  →  lock-free ring
        ▼
wgpu (Metal), offscreen — WGSL scenes with audio uniforms, a 128×512
        spectrum-history texture, and previous-frame feedback (ping-pong)
        ▼
POSIX shared memory  →  kitty graphics protocol
        auto-probed per terminal (functional probes, not `a=q` queries):
        · frame mode: root image once (a=T,t=s), then in-place frame
          updates (a=f,r=1,t=s) — Kitty, Ghostty 1.4+
        · retransmit mode: delete-all + fresh a=T per frame (mpv-style)
          — Ghostty 1.3.x, still shm-only
        ▼
Ghostty rebinds the texture and composites it in your pane

The PTY only ever carries ~100-byte escape sequences per frame — pixels travel through shm (t=s), never base64. The status line shows the measured PTY byte rate so you can verify (typically single-digit KB/s at 60fps; a base64 hot path would be ~300 MB/s).

Requirements

  • macOS 14.2+ (process-tap API), 15+ preferred. Apple Silicon recommended.
  • Ghostty (first-class) or Kitty (second-class). Nothing else: cymatic needs the kitty graphics protocol with POSIX shm transport. Terminal.app, iTerm2, and Alacritty are detected and refused with one line.
    • Ghostty 1.4+/tip and Kitty get in-place a=f frame updates.
    • Ghostty 1.3.x has no a=f (and known non-direct-medium quirks); cymatic auto-falls back to mpv-style a=T retransmit — still shm, never base64. Run cymatic doctor to see exactly what your terminal answered.
  • No multiplexers — not tmux, not herdr. A mux puts its own virtual terminal between cymatic and Ghostty: herdr's embedded libghostty-vt allows only the direct graphics medium (it answers t=s with EINVAL: unsupported medium), doesn't report pixel geometry, and never composites pixels into the host terminal; tmux needs unicode-placeholder routing that v0 doesn't implement. cymatic detects both and refuses with one line. Run it in a plain Ghostty/Kitty tab.
  • Local only — shm is by definition same-machine. SSH is out of scope.

Permissions

First run triggers macOS's one-time System Audio Recording prompt, attributed to your terminal (Ghostty). Grant it once. While the tap runs you'll see the purple recording indicator in the menu bar — that's the OS being honest, not a bug. No virtual audio driver, no BlackHole, no multi-output device: the tap is the capture path.

If you denied the prompt: System Settings → Privacy & Security → Screen & System Audio Recording → System Audio Recording Only → enable your terminal.

The now-playing line additionally triggers a one-time Automation prompt ("Ghostty wants to control Music") the first time metadata is polled. Denying it only hides track metadata; the visualizer keeps running. cymatic track-check tests this path standalone.

Usage

command what
cymatic run the visualizer (tap audio, 60fps target)
cymatic --audio fake built-in 128-BPM techno generator (no permission needed)
cymatic --scene tunnel start on a specific scene
cymatic --present retransmit force the mpv-style presenter (auto-probed by default)
cymatic --no-track hide the now-playing line (no Apple Events)
cymatic doctor capability report: terminal, gfx+shm probe, Metal, geometry
cymatic tap-check prove the tap works: live RMS/bands as text
cymatic track-check prove metadata works: title/artist/position polls
cymatic bench headless GPU benchmark (--width --height --frames)

Keys

key action
Tab / Shift-Tab next / previous scene
c cycle color mode (album → curated → drift → duotone)
16 jump to scene
r reload current scene from disk
q / Ctrl-C quit clean (delete image, unlink shm, release tap, restore terminal)

Scenes

  1. tunnel — ray-marched twisting tube; the live spectrum sculpts the walls by angle, beats kick the camera forward.
  2. starfield — deep-space ambient: domain-warped nebula breathing with the mids under a parallax star warp.
  3. waterfall — CQT-style scrolling spectrogram from the GPU history texture, palette-graded, with a song-progress marker.
  4. plasma — smooth MilkDrop feedback warp: previous frame swirled and re-injected with kaleidoscopic ink.
  5. ink — organic fluid: curl-noise advection with palette ink breathed in by wandering emitters; beats exhale puffs.
  6. horizon — synthwave: slatted sun over a scrolling perspective grid, spectrum glowing along the horizon.

All scenes are groove-locked: beats and energy drive motion (camera kicks, warp speed, flow), never full-screen luminance steps. --intensity (0..1) scales what flash/glow remains.

Color

Four selectable color sources feed the same 4-stop palette every scene reads (c cycles at runtime, --color picks the start):

mode source
album (default) dominant colors k-means'd from the current track's cover art (Music.app artwork / Spotify artwork URL); crossfades ~0.8s on track change; falls back to curated
curated hand-picked palette per scene
drift slow continuous hue rotation
duotone one scene accent on near-black

Scenes are plain WGSL files in scenes/ (or $CYMATIC_SCENES), live-reloaded on save — edit while the music plays. Each gets a prelude with audio uniforms (u.bass, u.beat, u.bpm, …), spectrum(x) / spectrum_at(x, age) sampling a 128-band × 512-row history texture, and prev(uv) for feedback. A broken shader keeps the last working pipeline and shows the error in the status line.

Resizing the pane rebuilds the framebuffer at the new pixel geometry (queried via CSI 16 t) — 1:1 pixels, no stretching.

Performance

Headless bench on Apple Silicon, full render + GPU→CPU readback:

scene 1280×720 2560×1440 3456×2000
tunnel 732 fps 125 fps
starfield 1229 fps
waterfall 1343 fps
plasma 1371 fps 676 fps

The v0 bar is ≥45 fps at 1280×720; there's more than an order of magnitude of headroom before the presenter, not the GPU, becomes the story.

Non-goals (v0)

  • Terminal bar charts of any kind. If the output looks like cava, it's a bug.
  • BlackHole / loopback drivers as the capture path.
  • tmux, SSH, Terminal.app/iTerm/Alacritty.
  • Windows/Linux (wgpu keeps the door open; the presenter and tap are macOS/kitty-specific).
  • Being a music player. Playback stays in Music.app.

Roadmap

  • libprojectM as scene backend #5 — MilkDrop presets rendered to FBO.
  • Per-process taps (visualize only Music.app, bundle-id filter).
  • Dirty-rect presenting for partial-update scenes (the a=f,x=,y= path exists in the encoder).

License

MIT

About

Music visualizer rendering real GPU pixels inside Ghostty/Kitty — Core Audio process tap → wgpu Metal scenes → kitty shm graphics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages