Profile
Back to NewsBack
GitHub Trending 11 min
Reader Mode
kushalpandya/Petrichor: An offline macOS music player

kushalpandya/Petrichor: An offline macOS music player

23 hours ago
Petrichor App Icon

Petrichor

An offline music player for macOS

Download for macOS



GitHub Downloads (all assets, all releases) GitHub Actions Workflow Status GitHub License Platform

GitHub Release



Screenshot


Summary

✨ Features

  • Everything you'd expect from an offline music player!
  • Supports over 40 audio file formats. See Supported Formats for the complete list.
  • Map your music folders and browse your library in an organized view.
  • Stream Internet Radio and organize saved stations into collections.
  • Synced lyrics of a playing track when available, including ability to download missing lyrics.
  • Create, import or export playlists.
  • Smart playlists with conditional, negative, and regular-expression rules.
  • Manage the play queue interactively using drag and drop.
  • Browse music using folder view when needed.
  • Pin _anything_ (almost!) to the sidebar for quick access to your favorite music.
  • Navigate easily: right-click a track to go to its album, artist, year, etc.
  • Native macOS integration with menubar and dock playback controls, plus dark mode support.
  • Miniplayer and immersive mode.
  • Gapless playback, Crossfade, ReplayGain volume normalization, Equalizer, Spatial Audio, and AirPlay 2 output.
  • Last.fm scrobbling support.
  • Shortcuts and Automation API support along with Siri.
  • Works well with large libraries containing thousands of songs.
💡 Tip: Petrichor relies heavily on tracks having good metadata for all its features to work well.

🎵 Supported Formats

Petrichor imports and plays the 46 file extensions below. The equivalent in-app list is generated from the playback engine itself, so it always reflects the build you are running.

