個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證A turntable on your Mac desktop that plays along with YouTube Music. Native Swift + a tiny Chrome extension, 100% local.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗An animated turntable on your Mac desktop that shows what's playing in YouTube Music (Chrome), live.
Personal project: no App Store, no accounts, no analytics. Everything stays on 127.0.0.1.
Website and download: https://rohan2025.github.io/33rpm/
music.youtube.com tab ──► Chrome extension (MV3) ──ws://127.0.0.1:33333──► 33rpm.app
content.js worker.js (WebSocket) ◄── play/pause/next/prev ──
33rpm has no window or Dock icon: it lives in the menu bar (the disc icon) and draws on your desktop, behind your windows. Opening it again from Applications or Spotlight brings the turntable back and shows the Setup Guide.
scripts/package.sh builds the shareable DMG and a Chrome Web Store zip into build/release/.
The store listing's privacy policy is PRIVACY.md.
Launch copy and images live in launch/.
scripts/build_app.sh release --run
This builds build/33rpm.app, ad-hoc signs it and opens it. For a quicker dev loop:
cd App && swift run
You can also open App/Package.swift in Xcode and press Run.
33rpm is a menu bar app (no Dock icon): look for the record icon in the menu bar.
On first launch the app creates a pairing token at
~/Library/Application Support/33rpm/token. Use Copy Pairing Token in the menu bar menu to copy it.
build/33rpm.app, not swift run)The scene sits at desktop level: above the wallpaper, below all windows and desktop icons, on every Space, and clicks pass through it. It won't be visible on a display while a full-screen app occupies it.
chrome://extensions in Chrome.extension/ folder in this repo.https://music.youtube.com and play something.After editing extension files, click the reload icon on the extension's card in chrome://extensions.
Already-open YouTube Music tabs get the new content script automatically.
navigator.mediaSession once per second (falling back to the player bar DOM) for
title, artist, album, artwork, play state, position and duration. It sends on any change, plus a heartbeat
every 5s while playing. Artwork URLs are rewritten to 1200×1200.127.0.0.1:33333 only, rejects any WebSocket whose Origin isn't
chrome-extension://…, and drops connections that don't send the right token within 5 seconds.
When the socket closes (Chrome quit, tab closed) it goes idle.lsof -i :33333 to find it.chrome://extensions → 33rpm Bridge → service worker).~/Library/Application Support/33rpm/token and relaunch the app.Each scene is a folder in Scenes/ (Evening, RainyWindow, Minimal) holding a scene.json and PNG layers.
All three ship with photoreal art generated in ChatGPT: one room photo per scene (the Rainy and Minimal rooms are
edits of the Evening photo, so the console and turntable sit in exactly the same place) plus a top-down record,
tonearm and crate front shared by all three. The originals are kept in each scene's _sources/ folder (not bundled).
Any slot without a PNG is drawn as a simple placeholder. The released app only uses the rooms sealed inside it;
to work on the art, run a debug build (scripts/build_app.sh debug --run), which reads this folder.
For ready-made AI image prompts and the recommended "one painted room" approach, see
Scenes/ART_PROMPTS.md.
A debug build picks up changes live (no rebuild): save a PNG or edit scene.json and the desktop updates in a moment.
Files are looked up in this order, first match wins:
~/Library/Application Support/33rpm/Scenes/<Scene>/ (personal overrides)Scenes/<Scene>/ in this repo33rpm.app at build time| File | Where it lives | Notes |
|---|---|---|
background.png | scene | Full room: wall, floor, lamp. Covers the whole canvas (1500×1000). |
plant.png | scene | Transparent PNG. |
console.png | scene | Walnut record console, drawn in perspective. |
sleeve_shadow.png | scene | Optional extra shadow behind the leaning sleeve (one is cast automatically). |
crate.png | scene | Front panel of the crate only (transparent above it): played sleeves stand behind it and poke out the top. |
deck_base.png | deck | Turntable top, drawn straight down from above. |
platter.png | deck | Top-down, square canvas, circle filling it. |
record.png | deck | Top-down vinyl, square. The album art label is added on top automatically. |
tonearm.png | deck | Top-down, arm pointing straight down, pivot near the top (see anchor). |
lid.png | deck | Smoked glass, top-down, semi-transparent. |
default_label.png | — | Optional. Centre label shown when nothing is playing. |
default_sleeve.png | — | Optional. Sleeve on the wall when nothing is playing. |
| — | scene | up_next entry places the Up Next card (frame, rotation); no PNG, it's drawn as paper. |
lamp_glow.png | scene | Optional soft light pool over the lamp; flickers gently while music plays. |
ambient.mp4 | scene | Optional short looping video (rain, candle, city lights), muted. Placed by the video slot. |
The sleeve entry in layers has no PNG: it shows the current album art, leaning by rotation degrees.
dust and rain entries are particle effects drawn by the app (no PNG), placed by their frame.
Scene layers are placed flat on the canvas. Deck layers are drawn top-down and the app tilts the
whole deck into perspective (deck.tilt), so the record and tonearm rotate correctly in 3D.
Use transparent PNGs at about 2× the frame size for sharpness.
canvas: design size; the scene scales to fill the window.edgeFade: soft fade into the wallpaper at the edges, in canvas points (Small/Medium only; 0 = hard edges).layers: scene layers in back-to-front order, frame: [x, y, width, height] with a top-left origin.
{ "slot": "deck" } marks where the turntable goes in that order.deck: center and size of the deck top in canvas points, tilt in degrees (0 = seen from above),
perspective (bigger = flatter), and its own layers in deck coordinates.controls: optional center [x, y] and scale for the hover playback controls (default: just in front of the deck).focus: canvas point kept in view when Full size crops the scene to a screen of a different shape.anchor: [x, y] (pivot as a fraction of the frame), opacity, hidden,
blend (screen, add, softLight, overlay, multiply), flicker (lamp_glow, 0–1),
amount (dust/rain density), file (video file name, default ambient.mp4).record: rpm, labelDiameter (fraction of the record), sheenOpacity, spinUpSeconds, spinDownSeconds.crate: sleeveSize (sleeve width as a fraction of the crate), peek (how much sticks out the top),
browseLift (how far a hovered sleeve rises).tonearm: restAngle, startAngle (outer groove), endAngle (run-out groove), moveSeconds.
Angles are degrees clockwise from the PNG's straight-down orientation.To check a layout without looking at the desktop, render it to a PNG
(--demo-history 8 fills the crate with fake sleeves, --browse 2 pulls one out):
build/33rpm.app/Contents/MacOS/33rpm --snapshot ~/Desktop/scene.png --playing --progress 0.5 --demo-history 8
Pick a scene from menu bar → Scene: Evening (warm lamp, dust in the light), Rainy Window
(cool room, rain on the window) or Minimal (deck, sleeve and crate on a shelf). Each scene folder can have
its own PNGs and ambient.mp4; anything missing falls back to that scene's placeholder.
Ambient motion (dust, flicker, rain, video) runs only while music is playing. Everything freezes while the display sleeps, the screen is locked, or the scene is fully covered (for example by a full-screen app), and the app itself stays at about 0% CPU: the animation runs in macOS's compositor.
When a song leaves the deck (next track, or Chrome closes) after playing at least 3 seconds, its sleeve slides
off the wall into the crate. The crate keeps the last 20 and remembers them across restarts
(~/Library/Application Support/33rpm/history.json plus artwork in History/; delete both to empty the crate).
Hover over the crate to flip through: move left (newest) to right (oldest), or scroll. Click the raised sleeve to play that song in your YouTube Music tab (opened in Chrome if the extension isn't connected). Sleeves saved before version 0.2 of the extension don't know their video, so clicking them opens a search instead. A replayed song goes back into the crate at the front, not twice.
scripts/composite_receiver.py; its live display and knobs
are placed by the receiver block in scene.json.Clicks only stop passing through to the desktop while the pointer is over the turntable, the receiver's knobs, the card or the crate.
Menu bar → Settings… (⌘,): record speed (12–45 rpm; real is 33⅓, "Relaxed 22" is calmer on screen), show/hide the hover controls, the Up Next card and the ambient effects, plus scene, size and launch at login.
extension/ Chrome extension (manifest, content script, worker, options popup)
App/ Swift package for the macOS app
Scenes/ Scene folders: scene.json + your PNG layers
scripts/ build_app.sh
MIT, see LICENSE. The room art, record, tonearm, crate and sleeve images were generated for this project with ChatGPT.