AudiBar - Menu Bar Audio Player

Return to Projects
AudiBar - Menu Bar Audio Player

A menu bar audio player for macOS. Drag and drop audio files to play them instantly. Supports playlists, shuffle, auto-play, and multiple audio formats including MP3, WAV, FLAC, and M4A. No library management or complexity.

Project Type
macOS App
Current Status
Published
App Version
1.0.3 (Build 5)
Development
85 hrs (website & app)

v1.0.3 Update - 27th November 2025

Added

  • Audio streaming support with playlist parsing for multiple formats:
    • M3U/M3U8 (standard playlists and HLS streams)
    • PLS (Shoutcast/Winamp playlists)
    • XSPF (XML Shareable Playlist Format)
    • ASX/WAX (Windows Media playlists)
    • RSS/XML (podcast feeds with audio enclosures)
    • Direct stream URLs (http/https)
  • Stream list panel with saved stations and sorting options
  • Playback controls in playlist panel: seek slider, previous/next track buttons, and current time display
  • Playlist sorting: Sort by track title or duration in ascending/descending order
  • Settings panel for app configuration
  • Smart track naming: Automatic metadata extraction from audio files (artist, title, album), with filename fallback when metadata is unavailable
  • Custom tooltip system with auto-dismiss behavior

Changed

  • Redesigned UI theme: Unified app-themed panels for playlist, streams, settings, about, and upgrade windows
  • Playlist navigation: Window now uses standard macOS controls (close, minimize, maximize) instead of custom button
  • Button labels: Moved to tooltips for cleaner interface
  • Enhanced track highlighting: Improved visual indication of currently playing track
  • Tabbed interface: Playlist panel now contains separate tabs for track playlist and stream lis

