Reflecting on a year of building Petrichor

doublslash ·

18 min read Original article ↗

I spent the last year building Petrichor, an offline music player for macOS, the kind of player I would want to use, and the kind that didn’t exist on the platform. It is the only thing I work on during my weekends when I’m sitting at my desk, and I’m proud of what Petrichor has managed to achieve in the past year, while giving me the kind of creative satisfaction that I haven’t had in a long, long time. On the work front, I was close to being burned out (for reasons I won’t get into), but this project became a comforting space that I’d look forward to returning to. This essay covers the journey of conceiving Petrichor, building it, marketing it, maintaining it, and eventually spawning another engineering endeavor that I didn’t imagine before.

Petrichor
Petrichor

Background

I’ve been a JavaScript engineer working on web frontends for over 13 years, so writing Mac apps wasn’t something I was familiar with. I’ve built desktop apps in C#.NET a couple of times in the past, so building a platform-native desktop app was still something I’d done and enjoyed.

Playing music has almost always been an offline-first experience for me despite having active subscriptions to both Spotify and Apple Music, as my music collection is largely frozen in time and is rarely updated, although it has evolved over the years to have roughly 3k tracks spread across albums, compilations, and languages. So a feature-rich offline music player would be one of my most-used apps.

I’ve also been an active user of Linux besides macOS, where I used Rhythmbox for years, and while it wasn’t perfect, it still came close to becoming my go-to option. When I moved to using a Mac as a part of my job in 2017, I missed such a music player on the platform until I discovered Swinsian, and it felt like a perfect replacement. However, updates became infrequent, and there was a gap of years between version 2.0 and the more recent 3.0 release.

In 2022, I stepped away from writing code in my job and instead started managing an engineering team, and this was also a time when I noticed Swinsian lagging behind, but I didn’t have the kind of time or mental bandwidth to even imagine building an app I was missing.

In early 2025, after managing an engineering team for close to 3 years, I decided to step down and become an individual contributor again at work because the mental exhaustion I was going through had started affecting me personally. Moving to an IC role freed up my calendar; a 5-day work week didn’t feel like a bottomless pit where a weekend was never enough to recover, and instead, I had this new-found energy to be a builder again. As the industry was undergoing a seismic shift due to the emergence of AI, prototyping an idea had also become exponentially faster, and that’s when I decided to use my evenings and weekends to build Petrichor. While naming is hard, coming up with this name was obvious to me. I’ve grown up in India, where the first rain of the season brings a distinct fragrance, and ever since I learnt about the word representing it, I had to use it right away.

Early Days of Prototyping

Unlike my fresh-grad self, who’d build stuff left and right without giving much thought to design and architectural choices, this was the first project I wanted to take seriously, while making use of all of my learnings from the past decade-plus of engineering experience.

It was March 2025 when the idea started to take shape in my head about what kind of music player I was missing on Macs, and the app I would build to use myself, before ever thinking about putting it out there for others to download and use.

I wasn’t too familiar with the basics of building apps for Mac, although I was following the evolution of Swift, and having covered it on my blog years ago, I knew that if I was aiming to have a true Mac experience, Swift would be it, even at the cost of not being able to use it anywhere else (especially Linux). While conceptualizing, I looked at several existing offline music players available on Mac (besides Swinsian) like Foobar2000, Cog, and streaming music player client apps like Spotify, Apple Music, and Tidal to draw some ideas that translate well into an offline player (not necessarily in the first iteration but eventually).

Role of AI

I want to get this out of the way. I recognize how developers are vibing apps and projects using agentic AI these days, and I’m not claiming that Petrichor is entirely “hand-coded”, but calling it vibe-coded wouldn’t be fair. I use Claude/GPT to prototype features and fix bugs, but not a single line goes into main mindlessly. I’ve learnt enough Swift over the past year to know what should be shipped and what shouldn’t be, even if I’m not following the “best practices” of Swift mac app development. With that established, let’s continue with this essay.

A Working Prototype

In the first couple of weeks, I leveraged Claude’s web chat along with Xcode open on the side to have it teach me Swift by drawing parallels with modern frontend web development that I had been doing for years, so that the concepts would still land with me and I could make sense of how things were wired, even if it wasn’t the best way to do things. Here’s a glimpse of what the progress during the first week of development looked like:

Petrichor Prototype

This was a good start: I could actually play music from the app I just built, but it was still a first step and there was a long way to go before I could daily-drive it. The initial implementation didn’t quite bother supporting various file formats, as my own collection was primarily MP3 and AAC files, both being supported well by CoreAudio, so using AVAudioPlayer was sufficient to get the things moving.

It was only after getting the audio playback sorted that I realized it was only half the work done. The other half around figuring out the UI was going to be way too time-consuming, and this is where I sorely missed the hot reloading aspects of web frontend development. While Xcode supports a similar feature via preview blocks in SwiftUI, it is still not as fast (or reliable). I continued leveraging it to get the polish in place, along with building the layout I was shooting for.

High-Level Architecture

