Skip to content

Latest commit

 

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Synesthia

A Metal-powered music visualizer for macOS, in the spirit of the classic iTunes/Music visualizer — but tone-reactive: a 64-band log-spaced FFT drives every visual, so bass, mids, and treble each shape the picture differently. It names the track you're listening to in Music or Spotify without asking for a single permission to do it.

synesthia.app — download, screenshots, and support.

status license macOS 15+

Install

The notarized build is at synesthia.app/download — universal (Apple silicon and Intel), macOS 15 or later, and it updates itself via Sparkle. Or build it from source.

Using it

  1. First launch opens a welcome sheet explaining each source and what it costs. Continue grants nothing and plays a bundled demo loop, so you can see it work before deciding. Pause it from the source menu, the play/pause button, or the space bar.
  2. Pick System audio to visualize what you're actually listening to. macOS asks for Screen & System Audio Recording — the only sanctioned way to hear system output (audio only; the video leg is 2×2 px and discarded). If you grant it after the first attempt, the app retries on its own.
  3. Start playing something in Music or Spotify and the now-playing badge fills in by itself — title, artist, album, and the player's icon. This needs no permission of any kind; the players broadcast it.
  4. Optional: Control Music… / Control Spotify… in the source menu adds play/pause/skip buttons and real album artwork. That one does ask for Automation, only when you click it, and only in the direct-download build.
  5. Shortcuts: Space play/pause · ⌘1–⌘4 switch visualizer · ⌘→/← next/previous track · ⌘L start/stop listening · ⌘O open an audio file · green button / ⌃⌘F fullscreen.

Audio sources (control bar, left menu)

Source What it does Metadata shown
System audio Anything the Mac plays — Music, Spotify, a browser, a game Title, artist, album, player icon
Audio input Mic, line-in, or any input device (picker in menu) Device name
Audio file Plays a local file in-app, loops it Filename

There is no separate "Music app" source any more, and that is an improvement rather than a removal: it was never a different way of hearing anything (Music has no audio stream to tap, so it always went through the same system-audio capture) and the metadata it used to be needed for now works for every recognized player. Now-playing is a layer on top of System audio, not a mode you have to switch into.

Recognized players today are Music and Spotify. Adding another is one table row in NowPlayingObserver.swift — see docs/macos-integration.md.

Visualizers

Name Idea Options
Nebula ~100k GPU-simulated particles orbiting a bass core, galaxy disc, and comet halo Density, Trails, Turbulence
Spectrum Tunnel Flight through a tube whose angular slices are the live spectrum Twist, Glow
Aurora Layered ribbons riding the waveform, each glowing with its slice of the spectrum Ribbons, Wave height
Bars A studio console: LED spectrum wall with peak holds, analog VU meters, and a desk of moving faders Columns, Segments, Desk

All visualizers share global Sensitivity, Speed, and five color palettes (Prism, Ember, Ocean, Violet, Mono) — see the slider icon in the control bar. Normalize Loudness (on by default, in Settings — ⌘,) slowly adapts the analysis to how loud the source actually is, so a quiet mic and loud mastered music read the same without retuning Sensitivity. Settings persist across launches.

Troubleshooting

  • Visuals don't react — check System Settings › Privacy & Security › Screen & System Audio Recording, then click play again. If they react but weakly, give Normalize Loudness a few seconds to adapt (or check it's on in Settings, ⌘,), or raise Sensitivity in the options popover.
  • No track info — the source must be System audio, and the badge only appears once the player changes something: players broadcast their state on a transition, not on request, so skip a track or hit pause and it will fill in. Only Music and Spotify are recognized.
  • No album artwork — cover art isn't in what the players broadcast, so the badge draws a palette-derived tile with the player's icon instead. Real artwork needs Control Music… (Automation permission, direct-download build, Music only — Spotify offers no local artwork).

Contributing

What you need

Everything below is either bundled with macOS/Xcode or one command away. There is no dependency you have to hunt down.

Why How
macOS 15 (Sequoia) or later the deployment target is 15.0 —
Xcode 26.6+ Swift 6.3 toolchain, SDKROOT = macosx Mac App Store, or brew install --cask xcodes
Command line tools xcodebuild, codesign, hdiutil, sips, afconvert xcode-select --install
Metal Toolchain compiles Shaders.metal at build time xcodebuild -downloadComponent MetalToolchain
Node ≥ 22.12 the website and the Cloudflare tooling brew install node
Python 3 make demo-track, make check-metadata ships with macOS; stdlib only, no pip installs

Sparkle's command line tools (generate_appcast, sign_update) are not installed separately — they arrive with the Swift Package Manager artifact the moment you build the Synesthia Direct target, and the scripts find them there.

Only if you are cutting a release you also need an Apple Developer Program membership, the certificates in docs/distribution.md, and npx wrangler login for the Cloudflare side.

First run