| Format | Extensions | Type | | ----------------------- | --------------------------------------------------- | --------------------- | | MP3 / MPEG Audio | .mp3 .mp1 .mp2 .mpa .m1a .m2a .mpeg | Lossy | | AAC | .aac .m4a .m4r .adts .latm .loas .xhe | Lossy | | ALAC (Apple Lossless) | .alac .m4a | Lossless | | FLAC | .flac | Lossless | | Ogg Vorbis | .ogg .oga .ogx | Lossy | | Opus | .opus | Lossy | | Speex | .spx | Lossy | | WAV | .wav | Lossless | | Wave64 | .w64 | Lossless | | AIFF | .aiff .aif | Lossless | | APE (Monkey's Audio) | .ape | Lossless | | WavPack | .wv | Lossless | | TTA (True Audio) | .tta | Lossless | | TAK | .tak | Lossless | | Shorten | .shn | Lossless | | Musepack | .mpc | Lossy | | DSD | .dsf .dff | Lossless | | WMA | .wma | Lossy † | | AC-3 | .ac3 | Lossy | | E-AC-3 | .eac3 .ec3 | Lossy | | DTS | .dts | Lossy | | AMR / AMR-WB | .amr .awb | Lossy | | CAF (Core Audio Format) | .caf .caff | Container1 | | Matroska Audio | .mka | Container1 | | AU / NeXT | .au .snd | Lossless | | Sound Designer II | .sd2 | Lossless |

1 Containers hold whichever codec was used, and WMA exists in both lossy and lossless variants. Petrichor reads the actual encoding from the file rather than guessing from the extension, so the Lossless badge on a track is always accurate.

Not supported: DRM-protected files (.m4p, .aa, .aax), audiobook containers (.m4b), tracker modules (.mod, .it, .s3m, .xm), RealAudio (.ra, .ram), and MIDI (.mid, .midi).

Starting 1.7, Petrichor no longer supports or imports the tracker-module formats MOD, IT, S3M, and XM that were supported in 1.6.x.

⌛ Roadmap

  • ~~Automatic in-app updates~~ (✅ v1.0.0 )
  • ~~Better file format support (eg; Opus & OGG)~~ (✅ v.1.2.0)
  • ~~Audio Equalizer~~ (✅ v.1.2.0)
  • ~~Miniplayer and full-screen modes~~ (✅ v.1.6.0)
  • ~~Smart playlists with user-configurable conditional filters~~ (✅ v.1.6.0)
  • ~~Spatial Audio & Gapless playback~~ (✅ v.1.6.0)
  • ~~Crossfading support~~ (✅ v1.7.0)
  • ~~AirPlay 2 casting support~~ (✅ v1.7.0)
  • ~~Internet Streaming Radio~~ (✅ v1.7.0)
  • Natural language search and smart playlists using Apple Intelligence
  • Audio Enhancements via Apple Audio Units
  • Back-up and import of preferences and library data
  • ... and much more!

 Requirements

  • macOS 14 or later (Apple Silicon or Intel)

⚙️ Installation

Manually

  • Go to Releases and download the latest .dmg.
  • Open the it and drag the app icon into the Applications folder.
  • In Applications, right-click Petrichor > Open.

Homebrew

brew install --cask petrichor

🚀 Get Started

Petrichor User Guide

📷 Screenshots

Refer to official website

🔒 Privacy & Data Access

  • Petrichor is sandboxed and notarized by Apple.
  • It has two permissions on macOS as follows;
- Read-write access - To read and write into user-selected files and folders, write access is only used for exporting M3U playlist files. - Network access - To check for and install app updates. - Fetch artist images and bios from online sources (disabled by default) - Download track lyrics from the internet (disabled by default) - Stream Internet Radio stations (disabled by default) - Last.fm scrobbling (disabled by default) - When enabled, app may ask to store your Last.fm session information in macOS Keychain, if you choose to allow it, macOS will ask for your user account password to store the information in Keychain. - App does not store your Last.fm username or password, you still have to provide it on Last.fm website that opens in browser during configuration, once done, app only receives a session key to scrobble track playbacks with your account. - Reporting issues from within the app (user opt-in only) - Users can report a problem from within the app by going to Menu > Help > Report a Problem..., this opens a form where user is asked to fill out a few details, including detailed app diagnostics, along with app's log file. - This is optional feature and uses network only when user manually initiates this action and sends the information over the internet to app's backend server securely.
  • It doesn't (and never will) have any analytics on how you use the app.
  • It never changes your audio files or folder structure in any way.
  • Your library data remains offline always.

🏗️ Development

Motivation

I have a large collection of music files that I’ve gathered over the years, and I missed having a good offline music player on macOS. I've used several free and paid options but I missed the simplicity and features commonly found in streaming apps; so I built Petrichor to scratch that itch and learn Swift and macOS app development along the way!

Implementation Overview

  • Built with Swift and SwiftUI with some parts in AppKit for the best macOS integration.
  • Once folders containing music files are added, the app scans them, extracts required metadata, and populates the SQLite database.
  • The app does not alter your music files, it only reads from the directories you add.
  • Tracks searching is handled by SQLite FTS5.
  • Playback and metadata reading are handled by CrescendoKit, which uses FFmpeg and TagLib for broad format support.
View Database Schema
erDiagram
    folders {
        INTEGER id PK "AUTO_INCREMENT"
        TEXT name "NOT NULL"
        TEXT path "NOT NULL UNIQUE"
        INTEGER track_count "NOT NULL DEFAULT 0"
        DATETIME date_added "NOT NULL"
        DATETIME date_updated "NOT NULL"
        BLOB bookmark_data "Security-scoped bookmark"
        TEXT shasum_hash "Folder content hash"
    }

artists { INTEGER id PK "AUTO_INCREMENT" TEXT name "NOT NULL" TEXT normalized_name "NOT NULL UNIQUE" TEXT sort_name BLOB artwork_data TEXT bio TEXT bio_source DATETIME bio_updated_at TEXT image_url TEXT image_source DATETIME image_updated_at TEXT discogs_id TEXT musicbrainz_id TEXT spotify_id TEXT apple_music_id TEXT country INTEGER formed_year INTEGER disbanded_year TEXT genres "JSON array" TEXT websites "JSON array" TEXT members "JSON array" INTEGER total_tracks "NOT NULL DEFAULT 0 CHECK >= 0" INTEGER total_albums "NOT NULL DEFAULT 0 CHECK >= 0" DATETIME created_at "NOT NULL" DATETIME updated_at "NOT NULL" }

albums { INTEGER id PK "AUTO_INCREMENT" TEXT title "NOT NULL" TEXT normalized_title "NOT NULL" TEXT sort_title BLOB artwork_data TEXT release_date INTEGER release_year "CHECK 1900-2100" TEXT album_type INTEGER total_tracks "CHECK >= 0" INTEGER total_discs "CHECK >= 0" TEXT description TEXT review TEXT review_source TEXT cover_art_url TEXT thumbnail_url TEXT discogs_id TEXT musicbrainz_id TEXT spotify_id TEXT apple_music_id TEXT label TEXT catalog_number TEXT barcode TEXT genres "JSON array" DATETIME created_at "NOT NULL" DATETIME updated_at "NOT NULL" }

album_artists { INTEGER album_id FK "NOT NULL" INTEGER artist_id FK "NOT NULL" TEXT role "NOT NULL DEFAULT 'primary'" INTEGER position "NOT NULL DEFAULT 0" }

genres { INTEGER id PK "AUTO_INCREMENT" TEXT name "NOT NULL UNIQUE" }

tracks { INTEGER id PK "AUTO_INCREMENT" INTEGER folder_id FK "NOT NULL" INTEGER album_id FK TEXT path "NOT NULL UNIQUE" TEXT filename "NOT NULL" TEXT title TEXT artist TEXT album TEXT composer TEXT genre TEXT year REAL duration "CHECK >= 0" TEXT format INTEGER file_size DATETIME date_added "NOT NULL" DATETIME date_modified BLOB track_artwork_data BOOLEAN is_favorite "NOT NULL DEFAULT false" INTEGER play_count "NOT NULL DEFAULT 0" DATETIME last_played_date BOOLEAN is_duplicate "NOT NULL DEFAULT false" INTEGER primary_track_id FK TEXT duplicate_group_id TEXT album_artist INTEGER track_number INTEGER total_tracks INTEGER disc_number INTEGER total_discs INTEGER rating "CHECK 0-5" BOOLEAN compilation "DEFAULT false" TEXT release_date TEXT original_release_date INTEGER bpm TEXT media_type "Music/Audiobook/Podcast" INTEGER bitrate "CHECK > 0" INTEGER sample_rate INTEGER channels "1=mono, 2=stereo" TEXT codec INTEGER bit_depth TEXT sort_title TEXT sort_artist TEXT sort_album TEXT sort_album_artist TEXT extended_metadata "JSON" BOOLEAN lossless }

playlists { TEXT id PK "UUID" TEXT name "NOT NULL" TEXT type "NOT NULL (regular/smart)" BOOLEAN is_user_editable "NOT NULL" BOOLEAN is_content_editable "NOT NULL" DATETIME date_created "NOT NULL" DATETIME date_modified "NOT NULL" BLOB cover_artwork_data TEXT smart_criteria "JSON" INTEGER sort_order "NOT NULL DEFAULT 0" }

playlist_tracks { TEXT playlist_id FK "NOT NULL" INTEGER track_id FK "NOT NULL" INTEGER position "NOT NULL" DATETIME date_added "NOT NULL" }

track_artists { INTEGER track_id FK "NOT NULL" INTEGER artist_id FK "NOT NULL" TEXT role "NOT NULL DEFAULT 'artist'" INTEGER position "NOT NULL DEFAULT 0" }

track_genres { INTEGER track_id FK "NOT NULL" INTEGER genre_id FK "NOT NULL" }

pinned_items { INTEGER id PK "AUTO_INCREMENT" TEXT item_type "NOT NULL (library/playlist)" TEXT filter_type "For library items" TEXT filter_value "Artist/album name" TEXT entity_id "UUID for entities" INTEGER artist_id "Database ID" INTEGER album_id "Database ID" TEXT playlist_id "For playlist items" TEXT display_name "NOT NULL" TEXT subtitle "For albums" INTEGER sort_order "NOT NULL DEFAULT 0" DATETIME date_added "NOT NULL" }

artist_aliases { TEXT normalized_alias PK TEXT display_name "NOT NULL" INTEGER canonical_artist_id FK "NOT NULL" DATETIME created_at "NOT NULL" }

album_aliases { TEXT normalized_key PK TEXT display_title "NOT NULL" INTEGER canonical_album_id FK "NOT NULL" DATETIME created_at "NOT NULL" }

background_migrations { TEXT identifier PK DATETIME completed_at TEXT progress BOOLEAN resumable "DEFAULT true" }

tracks_fts { INTEGER track_id "NOT INDEXED" TEXT title TEXT artist TEXT album TEXT album_artist TEXT composer TEXT genre TEXT year }

folders ||--o{ tracks : contains albums ||--o{ album_artists : "has artists" artists ||--o{ album_artists : "appears on" albums ||--o{ tracks : contains artists ||--o{ track_artists : "appears in" tracks ||--o{ track_artists : "has artists" tracks ||--o{ tracks : "duplicate of" genres ||--o{ track_genres : "categorizes" tracks ||--o{ track_genres : "has genres" playlists ||--o{ playlist_tracks : contains tracks ||--o{ playlist_tracks : "appears in" tracks ||--|| tracks_fts : "searchable in" artists ||--o{ artist_aliases : "merged into" albums ||--o{ album_aliases : "merged into"

Credits

Petrichor wouldn't be possible without following open source projects!

Development Setup

  • Make sure you’re running macOS 14 or later.
  • Install Xcode.
  • Clone the repository and open Petrichor.xcodeproj

Build & Release

You can build your own .dmg installer using the build-installer.sh script, although it requires you to have a paid Apple Developer account to notarize the compiled binary and installer, you can use --bypass-notary option if you don't want to notarize. To use the script, make sure you have following tools installed along with Xcode;

💖 Sponsors

Thank you to all the sponsors for supporting Petrichor's development!

Sponsors

📝 License

  • Petrichor is licensed under MIT
  • GRDB.swift and Sparkle are licensed under MIT.
  • The playback engine and metadata reader come from CrescendoKit (proprietary, binary distribution), which dynamically links FFmpeg (LGPL-2.1-or-later) and statically embeds TagLib (MPL-1.1).
For complete third-party license information, see ACKNOWLEDGEMENTS.md
Chat with me