demo-readme.mp4
ggwave-kmp sends and receives short messages through sound on Android, iOS and the JVM (Windows, macOS, Linux). It is a Kotlin Multiplatform wrapper around ggwave, a C++ library that encodes data as audio using an FSK-based protocol (technical details).
| Module | Artifact | Use it when |
|---|---|---|
:ggwave |
io.github.wooram-yang:ggwave-kmp |
You want the library to handle the microphone and speaker. |
:ggwave-core |
io.github.wooram-yang:ggwave-core |
Your app already owns audio input and output and only needs encoding and decoding. |
ggwave-kmp depends on ggwave-core, so add only one of them. :sampleApp is a Compose Multiplatform chat app built on :ggwave.
Both artifacts are published to Maven Central.
repositories {
mavenCentral()
}
kotlin {
sourceSets.commonMain.dependencies {
implementation("io.github.wooram-yang:ggwave-kmp:0.4.0")
// or, for encoding and decoding only:
// implementation("io.github.wooram-yang:ggwave-core:0.4.0")
}
}GgwaveSession owns the audio devices. Incoming messages and state are exposed as flows.
import io.github.moreflow.ggwave.GgwaveSession
val session = GgwaveSession.create()
// `scope` is a CoroutineScope you own, for example viewModelScope.
// Receive: collect first, then start the microphone.
scope.launch {
session.receivedMessages.collect { message -> println("received: $message") }
}
scope.launch {
session.captureErrors.collect { error -> println("capture failed: ${error.message}") }
}
session.startCapture()
// Send: suspends until playback finishes.
scope.launch {
session.send("hello")
}
// When done.
session.stopCapture()
session.close()send()rethrows the error if encoding or playback fails, and throwsGgwaveExceptionif playback does not finish within 60 seconds. Calls are serialized, one message at a time.send(message, protocol)overridesGgwaveConfig.protocolfor one message, for exampleGgwaveProtocol.UltrasoundFastfor an inaudible transfer.isCapturing,isPlayingandcaptureLevel(input peak, 0 to 1) areStateFlows for UI state.- Only one
GgwaveSessioncan be active per process.create()throws until the previous session is closed.
GgwaveSession.create() accepts a GgwaveConfig:
| Property | Default | Meaning |
|---|---|---|
sampleRate |
48_000 |
Capture and playback sample rate, 8,000 to 96,000 Hz. |
captureBufferSize |
4096 |
Capture buffer size in bytes; must be a positive even number. |
volume |
50 |
Playback volume passed to ggwave, 0 to 100. |
protocol |
AudibleFast |
Default protocol for send(). Receiving detects every protocol automatically. |
| Family | Character |
|---|---|
Audible* |
Audible tones. The most reliable choice. |
Ultrasound* |
Nearly inaudible, about 15 to 19.5 kHz. This is close to the limit of phone speakers, so reception varies with the device pair, distance and orientation. iPhone speakers in particular emit it weakly. |
DualTone* |
Two tones played at once. |
Each family comes in Normal, Fast and Fastest. Faster variants send sooner but are harder to pick up when the signal is weak.
Use ggwave-core when you already have a PCM pipeline (16-bit mono samples):
import io.github.moreflow.ggwave.GgwaveCodec
import io.github.moreflow.ggwave.GgwaveProtocol
val encoder = GgwaveCodec.create()
val pcm: ShortArray = encoder.encode(
"hello".encodeToByteArray(),
protocol = GgwaveProtocol.AudibleFast,
volume = 50,
)
encoder.close()
val decoder = GgwaveCodec.create()
val payload: ByteArray? = decoder.decode(capturedPcm) // null until a full message is decoded
decoder.close()- Payloads can be up to 140 bytes (
GgwaveCodec.MAX_PAYLOAD_SIZE). - Each codec owns its own native instance and keeps decoder state between calls. Use separate instances for encoding and decoding, and don't call one instance from several threads at once.
GgwaveProtocoloffers audible, ultrasound and dual-tone variants, each in normal, fast and fastest speeds.GgwaveCodecConfigsets the input and output sample rates (default 48,000 Hz).
- Android: the
ggwave-kmpmanifest declaresRECORD_AUDIO, but your app must request it at runtime beforestartCapture(). The library shows no permission dialog. The sample does this inandroidApp. - iOS: add
NSMicrophoneUsageDescriptionto yourInfo.plist. The library asks for microphone permission when capture starts. A denial arrives throughcaptureErrors. - JVM: the native library is bundled in the artifact for Windows x64, Linux x64/arm64 and macOS x64/arm64, and loaded automatically. No
-Djava.library.pathis needed. Other platforms fail with aGgwaveException.
| 1. Press the receive button to listen | 2. Type a message on another device and send it | 3. The message appears on the receiver |
|---|---|---|
![]() |
![]() |
![]() |
Pick the send protocol from the dropdown above the input field.
| Needed for | Requirement |
|---|---|
| Everything | JDK 21 (Gradle can provision it), CMake 3.20 or newer, Ninja |
| Android | Android SDK and NDK 29.0.14206865 (GGWAVE_ANDROID_NDK_VERSION in gradle.properties, or set ANDROID_NDK_HOME) |
| iOS | macOS with Xcode |
| Windows JVM | MinGW-w64 (gcc, g++) |
| macOS JVM | Xcode Command Line Tools (clang, clang++) |
| Linux JVM | gcc, g++ and JDK headers |
Tested with Android Studio Panda 4.
| Platform | How |
|---|---|
| Desktop (JVM) | ./gradlew :sampleApp:run |
| Android | ./gradlew :androidApp:installDebug, or run androidApp from Android Studio |
| iOS | Set TEAM_ID in iosApp/Configuration/Config.xcconfig, then open iosApp/iosApp.xcodeproj in Xcode and run |
./gradlew :ggwave-core:jvmTest :ggwave:jvmTest
./gradlew :ggwave-core:publishToMavenLocal :ggwave:publishToMavenLocalAdd mavenLocal() to your repositories to use a locally published build. Release publishing is described in PUBLISHING.md.
ggwave-kmp is released under the MIT License. It includes a vendored copy of ggwave and other third-party code; see THIRD_PARTY_NOTICES.md.



