Vibresia
This server has a remotely hosted endpoint. Open its connector listing for connection details, authentication requirements, and setup instructions:
Vibresia
A free macOS app for visual worlds that move with your music. It listens to whatever your Mac is playing and turns it into live scenes, from neon waveforms to a pixel-art band that walks on stage when it hears their instrument — and you can write a world of your own, or have an AI assistant write it for you.
Website & install: https://vibresia.com
curl -fsSL https://vibresia.com/install.sh | bashOr ask an AI assistant: "install vibresia from https://vibresia.com".
If you enjoy it, you can buy me a coffee.
Found a bug or have an idea? File an issue. Pull requests are very welcome, especially new vibescapes: see CONTRIBUTING.md.
Repository layout
Path | What |
| The macOS app (Swift, SwiftUI, OpenGL). See its README. |
| The landing page, installer ( |
| Anonymous page-view / click tracking for the landing page. |
| Changelog and open issues, fetched from the GitHub API when scrolled into view. |
| Cloudflare Worker: serves |
| Tests for the shared-vibescape endpoints. |
| D1 schema — one table, one row per shared vibescape. |
| What Google is pointed at. One URL; the plain-text agent pages are deliberately left crawlable but unlisted. |
| Cloudflare response headers (caching, security) and canonical-path redirects. |
| Fails if a vibescape is missing from the landing page, or the version in its structured data is stale. |
| Fails if the site and the app report to different PostHog projects. |
| Fails if a Discord webhook URL, a notify token or a share token is anywhere in the tree. |
| Deploys the Worker (and |
Related MCP server: hydra-mcp
Developing
cd VisualiserMac
./build-app.sh debug # build/Vibresia.app
open build/Vibresia.appCustom vibescapes and the local MCP server
A vibescape is a fragment shader — one function, vec3 render(vec2 p, float t) —
and the app supplies the uniforms, the helpers and everything else.
Vibresia ships a local MCP server, so an AI agent can write a vibescape, check it compiles, look at the picture and try again without anybody copying code by hand:
claude mcp add vibresia -- "/Applications/Vibresia.app/Contents/MacOS/Vibresia" --mcpIt's the same binary run with --mcp, speaking JSON-RPC over stdin — no
window, no port, no analytics. It reads and writes the same files the app
does, so a vibescape an agent writes shows up in the app within a second. See
VisualiserMac/README.md
for the tools it offers.
Version numbers
The build version is the date it was built, so there's no number to bump:
MAJOR.MINOR.PATCHMAJOR counts years since 2026, MINOR is the month, and PATCH is the day of the month followed by the build number of that day — 0.10.201 is the 20th of October 2026, the first build of that day.
VisualiserMac/version.sh computes it, and build-app.sh bakes it into the app. release.sh records it in VisualiserMac/VERSION and the landing page before tagging the release, so the app, the site and the git tag always agree.
Landing page analytics
site/analytics.js is a ~180-line PostHog client — no SDK, no session replay,
no cookies — reporting into the same PostHog project as the app, separated by a
surface property (web vs mac_app).
Events: $pageview, demo_played, vibescape_card_viewed (which vibescape cards got
scrolled to), download_clicked, install_command_copied (ask-ai or curl —
the interesting split, since they are different audiences), coffee_clicked
(by placement) and outbound_clicked.
To see them without touching production, point the meta tag at a local sink:
python3 - <<'PY'
p='site/index.html'; s=open(p).read()
open(p,'w').write(s.replace('https://us.i.posthog.com">','http://127.0.0.1:8099">'))
PY
(cd site && python3 -m http.server 8098) # then open http://127.0.0.1:8098The footer has a one-click opt-out, and Do Not Track is honoured: both stop the page sending anything at all.
Download notifications (Discord)
Every download goes through /download on the Worker, which 302s to the latest
GitHub release and posts an embed to Discord. Server-side rather than from the
page, for two reasons: a webhook URL in the HTML would be public, and a click
that never completes isn't a download.
The embed carries source, city/country (from Cloudflare's geo), referrer, browser, and whether that browser has downloaded before.
npx wrangler secret put DISCORD_WEBHOOK_URL # once; the webhook URL in Discord
npx wrangler deploy
curl https://vibresia.com/api/health # {"discord":"configured"}Errors from the app. The app can't hold the webhook (it's in the app
bundle, where anyone can read it), so it posts to POST /api/notify on this
site and the Worker forwards it. Set the shared token as a second secret, then
bake the same value into the app:
npx wrangler secret put NOTIFY_TOKEN # any random string
REPORT_TOKEN=<the same string> ./build-app.sh releaseThree things arrive: audio_error (the audio source failed or was cut off —
the failure a stack trace will never show you), source_failure and
app_crash (with the top frames). A denied permission prompt is a choice, not
a fault, so it isn't reported.
Unlike usage analytics, this is not behind the in-app toggle: a crash is
the report you can least afford to lose, and the user asked for that source
explicitly. What's sent is app state — vibescape, genre, error text — never audio or
track names. Errors get their own cooldown (one a minute, 60/hour) so a broken
install can't flood the channel, and DISCORD_ERRORS_ENABLED = "false" turns
them off without touching download alerts.
Locally, .dev.vars (gitignored) points the webhook at a stub:
npx wrangler dev --port 8787
curl -s 'http://127.0.0.1:8787/download?src=hero&seen=0' -A 'Mozilla/5.0'
npm test # 13 tests, no networkNotes:
The webhook URL and the notify token are both credentials.
scripts/check-no-webhook-leak.shfails if either appears in the tree — delphina had the same problem and it ended up baked into dozens of release tags. If it ever leaks, recreate the webhook in Discord and put the new one back withwrangler secret put.Bots and link previewers are filtered out, a double-clicked button only posts once (5s debounce, 30/hour), and the post goes out via
ctx.waitUntilwith a timeout and one retry — so Discord being down can't slow down or break a download.Switch it off by setting
DISCORD_NOTIFICATIONS_ENABLEDto"false"inwrangler.tomland redeploying; the site keeps working either way.install.shstill downloads straight from GitHub, so script installs don't produce notifications.
Shared vibescapes
A custom vibescape — one somebody wrote — used to travel as its payload in
the link: vibresia.com/share/?d=<base64 of the deflated shader>. That
works with no backend at all, which is why it was built that way. But the
link was around 370 characters, and the server answered it with a 307 to
add the trailing slash, which WhatsApp's link-preview crawler did not
follow — so the preview was blank and the link looked broken in a chat.
Now the payload is stored and the link is short:
POST /api/share {payload, name, author, image, key} → {"url": "https://vibresia.com/s/k3f9x2"}
GET /s/k3f9x2 the share page, with the payload inlined
GET /og/k3f9x2.jpg a picture of this vibescapeThe same vibescape always gets the same link. A share used to be made fresh every time — a new row, a new id, a new URL — so sharing the same vibescape twice gave you two links, and re-sharing one somebody sent you gave you a third. The app hashes the vibescape's contents (name, author, url, shader) and sends the hash; the Worker looks for a share with that hash, and for one with the same payload, and returns the row it finds rather than making another. The
keycolumn has a partial unique index on it, so two devices sharing at once land on one row.A vibescape that arrived on a link keeps the link it arrived on. The page puts the share's id in the deep link, the app remembers it with the vibescape, and sharing it on hands that link back: the person who sent it to you sent you that link, and the person you send it to should get the same one. Edit the vibescape and the hash changes, so it gets a link of its own.
The picture says who made it. The app stamps the Vibresia mark and the credit onto the picture it uploads, and the built-in pictures on the site are drawn by the same code — see
VisualiserMac/Sources/VibresiaBrand.
The gallery
vibresia.com/vibescapes lists the built-in vibescapes and the ones people
have shared, in one list, newest first.
Sharing a vibescape has always made a link anybody can open. The gallery is different: it lists them, so anything in it is findable by anybody at all — and the name and the author on it are public. The share sheet says so, and has a tickbox, on by default.
The app asks.
POST /api/sharetakespublic, and the share sheet has a tickbox next to the name.The Worker stores it in an
is_publiccolumn and serves the list at/vibescapes(the page) and/api/gallery(the same thing as JSON). The cards are built on the server, so the page works with no JavaScript and a crawler sees the list.The built-ins are in it too, dated by the day they first appeared — the commit that first mentioned the vibescape — so they can be sorted against the shares people have made.
scripts/generate-share-pages.pyworks that date out and writes it into the share manifest. Their cards use the clean still, the same as everybody else's, rather than the framed share graphic.The shares that were made before the gallery existed are not in it. They were never offered the choice, and publishing them without asking would put somebody else's name on the front page of the site. Sharing one again with the tickbox on puts it in.
The admin
vibresia.com/admin — unlinked, and not in any sitemap. It asks for a
token and then lists every share, the ones in the gallery and the ones
hidden, with three things you can do to each:
Hide takes it out of the gallery and leaves the link working. The right one for a share that is fine but should not be listed.
Show puts it back.
Delete takes the row, and the link with it: the picture and the name go too. It asks first, because it cannot be undone.
The token is a Worker secret called ADMIN_TOKEN, and it is not the share
token — that one ships inside the app, so anybody can read it out of the
bundle. This one never leaves the server, and it is the only thing between
the public internet and the gallery. The page keeps it in sessionStorage,
so it lives in that tab and not anywhere else.
npx wrangler secret put ADMIN_TOKEND1, not Workers KV. KV is eventually consistent — a link opened ten seconds after it was shared could reach an edge that hadn't seen the write and 404, which is the feature failing at exactly the moment it is used. D1 is SQLite-backed and strongly consistent, so the link works the moment it exists. It is free: 100,000 rows written a day, 5,000,000 read, 5 GB stored. A share is a row with a payload and a ~120 KB picture in it.
The page is served, not redirected to. A redirect is what the preview crawler was not following. The Worker fetches
/share/and rewrites the parts that are specific to the share: the title, the description, and theog:imagethat points at the stored picture. Crawlers do not run the page's JavaScript, so that has to be right in the HTML itself.The picture is the vibescape. The app renders it at 1200x630, at the same moment the share page draws its still (six seconds in, so anything driven by time has reached its stride), so the preview in a chat is the picture the recipient gets.
The long link still works. It carries the payload with it, so it needs nothing stored. The app falls back to it if the site can't be reached — sharing is not allowed to depend on the network.
The endpoint needs a token, which is a speed bump rather than a lock: it ships inside the app, so anyone can read it out of the bundle. What it stops is a script creating shares to burn the daily write quota.
npx wrangler secret put SHARE_TOKEN # once; the same value goes in VisualiserMac/.env
npx wrangler d1 migrations apply vibresia-shares --remote
node scripts/backfill-share-payload-hashes.mjsThe order matters. The migration adds the columns the Worker looks shares up by, and the backfill fills in the hashes for the shares that were made before the columns existed. Run the migration first, then the backfill, then deploy the Worker. The Worker survives a database that hasn't been migrated — it falls back to storing every share on its own, which is what it used to do — but the same-link behaviour needs the new columns.
SHARE_TOKEN is a credential in the same class as NOTIFY_TOKEN, so
scripts/check-no-webhook-leak.sh fails if it ends up in the tree.
The public MCP server
https://vibresia.com/mcp — a remote Model Context Protocol server, so an
agent running in a browser can write a vibescape without the app being
installed and without a GPU anywhere.
It is the same idea as the app's own local server, with the parts that need a Mac left out: it cannot draw, so it cannot show a preview or check that a shader compiles, and it cannot see the music playing on your Mac. What it can do is write a vibescape, lint it, and hand back a link.
The browser does the drawing. A shader cannot be run in a Worker, but the share page draws the vibescape live with WebGL, so what comes back from a tool is a link where the world is already running — and it works for whoever opens it, with or without the app.
Making one and sharing it are two acts.
create_vibescapemakes a share and leaves it unlisted;share_vibescapeputs it in the public gallery, which is where it belongs because that is what sharing is for.No authentication. Both directories offer it, and a server that wanted an account would keep people out of the thing it exists for. The writes are rate-limited per address and land unlisted, so the cost of abuse is bounded and does not reach the gallery.
/.well-known/mcp.jsonis the discovery file the MCP registry reads, and the way the domain proves it owns the server it claims.
scripts/check-mcp-reference.py fails the release if the two copies of the
vibescape reference — the app's and the website's — drift apart, so an
agent is not told different things depending on which one it asked.
npx wrangler secret put SHARE_TOKEN # only for the app's own sharing
python3 scripts/check-mcp-reference.py # and --write to copy the app's text overChangelog and open issues
Both sections on the landing page read from api.github.com at runtime rather
than being baked in, so they can't go stale between deploys. Three things keep
that honest:
Cached in
sessionStoragefor 15 minutes. GitHub's unauthenticated API allows 60 requests an hour per IP, and every visitor would be spending from the same one.Lazy — the fetch starts when the section scrolls into view.
Never fatal — if GitHub is down, rate-limiting us, or the JSON moves, the section says so and leaves a link to the repo behind. Issue titles are rendered through
textContent, neverinnerHTML, because they're whatever someone typed into a text box.
SEO
Submit the sitemap once in Search Console —
https://vibresia.com/sitemap.xml. Re-submit after site deploys; it also carrieslastmod.Structured data is
SoftwareApplication+OrganizationJSON-LD inindex.html. The version is the build's date, so there's nothing to bump by hand —VisualiserMac/version.sh --writeputs it in bothindex.htmlandVisualiserMac/VERSION, andscripts/check-vibescapes.shfails the release if they drift apart.Turn on "Always Use HTTPS" in the Cloudflare zone for
vibresia.com. The Worker's ownhttp:→https:redirect covers the same ground, but the zone setting is what stops a duplicate being served at all. The oldaudiovane.minithlabs.comname is still attached to the Worker so it can redirect — see the note below.The old domain is a redirect, not a second site.
audiovane.minithlabs.comis attached to the same Worker, which answers it with a 301 tovibresia.comkeeping the path — so the old install command and any old link still work. Its DNS has to keep pointing at Cloudflare for that to happen. One thing a domain change can't carry over: the analytics opt-out is in the browser'slocalStorage, which is per-origin, so anyone who opted out of analytics on the old site has to do it again on the new one.The Worker is attached with routes, not custom domains.
vibresia.comwas pointed at Cloudflare before the Worker existed, so its DNS records are ones Cloudflare didn't make; a custom domain would want to own them and refuses to overwrite them. A route binds the hostname to the Worker and leaves the DNS alone.run_worker_firstis on for the same reason: the redirects are decided in the Worker, and an asset served straight from the edge would skip them.Caching is Cloudflare's default (
max-age=0, must-revalidate), and aCache-Controlrule insite/_headersappends to that rather than replacing it, so it can't be changed that way. Repeat visits revalidate with a 304 from the ETag instead of re-downloading 5.9MB, which is the part that matters.Page is a single static file: one H1, 16 lazy-loaded images with descriptive alt text, no framework. Keep it that way — LCP measured ~0.08s locally and CLS ~0.005 before any of this.
Exporting a video
The share sheet can save the last ten seconds of a vibescape, with the music, as a movie in Downloads.
The picture is the app's own renderer — the same draw the window is doing, over the last ten seconds of its clock — so it is the vibescape rather than a picture of it. The sound is the last ten seconds of what the app was hearing, which is the music as it was.
The clip is as long as the audio the app has: ten seconds of music if there has been ten seconds of music, and what there is if there hasn't. The app has to be listening to something for there to be any.
./build/Vibresia.app/Contents/MacOS/Vibresia --export-video band /tmp/out.mp4The song, and its genre
The Band's genre comes from the song when the app can work out what is playing. Two ways, and they sit side by side:
By name. Apple Music and Spotify both post a notification when the track changes, with the title and the artist in it. The app looks the song up in Apple's public catalogue, which knows its genre — so the stage is chosen for the track rather than guessed at from the waveform.
VisualiserMac/Sources/VisualiserMac/Audio/SongIdentifier.swift.By listening. ShazamKit matches a signature of the sound against Shazam's catalogue, which works with anything that makes a sound — a browser, a film — rather than only with the players that post notifications.
VisualiserMac/Sources/VisualiserMac/Audio/ShazamIdentifier.swift.
The first one is what ships. The second one is behind a flag, because matching against Shazam's catalogue needs the app to be allowed to ask, and being allowed is decided by the ShazamKit service on the app's identifier in the developer portal.
What ShazamKit needs, and why it isn't on by default
Apple issues the com.apple.developer.shazamkit entitlement for apps signed
with a provisioning profile. This app is signed with a Developer ID
certificate and notarized, so the entitlement has to be declared in the
signature by hand — and declaring one the team hasn't been granted is not a
build that fails to match, it is a build that will not launch. macOS kills
the process:
$ SHAZAMKIT=1 ./build-app.sh debug && ./build/Vibresia.app/Contents/MacOS/Vibresia
Killed (exit 137)The same app without the entitlement runs, and the match comes back
com.apple.ShazamCore error 102 — "not allowed", not "not found".
So the entitlement is opt-in (SHAZAMKIT=1) and only worth turning on once
the ShazamKit service is enabled for com.minithlabs.visualiser in the
developer portal and Apple grants it for a Developer ID signature.
The spike is still here, because that question deserves an answer rather than a forum post:
./build/Vibresia.app/Contents/MacOS/Vibresia --shazam-spikeWhat leaves the machine
The audio never does — it is analysed locally and drawn locally, and the band's stage is drawn from what the shader does with it.
The name of the song does, when a music app says what it is playing: the title and the artist are looked up in Apple's public catalogue, which is the same search the iTunes Store does. Nothing else is sent, and no account is involved. The catalogue's answer is a genre, and the genre is a local decision from then on.
The lookup needs both the title and the artist to match. The catalogue will happily answer "Me and Your Mama" for "Redbone" — same artist, different song — and a wrong song means a wrong genre on the stage, so a result that isn't clearly the right song is thrown away rather than used. When the catalogue doesn't know the song, the app falls back to the audio heuristic it has always used, and nothing is lost.
To try the lookup without a music app in the picture:
./build/Vibresia.app/Contents/MacOS/Vibresia --song-lookup "bad guy" "Billie Eilish"Tuning The Band
VisualiserMac/Tests/band-fixtures.yaml lists YouTube videos labelled with their genre and who's playing. scripts/band-eval.py plays each one in Chrome while Vibresia listens, then prints a scorecard (genre right? right musicians on stage?) and saves the detector's features and per-genre scores to VisualiserMac/Tests/eval-results/ for tuning. Vibresia needs system audio permission first; keep other audio off while it runs.
scripts/band-eval.py --check # validate the fixtures file
scripts/band-eval.py # run them all (about a minute each)
scripts/band-eval.py --only jazzReleasing
Add any new vibescape to
site/index.html(a card withdata-vibescape="<case name>").VisualiserMac/release.shruns both site checks, decides the version from the date, records it inVisualiserMac/VERSIONand the landing page, builds, zipsVibresia.appand publishes GitHub releasev<VERSION>. The installer and the app's update check always use the latest release. If a Developer ID certificate is in the keychain, the release is notarized as part of this — the ticket is stapled onto the app between the build and the zip, because signing the bundle again would invalidate it. See Signing and notarization.npx wrangler deployif the site or worker changed, then re-submit the sitemap. First deploy after cloning needsnpx wrangler secret put DISCORD_WEBHOOK_URL(see Download notifications).
This server cannot be deployed
Maintenance
Related MCP Connectors
Write audio-reactive MilkDrop presets and test them live in your Groovalizer music visualizer tab
Build 3D web apps: physics, audio, music, Blender import. Live edits, undo, versions, publishing.
Create and track AI music videos and audio-reactive visuals from songs.
A visual brain for your agents: live, reusable UI fragments and skills to pull into code.
Related MCP Servers
- FlicenseCqualityDmaintenanceEnables AI agents to generate, manipulate, and perform algorithmic music using Strudel.cc live coding environment. Provides 46+ tools for pattern generation across multiple genres, music theory operations, real-time audio analysis, and AI-powered composition.50-
- AlicenseAqualityCmaintenanceEnables an AI agent to live-code the Hydra video synth alongside a person, with dry-run validation, shared editing, screenshots, crossfades, and Syphon/NDI video output.191MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to inspect a running Three.js scene in the browser and try out live edits to objects, materials, shaders, lights, camera and animation without touching project files. Every change is recorded and can be exported as ready-to-paste vanilla Three.js or React Three Fiber code, alongside performance audits and frame capture.4,560 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables coding agents to compose, edit and run multi-agent pipelines represented as songs, where channels are agents, notes are agent calls, and mixer buses carry each agent's output to the next. Lets agents create and modify arrangements, validate and render projects headlessly, play them back live in a studio window, and export the result as WAV or MP3, with every change undoable and cached takes replayed for free.250 npmMIT