Getting a bunch of files into the UI and playing them made for a prototype; building a consumer app with table-stakes stuff like data persistence, state management and retention, user customizability, and respect for the macOS permissions model was another elaborate engineering exercise.

For over a week, I went back and forth between exploring various ways to extract and store library data because one key thing that I was leaning on to build this experience was the metadata that song files have, so building a relationship graph of complete library metadata would allow for things that only streaming players have had. While I was aware of TagLib as a de facto way of metadata extraction, the initial approach of the data extraction was hand-rolled purely in Swift, which in hindsight was a terrible idea, but I’m glad I learned and improved it.

For data persistence, I did consider SwiftData with the intention of sharing the app’s code to a future iOS version, should I ever build one, but I’ve always been a fan of SQLite and was familiar with some of its abilities that I felt would come in handy for my app, so I learned about GRDB.swift and decided to go ahead with it after I figured that this wouldn’t pose a risk to my iOS aspirations. While the database schema of the app has evolved quite a bit over the year, here’s the mermaid representation of schema at the time of writing this:

Petrichor
Petrichor

I understand that the current schema isn’t perfect and reflects my own naive understanding of a good database structure, or in other words, it is normalized with denormalized parts where it matters. And it has worked so far, so I’m not touching it unless something is genuinely required to be fixed and/or optimized for better user experience and app performance.

If I were to describe how data flows and what the app lifecycle looks like, it can be summarized in the following steps:

  1. App is launched for the first time, asking the user to add library folders.

  2. Folders are added, causing an SQLite database to be created and its schema initialized.

  3. Library scan is triggered to perform metadata extraction:

    • Once a few tracks are available, the app’s UI is ready to use, moving the rest of the extraction to the background.

    • Extraction includes metadata tags & album artwork to store in the DB.

    • Song files remain unmodified; the database then acts as a bridge between the app and track files.

  4. When a song is played, its information is accessed from DB while the actual file is played from the filesystem.

  5. Any app-specific operations are always done in the database without ever touching the tracks in the filesystem.

  6. When the app is quit, the current app state is retained via UserDefaults to show again when the app is relaunched.

That’s essentially the entire sequence and everything else is just a masonry of user experience enhancements making the app what it is today. Once this foundation was working, the next challenge was getting the app into other people’s hands.

Journey to a Stable Release

As I got closer to having a build that I felt was a good enough alpha to put in the hands of some of my friends and other interested users, I had to figure out the logistics of how to build DMG installers that could be shared with someone. This is where Claude was most handy, as I didn’t want to spend much time figuring out these details when building the app itself was hard enough given my lack of Swift experience. I came across xcpretty & create-dmg, both of which allowed for building the installer via a shell script that helped me unblock the distribution.

By the time I was ready to tag the first alpha build (after over a month of development), the app looked a lot different and more polished. I shared it with friends to get some initial feedback on the experience before I was ready to share it with a wider audience. Here’s a screenshot of what it looked like:

Alpha 1
Alpha 1

This felt like a good time to share the project over the internet, so I posted about the first alpha build in two places which I thought were good spots: r/macapps and HackerNews. The response I received on both felt unreal. Within a couple of days, the app had garnered well over 500 stars on GitHub, while the HackerNews post stayed in the top 10 for several hours. More importantly, I received a ton of immediate feedback to act on towards the preparation of upcoming alphas and betas.

Confronting Bad Design Choices

