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
NSTableViewwrapped inNSViewRepresentablefor 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
fileHashcomparison - Progress reporting
AudioService:
AVPlayerwrapper- Play, pause, stop, seek, next, previous, volume
- Periodic time observation for progress bar
- Updates
playCount/lastPlayedAtin 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)
NSTableViewviaNSViewRepresentable- 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
fileURLuniqueness 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
playCountincremented andlastPlayedAtupdated 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