Removed

  • Light/dark mode support (replaced with consistent app theme)
  • Button labels in playlist controls (now shown as tooltips on hover)
  • Custom playlist close button (uses native window controls

Personal problems are best

During development of my previous app, Chronode, I was listening to some DJ mix files using Apple Music as a playlist. I found Apple Music created random files and empty folders making my iCloud Drive messy. After looking in the App Store for a simple audio player that lived in the menu bar where I could play MP3s without having an app open cluttering up my dock or desktop.

I couldn't find one. So I took a week out to build one and the result was AudiBar. A lightweight menu bar audio player that does one thing exceptionally well: plays audio files without all the complexity of traditional music players.

Simple Concepts

The concept and features needed would be simple:

  • Simple menu bar player with play/pause, current track title ticker + timers (elapsed/total time)
  • Some playlist support where I could add files, auto-play and shuffle
  • No open apps on the desktop and nothing showing in the do

I've done my best to document this development build, including the challenges I faced and how I solved them.

The Audibar playlist panel

The Vision

AudiBar is for anyone who plays music files on their Mac. Whether you're listening to DJ mixes, podcasts, audiobooks, or just your music collection, if you want simple playback without the bloat of a full music player, AudiBar is for you.

The core philosophy was "do one thing well". No feature creep, no unnecessary complexity. Just reliable, fast audio playback accessible from the menu bar

Technical Foundation

I built AudiBar using:

  • Swift with SwiftUI for the UI layer
  • AppKit for menu bar integration (bridged with SwiftUI via NSHostingView)
  • AVFoundation for audio playback
  • StoreKit 2 for in-app purchases
  • macOS 13.5+ as the minimum deployment target

The architecture follows an MVVM-style pattern with observable objects managing state. I chose to bridge SwiftUI and AppKit to get the best of both worlds: AppKit's stability for menu bar presence and SwiftUI's reactive nature for the UI.

Development

Core Playback

I started by building the essential playback functionality. The first challenge was implementing a three-state UI system:

  1. No track loaded, showing just a app icon
  2. Loading, displaying a spinner during file initialisation
  3. Track loaded, showing playback controls and track info

The loading state turned out to be crucial. Without it, the app appeared frozen when loading iCloud Drive files that needed downloading. Adding a 200ms delay before loading ensured the loading UI always appeared, preventing that "stuck" feeling.

Key Challenge: Button Click Detection

Early on, I ran into a frustrating bug where the play/pause button stopped responding. After debugging, I discovered that GeometryReader in my scrolling text view was intercepting mouse events. The fix was simple but not obvious: adding .allowsHitTesting(false) to the geometry reader prevented it from blocking clicks.

Playlist Feature

Once basic playback worked, I added playlist functionality. This required:

  • Creating a custom borderless NSPanel with rounded corners and shadow
  • Implementing PlaylistTrack model with security-scoped bookmarks for persistent file access
  • Building drag-and-drop support for files and folders
  • Creating a custom scrollbar (8pt wide with accent color theming)

Major Challenge: iCloud Drive Files

Drag-and-drop broke completely for iCloud files. The issue was that standard file providers didn't handle iCloud's download-on-demand model. I had to switch to loadObject(ofClass: URL.self) with proper async/await handling and trigger downloads manually using FileManager.startDownloadingUbiquitousItem. This ensured iCloud files downloaded before processing.

Audibar Add stream modal

Performance Optimisation

My initial implementation used AVAudioPlayer to extract duration from audio files when building playlists. This was painfully slow! 50-100ms per file with 5-10MB memory usage per track. I refactored to use AVAsset instead, which dropped processing time to 5-10ms per file and reduced memory usage by 50-100x. This made adding entire folders to playlists feel instant.

I also implemented recursive directory scanning so users could drag entire music folders onto the playlist panel so it would find all audio files automatically.

Auto-Play & Visual Polish

With playlists working, I added auto-play functionality. When a track finishes, it automatically loads the next one. When reaching the end of the playlist, it loops back to track 1. This required implementing a callback system in the audio player manager that fired when tracks completed.

Light/Dark Mode Adaptation

I spent significant time making the app look great in both light and dark modes. The playlist panel uses .regularMaterial in dark mode for that modern translucent effect, but switches to a solid background in light mode for better readability. Playing track highlights, borders, and shadows all adapt their opacity based on the color scheme.

I also matched the custom scrollbar knob to the user's system accent color, so it feels truly native to macOS.

About Window

I created a simple About window with app info, version details, and a custom dotted underline for the website link. The window management was tricky. I had to ensure it properly activated and came to the foreground when opened from a menu bar app (using NSApp.activate(ignoringOtherApps: true)).

Freemium Model

This is where things got interesting. I decided to gate the playlist feature behind a premium upgrade to make the app sustainable. I implemented full StoreKit 2 integration with:

  • LicenseManager for tracking premium status (UserDefaults-based)
  • StoreManager for handling purchases with async/await
  • Transaction verification and real-time monitoring
  • Comprehensive error handling for all StoreKit edge cases

The Upgrade Flow

I built a complete upgrade window with state-based rendering:

  • Loading state while fetching products
  • Error states for network issues, missing products, etc.
  • Purchase view with processing indicators
  • Success view with a nice fade-in animation

The pricing landed at £2.99 for the UK market which is faily comparable to other quality menu bar utilities available.

Critical Performance Fix: The CPU Monster

Around this time, I noticed the app was using 60%+ CPU while playing audio. This was completely unacceptable by anyones standards. Hell even 10% was way too high. After profiling, I found the culprit: my demonic scrolling ticker text animation sucking up resources.

I was using SwiftUI's withAnimation().repeatForever(), which caused continuous state updates and view re-evaluations. The fix required a complete architectural change. Fortunately this wasnt too impactful of on development time. I simply switched to Core Animation with CABasicAnimation which moved the animation to the GPU compositor thread and dropped CPU usage from 60%+ to 0-1%. Problem solved.

Audibar context menu

Security & Resource Management

As I prepared for App Store submission, I did a comprehensive code review and uncovered several critical issues:

Security-Scoped Resource Leaks

I discovered hundreds of potential resource leaks in my playlist code. Every time I accessed a file using security-scoped bookmarks, I was calling startAccessingSecurityScopedResource() but not always cleaning up properly.

The fix was implementing a closure-based API:

func accessURL<T>(_ operation: (URL) throws -> T) rethrows -> T 
{
    let url = resolveURL()
    
    guard url.startAccessingSecurityScopedResource() else 
    {
        throw AudioError.fileNotFound
    }
    
    defer { url.stopAccessingSecurityScopedResource() }
    
    return try operation(url)
}

This automatic cleanup pattern prevented kernel resource exhaustion.

The "+ Current" Button Mystery

After implementing the security fixes, I discovered the "+ Current" button (for adding the playing track to the playlist) stopped working after app restarts. The problem was subtle: restored tracks didn't maintain active security-scoped access.

I had to restructure the lifecycle management so AudioPlayerManager.loadAudioFile() starts security-scoped access at the very beginning and maintains it for the entire lifetime of currentFileURL. This fixed the issue while preventing leaks on all error paths.

Thread Safety with Swift Concurrency

I modernised the codebase to use Swift's new concurrency model properly, which involved a fair bit of refactoring. I added @MainActor to AudioPlayerManager to ensure all UI updates happen on the main thread, marked LocalisationService as nonisolated since it's genuinely thread-safe, and fixed all async/await usage to respect actor isolation. Swift 6's strict concurrency checking doesn't mess around and will happily throw warnings at you until you get everything right.

The effort was worth it though. I eliminated every single Swift concurrency warning, and more importantly, I actually understand the threading model now instead of just hoping things work. No more random crashes from updating UI on background threads or race conditions in the audio player state. The app is rock solid because the compiler is enfor

Testing & Final Polish

I built a comprehensive test suite with 59 tests across 9 test files, covering everything from core audio playback functionality to playlist management (add, remove, shuffle, next track), security-scoped bookmark handling, localisation and license management, file processing with format validation, and a ton of edge cases and error handling. Writing tests for a menu bar app felt a bit overkill at first, but it gave me confidence that the core functionality actually worked correctly, especially the tricky bits around security-scoped resources and iCloud file handling.

I also created a bash script to automatically manage the test target, which means I never have to manually edit the Xcode project when adding new tests. The script uses the xcodeproj Ruby gem to programmatically add new test files, configure build settings, and keep everything in sync. It's one of those quality-of-life improvements that seems unnecessary until you've manually added test files to Xcode for the fifth time and realise there has to be a better way.

Debug Tooling

For development, I built a comprehensive debug view that lets me simulate all purchase states without having to go through the actual App Store purchase flow every single time. I can trigger scenarios like "no products available", network errors, purchase errors, and successful purchases with a single button click.

This was an absolute lifesaver during testing. Instead of waiting for StoreKit to load products, triggering real purchases with sandbox accounts, and dealing with Apple's servers, I could test the entire upgrade flow in seconds. The debug build also clears UserDefaults on launch, which ensures every test run starts from a clean slate with isPremium = false.

App Store Submission

The final sprint involved preparing everything for App Store submission:

Marketing Content

I prepared demo tracks for the marketing materials and screenshots. To avoid any copyright issues, I made up all the track, artist, and album names usingrandom things that popped into my head at the time. I stripped embedded cover art to keep file sizes reasonable.

After registering the audibar.app domain, I spent two days building a single-page website from scratch using Laravel and Livewire. The design is clean and simple, drawing inspiration from TailwindPlus for the various sections. Privacy and terms policies were also created to go with it.

A full day went into creating all the marketing materials. The YouTube video runs about 84 seconds and demos all the features of the app. For the App Store, I created a preview video (converted to 30fps for compliance and trimmed to exactly 30 seconds) and 3 screenshot images showing drag & drop functionality, right-click context menus, and the playlist panel in full swing.

Build Automation

After manually incrementing build numbers and creating archives a few times, I got tired of the repetitive process and created a bash script to automate the whole thing. The script handles incrementing build numbers, running clean builds, creating Release archives, and showing version/build info so I know exactly what I'm uploading.

It even provides next steps guidance because I kept forgetting what to do after the archive was created. Now instead of a ten minute manual process with room for error, it's a single command that does everything correctly every time. Future releases will be so much easier.

Final Fixes

Just before submission, I fixed one last z-ordering bug: the context menu appeared behind the playlist panel. Changing the panel's window level from .popUpMenu to .floating solved it.

Submission

On October 18, 2025, I submitted AudiBar v1.0.1 (Build 3) to the App Store. By October 19th, it was approved without any issues, and honestly, much faster than I expected for a first-time submission. I manually released it to the world on the same day. Fingers and toes crossed it's received well and people find it as useful as I do. Now comes the nerve-wracking part: waiting to see if anyone actually downloads it.

Technical Achievements

Performance Metrics

By launch, I achieved excellent performance metrics:

  • CPU Usage: 3% when playing, 0% when idle
  • Memory Footprint: 37-38 MB stable
  • Energy Impact: Low when playing, Zero when idle
  • File Processing: 5-10ms per file (10x faster than initial implementation)

Supported Formats

AudiBar supports all common audio formats natively - MP3, WAV, FLAC, M4A, AAC, ALAC, AIFF, CAF, and MP4. All of these are handled through Core Audio without any external dependencies, which keeps the app lightweight and means I don't have to worry about maintaining third-party audio libraries. If macOS can play it natively, AudiBar can play it. This covers pretty much everything a typical user would throw at it, from ancient MP3s to high quality FLAC files.

Audibar Stream list

Localisation

Full localisation support for three English variants:

  • English (US)
  • English (UK)
  • English (Australia)

I wanted to build in proper localisation support from the start, even though I'm only targeting English-speaking markets initially. Setting up the infrastructure now with three English variants (because British and Australian users really do notice when you spell "licence" as "license") means I can easily add French, German, Spanish, or any other language down the line without refactoring everything.

All user-facing strings are externalised through LocalisationService, so future translations are just a matter of adding new string files. It's one of those "do it right the first time" decisions that takes an extra hour upfront but saves you from a massive headache later.

Lessons Learned

1. Profile Early, Profile Often

The 60% CPU bug could have been caught much earlier if I'd been monitoring performance from the start. I only noticed it because my laptop fan started sounding like a jet engine during playback. Now I know to keep Activity Monitor open in a second window during development. It's become my new best friend! Don't wait until your Mac takes off to check performance.

2. Security-Scoped Bookmarks Are Tricky

Apple's documentation doesn't emphasise enough how easy it is to leak these resources. I burned through hundreds of kernel resources before realising what was happening. The closure-based pattern I developed should honestly be the default approach. It's one of those "why isn't this in the documentation?" moments. Future me (and my system resources) will thank past me for figuring this out.

3. SwiftUI + AppKit Bridge Carefully

While bridging SwiftUI and AppKit works well, you need to be careful about event handling, window management, and lifecycle issues. I spent way too much time debugging why buttons weren't clicking or windows weren't appearing. Test interactions thoroughly, and I mean click everything, multiple times, in different orders. What works in pure SwiftUI doesn't always translate when AppKit enters the chat.

4. StoreKit 2 Is Actually Great

Despite initial trepidation (and horror stories about StoreKit 1), StoreKit 2's async/await API is much nicer than the old delegate-based approach. Transaction verification and monitoring are straightforward. I was pleasantly surprised. It's one of the few Apple APIs where the modern version is genuinely better, not just different. If you're still avoiding in-app purchases because of old StoreKit nightmares, give version 2 a shot.

5. Test on Real Hardware Early

Some issues (like iCloud file handling) only appeared when testing with actual iCloud Drive files, not local files. Simulators and local testing will lie to you. They'll make everything look perfect until you try the real thing. I learned this the hard way when drag-and-drop suddenly broke for iCloud files. Save yourself the headache and test with real-world scenarios from day one.

Future Enhancements

While the v1.0 feature set is complete and polished, I have ideas for future versions:

  • Keyboard shortcuts for play/pause
  • Media key integration
  • Recently played list
  • Basic EQ or audio effects
  • Loop/repeat options
  • Scrubbing/seeking controls
  • Playlist persistence across restarts

These are intentionally scoped for future releases to keep v1.0 focused and stable.

Conclusion

Building such a simple and small app like AudiBar taught me far more than I expected and revealed a completely different set of challenges related to memory management, security-scoped resources, AppKit quirks, and performance optimisation at a level web developers rarely encounter.

The app does exactly what I set out to build: provides simple, fast audio playback from the menu bar without unnecessary complexity. It's lightweight, performant, and actually useful to anyone who plays music on their Mac!.

Most importantly, it's my first shipped macOS application. Seeing it go from concept to App Store submission inside 10 days feels like a genuine achievement.


Comments (0)

Optional. If given, is displayed publicly.

Optional. If given, is not displayed anywhere.

Required with 25 characters minimum

Comments are reviewed for spam and may require approval.

No comments yet. Be the first to share your thoughts!