soundscape
0.4.0indexedState-of-the-art audio toolkit: playback, recording, DSP effects, HLS streaming, background media controls, pluggable transcription and Compose UI components driven by a single coherent API.
State-of-the-art audio toolkit: playback, recording, DSP effects, HLS streaming, background media controls, pluggable transcription and Compose UI components driven by a single coherent API.
A state-of-the-art audio library for Kotlin Multiplatform / Compose Multiplatform. Playback, recording, effects, streaming, background controls, and pluggable transcription, with one coherent API across Android, iOS, macOS native, Desktop JVM, and WebAssembly.
The KMP/CMP audio space is a patchwork of half-finished solo libraries. Some do playback, some do recording, none cover the full stack with parity across targets. soundscape is the one library that ships a single coherent surface across every CMP target.
In v0.3:
soundscape-transcription-whisper (whisper.cpp via whisper-jni, Desktop JVM)soundscape-desktop-ffmpeg (AAC/ALAC/Opus/etc on Desktop via JavaCV's GPL FFmpeg bundle - GPL license applies)Deferred to v0.3.x point release (CHANGELOG.md has the rationale):
MTAudioProcessingTap (design doc captured; needs MediaToolbox cinterop work).// build.gradle.kts
commonMain.dependencies {
implementation(project.dependencies.platform(libs.soundscape.bom))
implementation(libs.soundscape.player)
implementation(libs.soundscape.ui.compose)
}
import io.github.nadeemiqbal.soundscape.core.AudioSource
import io.github.nadeemiqbal.soundscape.player.MediaItem
import io.github.nadeemiqbal.soundscape.player.SoundscapePlayer
val player = SoundscapePlayer.create()
player.setQueue(
items = listOf(
MediaItem(id = "1", source = AudioSource.Url("https://example.com/track-a.mp3")),
MediaItem(id = "2", source = AudioSource.Url("https://example.com/track-b.mp3")),
),
)
player.play() // fire-and-forget; observe via player.state / position
Transport methods (setQueue, play, , , , , ) are intentionally so the WASM backend can call synchronously from a user-gesture event handler. The browser autoplay policy rejects deferred calls. Wire them directly into Compose lambdas without .
val recorder = Recorder.create()
val session = recorder.start(RecordingConfig.preset(RecordingConfig.Quality.Voice, "/path/to/out.wav"))
recorder.levels.collect { rms -> println("level=$rms") }
val result = recorder.stop()
// result.outputPath is a filesystem path on Android/Apple/Desktop, or a blob URL on WASM.
val chain = EffectsChain()
.equalizer(EffectsChain.FlatEq10Band.map { it.copy(gainDb = 3f) })
.reverb(ReverbPreset.Hall, wetDryMix = 0.4f)
.compressor(thresholdDb = -12f, ratio = 4f)
.gain(db = -3f)
player.setEffectsChainHandle(chain)
// Android: native audiofx attached to the ExoPlayer audio session.
// Apple: AVAudioEngine path activates for AudioSource.File items.
// Web: Web Audio graph wired between the audio element and destination.
// Desktop: pure-Kotlin DSP inserted in the PCM pipeline.
val bg = BackgroundController.create()
bg.bind(player)
bg.setMetadata(Metadata(title = "Track A", artist = "Artist", durationMs = 240_000))
bg.transportActions.collect { action -> /* react to lockscreen / media-key events */ }
// In your Application.onCreate
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
AndroidContextHolder.install(this)
}
}
Add to your manifest if you use the Recorder:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
samples/unified/ is a single Compose Multiplatform app that runs on every target with seven tabs covering every module (Player, Recorder, Effects, Streaming, Background, Transcribe, Visualizer).samples/platforms/ ships separate native-idiomatic shells for Android, Desktop, Web, and iOS that demonstrate the platform-only OS integrations (MediaSession on Android, NowPlaying on iOS/macOS, etc).Run the desktop sample:
./gradlew :samples:unified:desktopApp:run
Each module publishes its own artifact and depends only on :core plus the modules it logically needs. Use the BOM to pin every artifact at one version.
ui-compose -> player, recorder, effects -+
background -> player --------------------+
streaming -> player --------------------+-> core
transcription --------------------------+
player -> effects (for backend offload bridges) ----+
Flow-driven throughout. StateFlow for state, Flow for streams (position, levels, transcripts), sealed for errors. Transport methods (, , , , ) are non-suspend so WASM can call them inside the same JS turn as the user gesture; everything else that does real I/O (, ) remains .
MTAudioProcessingTap, Desktop OS controls (Linux MPRIS / Windows SMTC / macOS NowPlaying-via-JVM), soundscape-transcription-whisper (whisper.cpp via JNI), soundscape-desktop-ffmpeg (AAC/ALAC on Desktop), lower-latency Android engine (AAudio).PRs welcome. See CONTRIBUTING.md.
Apache 2.0. See LICENSE.
MediaRecorder + getUserMedia and returns a blob URL. AAC/Opus where the platform encoder is natively available.commonMain for unit tests, with real platform offload in v0.2:
android.media.audiofx (Equalizer + PresetReverb + DynamicsProcessing limiter on API 28+ + LoudnessEnhancer).AVAudioEngine + AVAudioUnitEQ + AVAudioUnitReverb (file-source playback; URL streams await MTAudioProcessingTap in v0.3).MediaElementAudioSourceNode → BiquadFilterNode chain → DynamicsCompressorNode → ConvolverNode reverb → GainNode).commonMain, ExoPlayer HLS/DASH on Android, HLS native on Apple, hls.js on WASM.MediaSession + lockscreen, iOS/macOS native MPNowPlayingInfoCenter + MPRemoteCommandCenter, navigator.mediaSession on WASM. Desktop JVM (Linux MPRIS / Windows SMTC / macOS NowPlaying via JNA) is a structural shell in v0.2; full method dispatch lands in v0.3. macOS users should target macosArm64 natively for production NowPlaying coverage.Transcriber SPI. Native SFSpeechRecognizer / SpeechRecognizer / Web Speech in v0.2; whisper.cpp adapter ships as a separate soundscape-transcription-whisper artifact in v0.3 (native binary distribution).Waveform, Scrubber, PlayerControls, EqualizerView, LevelMeter, RecorderButton driven by the player/recorder Flows.| Module | Android | iOS | macOS native | Desktop JVM | WASM |
|---|
soundscape-core | ✅ | ✅ | ✅ | ✅ | ✅ |
soundscape-player | ✅ | ✅ | ✅ | ✅ | ✅ |
soundscape-recorder | ✅ | ✅ | ✅ | ✅ | ✅ MediaRecorder |
soundscape-effects (offload) | ✅ audiofx | ✅ AVAudioEngine (file sources) | ✅ AVAudioEngine | ✅ pure-Kotlin DSP | ✅ Web Audio |
soundscape-streaming | ✅ HLS+DASH | ✅ HLS | 🚧 v0.3 | ✅ HLS | ✅ HLS |
soundscape-background | ✅ MediaSession | ✅ NowPlaying | ✅ NowPlaying | 🚧 v0.3 (use macosArm64 instead) | ✅ MediaSession API |
soundscape-transcription | ✅ SpeechRecognizer | ✅ SFSpeechRecognizer | ✅ SFSpeechRecognizer | 🚧 v0.3 via -whisper | ✅ Web Speech (Chromium) |
soundscape-ui-compose | ✅ | ✅ | ✅ | ✅ | ✅ |
# gradle/libs.versions.toml
[versions]
soundscape = "0.4.0"
[libraries]
soundscape-bom = { module = "io.github.nadeemiqbal:soundscape-bom", version.ref = "soundscape" }
soundscape-core = { module = "io.github.nadeemiqbal:soundscape-core" }
soundscape-player = { module = "io.github.nadeemiqbal:soundscape-player" }
soundscape-recorder = { module = "io.github.nadeemiqbal:soundscape-recorder" }
soundscape-effects = { module = "io.github.nadeemiqbal:soundscape-effects" }
soundscape-streaming = { module = "io.github.nadeemiqbal:soundscape-streaming" }
soundscape-background = { module = "io.github.nadeemiqbal:soundscape-background" }
soundscape-transcription = { module = "io.github.nadeemiqbal:soundscape-transcription" }
soundscape-ui-compose = { module = "io.github.nadeemiqbal:soundscape-ui-compose" }
pausestopseekToskipToNextskipToPrevHTMLAudioElement.play()onClickscope.launch {}SoundscapeExceptionplaypauseseekTosetQueueskip*Recorder.startTranscriber.transcribesuspendSurfaced from shared tags and platforms — no rankings paid for.