
Unified low-latency audio engine with real-time, lock-free DSP: 16 built-in effects, hot-swappable effect chains, low-overhead callback I/O, streaming, real-time metering, file decode/encode.
Low-latency audio SDK for Kotlin Multiplatform
Klarinet is an open-source Kotlin Multiplatform audio library that provides a unified, idiomatic API for low-latency audio playback, recording, file I/O, and real-time effects processing across 9 platforms and 14 KMP targets.
Write your audio code once in Kotlin. Klarinet delegates to the best native backend on each platform — Google Oboe on Android, AVAudioEngine on Apple, miniaudio on JVM/Linux/Windows, and the Web Audio API in the browser — while preserving low-latency characteristics and real-time safety.
Build cross-platform DAWs, beat makers, or loop stations. Klarinet's low-latency audio engine and real-time effects chain let users play instruments, apply reverb/delay/compression, and record --- all with professional-grade latency on both Android and iOS.
Add voice processing to chat apps, walkie-talkies, or podcast tools. Use the noise gate to suppress background noise, the compressor to normalize volume levels, and the EQ to shape voice tone --- all running natively in the audio callback with zero overhead.
Integrate dynamic audio into games without platform-specific code. Generate procedural sound effects with the callback API, apply spatial effects, and mix multiple audio sources. The low-latency performance mode ensures sounds play the instant they're triggered.
Build spectrum analyzers, tuners, or audio visualizers. Klarinet's levelFlow() provides real-time metering at 20+ updates/sec via Kotlin Flows, and the callback API gives you raw PCM samples for FFT, pitch detection, or waveform rendering.
Create hearing aids, sound amplifiers, or audio processing apps for accessibility. The real-time effects chain can boost specific frequencies (parametric EQ), compress dynamic range (limiter), or amplify quiet sounds (gain) --- all with the sub-10ms latency required for live audio passthrough.
Build music education apps with real-time feedback. Students play an instrument into the mic, your app analyzes pitch and timing via the callback, and displays results --- while simultaneously applying effects to make practice sound better.
Decode MP3/AAC/WAV files where the platform reader exists, apply effects (noise gate + compressor + EQ is a classic podcast chain), and encode WAV (AAC/M4A on Android and Apple except watchOS). MP3 cannot be encoded. Use AudioFileReader.asFlow() to stream large files without loading them entirely into memory.
klarinet-coroutines module with Flow-based metering and async I/O| Platform | Targets | Backend | Audio Streaming | File I/O (WAV) | File I/O (AAC/M4A) | Min Version |
|---|---|---|---|---|---|---|
| Android | arm64-v8a, armeabi-v7a, x86_64 | Google Oboe (AAudio / OpenSL ES) | ✅ | ✅ | ✅ | API 24 |
| iOS / iPadOS | arm64, simulatorArm64, x64 | AVAudioEngine | ✅ | ✅ | ✅ | iOS 15+ |
| macOS | arm64 | AVAudioEngine | ✅ | ✅ | ✅ | macOS 12+ |
| tvOS | arm64, simulatorArm64 | AVAudioEngine | ✅ | ✅ | ✅ | tvOS 15+ |
| watchOS | arm64, simulatorArm64 | AVAudioEngine | ✅ | ✅ | ❌ | watchOS 8+ |
| JVM Desktop | macOS, Linux, Windows | miniaudio (WASAPI / Core Audio / ALSA) | ✅ | ✅ | ❌ | JDK 11+ |
| Linux Native | x64, arm64 | miniaudio (ALSA / PulseAudio / JACK) | ✅ | ✅ | ❌ | — |
| Windows Native | x64 | miniaudio (WASAPI / DirectSound) | ✅ | ✅ | ❌ | — |
| JS / Browser | browser | Web Audio API | ✅ | ✅ | decode only | modern browsers |
watchOS support covers real-time audio streaming and WAV file writing. The following limitations apply due to the Kotlin/Native watchOS bindings not exposing the ExtAudioFile APIs:
| Feature | Status | Notes |
|---|---|---|
Audio playback (AudioStream output) |
✅ Supported | AVAudioEngine, same API as iOS/tvOS |
Audio recording (AudioStream input) |
✅ Supported | Requires mic permission; hardware mic available on Series 3+ |
| Audio session management | ✅ Supported | Full AVAudioSession integration |
WAV file writing (AudioFileWriter) |
✅ Supported | PCM encoding via posix I/O |
| AAC/M4A file writing | ❌ Not available | Throws UnsupportedFormatException
|
File reading (AudioFileReader) |
❌ Not available | Throws UnsupportedFormatException for all formats |
| Audio effects chain | ✅ Supported | Kotlin-side effect processing |
Swift interop (AudioStreamCallbackImpl) |
✅ Supported | Same callback bridge as iOS |
These limitations are a Kotlin/Native toolchain constraint, not a watchOS platform restriction. The underlying Apple APIs (ExtAudioFile) exist on watchOS but are not yet exposed in the K/N bindings.
JVM desktop support uses miniaudio for low-latency audio I/O across macOS, Linux, and Windows.
| Feature | Status | Notes |
|---|---|---|
Audio playback (AudioStream output) |
✅ Supported | miniaudio callback-driven, low latency |
Audio recording (AudioStream input) |
✅ Supported | miniaudio capture device |
| Audio session management | N/A | No audio session concept on desktop |
| WAV file read/write | ✅ Supported | via miniaudio decoder/encoder |
| MP3 file reading | ✅ Supported | via miniaudio (dr_mp3) |
| AAC/M4A file read/write | ❌ Not available | Throws UnsupportedFormatException
|
| Push-mode write/read | ❌ Not available | Use callback mode via AudioStreamCallback
|
| Audio effects | ✅ Supported | Shared C++ DSP core on the miniaudio callback |
Browser support uses the Web Audio API. The public Klarinet API is the same; the backend is not the C++ DSP core.
| Feature | Status | Notes |
|---|---|---|
Audio playback (AudioStream output) |
✅ Supported |
AudioWorklet renders; Kotlin onAudioReady runs on the main thread via the worklet port |
Audio recording (AudioStream input) |
✅ Supported |
getUserMedia on start(); state goes STARTING then STARTED
|
| Audio session management | N/A | No-op, same as JVM desktop |
| WAV file write | ✅ Supported | In-memory store (no disk). Re-read with AudioFileReader or export with audioFileWavBytes
|
| WAV / MP3 / AAC / M4A read | ✅ After decode | Call decodeAudioFile(url), decodeAudioBytes, putWavBytes, or putAudioFile before AudioFileReader
|
| Compressed file write | ❌ Not available | Throws UnsupportedFormatException
|
| Audio effects | ✅ Supported | Web Audio nodes mapped from Klarinet Studio (not bit-identical to C++ DSP) |
| Push-mode write/read | ✅ Non-blocking |
timeoutNanos is ignored; JS cannot block the main thread |
AudioStream.start() returns immediately. Observe onStreamStateChanged (or stateFlow()) for STARTED. Output sample rate is the AudioContext rate.
decodeAudioFile("https://example.com/song.mp3").then { reader ->
val stream = engine.playFile("https://example.com/song.mp3")
stream.start()
reader
}| Platform | Reason |
|---|---|
| Wasm / WasmJS | Not implemented. Use the JS / Browser target. |
// build.gradle.kts
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.vectencia.klarinet:klarinet:0.5.0")
// Optional: Coroutines extensions
implementation("com.vectencia.klarinet:klarinet-coroutines:0.5.0")
}
}
}Klarinet exports a Kotlin/Native framework that Swift can import directly:
import Klarinet
let engine = AudioEngine.companion.create()
// ... use the full Klarinet API from SwiftSee the SwiftUI Demo for a complete example.
A one-file JVM program that plays a 440 Hz sine for one second:
./gradlew :sample:run
./gradlew :sample:run --args=fade # 2s fade 0→full, hold, 2s full→0
./gradlew :sample:run --args=sleep # 1s play, 1s fade, stream stops
./gradlew :sample:run --args=scene # 440 Hz → 2s crossfade to 660 HzThe source is in sample/src/main/kotlin/Main.kt.
import com.vectencia.klarinet.*
import kotlin.math.sin
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()
// ... audio is playing ...
stream.stop()
stream.close()
engine.close()val engine = AudioEngine.create()
val stream = engine.openStream(
config = AudioStreamConfig(
sampleRate = 48000,
channelCount = 1,
direction = StreamDirection.INPUT,
),
callback = object : AudioStreamCallback {
override fun onAudioReady(buffer: FloatArray, numFrames: Int): Int {
// Process captured audio in `buffer`...
return numFrames
}
},
)
stream.start()
// ... recording ...
stream.stop()
stream.close()
engine.close()Klarinet includes 16 built-in effects powered by a shared C++ DSP core. Effects run natively in the audio callback --- zero JNI overhead on Android, zero interop overhead on Apple.
val engine = AudioEngine.create()
// Create effects
val reverb = engine.createEffect(AudioEffectType.REVERB)
reverb.setParameter(ReverbParams.ROOM_SIZE, 0.7f)
reverb.setParameter(ReverbParams.WET_DRY_MIX, 0.3f)
val compressor = engine.createEffect(AudioEffectType.COMPRESSOR)
compressor.setParameter(CompressorParams.THRESHOLD, -20f)
compressor.setParameter(CompressorParams.RATIO, 4f)
// Build an effect chain
val chain = engine.createEffectChain()
chain.add(compressor)
chain.add(reverb)
// Attach to a stream
val stream = engine.openStream(config, callback)
stream.effectChain = chain
stream.start()
// Update parameters in real-time (lock-free, audio-thread safe)
reverb.setParameter(ReverbParams.WET_DRY_MIX, 0.6f)
// Hot-swap: add/remove effects while audio is playing
chain.add(engine.createEffect(AudioEffectType.DELAY))
chain.remove(compressor)| Category | Effects |
|---|---|
| Basic | Gain, Pan, Mute/Solo |
| Dynamics | Compressor, Limiter, Noise Gate |
| EQ / Filters | Parametric EQ (8-band), Low-Pass, High-Pass, Band-Pass |
| Time-based | Delay, Reverb (Freeverb) |
| Modulation | Chorus, Flanger, Phaser, Tremolo |
Decode support is not universal: watchOS has no AudioFileReader; JVM and native desktop cannot read AAC/M4A; JS must decodeAudioFile / putWavBytes / putAudioFile first. Encode is WAV on every platform (JS in-memory), AAC/M4A on Android and iOS/macOS/tvOS, and never MP3.
val reader = AudioFileReader("/path/to/song.mp3")
val info = reader.info
println("Format: ${info.format}") // MP3
println("Duration: ${info.durationMs} ms") // 210000
println("Sample rate: ${info.sampleRate}") // 44100
println("Channels: ${info.channelCount}") // 2
val tags = info.tags
println("Title: ${tags.title}") // "My Song"
println("Artist: ${tags.artist}") // "Artist Name"
reader.close()val reader = AudioFileReader("/path/to/audio.wav")
// Read all at once
val samples = reader.readAll() // FloatArray of interleaved PCM [-1.0, 1.0]
// Or stream in chunks
reader.seekTo(0)
while (!reader.isAtEnd) {
val chunk = reader.readFrames(maxFrames = 4096)
// Process chunk...
}
reader.close()// Play a file
val stream = engine.playFile("/path/to/song.mp3")
stream.start()
// Record to a file
val stream = engine.recordToFile(
filePath = "/path/to/recording.wav",
format = AudioFileFormat.WAV,
)
stream.start()The klarinet-coroutines module provides Flow-based observation and suspending file I/O without adding coroutines as a dependency to the core module.
import com.vectencia.klarinet.coroutines.*
// Observe stream state reactively (cold Flow; cancel the collector to stop polling)
stream.stateFlow().collect { state ->
println("Stream state: $state")
}
// Real-time audio level metering (20 updates/sec)
stream.levelFlow().collect { level ->
// Update VU meter UI (0.0 to 1.0)
}
// Wait for a stream state
stream.awaitState(StreamState.STARTED)
// Sleep timer countdown (polls remainingMs / state)
timer.remainingMsFlow().collect { msLeft -> /* UI */ }
timer.awaitState(SleepTimerState.COMPLETED)
// Interruptions (fans out; does not replace observeInterruptions)
session.interruptionFlow().collect { info -> /* UI / logging */ }
// Async file I/O (Dispatchers.IO, or Default on JS)
val samples = reader.readAllSuspend()
writer.writeFramesSuspend(samples, samples.size)
// Stream decoded audio as a Flow
reader.asFlow(chunkSize = 4096).collect { chunk ->
// Process each chunk
}graph TD
A["Your App (Kotlin/Swift)"] --> B[Klarinet SDK]
B --> C["C++ DSP Core<br/>(Effects, Filters, Reverb)"]
B --> D["Google Oboe<br/>(AAudio / OpenSL ES)"]
B --> E["AVAudioEngine<br/>(Core Audio)"]
B --> F["miniaudio<br/>(WASAPI / Core Audio / ALSA)"]
B --> G["Web Audio API<br/>(browser)"]
style A fill:#7F52FF,color:#fff
style B fill:#1a1a2e,color:#0ff
style C fill:#16213e,color:#0ff
style D fill:#0d7377,color:#fff
style E fill:#0d7377,color:#fff
style F fill:#0d7377,color:#fff
style G fill:#0d7377,color:#fffKlarinet is a single Kotlin Multiplatform module using expect/actual declarations:
commonMain --- Public API: AudioEngine, AudioStream, AudioStreamConfig, AudioStreamCallback, AudioEffect, AudioEffectChain, data classes, enumsandroidMain --- Android implementation via Google Oboe (C++17/JNI). The Oboe callback never enters the JVM: Kotlin onAudioReady runs on a worker thread and samples cross a lock-free FIFO. Effects still process on the audio thread.appleMain --- Apple implementation via AVAudioEngine. Shared across iOS, macOS, tvOS, and watchOS.jvmMain --- JVM desktop implementation via miniaudio. Covers macOS, Linux, and Windows from a single target with platform-specific native libraries bundled in the JAR.nativeDesktopMain --- Shared Linux (linuxX64, linuxArm64) and Windows (mingwX64) native implementation via miniaudio (cinterop).jsMain --- Browser implementation via the Web Audio API. Effects are Web Audio nodes; file I/O is an in-memory store plus decodeAudioData.cpp/dsp --- Shared C++ DSP library compiled for all native platforms. Contains all 16 effects, DSP primitives (Biquad, LFO, EnvelopeFollower, CircularBuffer), lock-free effect chain, and SPSC ring buffer.| Module | Artifact | Description |
|---|---|---|
klarinet |
com.vectencia.klarinet:klarinet |
Core SDK: audio I/O, effects, file I/O |
klarinet-android |
com.vectencia.klarinet:klarinet-jni |
Android Oboe/C++ JNI backend (pulled in by the KMP Android target) |
klarinet-coroutines |
com.vectencia.klarinet:klarinet-coroutines |
Optional Flow & suspending extensions |
demo |
--- | Shared Compose Multiplatform demo UI (Android, iOS, Desktop) |
demo-android |
--- | Android application entry point |
demo-native |
--- | Native console demo apps (Linux, Windows) |
demo-web |
--- | Browser demo (Kotlin/JS + Web Audio) |
sample |
--- | One-file JVM sine-wave sample |
iosApp |
--- | Native SwiftUI demo app (iOS, tvOS, watchOS) |
The project includes demo applications for every supported platform:
The same six screens as the Compose demo, plus local-file decode, URL decode, mic recording, and WAV download. Uses the Klarinet JS target.
Production (GitHub Pages):
/api/
The Pages workflow builds on every main push. Deploy is skipped with a warning if GET /repos/{owner}/{repo}/pages fails (Pages not enabled; typical on a new fork). Layout, enablement, local production preview, CORS/mic notes, and troubleshooting: GITHUB_PAGES.md.
# webpack-dev-server (not the Pages bundle)
./gradlew :demo-web:jsBrowserDevelopmentRun
# URL Gradle prints, usually http://localhost:8080
# same public/ tree as GitHub Pages (demo at /, Dokka at /api/)
make pages
python3 -m http.server 8080 --directory publicSix destinations via scrollable top tabs: Tone Gen (SleepTimer), Mic Meter, Latency, File, Effects (FADE_MS), Scenes (AudioScenePlayer). Runs on Android, iOS, and JVM Desktop (macOS, Linux, Windows) from shared Compose UI code. On Android the host calls DemoSession.manager.bind and streams use attach.
# Run desktop demo
./gradlew demo:runNative SwiftUI demo for each Apple platform. The iOS app mirrors Compose with six scrollable top-tab destinations (Tone Gen with SleepTimer, Mic Meter, Latency, File, Effects with FADE_MS, Scenes with AudioScenePlayer) and MVVM; tvOS and watchOS remain smoke-test tone UIs adapted for their form factors.
Minimal command-line demos that play a 440 Hz sine wave for 3 seconds, proving low-latency audio works on each native target.
Hosted Dokka HTML (same Pages deploy as the web demo): vectencia.github.io/Klarinet/api/. Generate locally with ./gradlew :dokkaGenerate (build/dokka/html/).
| Type | Description |
|---|---|
AudioEngine |
Entry point. Creates streams, effects, and queries devices. |
AudioStream |
Playback or recording stream with lifecycle control. |
AudioStreamConfig |
Stream configuration: sample rate, channels, format, direction. |
AudioStreamCallback |
Real-time callback for audio processing (onAudioReady). |
AudioEffect |
A single audio effect with typed parameters. |
AudioEffectChain |
Ordered chain of effects attached to a stream. |
AudioFileReader |
Decodes WAV/MP3/AAC/M4A to PCM where the platform decoder exists (not watchOS; JVM/native have no AAC/M4A; JS after decode). |
AudioFileWriter |
Encodes PCM to WAV on every platform; AAC/M4A on Android and iOS/macOS/tvOS. MP3 encode is not supported. |
AudioDeviceInfo |
Information about an audio input/output device. |
AudioSessionManager |
Session category, Android audio focus, interruption hooks. |
AudioScene / AudioSceneJson
|
Portable layer mix (ids, levels, effect params) as JSON. |
AudioScenePlayer |
Crossfades scenes with [GainParams.FADE_MS]; host opens each layer stream. |
LatencyInfo |
Input/output latency measurements. |
| Enum | Values |
|---|---|
AudioFormat |
PCM_FLOAT, PCM_I16, PCM_I24, PCM_I32
|
StreamDirection |
OUTPUT, INPUT
|
PerformanceMode |
NONE, LOW_LATENCY, POWER_SAVING
|
SharingMode |
SHARED, EXCLUSIVE
|
StreamState |
UNINITIALIZED, OPEN, STARTING, STARTED, PAUSING, PAUSED, STOPPING, STOPPED, CLOSING, CLOSED
|
AudioEffectType |
GAIN, PAN, MUTE_SOLO, COMPRESSOR, LIMITER, NOISE_GATE, PARAMETRIC_EQ, LOW_PASS_FILTER, HIGH_PASS_FILTER, BAND_PASS_FILTER, DELAY, REVERB, CHORUS, FLANGER, PHASER, TREMOLO
|
AudioFileFormat |
WAV, MP3, AAC, M4A
|
KlarinetException
+-- StreamCreationException
+-- StreamOperationException
+-- DeviceNotFoundException
+-- AudioSessionException
+-- PermissionException
+-- UnsupportedFormatException
+-- AudioFileException
+-- ResourceReleasedException
# All modules
./gradlew build
# Library only
./gradlew :klarinet:build
# Demo app (Android)
./gradlew :demo-android:assembleDebug
# Demo app (Desktop)
./gradlew :demo:run
# iOS app (via Xcode)
open iosApp/iosApp.xcodeproj
# Native console demos
./gradlew :demo-native:linkReleaseExecutableLinuxX64
./gradlew :demo-native:linkReleaseExecutableMingwX64# Kotlin tests (all platforms)
./gradlew :klarinet:allTests
# JS browser tests (Chrome Headless)
./gradlew :klarinet:jsBrowserTest :klarinet-coroutines:jsBrowserTest
# C++ DSP tests
./gradlew :klarinet:dspTests
# Coroutines module tests
./gradlew :klarinet-coroutines:allTests
# API docs (HTML)
./gradlew :dokkaGenerate
# output: build/dokka/html/index.html| Command | Description |
|---|---|
make build |
Build all modules |
make klarinet |
Build the library only |
make demo |
Build the demo app (Android) |
make sample |
Run the one-file JVM sine-wave sample |
make docs |
Generate Dokka HTML API docs (build/dokka/html/) |
make pages |
Production web demo + Dokka into public/ (GitHub Pages layout) |
make test |
Run Kotlin tests (:klarinet:allTests) |
make test-coroutines |
Run klarinet-coroutines tests |
make test-js |
Run JS browser tests (Chrome Headless) |
make test-dsp |
Run C++ DSP tests |
make test-all |
Run Klarinet + coroutines Kotlin tests, C++ DSP, iOS simulator, and consumer DSP link tests |
make clean |
Clean build artifacts |
make publish |
Publish to Maven Central |
Presets are data. Ship your own JSON (assets, files, network). Layer id is an opaque key; Klarinet never loads audio files from the scene document.
{
"id": "rain-night",
"layers": [
{ "id": "rain", "gainDb": -6.0, "effects": [{ "type": "REVERB", "params": { "0": 0.5 } }] },
{ "id": "wind", "gainDb": -12.0 }
]
}val scene = AudioSceneJson.decode(json)
AudioScenePlayer(engine).use { player ->
player.transitionTo(scene, fadeMs = 3000f) { layer ->
engine.openStream(config, callbackFor(layer.id))
}
}Same layer id keeps the stream and retargets gain. Other layers fade out and in so A → B has no hole. Close the player to release streams and chains.
Klarinet does not start a foreground service or draw a notification. The host must still satisfy platform background rules. The SDK exposes session category, audio focus, and interruption events so a call does not leave the stream silently dead.
val session = AudioSessionManager()
session.configure(AudioSessionCategory.PLAYBACK, AudioSessionMode.DEFAULT)
session.attach(stream) // pause on call, start again when shouldResume
session.observeInterruptions { /* optional UI */ }
session.setActive(true) // Android: call bind(context) firstPLAYBACK maps to AVAudioSessionCategoryPlayback. Interruptions come from AVAudioSessionInterruptionNotification.AudioSessionManager.bind(context) then setActive(true) requests AUDIOFOCUS_GAIN. Focus loss/gain is reported as interruptions.iOS / tvOS / watchOS — Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>Android — a media playback foreground service is still required for screen-off audio (Android 8+). Typical pieces:
FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PLAYBACK permissionsService with android:foregroundServiceType="mediaPlayback"
WAKE_LOCK if you hold a lock besides the serviceWithout that service, AudioStream will be killed when the app is backgrounded even if audio focus was granted.
Device QA (not automated here): start playback, background the app, confirm audio continues; place a call, confirm pause; end the call, confirm resume when shouldResume is true.
Klarinet exports a Kotlin/Native framework that native Swift apps can use directly:
import Klarinet // or ComposeApp if using the demo framework
// Create engine
let engine = AudioEngine.companion.create()
// Create a callback using the bridge class
let callback = AudioStreamCallbackImpl { buffer, numFrames in
let frames = numFrames.intValue
for i in 0..<frames {
buffer.set(index: Int32(i), value: sinf(phase) * 0.5)
}
return numFrames
}
// Configure and open stream
let config = AudioStreamConfig(
sampleRate: 48000,
channelCount: 1,
audioFormat: .pcmFloat,
bufferCapacityInFrames: 0,
performanceMode: .lowLatency,
sharingMode: .shared,
direction: .output
)
let stream = engine.openStream(config: config, callback: callback)
stream.start()
// Real-time level metering via polling
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
let level = stream.peakLevel // 0.0 to 1.0
}
// Cleanup
stream.stop()
stream.close()
engine.close()See CONTRIBUTING.md for development setup, build commands, and PR guidelines.
Copyright 2026 Vectencia
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Built with native performance by Vectencia
Low-latency audio SDK for Kotlin Multiplatform
Klarinet is an open-source Kotlin Multiplatform audio library that provides a unified, idiomatic API for low-latency audio playback, recording, file I/O, and real-time effects processing across 9 platforms and 14 KMP targets.
Write your audio code once in Kotlin. Klarinet delegates to the best native backend on each platform — Google Oboe on Android, AVAudioEngine on Apple, miniaudio on JVM/Linux/Windows, and the Web Audio API in the browser — while preserving low-latency characteristics and real-time safety.
Build cross-platform DAWs, beat makers, or loop stations. Klarinet's low-latency audio engine and real-time effects chain let users play instruments, apply reverb/delay/compression, and record --- all with professional-grade latency on both Android and iOS.
Add voice processing to chat apps, walkie-talkies, or podcast tools. Use the noise gate to suppress background noise, the compressor to normalize volume levels, and the EQ to shape voice tone --- all running natively in the audio callback with zero overhead.
Integrate dynamic audio into games without platform-specific code. Generate procedural sound effects with the callback API, apply spatial effects, and mix multiple audio sources. The low-latency performance mode ensures sounds play the instant they're triggered.
Build spectrum analyzers, tuners, or audio visualizers. Klarinet's levelFlow() provides real-time metering at 20+ updates/sec via Kotlin Flows, and the callback API gives you raw PCM samples for FFT, pitch detection, or waveform rendering.
Create hearing aids, sound amplifiers, or audio processing apps for accessibility. The real-time effects chain can boost specific frequencies (parametric EQ), compress dynamic range (limiter), or amplify quiet sounds (gain) --- all with the sub-10ms latency required for live audio passthrough.
Build music education apps with real-time feedback. Students play an instrument into the mic, your app analyzes pitch and timing via the callback, and displays results --- while simultaneously applying effects to make practice sound better.
Decode MP3/AAC/WAV files where the platform reader exists, apply effects (noise gate + compressor + EQ is a classic podcast chain), and encode WAV (AAC/M4A on Android and Apple except watchOS). MP3 cannot be encoded. Use AudioFileReader.asFlow() to stream large files without loading them entirely into memory.
klarinet-coroutines module with Flow-based metering and async I/O| Platform | Targets | Backend | Audio Streaming | File I/O (WAV) | File I/O (AAC/M4A) | Min Version |
|---|---|---|---|---|---|---|
| Android | arm64-v8a, armeabi-v7a, x86_64 | Google Oboe (AAudio / OpenSL ES) | ✅ | ✅ | ✅ | API 24 |
| iOS / iPadOS | arm64, simulatorArm64, x64 | AVAudioEngine | ✅ | ✅ | ✅ | iOS 15+ |
| macOS | arm64 | AVAudioEngine | ✅ | ✅ | ✅ | macOS 12+ |
| tvOS | arm64, simulatorArm64 | AVAudioEngine | ✅ | ✅ | ✅ | tvOS 15+ |
| watchOS | arm64, simulatorArm64 | AVAudioEngine | ✅ | ✅ | ❌ | watchOS 8+ |
| JVM Desktop | macOS, Linux, Windows | miniaudio (WASAPI / Core Audio / ALSA) | ✅ | ✅ | ❌ | JDK 11+ |
| Linux Native | x64, arm64 | miniaudio (ALSA / PulseAudio / JACK) | ✅ | ✅ | ❌ | — |
| Windows Native | x64 | miniaudio (WASAPI / DirectSound) | ✅ | ✅ | ❌ | — |
| JS / Browser | browser | Web Audio API | ✅ | ✅ | decode only | modern browsers |
watchOS support covers real-time audio streaming and WAV file writing. The following limitations apply due to the Kotlin/Native watchOS bindings not exposing the ExtAudioFile APIs:
| Feature | Status | Notes |
|---|---|---|
Audio playback (AudioStream output) |
✅ Supported | AVAudioEngine, same API as iOS/tvOS |
Audio recording (AudioStream input) |
✅ Supported | Requires mic permission; hardware mic available on Series 3+ |
| Audio session management | ✅ Supported | Full AVAudioSession integration |
WAV file writing (AudioFileWriter) |
✅ Supported | PCM encoding via posix I/O |
| AAC/M4A file writing | ❌ Not available | Throws UnsupportedFormatException
|
File reading (AudioFileReader) |
❌ Not available | Throws UnsupportedFormatException for all formats |
| Audio effects chain | ✅ Supported | Kotlin-side effect processing |
Swift interop (AudioStreamCallbackImpl) |
✅ Supported | Same callback bridge as iOS |
These limitations are a Kotlin/Native toolchain constraint, not a watchOS platform restriction. The underlying Apple APIs (ExtAudioFile) exist on watchOS but are not yet exposed in the K/N bindings.
JVM desktop support uses miniaudio for low-latency audio I/O across macOS, Linux, and Windows.
| Feature | Status | Notes |
|---|---|---|
Audio playback (AudioStream output) |
✅ Supported | miniaudio callback-driven, low latency |
Audio recording (AudioStream input) |
✅ Supported | miniaudio capture device |
| Audio session management | N/A | No audio session concept on desktop |
| WAV file read/write | ✅ Supported | via miniaudio decoder/encoder |
| MP3 file reading | ✅ Supported | via miniaudio (dr_mp3) |
| AAC/M4A file read/write | ❌ Not available | Throws UnsupportedFormatException
|
| Push-mode write/read | ❌ Not available | Use callback mode via AudioStreamCallback
|
| Audio effects | ✅ Supported | Shared C++ DSP core on the miniaudio callback |
Browser support uses the Web Audio API. The public Klarinet API is the same; the backend is not the C++ DSP core.
| Feature | Status | Notes |
|---|---|---|
Audio playback (AudioStream output) |
✅ Supported |
AudioWorklet renders; Kotlin onAudioReady runs on the main thread via the worklet port |
Audio recording (AudioStream input) |
✅ Supported |
getUserMedia on start(); state goes STARTING then STARTED
|
| Audio session management | N/A | No-op, same as JVM desktop |
| WAV file write | ✅ Supported | In-memory store (no disk). Re-read with AudioFileReader or export with audioFileWavBytes
|
| WAV / MP3 / AAC / M4A read | ✅ After decode | Call decodeAudioFile(url), decodeAudioBytes, putWavBytes, or putAudioFile before AudioFileReader
|
| Compressed file write | ❌ Not available | Throws UnsupportedFormatException
|
| Audio effects | ✅ Supported | Web Audio nodes mapped from Klarinet Studio (not bit-identical to C++ DSP) |
| Push-mode write/read | ✅ Non-blocking |
timeoutNanos is ignored; JS cannot block the main thread |
AudioStream.start() returns immediately. Observe onStreamStateChanged (or stateFlow()) for STARTED. Output sample rate is the AudioContext rate.
decodeAudioFile("https://example.com/song.mp3").then { reader ->
val stream = engine.playFile("https://example.com/song.mp3")
stream.start()
reader
}| Platform | Reason |
|---|---|
| Wasm / WasmJS | Not implemented. Use the JS / Browser target. |
// build.gradle.kts
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.vectencia.klarinet:klarinet:0.5.0")
// Optional: Coroutines extensions
implementation("com.vectencia.klarinet:klarinet-coroutines:0.5.0")
}
}
}Klarinet exports a Kotlin/Native framework that Swift can import directly:
import Klarinet
let engine = AudioEngine.companion.create()
// ... use the full Klarinet API from SwiftSee the SwiftUI Demo for a complete example.
A one-file JVM program that plays a 440 Hz sine for one second:
./gradlew :sample:run
./gradlew :sample:run --args=fade # 2s fade 0→full, hold, 2s full→0
./gradlew :sample:run --args=sleep # 1s play, 1s fade, stream stops
./gradlew :sample:run --args=scene # 440 Hz → 2s crossfade to 660 HzThe source is in sample/src/main/kotlin/Main.kt.
import com.vectencia.klarinet.*
import kotlin.math.sin
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()
// ... audio is playing ...
stream.stop()
stream.close()
engine.close()val engine = AudioEngine.create()
val stream = engine.openStream(
config = AudioStreamConfig(
sampleRate = 48000,
channelCount = 1,
direction = StreamDirection.INPUT,
),
callback = object : AudioStreamCallback {
override fun onAudioReady(buffer: FloatArray, numFrames: Int): Int {
// Process captured audio in `buffer`...
return numFrames
}
},
)
stream.start()
// ... recording ...
stream.stop()
stream.close()
engine.close()Klarinet includes 16 built-in effects powered by a shared C++ DSP core. Effects run natively in the audio callback --- zero JNI overhead on Android, zero interop overhead on Apple.
val engine = AudioEngine.create()
// Create effects
val reverb = engine.createEffect(AudioEffectType.REVERB)
reverb.setParameter(ReverbParams.ROOM_SIZE, 0.7f)
reverb.setParameter(ReverbParams.WET_DRY_MIX, 0.3f)
val compressor = engine.createEffect(AudioEffectType.COMPRESSOR)
compressor.setParameter(CompressorParams.THRESHOLD, -20f)
compressor.setParameter(CompressorParams.RATIO, 4f)
// Build an effect chain
val chain = engine.createEffectChain()
chain.add(compressor)
chain.add(reverb)
// Attach to a stream
val stream = engine.openStream(config, callback)
stream.effectChain = chain
stream.start()
// Update parameters in real-time (lock-free, audio-thread safe)
reverb.setParameter(ReverbParams.WET_DRY_MIX, 0.6f)
// Hot-swap: add/remove effects while audio is playing
chain.add(engine.createEffect(AudioEffectType.DELAY))
chain.remove(compressor)| Category | Effects |
|---|---|
| Basic | Gain, Pan, Mute/Solo |
| Dynamics | Compressor, Limiter, Noise Gate |
| EQ / Filters | Parametric EQ (8-band), Low-Pass, High-Pass, Band-Pass |
| Time-based | Delay, Reverb (Freeverb) |
| Modulation | Chorus, Flanger, Phaser, Tremolo |
Decode support is not universal: watchOS has no AudioFileReader; JVM and native desktop cannot read AAC/M4A; JS must decodeAudioFile / putWavBytes / putAudioFile first. Encode is WAV on every platform (JS in-memory), AAC/M4A on Android and iOS/macOS/tvOS, and never MP3.
val reader = AudioFileReader("/path/to/song.mp3")
val info = reader.info
println("Format: ${info.format}") // MP3
println("Duration: ${info.durationMs} ms") // 210000
println("Sample rate: ${info.sampleRate}") // 44100
println("Channels: ${info.channelCount}") // 2
val tags = info.tags
println("Title: ${tags.title}") // "My Song"
println("Artist: ${tags.artist}") // "Artist Name"
reader.close()val reader = AudioFileReader("/path/to/audio.wav")
// Read all at once
val samples = reader.readAll() // FloatArray of interleaved PCM [-1.0, 1.0]
// Or stream in chunks
reader.seekTo(0)
while (!reader.isAtEnd) {
val chunk = reader.readFrames(maxFrames = 4096)
// Process chunk...
}
reader.close()// Play a file
val stream = engine.playFile("/path/to/song.mp3")
stream.start()
// Record to a file
val stream = engine.recordToFile(
filePath = "/path/to/recording.wav",
format = AudioFileFormat.WAV,
)
stream.start()The klarinet-coroutines module provides Flow-based observation and suspending file I/O without adding coroutines as a dependency to the core module.
import com.vectencia.klarinet.coroutines.*
// Observe stream state reactively (cold Flow; cancel the collector to stop polling)
stream.stateFlow().collect { state ->
println("Stream state: $state")
}
// Real-time audio level metering (20 updates/sec)
stream.levelFlow().collect { level ->
// Update VU meter UI (0.0 to 1.0)
}
// Wait for a stream state
stream.awaitState(StreamState.STARTED)
// Sleep timer countdown (polls remainingMs / state)
timer.remainingMsFlow().collect { msLeft -> /* UI */ }
timer.awaitState(SleepTimerState.COMPLETED)
// Interruptions (fans out; does not replace observeInterruptions)
session.interruptionFlow().collect { info -> /* UI / logging */ }
// Async file I/O (Dispatchers.IO, or Default on JS)
val samples = reader.readAllSuspend()
writer.writeFramesSuspend(samples, samples.size)
// Stream decoded audio as a Flow
reader.asFlow(chunkSize = 4096).collect { chunk ->
// Process each chunk
}graph TD
A["Your App (Kotlin/Swift)"] --> B[Klarinet SDK]
B --> C["C++ DSP Core<br/>(Effects, Filters, Reverb)"]
B --> D["Google Oboe<br/>(AAudio / OpenSL ES)"]
B --> E["AVAudioEngine<br/>(Core Audio)"]
B --> F["miniaudio<br/>(WASAPI / Core Audio / ALSA)"]
B --> G["Web Audio API<br/>(browser)"]
style A fill:#7F52FF,color:#fff
style B fill:#1a1a2e,color:#0ff
style C fill:#16213e,color:#0ff
style D fill:#0d7377,color:#fff
style E fill:#0d7377,color:#fff
style F fill:#0d7377,color:#fff
style G fill:#0d7377,color:#fffKlarinet is a single Kotlin Multiplatform module using expect/actual declarations:
commonMain --- Public API: AudioEngine, AudioStream, AudioStreamConfig, AudioStreamCallback, AudioEffect, AudioEffectChain, data classes, enumsandroidMain --- Android implementation via Google Oboe (C++17/JNI). The Oboe callback never enters the JVM: Kotlin onAudioReady runs on a worker thread and samples cross a lock-free FIFO. Effects still process on the audio thread.appleMain --- Apple implementation via AVAudioEngine. Shared across iOS, macOS, tvOS, and watchOS.jvmMain --- JVM desktop implementation via miniaudio. Covers macOS, Linux, and Windows from a single target with platform-specific native libraries bundled in the JAR.nativeDesktopMain --- Shared Linux (linuxX64, linuxArm64) and Windows (mingwX64) native implementation via miniaudio (cinterop).jsMain --- Browser implementation via the Web Audio API. Effects are Web Audio nodes; file I/O is an in-memory store plus decodeAudioData.cpp/dsp --- Shared C++ DSP library compiled for all native platforms. Contains all 16 effects, DSP primitives (Biquad, LFO, EnvelopeFollower, CircularBuffer), lock-free effect chain, and SPSC ring buffer.| Module | Artifact | Description |
|---|---|---|
klarinet |
com.vectencia.klarinet:klarinet |
Core SDK: audio I/O, effects, file I/O |
klarinet-android |
com.vectencia.klarinet:klarinet-jni |
Android Oboe/C++ JNI backend (pulled in by the KMP Android target) |
klarinet-coroutines |
com.vectencia.klarinet:klarinet-coroutines |
Optional Flow & suspending extensions |
demo |
--- | Shared Compose Multiplatform demo UI (Android, iOS, Desktop) |
demo-android |
--- | Android application entry point |
demo-native |
--- | Native console demo apps (Linux, Windows) |
demo-web |
--- | Browser demo (Kotlin/JS + Web Audio) |
sample |
--- | One-file JVM sine-wave sample |
iosApp |
--- | Native SwiftUI demo app (iOS, tvOS, watchOS) |
The project includes demo applications for every supported platform:
The same six screens as the Compose demo, plus local-file decode, URL decode, mic recording, and WAV download. Uses the Klarinet JS target.
Production (GitHub Pages):
/api/
The Pages workflow builds on every main push. Deploy is skipped with a warning if GET /repos/{owner}/{repo}/pages fails (Pages not enabled; typical on a new fork). Layout, enablement, local production preview, CORS/mic notes, and troubleshooting: GITHUB_PAGES.md.
# webpack-dev-server (not the Pages bundle)
./gradlew :demo-web:jsBrowserDevelopmentRun
# URL Gradle prints, usually http://localhost:8080
# same public/ tree as GitHub Pages (demo at /, Dokka at /api/)
make pages
python3 -m http.server 8080 --directory publicSix destinations via scrollable top tabs: Tone Gen (SleepTimer), Mic Meter, Latency, File, Effects (FADE_MS), Scenes (AudioScenePlayer). Runs on Android, iOS, and JVM Desktop (macOS, Linux, Windows) from shared Compose UI code. On Android the host calls DemoSession.manager.bind and streams use attach.
# Run desktop demo
./gradlew demo:runNative SwiftUI demo for each Apple platform. The iOS app mirrors Compose with six scrollable top-tab destinations (Tone Gen with SleepTimer, Mic Meter, Latency, File, Effects with FADE_MS, Scenes with AudioScenePlayer) and MVVM; tvOS and watchOS remain smoke-test tone UIs adapted for their form factors.
Minimal command-line demos that play a 440 Hz sine wave for 3 seconds, proving low-latency audio works on each native target.
Hosted Dokka HTML (same Pages deploy as the web demo): vectencia.github.io/Klarinet/api/. Generate locally with ./gradlew :dokkaGenerate (build/dokka/html/).
| Type | Description |
|---|---|
AudioEngine |
Entry point. Creates streams, effects, and queries devices. |
AudioStream |
Playback or recording stream with lifecycle control. |
AudioStreamConfig |
Stream configuration: sample rate, channels, format, direction. |
AudioStreamCallback |
Real-time callback for audio processing (onAudioReady). |
AudioEffect |
A single audio effect with typed parameters. |
AudioEffectChain |
Ordered chain of effects attached to a stream. |
AudioFileReader |
Decodes WAV/MP3/AAC/M4A to PCM where the platform decoder exists (not watchOS; JVM/native have no AAC/M4A; JS after decode). |
AudioFileWriter |
Encodes PCM to WAV on every platform; AAC/M4A on Android and iOS/macOS/tvOS. MP3 encode is not supported. |
AudioDeviceInfo |
Information about an audio input/output device. |
AudioSessionManager |
Session category, Android audio focus, interruption hooks. |
AudioScene / AudioSceneJson
|
Portable layer mix (ids, levels, effect params) as JSON. |
AudioScenePlayer |
Crossfades scenes with [GainParams.FADE_MS]; host opens each layer stream. |
LatencyInfo |
Input/output latency measurements. |
| Enum | Values |
|---|---|
AudioFormat |
PCM_FLOAT, PCM_I16, PCM_I24, PCM_I32
|
StreamDirection |
OUTPUT, INPUT
|
PerformanceMode |
NONE, LOW_LATENCY, POWER_SAVING
|
SharingMode |
SHARED, EXCLUSIVE
|
StreamState |
UNINITIALIZED, OPEN, STARTING, STARTED, PAUSING, PAUSED, STOPPING, STOPPED, CLOSING, CLOSED
|
AudioEffectType |
GAIN, PAN, MUTE_SOLO, COMPRESSOR, LIMITER, NOISE_GATE, PARAMETRIC_EQ, LOW_PASS_FILTER, HIGH_PASS_FILTER, BAND_PASS_FILTER, DELAY, REVERB, CHORUS, FLANGER, PHASER, TREMOLO
|
AudioFileFormat |
WAV, MP3, AAC, M4A
|
KlarinetException
+-- StreamCreationException
+-- StreamOperationException
+-- DeviceNotFoundException
+-- AudioSessionException
+-- PermissionException
+-- UnsupportedFormatException
+-- AudioFileException
+-- ResourceReleasedException
# All modules
./gradlew build
# Library only
./gradlew :klarinet:build
# Demo app (Android)
./gradlew :demo-android:assembleDebug
# Demo app (Desktop)
./gradlew :demo:run
# iOS app (via Xcode)
open iosApp/iosApp.xcodeproj
# Native console demos
./gradlew :demo-native:linkReleaseExecutableLinuxX64
./gradlew :demo-native:linkReleaseExecutableMingwX64# Kotlin tests (all platforms)
./gradlew :klarinet:allTests
# JS browser tests (Chrome Headless)
./gradlew :klarinet:jsBrowserTest :klarinet-coroutines:jsBrowserTest
# C++ DSP tests
./gradlew :klarinet:dspTests
# Coroutines module tests
./gradlew :klarinet-coroutines:allTests
# API docs (HTML)
./gradlew :dokkaGenerate
# output: build/dokka/html/index.html| Command | Description |
|---|---|
make build |
Build all modules |
make klarinet |
Build the library only |
make demo |
Build the demo app (Android) |
make sample |
Run the one-file JVM sine-wave sample |
make docs |
Generate Dokka HTML API docs (build/dokka/html/) |
make pages |
Production web demo + Dokka into public/ (GitHub Pages layout) |
make test |
Run Kotlin tests (:klarinet:allTests) |
make test-coroutines |
Run klarinet-coroutines tests |
make test-js |
Run JS browser tests (Chrome Headless) |
make test-dsp |
Run C++ DSP tests |
make test-all |
Run Klarinet + coroutines Kotlin tests, C++ DSP, iOS simulator, and consumer DSP link tests |
make clean |
Clean build artifacts |
make publish |
Publish to Maven Central |
Presets are data. Ship your own JSON (assets, files, network). Layer id is an opaque key; Klarinet never loads audio files from the scene document.
{
"id": "rain-night",
"layers": [
{ "id": "rain", "gainDb": -6.0, "effects": [{ "type": "REVERB", "params": { "0": 0.5 } }] },
{ "id": "wind", "gainDb": -12.0 }
]
}val scene = AudioSceneJson.decode(json)
AudioScenePlayer(engine).use { player ->
player.transitionTo(scene, fadeMs = 3000f) { layer ->
engine.openStream(config, callbackFor(layer.id))
}
}Same layer id keeps the stream and retargets gain. Other layers fade out and in so A → B has no hole. Close the player to release streams and chains.
Klarinet does not start a foreground service or draw a notification. The host must still satisfy platform background rules. The SDK exposes session category, audio focus, and interruption events so a call does not leave the stream silently dead.
val session = AudioSessionManager()
session.configure(AudioSessionCategory.PLAYBACK, AudioSessionMode.DEFAULT)
session.attach(stream) // pause on call, start again when shouldResume
session.observeInterruptions { /* optional UI */ }
session.setActive(true) // Android: call bind(context) firstPLAYBACK maps to AVAudioSessionCategoryPlayback. Interruptions come from AVAudioSessionInterruptionNotification.AudioSessionManager.bind(context) then setActive(true) requests AUDIOFOCUS_GAIN. Focus loss/gain is reported as interruptions.iOS / tvOS / watchOS — Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>Android — a media playback foreground service is still required for screen-off audio (Android 8+). Typical pieces:
FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PLAYBACK permissionsService with android:foregroundServiceType="mediaPlayback"
WAKE_LOCK if you hold a lock besides the serviceWithout that service, AudioStream will be killed when the app is backgrounded even if audio focus was granted.
Device QA (not automated here): start playback, background the app, confirm audio continues; place a call, confirm pause; end the call, confirm resume when shouldResume is true.
Klarinet exports a Kotlin/Native framework that native Swift apps can use directly:
import Klarinet // or ComposeApp if using the demo framework
// Create engine
let engine = AudioEngine.companion.create()
// Create a callback using the bridge class
let callback = AudioStreamCallbackImpl { buffer, numFrames in
let frames = numFrames.intValue
for i in 0..<frames {
buffer.set(index: Int32(i), value: sinf(phase) * 0.5)
}
return numFrames
}
// Configure and open stream
let config = AudioStreamConfig(
sampleRate: 48000,
channelCount: 1,
audioFormat: .pcmFloat,
bufferCapacityInFrames: 0,
performanceMode: .lowLatency,
sharingMode: .shared,
direction: .output
)
let stream = engine.openStream(config: config, callback: callback)
stream.start()
// Real-time level metering via polling
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
let level = stream.peakLevel // 0.0 to 1.0
}
// Cleanup
stream.stop()
stream.close()
engine.close()See CONTRIBUTING.md for development setup, build commands, and PR guidelines.
Copyright 2026 Vectencia
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Built with native performance by Vectencia