As more users learned about the project and used the alpha builds, I started getting critical feedback which uncovered some fundamental flaws in my app’s design and architecture. While the list of such factors would be endless, some highlights include: an inefficient approach to loading library data into memory on app launch (#63), a buggy implementation of AppKit NSTableView-based tracks list (#46), and of course, the app and installer not being notarized, as I hadn’t paid for an Apple Developer account just yet.

As public forums go, not all the feedback I got was useful; some comments were just people roasting the app, but the number of issues that users created on the GitHub project reinforced that what I was building was genuinely needed by at least some users, so that motivated me enough to continue iterating and push out alphas.

First Stable Release

After 4 alpha and 4 beta builds over the course of 45 days, and a ton of learning during this time, on 24th August 2025, I shipped the first stable release of the app, and it addressed almost all user suggestions and reported bugs, and included UI polish, a notarized app and installer, Homebrew support, automatic updates and more. It felt like a huge milestone, but being the first user of my app, I knew how far I’d have to go before it became something I could respect and appreciate. And while perfect is the enemy of good, the app was still not good yet.

Grind Towards Perfection

The first stable release garnered over 400 downloads, a modest figure for an app whose first alpha was put together in a span of a couple months by an inexperienced Mac app developer like me. But an indie app getting such a response motivated me enough to keep doing it, so I continued to enhance and refine it in my spare time, while slowly working towards the goals I had set for the app. The next set of challenges was to improve file format support and equalizer adjustments, both of which were far from trivial to implement in my experience.

Search for Audio Playback System

One thing that I had learnt in the past couple of months of development towards putting out the first release was that AVFoundation (and CoreMedia at large) on Apple’s platforms is both extremely capable and very limited out-of-the-box at the same time. For instance, getting audio to play from a file is extremely easy to achieve, but the moment you get into power-user territory of features like decoding third-party file formats and audio enhancements (i.e. EQ), you have to get your hands dirty handling things like PCM conversions, chaining audio effects pipelines, etc., manually. AVFoundation provides all the APIs you need to achieve this and more, but you’re on your own to figure out the parts that cannot be documented realistically.

Since building the user-facing features was hard and time-consuming enough for me, I didn’t want to spend time reinventing the wheel of building a playback system myself, especially when this problem has already been solved on the platform. And so my search for a library began, one that could solve these two things for me: decoding a wide range of file formats beyond what CoreAudio supports and allowing me to do audio enhancements like multi-band equalizer.

After a few weeks of research as well as one full release cycle (v1.1), I came across an excellent package: SFBAudioEngine, which had exactly what I was looking for, and most importantly, the project was alive and kicking, with the maintainer actively making enhancements. After spending a couple of days integrating the package into Petrichor, I was thrilled to see how it was able to power features I was struggling to add for months!

So much so that I wrote to the project maintainer (Stephen Booth) sharing my gratitude for building the library and making it publicly available to use. Eventually, the next release (v1.2) included it, leading to twice the total downloads.

With the playback backend sorted, I had the capacity to focus on core app UX, iterate fairly rapidly, and add commonly requested features from the community of users. SFBAudioEngine solved a critical aspect of the app at the time, but by the spring of 2026, I realized Petrichor’s roadmap had started to outgrow it.

Owning the Complete Stack

In April 2026, I completed a big release of the app, v1.5. It included a lot of enhancements and fixes that had been sitting in my backlog for months. As it went out, followed by patch releases a couple of weeks later, I was thinking about the future of the project. Even with all the bells and whistles of SFBAudioEngine, it still lacked some of the modern features that Apple’s platforms provided, things that most of my users would often request, and more importantly, things I missed in my app.

To put the problem in other words, I was in a situation where I owned a Sandwich Shop that was doing fine, but my bread was still coming from a bakery I didn’t own. My requirements for a perfect playback backend were clear: have a complete audio platform in Swift that can handle everything audio while supporting modern features that users expect from a Mac audio player. So I again went down the rabbit hole of searching for a library which had the features of the existing playback engine but none of the limitations that I was facing, and it led nowhere. It almost felt like a pipe dream, but the fact that most other music players already had parts of it solved reinforced the idea in my mind that it was time to own the bakery too.

I spent the following couple of weeks studying concepts like PCM buffers, fundamentals of audio decoding, how DSP works in general, and how libraries like FFmpeg and Symphonia (Rust) handle audio decoding that can be consumed by a separate codebase written in Swift. This led to the creation of Crescendo.

Crescendo
Crescendo

Unlike Petrichor, the design goals for Crescendo were established from the get-go, as I set out to build it after nearly a year of maintaining the app, so I knew exactly what was needed then and what would come up later that the library would need to provision for.

Thus, the main app took a back seat for a couple of months as I invested most of my nights and weekends in building the library. My approach to building it was also fundamentally different from how I prototyped the app. I was looking at it from my architectural brain, with the lessons from all the bad/naive decisions I made originally while building Petrichor.

As the project progressed and I achieved the design goals I had, I was getting fairly ambitious, aiming not just to build a good playback platform for Petrichor, but the best damn playback platform any music player has ever had.

When the first stable release of the package was ready, I integrated it into Petrichor. I almost shed a tear seeing how well it worked with the app. You can read up more about what Crescendo can do on its project page.

Present and Future

With a mountain of critical parts figured out, I can’t wait to build what’s next for Petrichor. While the idea started as something I wanted to build for myself, it ended up becoming something that thousands of users are actively using today. I’ve had 18 releases in total, both major and patch. Some users also write to me appreciating the effort I’ve put into this hobby project. Petrichor was even featured in the January 2026 edition of a German magazine, Mac & i, which was the last thing I expected.

I barely make any money with Petrichor, and that’s okay; I’ve used hundreds of open source creations over the years, so this app is my way of giving back, and saying thank you!

I cannot even begin to express the extent of my delight upon discovering Petrichor today. I’ve been searching for quite some time for pretty much exactly what you’ve created, but was unable to find something that made me particularly happy until today. I am immensely grateful for all the effort and love you’ve clearly poured into it.

And some more kind words…

I just wanted to say thank you for the Petrichor app. It is a really incredible app, with such sophisticated UI, and it works perfectly. The thoughts and thinking you are investing in the development are really seen in the app.

As I prepare the upcoming release of Petrichor, v1.6.0 (at the time of writing this), it is becoming the biggest release I’ve ever worked on, and the motivation comes from the fact that my grind to create a complete experience is becoming a reality now.

Looking back, I had never thought I’d have an open source project garnering this kind of popularity in the Apple ecosystem. More than the downloads or recognition, building it has reminded me why I enjoyed building software in the first place. I’ve had this site running for nearly 15 years, and the last original blog post I wrote for it was over 10 years ago! So if I were to come back and write again, it had to be something special.