I wanted a music player I actually want to use. And I wanted to learn Swift. So I started building one.
The project is called Echo. It’s a native macOS app, built with SwiftUI, that
plays MP3s from your ~/Music folder. The scope is intentionally small: small
enough to ship something real, but complex enough to touch the interesting parts
of the platform (audio APIs, system integration, file access, UI state
management).
This post is a running devlog. I’ll update it as I build.
# Day 1: June 16
The first day was setup and getting something on screen. Xcode project, README,
MIT license. The first real decision was sandbox entitlements. On macOS, apps
don’t get filesystem access by default. You have to declare what you need in an
entitlements file. I needed access to ~/Music, so I added
com.apple.security.files.user-selected.read-only and the music folder bookmark
entitlement. Nothing in code. Just an XML file that the OS reads. So when it
silently fails, there’s nothing to grep for.
Then I built a Home view backed by a FileListViewModel that scans ~/Music
and returns the MP3 files. Nothing plays yet. Then I added an AudioPlayer
class wrapping AVAudioPlayer, Apple’s straightforward playback API. Wired up
play and pause. By the end of the day, tapping a file starts playback.
Coming from web development, SwiftUI’s declarative model feels familiar.
Describe what the UI should look like given state; the framework handles
updates. @State and @ObservedObject are doing the same job as React’s
useState and useContext, just baked into the language syntax. The sandbox
roughness aside, the day went faster than expected. Something plays. That’s
enough.
# Day 2: June 17
The first thing I did today was tear apart yesterday’s structure. The initial
setup had everything tangled together, with view models holding UI concerns next
to audio logic. I split it into core/ for pure Swift services with no SwiftUI
dependency, and ui/ for views and view models. This forces a clean dependency
direction: UI depends on core, never the other way around.
I added prev/next buttons. The queue is an array of songs with a current index,
and advancing means incrementing the index and calling play(_:) on the next
item. Auto-advance was a small satisfying piece: AVAudioPlayer calls a
delegate method when a track finishes, and I forward that to the view model,
which moves the queue forward automatically.
For the progress bar, AVAudioPlayer doesn’t push updates. You have to poll
currentTime on a timer. So I set up a Timer that fires every 0.5 seconds and
updates a @Published property on the view model. Seeking was simpler: dragging
the scrubber just sets currentTime directly on the player.
MP3 files can embed album art in their metadata. I load it using AVURLAsset,
reading the commonMetadata array and looking for the artwork key. It’s
async, so there’s a brief moment where a placeholder shows before the artwork
resolves. Acceptable.
The most interesting part of the day was system Now Playing integration. macOS
has a system-level Now Playing concept that surfaces in the menu bar and handles
media keys. MPNowPlayingInfoCenter is a dictionary you write to with the
current track’s title, artist, artwork, duration, and elapsed time.
MPRemoteCommandCenter is where you register handlers for play, pause, next,
previous, and seek. When the user presses a media key or uses the Now Playing
widget, your handler fires. Getting artwork into the system display requires
converting NSImage to MPMediaItemArtwork, which takes a closure returning a
scaled image at a given size. Small detail, but easy to miss.
# Day 3: June 18
Today I added a sidebar for navigation and moved the player controls to the
bottom edge of the window. The bottom placement feels natural, which is what
most music players do, and it keeps the controls out of the way of the content
above. Navigation state lives in its own AppNavigationState object, a small
ObservableObject that any view can hold a reference to and call a method on.
Cleaner than scattering navigation logic across view models.
I added a settings page, wired up to ⌘,. Most macOS users will try it
instinctively. If it doesn’t work, the app feels unfinished. The song list now
also shows title, artist, and album from MP3 tags instead of raw filenames. Same
AVURLAsset metadata pipeline used for artwork; it’s all in commonMetadata,
keyed by identifier.
SwiftUI makes light/dark mode largely automatic if you use semantic colors like
Color.primary. But some of the custom palette colors needed explicit light and
dark variants defined in the asset catalog. Not hard, just easy to forget until
you switch modes and something looks wrong.
Two bugs needed fixing. The system’s Now Playing display was showing the app
icon instead of album art. The fix was making sure MPNowPlayingInfoCenter gets
fresh artwork set whenever the track changes. It was being set too late in the
update sequence. The other: closing the window was stopping playback. On macOS,
closing a window doesn’t have to mean quitting; apps are expected to keep
running. The fix was adjusting behavior so closing hides the window rather than
terminating the audio session.
I’m updating this post daily as I build. Next up: search and filtering, shuffle, repeat.
# Day 4: June 21
I did not work on it for the past 2 days.
Today was a big architectural shift and the first time the app does something genuinely interesting with music.
The first commit pulled AudioPlayer, MusicLibrary, NowPlayingService, and
Song out of the main app target and into a local Swift Package called
EchoCore. The app module now imports it like any other dependency. The
motivation is clean separation: EchoCore has no SwiftUI dependency, so it can
be tested in isolation. Swift packages get their own test targets and their own
scheme in Xcode, which means running core tests doesn’t require building the
full UI. This is the standard Apple pattern for framework vs. app, except the
package lives in the same repo rather than a separate one.
With the package structure in place, I added FeatureExtractor. It uses
AVAudioFile and AVAudioPCMBuffer to read raw PCM samples from an audio file,
then derives three acoustic features: tempo (via autocorrelation on the energy
envelope), key (by computing a chroma vector and comparing it against
major/minor key profiles), and loudness (RMS amplitude in dBFS). None of this is
production-grade DSP. The goal is to have something numeric to compare songs
against, not to build a serious audio analysis library.
FeatureStore sits on top of the extractor and handles caching. Extracted
features are persisted to disk via Codable and JSONEncoder into the app’s
Application Support directory, so the whole library doesn’t get re-analyzed on
every launch. On first load, MusicLibraryViewModel kicks off extraction for
any song that doesn’t have a cached entry yet.
The most satisfying piece was SimilarityEngine. It takes a source track’s
features and returns the N closest songs from the library. Similarity is a
weighted Euclidean distance across normalized feature dimensions: tempo
difference, key compatibility (using the circle of fifths), and loudness
proximity each contribute a score that gets combined into a single value. The
Now Playing screen now shows a list of recommended tracks below the current
song’s artwork and controls. It’s the first time the app feels like it has an
opinion about music rather than just being a file browser.
The similarity model is naive. Key estimation from chroma vectors is rough, and
tempo detection via autocorrelation misses a lot. There’s no genre or mood
signal at all. The infrastructure is in place, though. FeatureStore and
SimilarityEngine are both easily swappable behind their interfaces, so
improving the model later won’t require touching the rest of the app.
# Questions?
# When will I release it?
Probably never. Apple Developer Program is $99 per year. I have no income to pay for that.