git clone git@github.com:jonjaques/synesthia.git && cd synesthia
make install     # npm deps for the root: Prettier
make build       # Debug build of the App Store target
make test        # 19 Swift Testing cases over AudioAnalyzer

The website is a separate project with its own lockfile — cd web && npm ci, then npm run dev. See web/README.md.

Xcode works normally too — open Synesthia.xcodeproj and pick a scheme. Any .swift file you add anywhere under Synesthia/ is compiled automatically; the project uses a synchronized file group, so never add file references by hand.

Everyday workflow

make run                  # build, then launch the app
make build-direct         # the Sparkle-enabled build, for updater work
make lint                 # check formatting
make format               # fix what lint complains about
make healthcheck          # run this before pushing: lint + test + build-direct

make lint is formatting only: Prettier across everything outside web/, then swift format lint --strict over the app, the tests and scripts/shotkit.swift — under --strict any diagnostic, down to indentation, is an error. make format fixes both in place. swift-format ships inside the Xcode toolchain, so there is nothing to install, and Xcode's own Editor ▸ Structure ▸ Format File with swift-format reads the same .swift-format at the repo root. On top of formatting, the build is a gate too: every configuration is expected to compile with zero warnings.

make healthcheck is the whole PR gate in one target — lint, the test suite, then the Direct build — and is exactly what CI runs.

CI

.github/workflows/healthcheck.yml runs on every pull request push, as two independent jobs matching the two projects in the repo:

Job Runner What it does
web ubuntu-latest Everything from inside web/: npm ci, check, typecheck, build
apple macos-26 make install && make healthcheck

The runner has no signing identity, so the Makefile ad-hoc signs whenever CI is set — enough for the sandbox, and the real releases are signed by scripts/build-direct.sh on a machine that has the certificates.

Every target

make on its own lists them. Each is a thin wrapper around xcodebuild or a script in scripts/, and every script stays runnable directly with --help.

Target What it does
build Build the App Store target. CONFIGURATION=Debug by default
build-direct Build Synesthia Direct — the only target that links Sparkle
run Build, then open the resulting .app
test The SynesthiaTests Swift Testing suite
clean xcodebuild clean plus rm -rf build/
app-path Print where the built .app actually landed for this CONFIGURATION
install npm install at the root — Prettier, nothing else
healthcheck The PR gate: lint, test, build-direct
lint · format Check / fix formatting: Prettier, then swift-format
demo-track Regenerate the bundled 32 s demo loop, deterministically
screenshots Drive the real UI and capture every visualizer (see below)
check-metadata Check the App Store listing drafts against Apple's field limits
bump Raise the version everywhere it is recorded — see Releasing
direct · direct-fast Notarized direct-download build; -fast skips the notary round trip
appstore · appstore-upload Archive and validate the store build, optionally upload it
sparkle-keys One-time: create the EdDSA update-signing key
appcast Regenerate the signed Sparkle feed into build/releases
publish-release · publish-dry-run Upload the release to R2, or show what would go

Variables: CONFIGURATION (Debug default, then Direct and Release — docs/distribution.md explains why the store and direct builds differ), BUMP for make bump, DESTINATION for make test, and ARGS to forward flags to the wrapped script, e.g. make screenshots ARGS="--only nebula".

Screenshots

make screenshots relaunches the app once per registered visualizer with -visualizerID in its argument domain, sizes the window, captures it, then repeats in fullscreen. Each run writes under its own prefix (20260725-174312Z-nebula-windowed.png) so takes accumulate rather than overwrite; ARGS="--prefix hero" names one yourself, --prefix '' drops it. It assumes Music or Spotify is already playing so the now-playing badge and artwork are populated (it passes -playerControlEnabled YES to seed the state at launch, since players only broadcast on a transition). Because it drives another app's window, the terminal you run it from needs two grants in System Settings › Privacy & Security: Accessibility (resize the window, move the pointer so the auto-hiding chrome reappears) and Screen & System Audio Recording (screencapture itself). The script checks both up front and tells you what's missing. ./scripts/take-screenshots.sh --help lists the options.

Releasing

Synesthia ships through two independent channels, built from two targets over three configurations. The short version: Synesthia Direct / Direct is the notarized download from synesthia.app and is the only one with Sparkle and the Apple Events layer (transport control and cover art); Synesthia / Release is the Mac App Store build and contains neither. Both show the now-playing badge, which needs no permission at all. docs/distribution.md has the full rationale; docs/app-store-launch-plan.md tracks what is still outstanding.

1. Cut a version

make bump                 # patch: 1.0 -> 1.0.1
make bump BUMP=minor      #        1.0.1 -> 1.1
make bump BUMP=major      #        1.1 -> 2.0
make bump BUMP=2.5        # or set it explicitly

The version lives in project.pbxproj as build settings, duplicated across every target and configuration — nine copies of each. make bump writes them all, always increments the build number, refuses to run if they have drifted apart, and updates the website's RELEASE.version too.

