You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/docs/superpowers/specs/2026-05-23-music-player-des...

7.6 KiB

Music Player — Design Spec

A high-performance macOS music player built with SwiftUI + AppKit. Reads a local music collection from disk, indexes metadata into SQLite for fast search/sort/filter, and plays audio via AVPlayer.

Constraints

  • Library size: 10,000+ tracks, 100GB+, growing
  • All search, sort, and filter operations must feel instant (<50ms)
  • macOS 15.6+ (current project target)
  • No external music service integration — local files only

Data Model

SQLite database via GRDB, stored at ~/Library/Application Support/Music/db.sqlite.

tracks table

Column Type Notes
id INTEGER PRIMARY KEY Auto-increment
fileURL TEXT, UNIQUE Absolute path to audio file
title TEXT From metadata, fallback to filename
artist TEXT
albumArtist TEXT
album TEXT
genre TEXT
year INTEGER
trackNumber INTEGER
discNumber INTEGER
duration DOUBLE Seconds
bpm INTEGER
composer TEXT
fileFormat TEXT mp3, m4a, flac, etc.
bitrate INTEGER kbps
sampleRate INTEGER Hz
fileSize INTEGER Bytes
artworkData BLOB, nullable Embedded cover art
playCount INTEGER DEFAULT 0 Tracked by app
lastPlayedAt DATETIME, nullable Tracked by app
rating INTEGER DEFAULT 0 0–5, tracked by app
dateAdded DATETIME When imported
dateModified DATETIME File modification date
fileHash TEXT fileSize + modificationDate for change detection

Indexes

  • Individual: artist, album, genre, year
  • Compound: (albumArtist, album, discNumber, trackNumber) for album browsing order

FTS5 virtual table

  • Indexed columns: title, artist, albumArtist, album, genre, composer
  • Kept in sync via GRDB FTS5 triggers

Architecture

Four layers, strict downward dependency only.

UI Layer

  • NSTableView wrapped in NSViewRepresentable for the track list
  • SwiftUI for search bar and player controls
  • No business logic in views

ViewModel Layer

LibraryViewModel (ObservableObject):

  • Holds current query state: search text, sort column, sort order
  • Subscribes to GRDB ValueObservation — receives [Track] on every query change
  • Exposes results to the NSTableView data source

PlayerViewModel (ObservableObject):

  • Current track, playback state (playing/paused/stopped), current time, duration
  • Queue management: current queue (array of tracks), current index
  • Shuffle state and shuffled queue

Service Layer

DatabaseService:

  • Owns the GRDB DatabasePool
  • Schema migrations
  • Query builders for search (FTS5 MATCH), sort (ORDER BY indexed columns), filter

ScannerService:

  • Background folder walking with FileManager
  • Metadata extraction via AVAsset.metadata (async)
  • Artwork extraction from embedded tags
  • Incremental rescan via fileHash comparison
  • Progress reporting

AudioService:

  • AVPlayer wrapper
  • Play, pause, stop, seek, next, previous, volume
  • Periodic time observation for progress bar
  • Updates playCount/lastPlayedAt in DB when track finishes or passes 50% played

Data Layer

  • SQLite + FTS5 via GRDB
  • Database file at ~/Library/Application Support/Music/db.sqlite

Data Flows

Scan: User picks folder → ScannerService walks directory on background thread → extracts metadata with AVAsset → inserts into DB → ValueObservation fires → LibraryViewModel updates → NSTableView reloads

Search: User types in search bar → LibraryViewModel updates query string → new SQL query with FTS5 MATCH → ValueObservation emits new results → table reloads

Sort: User clicks column header → LibraryViewModel updates sort column/order → new SQL ORDER BY → same reactive flow

Play: User double-clicks row → PlayerViewModel receives track → AudioService loads file URL → playback starts → controls update

UI Layout

Single window, three vertical zones.

Zone 1: Search Bar (top)

  • Full-width text field with placeholder: "Search by title, artist, album, genre..."
  • Track count label on the right (e.g., "10,342 tracks")
  • SwiftUI TextField

Zone 2: Track List (middle, fills available space)

  • NSTableView via NSViewRepresentable
  • Columns: Title, Artist, Album, Genre, Duration
  • Sortable by clicking column headers (ascending/descending toggle)
  • Currently playing track: blue left accent bar + play indicator in title cell
  • Alternating row colors (white / light gray) for readability
  • Double-click to play, single-click to select
  • Light theme: white backgrounds, Apple system gray text hierarchy

Zone 3: Player Controls (bottom)

Three sections in a horizontal grid:

Left — Now Playing:

  • Album artwork thumbnail (44×44, rounded corners)
  • Track title (bold) + "Artist — Album" subtitle

Center — Transport:

  • Shuffle toggle (dimmed when off, highlighted when on)
  • Previous, Play/Pause, Next buttons
  • Progress bar with current time / total time

Right — Volume:

  • Speaker icon + volume slider

Scanner & Import

Folder scanning (primary)

  • User selects root folder via NSOpenPanel
  • Recursive directory walk on background thread
  • Supported extensions: .mp3, .m4a, .aac, .wav, .aiff, .alac, .flac
  • Metadata extraction via AVAsset.metadata
  • Artwork from embedded tags → stored as BLOB
  • fileHash = file size + modification date (fast, sufficient for change detection)
  • Progress reported: "Scanning... 4,231 / 10,342 tracks"
  • Watched folder path stored in UserDefaults

Drag and drop (secondary)

  • Files/folders dropped onto the window
  • Folders recursively scanned same as above
  • Duplicates detected by fileURL uniqueness constraint — silently skipped

Incremental rescan

  • Triggered on app launch or manually
  • New files → insert
  • Changed files (different fileHash) → re-extract metadata, update row
  • Missing files → remove from DB

Playback & Queue

Queue model

  • Current track list (full library or filtered/searched results) acts as the play queue
  • Double-click a track → starts playing from that position in the current queue
  • Next/Previous navigate within the queue
  • Searching/filtering while playing does not interrupt playback — queue only changes on explicit play action

Shuffle

  • Toggle on → build shuffled copy of current queue, starting from current track
  • Toggle off → return to original queue position
  • New search/filter while shuffle is on → re-shuffle new result set

Play tracking

  • playCount incremented and lastPlayedAt updated when a track finishes or passes 50% played

Not in v1

  • Repeat modes (repeat-one, repeat-all)
  • Crossfade
  • Equalizer
  • Playlists

Error Handling

  • Unreadable audio file (corrupt, DRM, unsupported) → skip during scan, log to console
  • File moved/deleted after import → AVPlayer fails to load → inline error in player controls ("File not found"), auto-advance to next track
  • Metadata extraction fails → use filename as title, "Unknown" for artist/album, still import
  • Folder permission denied → system alert, prompt to grant access in System Settings
  • Database migration failure → surface error, do not launch with broken schema

Dependencies

  • GRDB (Swift Package) — SQLite wrapper with reactive observation
  • AVFoundation (system) — audio playback and metadata extraction
  • AppKit (system) — NSTableView for high-performance track list
  • SwiftUI (system) — search bar, player controls, app structure

Audio Format Support

Natively handled by AVFoundation on macOS:

  • MP3, AAC/M4A, WAV, AIFF, ALAC, FLAC