Skip to content

Repository files navigation

audio-stream-player

Low-latency raw PCM streaming audio player for Kotlin Multiplatform (Android, iOS, macOS).

Maven Central License Kotlin Platforms

Designed for feeding audio as it arrives from a TTS or realtime voice API (OpenAI, ElevenLabs, Cartesia, Google, ...): feed arbitrary-length PCM chunks, playback starts immediately and plays gaplessly.

The demo app running side by side on Android (Jetpack Compose) and iOS (SwiftUI), streaming a jittery TTS-style byte stream from shared Kotlin code, with a buffer gauge and underrun recovery

One shared engine, two native UIs: the demo apps stream a jittery synthetic utterance while showing buffered audio and underrun recovery.

🔊 Watch the demo with sound – real captured playback: first Android streams with jitter off (gapless), then iOS streams over a simulated congested network and audibly recovers from underruns.

This is the Kotlin Multiplatform sibling of the audio_stream_player Flutter package and tracks its core behavior. The native engines are the same: AudioTrack in MODE_STREAM on Android, AVAudioEngine + AVAudioPlayerNode on Apple platforms.

Installation

Available on Maven Central. Add mavenCentral() to your repositories, then:

// build.gradle.kts
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("com.adrianczuczka:audio-stream-player:0.1.0")
        }
    }
}

With a version catalog:

# gradle/libs.versions.toml
[libraries]
audio-stream-player = { module = "com.adrianczuczka:audio-stream-player", version = "0.1.0" }
  • In a plain Android project the same coordinate resolves to the Android variant through Gradle module metadata; no -android suffix needed.
  • kotlinx-coroutines-core is an api dependency, so Flow and StateFlow are available without declaring it yourself.

Usage

val player = AudioStreamPlayer(sampleRate = 24000)
player.play()
ttsResponse.collect { chunk -> player.feed(chunk) }
player.endOfStream() // suspends until the last sample has played
player.dispose()
  • Chunks may be any length – frame alignment across chunk boundaries is handled internally.
  • sampleRate/channels/format describe the data you feed, not the device; resampling is handled natively.
  • Buffering is unbounded; use bufferedDuration() for backpressure if the source can outrun playback indefinitely.

Observing playback

player.state    // StateFlow<PlayerState>: IDLE, PLAYING, PAUSED
player.events   // SharedFlow<AudioStreamPlayerEvent>: Underrun, Error

An underrun (buffer ran dry mid-stream) keeps the player in PLAYING; playback resumes automatically when more data is fed.

iOS audio session

By default the player configures the audio session for playback (category playback, mode spokenAudio) and activates it on play(). Pass configureAudioSession = false to manage the session yourself.

Demo apps

The GIF above is the checked-in demo, one app per platform sharing one engine:

  • demo-shared – common Kotlin driving the player: synthesizes a robot-speech utterance, feeds it in ~100ms chunks with network-like jitter, and reduces state, events, and buffer level into one UI state.
  • demo-android – Jetpack Compose UI. Run with ./gradlew :demo-android:installDebug.
  • demo-ios – SwiftUI consuming the shared engine through a Kotlin/Native framework. Boot a simulator, then run ./demo-ios/build-and-run.sh (no Xcode project needed).

Supported targets

Platform Backend Targets Minimum
Android AudioTrack (MODE_STREAM) android API 23
iOS AVAudioEngine iosArm64, iosSimulatorArm64, iosX64 14.0
macOS AVAudioEngine macosArm64, macosX64 11.0

License

MIT

About

Low-latency raw PCM streaming audio player for Kotlin Multiplatform (Android, iOS, macOS)

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages