If you’ve ever tried building an audio app that runs on both Android and iOS, you know the pain. On Android, you’re wrangling Oboe or dropping down to OpenSL ES. On iOS, you find yourself deeply immersed in AVAudioEngine and Core Audio. There are two entirely different APIs, two codebases, and two sets of bugs. And do you want real-time effects processing? Good luck maintaining parity across both.
That’s precisely why I built Klarinet.
Why “Klarinet”?
If you’re wondering about the name: Google’s low-latency audio library for Android is called Oboe. A clarinet is in the same family of wind instruments, so the connection felt natural. And since this is a Kotlin library, the K was a no-brainer. Oboe, Clarinet, Kotlin… Klarinet.
What is Klarinet?
Klarinet is an open-source Kotlin Multiplatform (KMP) audio library that gives you a single, idiomatic Kotlin API for low-latency audio playback, recording, file I/O, and real-time effects processing across Android and Apple platforms.
You write your audio code once in Kotlin. Under the hood, Klarinet delegates to the best native backend on each platform: Google Oboe (AAudio/OpenSL ES) on Android, AVAudioEngine on Apple. No compromises on latency. No abstraction tax.
The library is available on Maven Central right now:
kotlin
implementation("com.vectencia.klarinet:klarinet:0.0.1")
The Problem Klarinet Solves
Cross-platform UI frameworks like Compose Multiplatform have matured significantly. You can share screens, navigation, and business logic across Android and iOS without much friction. But audio has been left behind.
There’s no shared audio layer in KMP. If your app needs to play a tone, record from a mic, decode an MP3, or apply a reverb, you’re writing platform-specific code. Twice. And keeping both implementations in sync as features evolve is a maintenance headache that compounds over time.
Klarinet fills that gap. It’s the audio layer that KMP has been missing.
What You Can Build With It
The use cases are broader than you might think:
Music production apps. DAWs, beat-makers, and loop stations. Klarinet’s effect chain supports 16 built-in effects (reverb, delay, compressor, EQ, chorus, and more), all powered by a shared C++ DSP core. Effects are hot-swappable, so you can add, remove, and reorder them while audio is streaming.
Voice and communication tools. Chat apps, podcast recorders, walkie-talkies. The noise gate, compressor, and parametric EQ run natively in the audio callback with zero overhead, which is exactly what you need for real-time voice processing.
Games. Procedural audio, dynamic sound effects, spatial mixing. The callback API gives you direct access to the audio buffer, and the low-latency performance mode ensures sounds trigger the instant you need them.
Audio analysis. Spectrum analysers, guitar tuners, and audio visualisers. The levelFlow() API provides real-time metering at 20+ updates per second via Kotlin Flows, and raw PCM access through the callback lets you run FFT, pitch detection, or whatever signal processing you need.
Accessibility tools. Hearing aids, sound amplifiers, frequency boosters. The real-time effects chain can handle parametric EQ, dynamic range compression, and gain adjustments with sub-10ms latency, which is critical for live audio passthrough.
How It Works
Here’s a minimal example that generates a 440Hz sine wave:
kotlin
val engine = AudioEngine.create()
val callback = object : AudioStreamCallback {
private var phase = 0.0
override fun onAudioReady(buffer: FloatArray, numFrames: Int): Int {
for (i in 0 until numFrames) {
buffer[i] = sin(2.0 * Math.PI * 440.0 * phase / 48000.0).toFloat()
phase += 1.0
}
return numFrames
}
}
val stream = engine.openStream(
config = AudioStreamConfig(
sampleRate = 48000,
channelCount = 1,
direction = StreamDirection.OUTPUT,
performanceMode = PerformanceMode.LOW_LATENCY,
),
callback = callback,
)
stream.start()
That’s it. Same code runs on Android and iOS. The AudioEngine creates a native Oboe stream on Android and an AVAudioEngine node on Apple. Your callback runs directly in the native audio thread.
Recording is just as straightforward. Swap StreamDirection.OUTPUT for StreamDirection.INPUT and call stream.read() instead.
The Effects Chain
This is where Klarinet gets interesting. The library ships with 16 built-in audio effects, all implemented in a shared C++ DSP core that compiles for every target platform:
-
Basic: Gain, Pan, Mute/Solo
-
Dynamics: Compressor, Limiter, Noise Gate
-
EQ/Filters: 8-band Parametric EQ, Low-Pass, High-Pass, Band-Pass
-
Time-based: Delay, Reverb (Freeverb algorithm)
-
Modulation: Chorus, Flanger, Phaser, Tremolo
Effects are created through the engine, chained together, and attached to a stream:
kotlin
val reverb = engine.createEffect(AudioEffectType.REVERB)
reverb.setParameter(ReverbParams.ROOM_SIZE, 0.7f)
val compressor = engine.createEffect(AudioEffectType.COMPRESSOR)
compressor.setParameter(CompressorParams.THRESHOLD, -20f)
val chain = engine.createEffectChain()
chain.add(compressor)
chain.add(reverb)
stream.effectChain = chain
All parameter updates are lock-free and audio-thread safe. You can tweak the reverb wetness or compressor ratio in your UI thread while the audio is playing, with no glitches or dropouts. And the chain itself is hot-swappable: add, remove, or reorder effects in real time.
Because the DSP runs in C++, there’s zero JNI overhead on Android and zero Kotlin/Native interop overhead on Apple. The effects process directly inside the native audio callback. That C++ core is a deliberate choice, not a default; I explain when native code is worth the cost (and when it isn’t) in when and why to use C++ on Android.
File I/O
Klarinet handles audio file decoding and encoding out of the box. WAV, MP3, AAC, and M4A are all supported.
kotlin
// Read metadata
val reader = AudioFileReader("/path/to/song.mp3")
val info = reader.info
println("${info.tags.title} by ${info.tags.artist} - ${info.durationMs}ms")
// Decode to PCM
val samples = reader.readAll()
// Or stream in chunks
reader.asFlow(chunkSize = 4096).collect { chunk ->
// Process each chunk without loading the entire file into memory
}
There are also convenience methods for common operations:
kotlin
// One-liner playback
val stream = engine.playFile("/path/to/song.mp3")
stream.start()
// One-liner recording
val stream = engine.recordToFile("/path/to/recording.wav", AudioFileFormat.WAV)
stream.start()
Coroutines Support
The core klarinet module has zero dependency on coroutines, keeping it lightweight. But if you’re already using coroutines (and you probably are), the optional klarinet-coroutines module adds Flow-based APIs and suspending functions:
kotlin
implementation("com.vectencia.klarinet:klarinet-coroutines:0.0.1")
This gives you reactive stream state observation, real-time level metering as a Flow, async file I/O on Dispatchers.IO, and streaming file decoding via asFlow().
SwiftUI Ready
If your iOS team prefers native SwiftUI over Compose Multiplatform, Klarinet works there too. The library exports a Kotlin/Native framework that Swift can import directly:
swift
import Klarinet
let engine = AudioEngine.companion.create()
let stream = engine.openStream(config: config, callback: callback)
stream.start()
The repo includes a full SwiftUI demo app with five screens (tone generator, mic meter, latency benchmark, file player, effects chain) built with pure SwiftUI and MVVM architecture.
Architecture
Klarinet is structured as a single KMP module with expect/actual declarations:
-
commonMain contains the entire public API surface:
AudioEngine,AudioStream, effects, file I/O, and all data types. -
androidMain implements everything via Google Oboe with C++17/JNI.
-
appleMain implements everything via AVAudioEngine, shared across iOS, macOS, and tvOS.
-
cpp/dsp is the shared C++ DSP library that compiles for all platforms. It contains the effect implementations, DSP primitives (biquad filters, LFOs, envelope followers, circular buffers), a lock-free effect chain, and SPSC ring buffers.
The language breakdown tells the story: 52.8% C++, 37.7% Kotlin, 7.1% Swift. The performance-critical audio processing lives in C++, the API and platform integration is in Kotlin, and the native iOS demo is in Swift.
Getting Started
Add the dependency to your KMP project:
kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.vectencia.klarinet:klarinet:0.0.1")
implementation("com.vectencia.klarinet:klarinet-coroutines:0.0.1") // optional
}
}
}
Check out the demo apps in the repository for working examples of every feature. The Compose Multiplatform demo runs on both Android and iOS, and the SwiftUI demo shows native integration patterns.
The project is Apache 2.0 licensed and open for contributions. Head over to the GitHub repo to star it, try it out, or open an issue.
What’s Next
This is version 0.0.1, the very first public release. The foundation is solid: low-latency I/O, a full effects chain, file decoding/encoding, and coroutines support. But there’s plenty more to come.
I’m planning to expand platform support, add more effects, improve the file format coverage, and build out more comprehensive documentation. If you’re working on an audio app with KMP and have specific needs, I’d love to hear about them.
You can find me on LinkedIn, Medium, and YouTube. And if Klarinet saves you from writing platform-specific audio code twice, drop a star on the repo. It helps more than you think.
I (David Cruz Anaya) am an independent senior mobile engineer (davthecoder). I build tools and educational content for the Android and KMP developer community, and write more over at davthecoder.com.

Loading comments…