Architecture
MixMusic is one Go program. MyGo draws its
windows in Go, with its ui package: immediate-mode views, which the GPU
draws, without a webview. The windows show the catalogs, the accounts and
the library of the services of internal/app, and the player in Go plays
the queue through MixMusic's own audio engine, or on Spotify's devices.
┌────────────────── the windows (the root package) ───────────────────┐
│ pages, shell, player bar, queue, lyrics, mini player, themes, │
│ plugins' pages and widgets │
└──────┬──────────────────────────┬────────────────────────┬──────────┘
│ App.Services │ the queue │ plugins
┌──────┴──────────────────┐ ┌─────┴──────────────┐ ┌───────┴──────────┐
│ internal/app: Catalog, │ │ internal/player │ │ internal/plugins │
│ Collection, Accounts, │ │ ├ internal/audio │ │ (sobek, a JS │
│ Preferences, Addons, │ │ └ Spotify devices │ │ engine in Go) │
│ System; the desktop │ └────────────────────┘ └──────────────────┘
├─────────┬─────────┬────────┬───────┬────────┬─────────┬─────────────┤
│ youtube │ spotify │ stream │ match │ lyrics │ library │ addons ... │
└─────────┴─────────┴────────┴───────┴────────┴─────────┴─────────────┘
The Go side
internal/mediadefines what every service shares: tracks, albums, artists, playlists, shelves. IDs carry their service:yt:<video id>,sp:<track id>,mx:<mix id>.internal/youtubereads YouTube Music through the API its website uses: search, suggestions, the home and explore pages, charts, moods and genres, albums, artists, playlists, related tracks, radios and plain lyrics. Signed in with the cookies of the user's session, which it signs its requests with as the website does, it reads and changes their library: liked songs, playlists, albums, subscriptions, likes and saves, and it deletes the playlists the user made, which it tells from those they saved by the menus YouTube sends with them. Its parsers are tested against recorded responses intestdata/, and against the live service withMIXMUSIC_LIVE=1 go test ./internal/youtube -run Live(MIXMUSIC_YT_COOKIESadds a signed-in account, andMIXMUSIC_YT_WRITE=1lets it make and delete a playlist there).internal/spotifysigns in with PKCE on a loopback callback, refreshes tokens, reads the Web API (search, albums, artists, playlists, the library), and controls Spotify Connect devices.internal/streamfinds the audio of a YouTube video through YouTube's player API, asked signed out as YouTube's visionOS app (Direct), else with yt-dlp: its Opus stream, else, of a video YouTube has only in AAC, its AAC-LC one (itag 140), never HE-AAC (itag 139). It keeps the stream until its URL expires, and opens it for the audio engine (Proxy.Open): chunked fetches from YouTube in the background, a new URL when YouTube refuses the old one, a new connection, from the byte it reached, when a download gets no bytes for 15 seconds or its connection drops, and an on-disk cache of tracks played to their end. A live video's audio is an HLS media playlist (RFC 8216) of AAC-LC in packed audio, format 234, or 233 with data saver, whichProxy.OpenLivefollows from near its live edge: it loads the playlist again about once per target duration, fetches each new segment once, and finds the stream again before its URL expires, never caching it. The player API also tells how loud YouTube measured each video (Stream.Loudness,Direct.Loudness). Its answers are tested against recorded ones intestdata/, and against YouTube withMIXMUSIC_LIVE=1 go test ./internal/stream -run Live.webmreads the audio of WebM files, as YouTube serves it: the packets of the first audio track with their times, and seeking by the Cues, reading a file played from its start straight through.opusdecodes Opus: a pure Go port of libopus 1.6.1's decoder (celt,silkand the range decoderrangedec, inopus/internal), which decodes the test vectors of RFC 8251 as libopus does, bit for bit.mp4reads the audio of MP4 files, as YouTube serves its AAC: the packets of the first audio track with their times, and the codec delay of the edit list, from progressive files and fragmented ones, seeking by the sample table, thesidxor themoofboxes.aacdecodes AAC-LC, in mono or stereo, with all of its tools, and reads the headers of ADTS frames: a pure Go decoder derived from SharpJaad, the C# port of JAAD, under SharpJaad's MIT license, which decodes YouTube's AAC within about 136 dB of ffmpeg's decoder, but for the few frames whose channels share their noise, as the standard has it and ffmpeg's decoder doesn't.resampleconverts it from its rate, 44.1 kHz for YouTube, to the engine's 48 kHz, with a polyphase filter of Kaiser-windowed sincs.ac3decodes AC-3, the 5.1 surround of YouTube's format 380: a pure Go decoder written from ATSC A/52, of every audio coding mode and tool, into six channels, with the stereo downmix the stream's own mix levels ask for. It decodes streams of ffmpeg's encoder as ffmpeg's decoder does to about 110 dB, and its tests edit those streams to use the tools the encoder doesn't: block switching, delta bit allocation, phase flags and dynamic range gains.internal/audiois MixMusic's audio engine: two decks, each a goroutine that reads a track, Opus in WebM withwebmandopus, AAC in MP4 withmp4,aacandresample, or the AAC of a live stream's packed audio, its ID3 tags passed over, which has no length and can't seek, as its first bytes tell, and decodes it into a ring the audio thread plays from, at 48 kHz, each turning a track louder than -14 LKFS down to it, from the loudness itsMeasuredsource tells (Even loudness), and the preamp, equalizer, levelling compressor, volume and analyser of the web player MixMusic had, ported from Chromium's Web Audio, which they match sample for sample.MIXMUSIC_LIVE=1 go test ./internal/audio -run Liveplays songs of both codecs, and a 24/7 live stream, as the app finds them on YouTube, against ffmpeg's decode.internal/audio/outputplays it on the system's output, without cgo: through WASAPI on Windows, an Audio Queue on macOS, and PulseAudio, which PipeWire serves too, or ALSA on Linux. On Windows it also hears what a process and its children play, through WASAPI's process loopback:internal/app's listener hears Spotify there, in MixMusic's webview or the Spotify app, into aTap, the analyser of the engine for audio outside it, which the visualizer and the plugins' analyser read while Spotify plays.internal/nowplayingshows what the engine plays in the system's media controls, and passes on what they ask: Now Playing on macOS, through MediaPlayer, and an MPRIS player on Linux, served by GIO's GDBus, both called with purego, and the SystemMediaTransportControls of a hidden window on Windows, reached through WinRT without cgo.internal/portalasks the desktop's Background portal, in a Flatpak, to let MixMusic run without a window and to start it at login: the sandbox's own autostart folder is one the desktop never reads.internal/appasks it for Start at login, and for close to the tray. packaging/flatpak holds the Flatpak itself.internal/taskbarputs previous, play or pause, and next on the thumbnail of the main window's taskbar button on Windows, through the shell's ITaskbarList3, and takes their clicks in a window procedure in front of MyGo's.internal/playerplays the queue: one goroutine owns the queue and what plays, and the network calls run beside it. It plays YouTube Music, and Spotify tracks matched to their recording, through the engine, and Spotify tracks on a Spotify device: a Spotify Connect device, or MixMusic itself, as below. It shuffles, repeats, crossfades, starts radios when the queue runs out, counts plays and keeps up to 500 tracks of the queue, around the one playing, inqueue.json, as it changes and as MixMusic quits. A track that can't play for want of the network waits for it where it was, and plays on once it answers; the player pauses as the computer sleeps.internal/onlinetells the player whether the network answers: a check of YouTube's and Spotify's addresses over HTTPS, shared by its callers, which audio coming from YouTube answers too, and a wait for the network, with checks further apart the longer it lasts. The system's word on the network, through MyGo, only has it check at once.internal/toolsfinds yt-dlp and a JavaScript runtime, and installs yt-dlp from its releases, checked against their published checksums.internal/matchfinds a Spotify track's recording on YouTube Music, by title, artists and duration, for playback without a Spotify device. It also finds the song of a music video, by its title and artists, which the video's radio starts from: YouTube Music makes the radio of a video out of videos.lyricsfinds synced lyrics on Unison and LRCLIB, and reads TTML and LRC.internal/librarykeeps liked songs, mixes, the history and stats inlibrary.json.internal/lastfmsigns in to Last.fm with the user's API account and scrobbles by Last.fm's rules, keeping what fails offline for later.internal/discordshows what plays on the user's Discord profile, over Discord's local pipe or socket.internal/diagwrites the log,mixmusic.login the logs folder, rotated at a megabyte and without the cookies, hashes and tokens a line may hold, and has Go write a crash tocrash.logthere, which the next start keeps ascrash-<time>.logand offers to report.internal/addonsvalidates theme and plugin manifests, lists the built-in and installed add-ons, and installs add-ons from the community registry.internal/colorfilereads the colours of pywal'scolors.json, matugen's JSON, MixMusic's matugen template and atheme.json, telling them apart by what they hold;internal/app/colorfile.gofollows the file the settings name with MyGo's file watch, and keeps the colours it read last while the file is half written.internal/pluginsruns the plugins the user enabled, each in a JavaScript runtime of its own, sobek (a fork of goja with ES modules), on a goroutine of its own: their modules,console, timers andfetch, and the API of plugins.md, built inapi.jsover Go natives. What they add to the interface the windows show: pages and widgets as views of elements, which the windows draw in their own styles, and canvases, whose drawing calls the plugin records and the window paints.internal/storewrites JSON files atomically. A file the user may edit while MixMusic runs,settings.json, it reads again when asked (File.Reload), and before each write, so that it never saves over an edit it hasn't read: it merges the edit in, by top-level keys. While the file doesn't decode, it leaves it as it is and holds its writes.internal/settingsdefines the settings, keeps them in range, and readssettings.json(Decode), at start and on each edit alike.internal/appgives the windows the services (App.Services), tells them when the settings, the accounts, the add-ons or the updates change (App.Watch), makes the player, and puts MixMusic on the desktop: the tray, media keys, global shortcuts, deep links, notifications, updates.
The windows
The root package draws the main window and the mini player. Pages fetch
what they show beside the main thread (fetch), and a window's state
belongs to its main thread: what finishes elsewhere comes back through
update.
-
Refresh (F5, ⇧⌘R on macOS,
refresh.go) asks again for the keys the page shown and the side panel fetched in the frame before, asfetchcollects them, and renders the plugins' views again (View.Remount). The page keeps its answers until the new ones come, and keeps them when a refresh fails, which one toast tells. Home's own tile and shelves read the library as it is, and need no refresh. -
Artwork downloads only for what shows in the window, scaled to the size it shows at, and the images shown most recently are kept, up to 48 MB of pixels (
artwork.go). An image that couldn't load tries again after 1 to 2 seconds, then less often, up to every 2 minutes, and at once when another of its host loads; one whose address gives no image waits half an hour. Both windows, the accent and Terminal's tint read the images throughartwork.Store(internal/artwork), which downloads each once, keeps it on disk as it came, up to 256 MB, and logs a host's failures at most once a minute, without the addresses. -
Themes give their tokens: colours, fonts, radius (
theme.go), and the accent follows the artwork of what plays. A colour file's colours replace the theme's, kept readable (colorfile.go). The windows draw the effects of the built-in themes, as their options say (effects.go): Mix's textures, Aurora's drifting lights and frosted dialogs, Sakura's petals, Terminal's scanlines, glow and phosphor artwork. -
Deep links and second launches open their pages in the main window (
App.OnNavigate).
Webviews
Two windows still show a web page, and only while they're needed:
- Spotify in MixMusic itself (
internal/app/spotifyhere.go): Spotify's Web Playback SDK, which decrypts Spotify's protected audio, in the hidden window ofpublic/spotify.html, the only page of the app, which it embeds. It opens at the first Spotify track that plays in MixMusic, and makes MixMusic a Spotify device of its own, which the player plays tracks on through the Web API and follows by asking the page (Page.Eval); the page gets its token fromSpotifyPage.Token. Only WebView2, on Windows, decrypts Spotify's audio. - Google's sign-in to YouTube Music, whose cookies MixMusic keeps.
Data
Everything lives in the user's data folder (%AppData%\MixMusic on
Windows, ~/Library/Application Support/MixMusic on macOS,
~/.config/MixMusic on Linux): settings.json, library.json,
queue.json, addons/, bin/ (yt-dlp), the YouTube session
(youtube-session.bin, encrypted with a key of the system's credential
store, which also keeps the Spotify token and the Last.fm connection), the
scrobbles still to send, and updater.json, the update preferences. On
Windows, a secret the Credential Manager can't keep, as in a logon session
without credentials, is sealed for the user with DPAPI in
spotify-token.dpapi, lastfm.dpapi or youtube-session-key.dpapi; on
Linux without a Secret Service, the secrets and the session are in plain
files only the user can read (spotify-token.json, lastfm.json,
youtube-session.json). secretStore (internal/app/secrets.go) moves a
secret found in a file into the credential store once it can keep it.
settings.json may be edited while MixMusic runs: App.watchSettings
watches it (mygo.WatchFiles, or a look every two seconds where it can't),
and App.ReloadSettings applies an edit as Settings applies a change, then
tells the windows (App.Watch). A file with a mistake is told of after a
second, and kept as settings.json.broken if MixMusic quits before it is
fixed; a settings.json that is a link stays one. The
cache folder holds audio, the covers shown (artwork/), matches, how loud
YouTube measured the tracks played (loudness.json), and yt-dlp's cache,
and the logs folder the log and the crash reports. MIXMUSIC_DATA_DIR
moves all three into one folder, for a portable copy or a test run.
MIXMUSIC_YTDLP_ONLY=1 has yt-dlp find every stream, as before MixMusic
asked YouTube's player API, to time one against the other or tell which
fails: the log tells which streams took over a second to find, and which
tracks over two to start.
Capture mode
MIXMUSIC_CAPTURE=<dir> screenshots pages of the main window, and
MIXMUSIC_CAPTURE_QUIT=1 quits once they are saved:
MIXMUSIC_CAPTURE=shots MIXMUSIC_CAPTURE_QUIT=1 MIXMUSIC_CAPTURE_ROUTES="/|/search?q=jazz|/settings" MIXMUSIC_CAPTURE_SIZE=1440x900 ./MixMusic
MIXMUSIC_CAPTURE_WAIT sets how long each step gets (5s). Steps that
aren't pages act: act:shot:<name> saves the window as it is,
act:mute mutes the player, act:play:<search> plays what a search
finds, act:resume plays the queue kept in the data folder,
act:wait:<duration> waits, act:palette opens the command palette,
act:refresh refreshes the page, as F5 does,
act:panel:<queue|lyrics|none> shows a side panel, act:scroll:<y|end>
scrolls the page, act:theme:<id> wears a theme,
act:colors:<off|pywal|matugen|path> takes the colours from a file, as
Settings › Appearance does, act:mini opens the mini player, which saves
mini.png, act:spotify logs whether MixMusic's own Spotify device
could play, act:title logs the window's title, which the screenshots
leave out, and act:memory and act:heap log the Go heap and save a
profile of it. Nothing plays unless the player is muted,
or MIXMUSIC_SILENT=1 has the audio engine play into no device, which
keeps the visualizer and the lyrics moving without a sound.
MIXMUSIC_CAPTURE_COMMUNITY=<dir> has the marketplace read the community
registry from a copy of community/, for add-ons not on GitHub yet. The
site's screenshots are made this way, from a throwaway profile, at
1536x900 (MIXMUSIC_CAPTURE_SIZE=1552x909: the size includes the
window's invisible borders). go test . -run Render with
MIXMUSIC_RENDER=<dir> draws the pages and themes without a window.