From ca5ebd5b03f5e579b88fe6cbc62d10700ff02285 Mon Sep 17 00:00:00 2001 From: TySmi Date: Tue, 13 Oct 2026 19:37:51 -0400 Subject: [PATCH] Play ALAC songs: FFmpeg audio decoder fallback Android has no Apple Lossless (ALAC) decoder, so ALAC .m4a files (for example Eminem's Recovery) "played" with the seek bar moving but no sound: the player had no decoder for the audio track. Adds Jellyfin's prebuilt Media3 FFmpeg decoder (GPL-3.0) and turns on extension renderers in fallback mode, so Android's own decoders are still used for FLAC, MP3, AAC and so on, and FFmpeg only steps in for formats Android can't decode. Checked on the emulator: "25 to Life" now produces a 44.1 kHz audio track (FFmpeg), FLAC still uses c2.android.flac.decoder. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 8 ++++++++ app/build.gradle.kts | 1 + .../java/com/tysmi/syncwave/playback/PlaybackService.kt | 8 +++++++- gradle/libs.versions.toml | 2 ++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index a5df825..bc3d7ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,11 @@ features, so keep layers clean and the shared models free of API details. routes, Compose shared-element transitions. - Network: Retrofit 3 + OkHttp 5 + kotlinx.serialization. Images: Coil 3 (disk cache). - Playback: Media3 1.11.1: ExoPlayer in a `MediaLibraryService`, `MediaController` in the UI. + Android has no ALAC (Apple Lossless .m4a) decoder: `org.jellyfin.media3:media3-ffmpeg-decoder` + (1.9.0+1, GPL-3.0, ~1.5 MB/ABI) is the fallback via `EXTENSION_RENDERER_MODE_ON` (Android's + decoders still first). Without it ALAC "plays" silently (position moves, no AudioTrack). Check + with `adb shell dumpsys media.audio_flinger` (an active track = sound). Bump it when + Jellyfin publishes a build for a newer Media3. - Storage: DataStore + Android Keystore (AES-GCM) for the encrypted login. - Colors from cover art: AndroidX Palette. - No DI framework: `AppContainer` wires everything by hand. @@ -161,6 +166,9 @@ adb -s emulator-5554 install -r app/build/outputs/apk/debug/app-debug.apk resent when the network comes back (needs ACCESS_NETWORK_STATE) or at next app start. Previews never report. Verified on the emulator against the owner's server: now playing, offline save with original time, sent 7 s after reconnecting, preview sent nothing. +13. ALAC fix (2026-10-13): ALAC songs (e.g. Eminem, Recovery) played silently: Android has no + ALAC decoder. Added the FFmpeg decoder fallback (see Tech). Verified on the emulator: "25 to + Life" now outputs 44.1 kHz audio via FFmpeg; FLAC still uses `c2.android.flac.decoder`. 12. Home tab (2026-10-13), now the start tab: rows Recently Played (`recent`), Recently Added (`newest`), Most Played (`frequent`), Favorites (getStarred2 albums + songs), Rediscover (`random`), each with See all (`AlbumListRoute` grid, `FavoritesRoute`). Rows load in diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d4c663e..d2b40ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(libs.media3.exoplayer) implementation(libs.media3.datasource.okhttp) implementation(libs.media3.session) + implementation(libs.media3.ffmpeg.decoder) implementation(libs.kotlinx.coroutines.guava) testImplementation(libs.junit) diff --git a/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt b/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt index f2d8e6c..276eb81 100644 --- a/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt +++ b/app/src/main/java/com/tysmi/syncwave/playback/PlaybackService.kt @@ -13,6 +13,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.analytics.AnalyticsListener import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -62,7 +63,12 @@ class PlaybackService : MediaLibraryService() { .setUpstreamDataSourceFactory(httpDataSource) .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) - val player = ExoPlayer.Builder(this) + // Android can't decode some formats our libraries use (Apple Lossless / ALAC .m4a): without + // a decoder the player "plays" silently. The FFmpeg decoders fill in, but only for formats + // Android itself can't handle (EXTENSION_RENDERER_MODE_ON keeps Android's decoders first). + val renderers = DefaultRenderersFactory(this) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + val player = ExoPlayer.Builder(this, renderers) .setMediaSourceFactory(DefaultMediaSourceFactory(this).setDataSourceFactory(audioSource)) .setLoadControl( DefaultLoadControl.Builder() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ab87b0..45a33df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,8 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp" media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" } media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +# FFmpeg audio decoders for Media3 (prebuilt by Jellyfin, GPL-3.0), for formats Android can't decode (ALAC). +media3-ffmpeg-decoder = { group = "org.jellyfin.media3", name = "media3-ffmpeg-decoder", version = "1.9.0+1" } kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "coroutines" } androidx-palette = { group = "androidx.palette", name = "palette-ktx", version.ref = "palette" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }