
Send and receive short messages via sound using FSK-based audio encoding; manages microphone/speaker or supplies PCM encoder/decoder, supports audible, ultrasound and dual-tone protocols with speed options.
https://github.com/user-attachments/assets/adafe305-122d-4f5c-bad2-5d8f4545c7d1
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 throws GgwaveException if playback does not finish within 60 seconds. Calls are serialized, one message at a time.send(message, protocol) overrides GgwaveConfig.protocol for one message, for example GgwaveProtocol.UltrasoundFast for an inaudible transfer.isCapturing, isPlaying and captureLevel (input peak, 0 to 1) are StateFlows for UI state.GgwaveSession can 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()GgwaveCodec.MAX_PAYLOAD_SIZE).GgwaveProtocol offers audible, ultrasound and dual-tone variants, each in normal, fast and fastest speeds.GgwaveCodecConfig sets the input and output sample rates (default 48,000 Hz).ggwave-kmp manifest declares RECORD_AUDIO, but your app must request it at runtime before startCapture(). The library shows no permission dialog. The sample does this in androidApp.NSMicrophoneUsageDescription to your Info.plist. The library asks for microphone permission when capture starts. A denial arrives through captureErrors.-Djava.library.path is needed. Other platforms fail with a GgwaveException.| 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.
https://github.com/user-attachments/assets/adafe305-122d-4f5c-bad2-5d8f4545c7d1
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 throws GgwaveException if playback does not finish within 60 seconds. Calls are serialized, one message at a time.send(message, protocol) overrides GgwaveConfig.protocol for one message, for example GgwaveProtocol.UltrasoundFast for an inaudible transfer.isCapturing, isPlaying and captureLevel (input peak, 0 to 1) are StateFlows for UI state.GgwaveSession can 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()GgwaveCodec.MAX_PAYLOAD_SIZE).GgwaveProtocol offers audible, ultrasound and dual-tone variants, each in normal, fast and fastest speeds.GgwaveCodecConfig sets the input and output sample rates (default 48,000 Hz).ggwave-kmp manifest declares RECORD_AUDIO, but your app must request it at runtime before startCapture(). The library shows no permission dialog. The sample does this in androidApp.NSMicrophoneUsageDescription to your Info.plist. The library asks for microphone permission when capture starts. A denial arrives through captureErrors.-Djava.library.path is needed. Other platforms fail with a GgwaveException.| 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.