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.