Always let the marketing version move, not just the build number. The DMG is named Synesthia-<version>.dmg, so two releases sharing a version collide on one filename in R2. publish-release hard-fails on that rather than silently serving old bytes under a new signature.

2. Release the direct download

make test                 # nothing ships that hasn't passed
make direct               # archive, sign, notarize app + DMG, staple, verify
make appcast              # add it to the signed Sparkle feed
make publish-dry-run      # confirm what is about to be uploaded
make publish-release      # DMGs first, then latest.json, then the appcast

Order is not cosmetic. make direct notarizes twice — the app before the disk image is built, so the copy users drag out of the DMG carries its own ticket, then the signed DMG itself. publish-release uploads artifacts before the appcast, because the appcast is the announcement: the moment it lists a version, installed copies start fetching that URL.

Afterwards, update RELEASE.size in web/src/consts.ts from the byte count make direct printed, then commit and push — Cloudflare Pages deploys the site from git, while the release artifacts themselves live in R2 and need no rebuild.

3. Release to the Mac App Store

make check-metadata       # listing copy against Apple's field limits
make appstore             # archive, assert, export
make appstore-upload      # …and send it to App Store Connect

make appstore refuses to continue unless the archive is universal, carries no get-task-allow, no Apple Events entitlement, no tell application "…" string for any player, and no Sparkle — the store build must not ship an updater. Flip APP_STORE_AVAILABLE in web/src/consts.ts once review actually passes.

How it works

Developer documentation lives in docs/ — architecture, the audio pipeline, rendering, the visualizer plugin system, and macOS integration, written for developers new to audio/graphics/macOS.

SystemAudioCapture (ScreenCaptureKit)  ─┐
InputDeviceCapture (AVAudioEngine tap) ─┼─▶ AudioAnalyzer ──▶ AudioSnapshot ──▶ MetalVisualizerView ──▶ active Visualizer
FilePlayer (AVAudioEngine + tap)       ─┘    (vDSP FFT)       (lock-guarded)      (MTKView, 60 fps)      (Metal pipelines)

NowPlayingObserver (distributed notifications, no permission) ──▶ title/artist/album/state
PlayerRemote (Apple Events, opt-in, direct build)            ──▶ transport + cover art
  • AudioAnalyzer ingests mono samples from any audio thread, runs a Hann-windowed 2048-point FFT (Accelerate/vDSP), and publishes a snapshot: 64 log-spaced bands (30 Hz–16 kHz), a 256-sample waveform, bass/mid/treble/level scalars, and a beat envelope from bass-transient detection. Visuals decay gracefully when the source goes silent.
  • The render loop pulls the latest snapshot each frame — no audio→UI publishing, no allocation churn on the audio thread.

Plugin architecture

Visualizers are plugins (Synesthia/Visualizers/VisualizerCore.swift):

  1. Conform a class to Visualizer and give it a static VisualizerDescriptor (id, display name, tagline, up to four VisualizerOption sliders).
  2. Add shader functions to Shaders.metal (compiled at build time into the app's default library). A future external bundle would instead ship MSL source and compile it at load time with MTLDevice.makeLibrary(source:).
  3. Add the descriptor to VisualizerRegistry.all's initial list, or call VisualizerRegistry.register(_:) at startup (the hook external bundles would use).

Declared options automatically appear in the Options popover and arrive in the shader as the p array in VizUniforms. Audio data arrives as a 64-float band array, a 256-float waveform, plus derived scalars — see AudioAnalyzer.swift.

Roadmap / ideas

Each of these is assessed in docs/roadmap.md — feasibility in the current code, rough cost, and the platform research behind the ones that depend on Apple's rules. Roughly in order of promise:

  • Beat-synced scene changes (auto-rotate visualizers every N bars).
  • More visualizers: flocking and fluid-like sims on the compute path Nebula opened up, raymarched geometry.
  • User-defined color palettes (palettes are already per-visualizer).
  • True external plugins: load visualizers from ~/Library/Application Support/Synesthia/Plugins via register(_:).
  • Screen saver target reusing the same render stack.
  • String Catalog localization — UI strings are currently literals; the project has STRING_CATALOG_GENERATE_SYMBOLS enabled and should migrate.
  • Visualizer test coverage — SynesthiaTests covers AudioAnalyzer thoroughly; the Metal visualizers have none and are much harder to assert on.

License

MIT © 2026 Jon Jaques.

The MIT grant covers the source. Two things in this repository are not code and are not licensed with it: the app icon (Synesthia/Icon.icon, assets/Icon Exports/) and the Synesthia name and wordmark, which identify the shipped app. Fork the code freely — just ship it under your own name and mark so nobody mistakes your build for this one.

Sparkle, the only third-party dependency, is separately MIT licensed and linked into the direct-download target only.

Links

About

A Metal-powered music visualizer for macOS, in the spirit of the classic iTunes/Music visualizer

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages