Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Theme
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Create API Key
Add Docs
Gramophone
https://github.com/foedusprogramme/gramophone
Admin
Gramophone is a minimalist Android music player built with Material Design 3, featuring synced
...
Tokens:
5,967
Snippets:
44
Trust Score:
6.8
Update:
3 months ago
Context
Skills
Chat
Benchmark
71.7
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Gramophone Gramophone is a modern, open-source music player application for Android, developed by the Akane Foundation. It leverages ExoPlayer (Media3) for high-quality audio playback with support for various audio formats including FLAC, ALAC, MP3, OGG, AAC, WAV, and MIDI. The application provides a Material Design 3 interface with features like lyrics display (LRC, TTML, SRT), ReplayGain support, shuffle modes, and seamless integration with Android's MediaStore for library management. The app is built using Kotlin with Jetpack Compose for some UI components, and follows modern Android architecture patterns with ViewModels, Flows, and coroutines for reactive data handling. Key features include USB Audio Class (UAC) support through the hificore module, playlist management, folder-based browsing, Bluetooth codec information display, and support for external equalizers. The minimum supported Android version is 5.0 (API 21), with the target SDK being Android 15 (API 35). ## Core Components ### GramophoneApplication - Application Entry Point The `GramophoneApplication` class initializes the app, sets up the media library reader, configures Coil for image loading (album art), and manages theme settings. ```kotlin // Access the application instance from any Activity val app = (context.applicationContext as GramophoneApplication) // Access the FlowReader for music library data val reader = app.reader // Refresh the music library CoroutineScope(Dispatchers.Default).launch { reader.refresh() } // Get the list of all songs as a Flow reader.songListFlow.collect { songs -> // songs: List<MediaItem> songs.forEach { song -> println("Title: ${song.mediaMetadata.title}") println("Artist: ${song.mediaMetadata.artist}") println("Album: ${song.mediaMetadata.albumTitle}") println("Duration: ${song.mediaMetadata.durationMs}ms") } } ``` ### FlowReader - Media Library Access `FlowReader` provides reactive access to the device's music library through Kotlin Flows. It reads data from Android's MediaStore and organizes it into songs, albums, artists, genres, dates, and playlists. ```kotlin // Create a FlowReader instance (typically done in Application class) val reader = FlowReader( context, minSongLengthSecondsFlow, // Filter short audio files blackListSetFlow, // Excluded folder paths shouldUseEnhancedCoverReadingFlow, // Enhanced album art loading recentlyAddedFilterSecondFlow, // Recently added threshold shouldIncludeExtraFormatFlow, // Include WAV, OGG, AAC, MIDI coverStubUri = "gramophoneAlbumCover" ) // Collect various library data CoroutineScope(Dispatchers.IO).launch { // Get all albums reader.albumListFlow.collect { albums -> albums.forEach { album -> println("Album: ${album.title} by ${album.albumArtist}") println("Year: ${album.albumYear}, Songs: ${album.songList.size}") } } } // Get all artists reader.artistListFlow.collect { artists -> artists.forEach { artist -> println("Artist: ${artist.title}, Albums: ${artist.albumList.size}") } } // Get playlists (includes favorites and recently added) reader.playlistListFlow.collect { playlists -> playlists.forEach { playlist -> println("Playlist: ${playlist.title}, ${playlist.songList.size} songs") } } // Get folder structure for browsing reader.folderStructureFlow.collect { rootNode -> // FileNode tree structure representing the file system } // Force library refresh reader.refresh() ``` ### GramophonePlaybackService - Media Playback Service `GramophonePlaybackService` is a `MediaLibraryService` that handles all audio playback using ExoPlayer. It supports media session controls, sleep timer, lyrics synchronization, ReplayGain, and audio format tracking. ```kotlin // Custom session commands available const val SERVICE_SET_TIMER = "set_timer" const val SERVICE_QUERY_TIMER = "query_timer" const val SERVICE_GET_AUDIO_FORMAT = "get_audio_format" const val SERVICE_GET_LYRICS = "get_lyrics" // Connect to the service from an Activity using MediaController val controllerViewModel: MediaControllerViewModel by viewModels() controllerViewModel.addControllerCallback(lifecycle) { controller, _ -> // Basic playback controls controller.play() controller.pause() controller.seekTo(positionMs) controller.seekToNextMediaItem() controller.seekToPreviousMediaItem() // Set shuffle and repeat modes controller.shuffleModeEnabled = true controller.repeatMode = Player.REPEAT_MODE_ALL // Set a playlist controller.setMediaItems(listOf(mediaItem1, mediaItem2)) controller.prepare() controller.play() // Get current playback state val currentSong = controller.currentMediaItem val position = controller.currentPosition val duration = controller.duration val isPlaying = controller.isPlaying } // Set sleep timer via custom command val timerCommand = SessionCommand(SERVICE_SET_TIMER, Bundle.EMPTY) timerCommand.customExtras.putInt("duration", 30 * 60 * 1000) // 30 minutes timerCommand.customExtras.putBoolean("pauseOnEnd", false) controller.sendCustomCommand(timerCommand, Bundle.EMPTY) // Query audio format information val formatCommand = SessionCommand(SERVICE_GET_AUDIO_FORMAT, Bundle.EMPTY) controller.sendCustomCommand(formatCommand, Bundle.EMPTY).addListener({ val result = it.get() val fileFormat = result.extras.getParcelableArrayList<Bundle>("file_format") val sinkFormat = result.extras.getBundle("sink_format") val trackFormat = result.extras.getParcelable<AudioTrackInfo>("track_format") }, MoreExecutors.directExecutor()) ``` ### ItemManipulator - Playlist and Media Management `ItemManipulator` provides static methods for creating, modifying, and deleting playlists and songs, handling Android's scoped storage permissions automatically. ```kotlin // Create a new playlist val playlistFile = ItemManipulator.createPlaylist(context, "My Playlist") // Creates: /Music/My Playlist.m3u // Add songs to a playlist val songs = listOf(File("/storage/emulated/0/Music/song1.mp3")) ItemManipulator.addToPlaylist(context, playlistFile, songs) // Replace playlist contents ItemManipulator.setPlaylistContent(context, playlistFile, newSongList) // Rename a playlist ItemManipulator.renamePlaylist(context, playlistFile, "New Name") // Delete a song (returns MediaStoreRequest for permission handling) val request = ItemManipulator.deleteSong(context, songFile, songId) if (request.startSystemDialog != null) { // Launch system permission dialog for Android 11+ intentSender.launch(IntentSenderRequest.Builder(request.startSystemDialog).build()) } else { // Execute directly on older Android or owned files request.continueAction?.invoke() } // Delete a playlist val deleteRequest = ItemManipulator.deletePlaylist(context, playlistId) // Set favorite status (Android 11+) val uris = setOf(ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId )) val intentSender = ItemManipulator.setFavorite(context, uris, true) if (intentSender != null) { // Launch permission dialog } // Check if write permission is needed val needsPermission = ItemManipulator.needRequestWrite(context, uri) ``` ### LrcUtils - Lyrics Parsing `LrcUtils` handles parsing and loading of lyrics from various formats including LRC (simple and extended), TTML (Apple Music), SRT (SubRip), and embedded ID3 tags. ```kotlin // Load lyrics from a sidecar file (looks for .lrc, .ttml, .srt) val options = LrcUtils.LrcParserOptions( trim = true, // Trim whitespace from lines multiLine = false, // Enable multiline mode errorText = "Failed to parse lyrics" ) val lyrics = LrcUtils.loadAndParseLyricsFile( musicFile = File("/path/to/song.mp3"), audioMimeType = "audio/mpeg", parserOptions = options ) when (lyrics) { is SemanticLyrics.SyncedLyrics -> { // Synchronized lyrics with timestamps lyrics.text.forEach { line -> println("[${line.start}ms - ${line.end}ms] ${line.text}") line.words?.forEach { word -> println(" Word: ${line.text.substring(word.charRange)} at ${word.begin}ms") } if (line.isTranslated) println(" (translation)") line.speaker?.let { println(" Speaker: $it") } } } is SemanticLyrics.UnsyncedLyrics -> { // Plain text lyrics without timing lyrics.unsyncedText.forEach { (text, speaker) -> println(text) } } null -> println("No lyrics found") } // Extract lyrics from audio file metadata (ID3 USLT/SYLT tags) val metadata: Metadata = // from ExoPlayer track LrcUtils.extractAndParseLyrics( sampleRate = 44100, audioMimeType = "audio/mpeg", metadata = metadata, parserOptions = options ).firstOrNull()?.let { lyrics -> // Process lyrics } // Parse LRC format directly val lrcContent = """ [00:12.00]First line of the song [00:17.20]<00:17.20>Word <00:18.40>by <00:19.00>word [00:24.00]Third line """ val parsed = parseLrc(lrcContent, trimEnabled = true, multiLineEnabled = false) ``` ### Data Models #### MediaItem (from Media3) Songs are represented as `MediaItem` objects from AndroidX Media3 with rich metadata: ```kotlin // Building a MediaItem val mediaItem = MediaItem.Builder() .setUri("file:///path/to/song.mp3") .setMediaId("MediaStore:12345") .setMimeType("audio/mpeg") .setMediaMetadata( MediaMetadata.Builder() .setTitle("Song Title") .setArtist("Artist Name") .setAlbumTitle("Album Name") .setAlbumArtist("Album Artist") .setTrackNumber(1) .setDiscNumber(1) .setReleaseYear(2024) .setGenre("Rock") .setDurationMs(240000) .setArtworkUri(Uri.parse("content://media/external/audio/albums/123")) .setExtras(Bundle().apply { putLong("album_id", 123L) putLong("artist_id", 456L) putLong("add_date", System.currentTimeMillis() / 1000) }) .build() ) .build() // Accessing metadata val title = mediaItem.mediaMetadata.title val artist = mediaItem.mediaMetadata.artist val albumId = mediaItem.mediaMetadata.extras?.getLong("album_id") ``` #### Album Interface ```kotlin interface Album : Item { val id: Long? val title: String? val songList: List<MediaItem> val albumArtist: String? val albumArtistId: Long? val albumYear: Int? val albumAddDate: Long? val albumModifiedDate: Long? val cover: Uri? } // Usage reader.albumListFlow.collect { albums -> albums.sortedByDescending { it.albumYear }.forEach { album -> println("${album.title} (${album.albumYear}) - ${album.albumArtist}") println("Cover: ${album.cover}") album.songList.forEach { song -> println(" ${song.mediaMetadata.trackNumber}. ${song.mediaMetadata.title}") } } } ``` #### Playlist Class ```kotlin class Playlist( val id: Long?, val title: String?, val path: File?, val dateAdded: Long?, val dateModified: Long?, val hasGaps: Boolean, // true if some referenced songs are missing val songList: List<MediaItem> ) // Special dynamic playlists // - RecentlyAdded: Songs added within the configured time window // - Favorite: Songs marked as favorites in MediaStore ``` ### MainActivity - Main Entry Point The single-activity architecture with fragment navigation: ```kotlin class MainActivity : BaseActivity() { val controllerViewModel: MediaControllerViewModel by viewModels() lateinit var playerBottomSheet: PlayerBottomSheet // Get the media controller fun getPlayer(): MediaController? = controllerViewModel.get() // Access the library reader val reader get() = gramophoneApplication.reader // Update/refresh the library fun updateLibrary(then: (() -> Unit)? = null) // Navigate to a new fragment fun startFragment(frag: Fragment, args: (Bundle.() -> Unit)? = null) { supportFragmentManager.commit { addToBackStack(System.currentTimeMillis().toString()) hide(supportFragmentManager.fragments.last()) add(R.id.container, frag.apply { args?.let { arguments = Bundle().apply(it) } }) } } // Add song to playlist dialog fun addToPlaylistDialog(song: File?) } // Intent actions supported Intent(context, MainActivity::class.java).apply { action = Intent.ACTION_SEARCH putExtra(SearchManager.QUERY, "search term") } // Shuffle all shortcut Intent("org.akanework.gramophone.action.SHUFFLE").apply { putExtra("item_name", "") // empty = all songs } // Auto-play on launch Intent(context, MainActivity::class.java).apply { putExtra(MainActivity.PLAYBACK_AUTO_PLAY_ID, "12345") putExtra(MainActivity.PLAYBACK_AUTO_PLAY_POSITION, 0L) } ``` ### ReplayGain Audio Processing The app includes a ReplayGain audio processor for volume normalization: ```kotlin // ReplayGain modes (configured via SharedPreferences) // 0 = disabled // 1 = track gain (normalize each song independently) // 2 = album gain (normalize within album context) // 3 = smart mode (album gain when playing album, track gain otherwise) // Settings keys prefs.getString("rg_mode", "0") // Mode selection prefs.getBoolean("rg_drc", true) // Dynamic range compression prefs.getInt("rg_rg_gain", 19) // Pre-amp for RG tracks (-15 to +15 dB, stored as 0-30) prefs.getInt("rg_no_rg_gain", 0) // Pre-amp for non-RG tracks prefs.getInt("rg_boost_gain", 0) // Additional boost ``` ## Summary Gramophone serves as a comprehensive music player for Android users who value audio quality and modern design. It's particularly well-suited for users with large local music libraries who want features like proper album art handling, lyrics display, gapless playback, and integration with external audio processing apps. The reactive architecture using Kotlin Flows ensures efficient memory usage and smooth UI updates when the music library changes. For developers looking to extend or integrate with Gramophone, the key integration points are: the `FlowReader` for library access, `MediaController` for playback control, `ItemManipulator` for playlist management, and `LrcUtils` for lyrics handling. The app follows Android's modern storage access guidelines with scoped storage support and graceful permission handling for media modification operations.