From 1fc8fbbc0dae15c71c4bac03323d1d1e40765a8c Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Wed, 17 Jun 2026 17:18:33 +0200 Subject: [PATCH] new: implement desktop audio player with Ogg Vorbis support using VorbisSPI --- core/audioplayer/build.gradle.kts | 4 ++ .../ajaury/gwenedeg/player/JvmAudioPlayer.kt | 54 ++++++++++++++++--- .../gwenedeg/player/JvmAudioSessionManager.kt | 12 +++++ .../player/di/AudioPlayerModule.jvm.kt | 11 +++- gradle/libs.versions.toml | 2 + 5 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioSessionManager.kt diff --git a/core/audioplayer/build.gradle.kts b/core/audioplayer/build.gradle.kts index 5fe38ea..89276af 100644 --- a/core/audioplayer/build.gradle.kts +++ b/core/audioplayer/build.gradle.kts @@ -43,6 +43,10 @@ kotlin { implementation(libs.media3.exoplayer) implementation(libs.media3.common) } + jvmMain.dependencies { + // Desktop audio: Ogg Vorbis SPI so javax.sound.sampled can decode .ogg + implementation(libs.vorbisspi) + } commonMain.dependencies { // DI implementation(project.dependencies.platform(libs.koin.bom)) diff --git a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt index ff484d8..58b368d 100644 --- a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt +++ b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt @@ -1,23 +1,61 @@ package fr.ajaury.gwenedeg.player +import java.io.BufferedInputStream +import java.net.URI +import javax.sound.sampled.AudioFormat +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.Clip + +/** + * Desktop [AudioPlayer] backed by [javax.sound.sampled]. + * + * The Ogg Vorbis recordings are decoded to PCM by the VorbisSPI providers on the classpath, which + * the JDK [AudioSystem] discovers automatically, so loading works the same as any natively + * supported format. + */ class JvmAudioPlayer : AudioPlayer { + private var clip: Clip? = null + + override fun load(uri: String) { + release() + + val encodedInput = BufferedInputStream(URI(uri).toURL().openStream()) + .let { AudioSystem.getAudioInputStream(it) } + val baseFormat = encodedInput.format + val pcmFormat = AudioFormat( + AudioFormat.Encoding.PCM_SIGNED, + baseFormat.sampleRate, + 16, + baseFormat.channels, + baseFormat.channels * 2, + baseFormat.sampleRate, + false, + ) + + val decodedInput = AudioSystem.getAudioInputStream(pcmFormat, encodedInput) + clip = AudioSystem.getClip().apply { open(decodedInput) } + } + override fun play() { - TODO("Not yet implemented") + clip?.start() } override fun pause() { - TODO("Not yet implemented") + clip?.stop() } override fun stop() { - TODO("Not yet implemented") + clip?.apply { + stop() + framePosition = 0 + } } override fun release() { - TODO("Not yet implemented") - } - - override fun load(uri: String) { - TODO("Not yet implemented") + clip?.apply { + stop() + close() + } + clip = null } } diff --git a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioSessionManager.kt b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioSessionManager.kt new file mode 100644 index 0000000..71e7609 --- /dev/null +++ b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioSessionManager.kt @@ -0,0 +1,12 @@ +package fr.ajaury.gwenedeg.player + +/** + * Desktop has no system audio focus model to honour, so activation is a no-op. + */ +class JvmAudioSessionManager : AudioSessionManager { + override fun activate() { + } + + override fun deactivate() { + } +} diff --git a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/di/AudioPlayerModule.jvm.kt b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/di/AudioPlayerModule.jvm.kt index 785aa27..99a5d2e 100644 --- a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/di/AudioPlayerModule.jvm.kt +++ b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/di/AudioPlayerModule.jvm.kt @@ -1,6 +1,13 @@ package fr.ajaury.gwenedeg.player.di +import fr.ajaury.gwenedeg.player.AudioPlayer +import fr.ajaury.gwenedeg.player.AudioSessionManager +import fr.ajaury.gwenedeg.player.JvmAudioPlayer +import fr.ajaury.gwenedeg.player.JvmAudioSessionManager import org.koin.core.module.Module +import org.koin.dsl.module -actual val audioPlayerModule: Module - get() = TODO("Not yet implemented") +actual val audioPlayerModule: Module = module { + single { JvmAudioPlayer() } + single { JvmAudioSessionManager() } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1d357ba..6c634df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ kotlinxCoroutines = "1.11.0" kotlinSerialization = "2.4.0" material3 = "1.11.0-alpha07" koin = "4.2.1" +vorbisspi = "1.0.3.3" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } @@ -40,6 +41,7 @@ lifecycle-viewmodelNavigation3 = { module = "org.jetbrains.androidx.lifecycle:li media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } navigation3 = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +vorbisspi = { module = "com.googlecode.soundlibs:vorbisspi", version.ref = "vorbisspi" } wrappers-browser = { module = "org.jetbrains.kotlin-wrappers:kotlin-browser", version.ref = "kotlinWrappers" } [plugins]