
Low-latency raw PCM streamer for TTS and real-time voice: accept arbitrary-length chunks, gapless playback with underrun recovery, automatic resampling, unbounded buffering, and observable playback state.
Low-latency raw PCM streaming audio player for Kotlin Multiplatform (Android, iOS, macOS).
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.
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.
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" }-android suffix needed.kotlinx-coroutines-core is an api dependency, so Flow and StateFlow
are available without declaring it yourself.val player = AudioStreamPlayer(sampleRate = 24000)
player.play()
ttsResponse.collect { chunk -> player.feed(chunk) }
player.endOfStream() // suspends until the last sample has played
player.dispose()sampleRate/channels/format describe the data you feed, not the device;
resampling is handled natively.bufferedDuration() for backpressure if the
source can outrun playback indefinitely.player.state // StateFlow<PlayerState>: IDLE, PLAYING, PAUSED
player.events // SharedFlow<AudioStreamPlayerEvent>: Underrun, ErrorAn underrun (buffer ran dry mid-stream) keeps the player in PLAYING;
playback resumes automatically when more data is fed.
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.
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).| Platform | Backend | Targets | Minimum |
|---|---|---|---|
| Android |
AudioTrack (MODE_STREAM) |
android |
API 23 |
| iOS | AVAudioEngine |
iosArm64, iosSimulatorArm64, iosX64
|
14.0 |
| macOS | AVAudioEngine |
macosArm64, macosX64
|
11.0 |
Low-latency raw PCM streaming audio player for Kotlin Multiplatform (Android, iOS, macOS).
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.
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.
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" }-android suffix needed.kotlinx-coroutines-core is an api dependency, so Flow and StateFlow
are available without declaring it yourself.val player = AudioStreamPlayer(sampleRate = 24000)
player.play()
ttsResponse.collect { chunk -> player.feed(chunk) }
player.endOfStream() // suspends until the last sample has played
player.dispose()sampleRate/channels/format describe the data you feed, not the device;
resampling is handled natively.bufferedDuration() for backpressure if the
source can outrun playback indefinitely.player.state // StateFlow<PlayerState>: IDLE, PLAYING, PAUSED
player.events // SharedFlow<AudioStreamPlayerEvent>: Underrun, ErrorAn underrun (buffer ran dry mid-stream) keeps the player in PLAYING;
playback resumes automatically when more data is fed.
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.
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).| Platform | Backend | Targets | Minimum |
|---|---|---|---|
| Android |
AudioTrack (MODE_STREAM) |
android |
API 23 |
| iOS | AVAudioEngine |
iosArm64, iosSimulatorArm64, iosX64
|
14.0 |
| macOS | AVAudioEngine |
macosArm64, macosX64
|
11.0 |