Free, open source music streaming for Android.
Stream any song, build your library, take it offline.
Download · Website · Features · Build from source · Report a bug · Support
- Overview
- Features
- Screenshots
- Installation
- How It Works
- Architecture
- Tech Stack
- Project Structure
- Building from Source
- Permissions and Privacy
- Troubleshooting
- FAQ
- Contributing
- Support
- Acknowledgements
- Disclaimer
- License
gamut is a complete music app for Android: search and play songs, build playlists, follow artists, download for offline listening, and keep everything in sync across devices. It has no subscription, no ads and no paywalled features, and it works without an account.
It is built entirely with Kotlin and Jetpack Compose, split into a modular Gradle project, with an optional Supabase backend that handles sync.
Typical workflow:
- Open Discover and browse charts, or search for any artist, song or album.
- Tap a result to start playback, then shape the sound with Mods and the equalizer.
- Like songs, add them to playlists with mood tags, and follow artists.
- Download what you want for offline playback.
- Hear something out loud? Tap the Shazam button to identify it.
No account required. Search, stream and build a library without signing up. Sign in with Google only if you want your favorites, playlists and followed artists to sync across devices.
| Area | What it does |
|---|---|
| Discover | Opens on albums on the rise, with Trending, Billboard, Latest and Albums charts. Search artists, songs and albums, with recent searches and quick-resume tiles |
| Library | Liked songs, playlists with mood tags, followed artists, offline downloads, plus the music already stored on your phone |
| Player | Colors drawn from the artwork, synced lyrics that follow the song line by line, queue, sleep timer, crossfade, and playback controls on the notification shade and lock screen |
| Mods | Nightcore, Daycore, Slowed + Reverb and Lo-Fi presets, or dial speed, pitch and bass yourself. Pitch can follow speed (tape style) or stay in key |
| Equalizer | 10-band parametric EQ from 60 Hz to 14 kHz, with presets, adjustable frequency and Q, and automatic headroom that lowers the level to match |
| Song recognition | Built-in Shazam button. Hold your phone near the music and gamut tells you what is playing |
| Offline | Download songs once and play them with no connection, individually or by playlist |
| Sync | Optional Google sign-in syncs favorites, playlists and followed artists to a new phone |
| Sharing | Send a song as a card image with its link, or just the URL, in the color you like |
| Everywhere | Android Auto browsing, notification and lock-screen controls, phones and tablets |
| Appearance | Light and dark themes throughout |
| Discover | Library | Favorites | Song recognition |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Every screen ships with matching dark and light themes. Library and Favorites pick up their background colors from the artwork that is playing.
gamut runs on Android 8.0 (API 26) and newer, on phones and tablets.
- Open the latest release and download
.apk. - Open the file on your phone. If Android asks, allow installs from your browser for this one time.
- Launch gamut. No account or setup is needed.
Updates are built in. gamut checks for new releases itself and downloads them in the background.
Note: Official release builds target
arm64-v8aandarmeabi-v7a, which covers virtually all Android phones, 64-bit and 32-bit. Build from source if you need another ABI.
gamut does not host any music. When you play a track, it finds a matching source on YouTube Music and streams it, caching the result so repeat plays and the next song in the queue start quickly.
flowchart LR
A([Track selected]) --> B["Search YouTube Music<br/>songs first, then broader"]
B --> C{"Score candidates<br/>title, artist, duration"}
C -->|best match above confidence floor| D["Resolve stream URL"]
C -->|nothing good enough| X([Playback error shown])
D --> E["NewPipe Extractor"]
E -->|timeout or failure| F["yt-dlp fallback"]
E --> G[("Cached stream URL")]
F --> G
G --> H["Media3 player<br/>Mod and EQ audio processors"]
- Matching. Candidates are ranked by how well their title and channel match the track, with a bonus for official catalog and "Topic" uploads and penalties for stray words and derivative uploads. The track's duration is used as a sanity check.
- Resolving. NewPipe Extractor is tried first. If it times out or fails, gamut falls back to an embedded yt-dlp, which updates itself in the background.
- Prefetching. Upcoming tracks are resolved ahead of time on suitable connections, and resolved URLs are cached until they expire.
- Metadata. Artwork, artists and album details are enriched from public catalogs through a small Supabase edge function (
resolve-metadata), with an allow-list on artwork hosts. - Staying alive. Streaming sources change often. A scheduled workflow tests new NewPipe Extractor and yt-dlp releases every six hours and proposes version bumps, and the app can prompt you to update when a fix ships.
gamut is a multi-module Gradle project. Feature modules depend on core modules, never on each other, and :app wires everything together with Hilt.
| Module | Responsibility |
|---|---|
:app |
Entry points, navigation, profile and account screens, in-app updater, Shazam listening service |
:feature:feature-search |
Discover, search, artist and album screens, charts, song recognition |
:feature:feature-library |
Favorites, playlists and mood tags, downloads UI, on-device music |
:feature:feature-player-ui |
Player screen, lyrics, queue, equalizer, Mod sheet, sleep timer, sharing |
:feature:feature-resolver |
Track-to-stream resolution, candidate scoring, prefetching, download manager, yt-dlp updater |
:core:core-player |
Media3 player, media session service, Mod and sound-engine audio processors, equalizer, Android Auto browse tree |
:core:core-database |
Room database, DAOs, download index, recently played |
:core:core-network |
HTTP clients for search, catalogs and recognition |
:core:core-sync |
Supabase client, Google sign-in, sync of favorites, playlists and follows |
:core:core-model / core-common |
Shared models and utilities |
:core:core-designsystem |
Theme, typography, shared Compose components |
:baselineprofile |
Baseline and startup profile generation |
:tools:extractor-smoke |
Smoke test used to validate extractor upgrades |
Android app
| Concern | Choice |
|---|---|
| Language and UI | Kotlin 2.0, Jetpack Compose, Material 3 |
| Dependency injection | Hilt (with KSP) |
| Playback | AndroidX Media3, custom audio processors for Mods and EQ |
| Persistence | Room |
| Networking | Retrofit, OkHttp, Ktor, kotlinx.serialization |
| Streaming sources | NewPipe Extractor, youtubedl-android (yt-dlp) |
| Images and color | Coil, AndroidX Palette, Haze (blur) |
| Background work | WorkManager |
| Sync and auth | Supabase (Kotlin client), Credential Manager with Google ID |
| Performance | Baseline Profiles, Profile Installer |
Backend (optional, for sync): Supabase Postgres with row level security, and two Deno edge functions.
Tooling: Gradle Kotlin DSL with a version catalog, GitHub Actions for CI, releases, database backups and dependency bumps.
.github/workflows/ ci, release, backup, keepalive, extractor-bump
app/ Application module (entry points, update flow, accounts)
baselineprofile/ Baseline profile generator
core/
├── core-common/ Shared utilities
├── core-database/ Room entities, DAOs, download index
├── core-designsystem/ Theme and shared Compose UI
├── core-model/ Domain models
├── core-network/ API clients (search, catalogs, recognition)
├── core-player/ Media3 player, audio processors, equalizer, media session
└── core-sync/ Supabase client, auth, sync
feature/
├── feature-library/ Favorites, playlists, downloads, local music
├── feature-player-ui/ Player, lyrics, queue, EQ, Mods, sharing
├── feature-resolver/ Source resolution, scoring, downloads
└── feature-search/ Discover, search, artist and album pages
gamut-brandkit/ Logos and Android drawables
docs/ Project website (GitHub Pages) and screenshots
scripts/ bump_extractors.py
supabase/
├── functions/ resolve-metadata, delete-account
├── migrations/ SQL schema
└── tests/ Row level security tests
tools/extractor-smoke/ Extractor smoke test
%% Generated by https://gitdiagram.com/serptail/gamut
flowchart TD
subgraph group_app["App and experience"]
node_appentry["App entry<br/>[MainActivity.kt]"]
end
subgraph group_discovery["Discovery and recognition"]
node_discover["Discover feed"]
node_search["Track search"]
node_catalogs["Music catalogs<br/>[DeezerChartsApi.kt]"]
node_recognition["Song recognition<br/>[ShazamSession.kt]"]
node_shazamservice["Recognition service"]
node_shazamclient["Shazam client<br/>[ShazamClient.kt]"]
end
subgraph group_playback["Playback and offline"]
node_tracksource["Source resolution"]
node_youtube["YouTube extractor"]
node_playerui["Player interface<br/>[PlayerScreen.kt]"]
node_musicplayer["Music player"]
node_session["Media session"]
node_audioprocessing["Audio effects"]
node_downloads["Offline downloads"]
node_downloadservice["Download service<br/>[DownloadService.kt]"]
node_storage["Storage settings<br/>[StorageSettings.kt]"]
end
subgraph group_data["Library and sync"]
node_library["Library and playlists"]
node_localmusic["Local music"]
node_database[("App database<br/>[GamutDatabase.kt]")]
node_downloadindex["Download index<br/>[DownloadIndex.kt]"]
node_sync["Cloud sync<br/>[SyncEngine.kt]"]
node_auth["Account sign-in<br/>[AuthRepository.kt]"]
end
node_listener(("Listener"))
node_supabase[("Supabase")]
node_listener -->|"opens app"| node_appentry
node_appentry -->|"browses"| node_discover
node_appentry -->|"searches"| node_search
node_discover -->|"loads charts"| node_catalogs
node_search -->|"searches tracks"| node_youtube
node_listener -->|"identifies music"| node_recognition
node_recognition -->|"uses service"| node_shazamservice
node_shazamclient -.->|"supports sessions"| node_recognition
node_listener -->|"selects track"| node_playerui
node_playerui -.->|"resolves track"| node_tracksource
node_tracksource -.->|"resolves stream"| node_youtube
node_youtube -.->|"provides stream"| node_musicplayer
node_musicplayer -.->|"processes audio"| node_audioprocessing
node_session -->|"controls playback"| node_musicplayer
node_listener -->|"builds library"| node_library
node_library -.->|"stores library"| node_database
node_listener -->|"browses device music"| node_localmusic
node_listener -->|"downloads tracks"| node_downloads
node_downloads -->|"resolves download"| node_tracksource
node_downloads -->|"records downloads"| node_downloadindex
node_downloadservice -->|"observes batch"| node_downloads
node_downloadindex -->|"reads database"| node_database
node_listener -->|"signs in optionally"| node_auth
node_auth -.->|"authenticates sync"| node_sync
node_sync -->|"syncs library"| node_supabase
node_sync -->|"syncs local data"| node_database
node_listener -->|"sets download options"| node_storage
click node_appentry "https://github.com/serptail/gamut/blob/main/app/src/main/kotlin/com/gamut/MainActivity.kt"
click node_discover "https://github.com/serptail/gamut/blob/main/feature/feature-search/src/main/kotlin/com/gamut/feature/search/DiscoverRepository.kt"
click node_search "https://github.com/serptail/gamut/blob/main/feature/feature-search/src/main/kotlin/com/gamut/feature/search/SearchRepository.kt"
click node_catalogs "https://github.com/serptail/gamut/blob/main/core/core-network/src/main/kotlin/com/gamut/core/network/DeezerChartsApi.kt"
click node_recognition "https://github.com/serptail/gamut/blob/main/feature/feature-search/src/main/kotlin/com/gamut/feature/search/ShazamSession.kt"
click node_shazamservice "https://github.com/serptail/gamut/blob/main/app/src/main/kotlin/com/gamut/ShazamListenService.kt"
click node_shazamclient "https://github.com/serptail/gamut/blob/main/core/core-network/src/main/kotlin/com/gamut/core/network/ShazamClient.kt"
click node_tracksource "https://github.com/serptail/gamut/blob/main/feature/feature-resolver/src/main/kotlin/com/gamut/feature/resolver/ResolveTrackSourceUseCase.kt"
click node_youtube "https://github.com/serptail/gamut/blob/main/feature/feature-resolver/src/main/kotlin/com/gamut/feature/resolver/NewPipeYoutubeSource.kt"
click node_playerui "https://github.com/serptail/gamut/blob/main/feature/feature-player-ui/src/main/kotlin/com/gamut/feature/playerui/PlayerScreen.kt"
click node_musicplayer "https://github.com/serptail/gamut/blob/main/core/core-player/src/main/kotlin/com/gamut/core/player/Media3MusicPlayer.kt"
click node_session "https://github.com/serptail/gamut/blob/main/core/core-player/src/main/kotlin/com/gamut/core/player/GamutMediaSessionService.kt"
click node_audioprocessing "https://github.com/serptail/gamut/blob/main/core/core-player/src/main/kotlin/com/gamut/core/player/SoundEngineProcessor.kt"
click node_downloads "https://github.com/serptail/gamut/blob/main/feature/feature-resolver/src/main/kotlin/com/gamut/feature/resolver/TrackDownloadManager.kt"
click node_downloadservice "https://github.com/serptail/gamut/blob/main/feature/feature-resolver/src/main/kotlin/com/gamut/feature/resolver/DownloadService.kt"
click node_library "https://github.com/serptail/gamut/blob/main/feature/feature-library/src/main/kotlin/com/gamut/feature/library/LibraryRepository.kt"
click node_localmusic "https://github.com/serptail/gamut/blob/main/feature/feature-library/src/main/kotlin/com/gamut/feature/library/LocalMusicScreen.kt"
click node_database "https://github.com/serptail/gamut/blob/main/core/core-database/src/main/kotlin/com/gamut/core/database/GamutDatabase.kt"
click node_downloadindex "https://github.com/serptail/gamut/blob/main/core/core-database/src/main/kotlin/com/gamut/core/database/DownloadIndex.kt"
click node_sync "https://github.com/serptail/gamut/blob/main/core/core-sync/src/main/kotlin/com/gamut/core/sync/SyncEngine.kt"
click node_auth "https://github.com/serptail/gamut/blob/main/core/core-sync/src/main/kotlin/com/gamut/core/sync/AuthRepository.kt"
click node_storage "https://github.com/serptail/gamut/blob/main/feature/feature-resolver/src/main/kotlin/com/gamut/feature/resolver/StorageSettings.kt"
classDef toneNeutral fill:#f8fafc,stroke:#334155,stroke-width:1.5px,color:#0f172a
classDef toneBlue fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#172554
classDef toneAmber fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef toneMint fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef toneRose fill:#ffe4e6,stroke:#e11d48,stroke-width:1.5px,color:#881337
classDef toneIndigo fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
classDef toneTeal fill:#ccfbf1,stroke:#0f766e,stroke-width:1.5px,color:#134e4a
class node_appentry toneBlue
class node_discover,node_search,node_catalogs,node_recognition,node_shazamservice,node_shazamclient,node_supabase toneAmber
class node_tracksource,node_youtube,node_playerui,node_musicplayer,node_session,node_audioprocessing,node_downloads,node_downloadservice,node_storage toneMint
class node_library,node_localmusic,node_database,node_downloadindex,node_sync,node_auth toneRose
class node_listener toneIndigo
- JDK 21
- Android SDK with platform 35
- Android Studio (current stable) or the command line
git clone https://github.com/serptail/gamut.git
cd gamut
./gradlew :app:assembleDebugThe APK is written to app/build/outputs/apk/debug/.
Run the unit tests:
./gradlew testDebugUnitTestSync is off by default in a fresh checkout. Everything else works without it. To enable Google sign-in and sync, create a local.properties file in the project root:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
GOOGLE_WEB_CLIENT_ID=your-google-web-client-id.apps.googleusercontent.comWhen SUPABASE_URL or SUPABASE_ANON_KEY is blank, the sync layer disables itself. local.properties is git-ignored.
| Permission | Why it is needed |
|---|---|
INTERNET |
Search, streaming, lyrics and catalog data |
RECORD_AUDIO |
Only while you run song recognition |
READ_MEDIA_AUDIO |
Playing music files already on your phone |
| Foreground services (media playback, data sync, microphone) | Background playback, downloads and recognition |
With sync, gamut stores only your favorite songs, playlists and the artists you follow, and nothing else. Read the full privacy policy.
| Symptom | Fix |
|---|---|
| "Songs can't be loaded right now" | The music source has probably changed. Check for an update inside the app or on the releases page, and try again later |
| A track starts slowly or fails once | Retry. Stream URLs expire, and gamut refreshes and re-resolves them automatically |
| Android blocks the install | Allow installs from your browser (or file manager) for this one time in system settings |
| Song recognition does nothing | Grant the microphone permission and hold the phone closer to the audio source |
| Local files do not appear | Grant the audio permission, then open Library → On this device |
| No Google sign-in in a self-built app | Sync needs the local.properties keys described in Optional: enable sync |
Still stuck? Open an issue with what happened and which phone you use.
Is gamut really free?
Yes. Streaming, downloads and every feature are free. gamut is open source under the GNU AGPLv3, so you can read the code, change it and share it, as long as your changes stay open under the same license.
Do I need an account?
No. Sign in with Google only if you want your favorites, playlists and followed artists to sync across devices.
Where does the music come from?
gamut searches and streams from YouTube Music, and also plays the files already on your phone, so everything lives in one library.
Does it work offline?
Yes. Songs you download in gamut play with no connection, and the files on your phone always play.
Is there an iPhone or desktop version?
No. gamut is Android only, with support for phones, tablets and Android Auto.
Bug reports, ideas and pull requests are all welcome.
-
Open an issue first for anything non-trivial, so the approach can be agreed on.
-
Fork the repository and create a branch.
-
Follow the official Kotlin code style (
kotlin.code.style=official) and the existing module boundaries. -
Make sure the same checks CI runs pass locally:
./gradlew :app:assembleDebug testDebugUnitTest supabase test db # if you touched supabase/
-
Open a pull request describing what changed and why.
When reporting a bug, include your Android version, device model and the steps that trigger it.
gamut is free and built in the open. If you enjoy it, any of these helps keep it that way:
- Star the repository so others can find it
- Donate on Ko-fi
- Sponsor on GitHub
- Tell a friend
gamut stands on the shoulders of excellent open source projects and public services:
- NewPipe Extractor and yt-dlp (via youtubedl-android) for stream resolution
- AndroidX Media3, Jetpack Compose, Room and Hilt
- Coil and Haze
- Phosphor Icons by Helena Zhang and Tobias Fried for the player and interface icons, through the PhosphorIcon-compose Compose library by @dev778g-me
- Paper Shaders for Android by @AndreFrelicot, a Jetpack Compose and AGSL port of the Paper Shaders effects by Paper Design, for the animated Mercury, Metaballs and Dithering backgrounds
- Amplituda by @lincollincol for audio waveform extraction
- Supabase for the optional sync backend
- LRCLIB for synced lyrics
- billboard-hot-100 by @mhollingshead for the weekly Billboard Hot 100 chart data
- MusicBrainz, the Cover Art Archive, Deezer and iTunes for catalog metadata and artwork
gamut is an independent project and is not affiliated with, endorsed by or sponsored by YouTube, Google, Shazam, Apple, Deezer, Billboard or any other third party mentioned here. All trademarks and album artwork belong to their respective owners. gamut does not host or distribute any audio. You are responsible for using it in accordance with the terms of the services it connects to and the laws where you live.
gamut is licensed under the GNU Affero General Public License v3.0. See LICENSE for the full text.
You are free to use, study, modify and share it, provided that derivative works, including those offered over a network, are released under the same license.
Made with love by @serptail
The gamut name, wordmark and ribbon mark in gamut-brandkit/ identify the project.



