
Bindings to WebRTC's resampler, exposing integer multi-rate and high-quality windowed-sinc resamplers with push/pull APIs, arbitrary-rate conversion, short/float multi-channel support, and bit-exact 1:1.
Kotlin Multiplatform bindings for the WebRTC resampler — the resampler extracted from WebRTC's common_audio/resampler, with its signal processing subset. It covers both the integer multi-rate resampler (webrtc::Resampler) and the high quality windowed sinc resamplers (webrtc::SincResampler, webrtc::PushSincResampler, webrtc::PushResampler<T>) that handle arbitrary ratios such as 44.1 kHz ⟷ 48 kHz.
| Platform | Targets | Mechanism |
|---|---|---|
| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | JNI (shared library via CMake) |
| Android (Kotlin/Native) | arm64-v8a, armeabi-v7a, x86, x86_64 | Kotlin/Native cinterop (static library) |
| JVM | Linux x86_64/aarch64, macOS arm64/x86_64, Windows x86_64 | JNI (per-OS/arch JAR resource, auto-extracted by NativeLoader) |
| iOS | arm64, x64, simulatorArm64 | Kotlin/Native cinterop (static library) |
| macOS | arm64, x86_64 | Kotlin/Native cinterop (static library) |
| Linux | x86_64 | Kotlin/Native cinterop (static library) |
| Windows | mingwX64 | Kotlin/Native cinterop (static library) |
| tvOS | arm64, simulatorArm64 | Kotlin/Native cinterop (static library) |
| watchOS | arm64, simulatorArm64, deviceArm64 | Kotlin/Native cinterop (static library) |
Kotlin Multiplatform / Android:
implementation("cn.enaium.webrtc.resampler:webrtc-resampler-kmp:1.0.1")Built with Kotlin 2.4.10: consumers need a Kotlin 2.4+ compiler, since older ones cannot read the 2.4 metadata of the published artifacts.
JVM: the right native binary is resolved automatically — the webrtc-resampler-kmp-jvm artifact pulls in the matching :jni-jvm-* sibling on the classpath:
webrtc-resampler-kmp-jni-jvm-linux-x86_64webrtc-resampler-kmp-jni-jvm-linux-aarch64webrtc-resampler-kmp-jni-jvm-darwin-x86_64webrtc-resampler-kmp-jni-jvm-darwin-aarch64webrtc-resampler-kmp-jni-jvm-windows-x86_64NativeLoader detects os.name/os.arch at runtime, extracts the matching binary from the classpath to a temp directory, and System.loads it. No java.library.path setup is required for downstream JVM consumers.
| Binding | Input | Ratios | Channels | Interface |
|---|---|---|---|---|
Resampler |
ShortArray |
fixed WebRTC pairs only (8/16/32/44/48/96 kHz combinations) | 1 or 2 | push |
PushSincResampler |
ShortArray, FloatArray
|
arbitrary (44.1 kHz ⟷ 48 kHz, …) | 1 | push |
PushResamplerInt16 / PushResamplerFloat
|
ShortArray / FloatArray
|
arbitrary | ≤ 8 | push |
SincResampler |
FloatArray |
arbitrary | 1 | pull |
Resampler rejects rate pairs its fixed filter bank cannot express; use one of the sinc resamplers for those (they cost more but are not limited to integer ratios). A Resampler is also the only resampler that is bit exact for 1:1 (same rate in and out).
import cn.enaium.webrtc.resampler.createResampler
import cn.enaium.webrtc.resampler.createPushSincResampler
// Fixed pair, int16, 48 kHz -> 16 kHz, one 10 ms block at a time.
createResampler(48000, 16000, 1).use { resampler ->
val input = ShortArray(480) // 10 ms at 48 kHz
val output = ShortArray(160) // 10 ms at 16 kHz
val written = resampler.resample(input, output)
check(written == 160)
}
// Arbitrary ratio, float, 48 kHz -> 44.1 kHz.
createPushSincResampler(480, 441).use { resampler ->
val input = FloatArray(480)
val output = FloatArray(441)
resampler.resample(input, output)
}Every resampler is an AutoCloseable handle over native state; the block sizes are fixed at construction time and are what fixes the ratio (both sides cover the same duration, typically 10 ms). Buffers that do not match those sizes, and rate pairs the legacy resampler cannot express, are reported as IllegalArgumentException — the underlying RTC_CHECKs never fire.
fun createResampler(): Resampler
fun createResampler(inputSampleRate: Int, outputSampleRate: Int, channels: Int): Resampler
fun createSincResampler(
ioSampleRateRatio: Double,
requestFrames: Int,
source: ResamplerSource
): SincResampler
fun createPushSincResampler(sourceFrames: Int, destinationFrames: Int): PushSincResampler
fun createPushResamplerInt16(sourceFramesPerChannel: Int, destinationFramesPerChannel: Int, channels: Int): PushResamplerInt16
fun createPushResamplerFloat(sourceFramesPerChannel: Int, destinationFramesPerChannel: Int, channels: Int): PushResamplerFloat
fun resamplerVersion(): String
fun sincResamplerKernelSize(): Int
fun pushSincAlgorithmicDelaySeconds(sourceRateHz: Int): Float| Member | Description |
|---|---|
resample(input, output): Int |
Resamples interleaved ShortArray samples, returns the number written |
reset(in, out, channels): Boolean |
Reconfigures and resets all states |
resetIfNeeded(in, out, channels): Boolean |
Same, but keeps the states when nothing changed |
inputSampleRate, outputSampleRate, channels
|
The current configuration |
| Member | Description |
|---|---|
resample(source, destination): Int |
One block of ShortArray or FloatArray in, one block out |
sourceFrames, destinationFrames
|
The block sizes the ratio was fixed from |
| Member | Description |
|---|---|
resample(input, output) |
One interleaved block, sourceFramesPerChannel * channels in |
sourceFramesPerChannel, destinationFramesPerChannel, channels
|
The block geometry |
| Member | Description |
|---|---|
resample(frames, destination) |
Pulls from the ResamplerSource and writes frames samples |
chunkSize, requestFrames
|
Frames the source is asked for |
flush() |
Drops buffered data and resets the internal indices |
setRatio(ioSampleRateRatio) |
Changes the ratio and rebuilds the kernels |
fun interface ResamplerSource {
fun read(frames: Int, destination: FloatArray)
}read is called on the thread that calls SincResampler.resample and must zero pad when fewer samples are available.
Both examples drive the frozen API with 10 ms blocks, the block size WebRTC's pipeline is built around.
examples/basic — Android demoA Jetpack Compose app that records from the microphone at 48 kHz, resamples to the selected output rate and plays the result back:
Resampler (fixed WebRTC rate pairs) or PushSincResampler (arbitrary ratios, which is what makes 44.1 kHz selectable)./gradlew :examples:basic:assembleDebugexamples/waveform — resampling you can see and hearA Kotlin Multiplatform app (JVM desktop, plus macOS/Linux/Windows native executables) that draws the signal before and after resampling with Dear ImGui + ImPlot in an SDL3 window:
./gradlew :examples:waveform:jvmRun # desktop JVM
./gradlew :examples:waveform:jvmRun --args="--frames 120" # bounded headless runNative executables (./examples/waveform/build/bin/<target>/<mode>Executable/waveform.kexe) take RESAMPLER_KMP_FRAMES to bound a headless run.
The viewer is built for the JVM and as native executables; Android is covered by examples/basic, which runs the binding through the AAR/JNI path.
git clone --recursive https://github.com/Enaium/webrtc-resampler-kmp.git
cd webrtc-resampler-kmp./gradlew :resampler:publishToMavenLocal./gradlew :resampler:jvmTest # JVM (JNI)
./gradlew :resampler:macosArm64Test # macOS native (cinterop)webrtc-resampler-kmp/
├── webrtc-resampler/ # Git submodule (C++ library)
├── jni/
│ ├── CMakeLists.txt # static library + JNI shared library build
│ ├── jni_bridge.cpp # JNI bridge (C++ → JVM/Android)
│ ├── c_api/ # C API (webrtc_resampler_c.h/.cc)
│ └── jvm/ # Per-OS/arch JNI publication subprojects
│ ├── darwin-aarch64, darwin-x86_64
│ ├── linux-x86_64, linux-aarch64
│ └── windows-x86_64
├── resampler/ # Kotlin Multiplatform module
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/ # expect declarations + common interfaces
│ ├── commonTest/
│ ├── jvmMain/ # JVM actual (JNI) + NativeLoader
│ ├── androidMain/ # Android actual (JNI)
│ ├── nativeMain/ # Native actual (cinterop)
│ └── nativeInterop/cinterop/
├── examples/
│ ├── basic/ # Android demo (record → resample → play)
│ └── waveform/ # KMP ImGui/ImPlot before/after viewer
├── scripts/ # Native build helpers
└── .github/workflows/ # publish + test
MIT — see the LICENSE file. The resampler itself is BSD 3-Clause, see the submodule.
Kotlin Multiplatform bindings for the WebRTC resampler — the resampler extracted from WebRTC's common_audio/resampler, with its signal processing subset. It covers both the integer multi-rate resampler (webrtc::Resampler) and the high quality windowed sinc resamplers (webrtc::SincResampler, webrtc::PushSincResampler, webrtc::PushResampler<T>) that handle arbitrary ratios such as 44.1 kHz ⟷ 48 kHz.
| Platform | Targets | Mechanism |
|---|---|---|
| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | JNI (shared library via CMake) |
| Android (Kotlin/Native) | arm64-v8a, armeabi-v7a, x86, x86_64 | Kotlin/Native cinterop (static library) |
| JVM | Linux x86_64/aarch64, macOS arm64/x86_64, Windows x86_64 | JNI (per-OS/arch JAR resource, auto-extracted by NativeLoader) |
| iOS | arm64, x64, simulatorArm64 | Kotlin/Native cinterop (static library) |
| macOS | arm64, x86_64 | Kotlin/Native cinterop (static library) |
| Linux | x86_64 | Kotlin/Native cinterop (static library) |
| Windows | mingwX64 | Kotlin/Native cinterop (static library) |
| tvOS | arm64, simulatorArm64 | Kotlin/Native cinterop (static library) |
| watchOS | arm64, simulatorArm64, deviceArm64 | Kotlin/Native cinterop (static library) |
Kotlin Multiplatform / Android:
implementation("cn.enaium.webrtc.resampler:webrtc-resampler-kmp:1.0.1")Built with Kotlin 2.4.10: consumers need a Kotlin 2.4+ compiler, since older ones cannot read the 2.4 metadata of the published artifacts.
JVM: the right native binary is resolved automatically — the webrtc-resampler-kmp-jvm artifact pulls in the matching :jni-jvm-* sibling on the classpath:
webrtc-resampler-kmp-jni-jvm-linux-x86_64webrtc-resampler-kmp-jni-jvm-linux-aarch64webrtc-resampler-kmp-jni-jvm-darwin-x86_64webrtc-resampler-kmp-jni-jvm-darwin-aarch64webrtc-resampler-kmp-jni-jvm-windows-x86_64NativeLoader detects os.name/os.arch at runtime, extracts the matching binary from the classpath to a temp directory, and System.loads it. No java.library.path setup is required for downstream JVM consumers.
| Binding | Input | Ratios | Channels | Interface |
|---|---|---|---|---|
Resampler |
ShortArray |
fixed WebRTC pairs only (8/16/32/44/48/96 kHz combinations) | 1 or 2 | push |
PushSincResampler |
ShortArray, FloatArray
|
arbitrary (44.1 kHz ⟷ 48 kHz, …) | 1 | push |
PushResamplerInt16 / PushResamplerFloat
|
ShortArray / FloatArray
|
arbitrary | ≤ 8 | push |
SincResampler |
FloatArray |
arbitrary | 1 | pull |
Resampler rejects rate pairs its fixed filter bank cannot express; use one of the sinc resamplers for those (they cost more but are not limited to integer ratios). A Resampler is also the only resampler that is bit exact for 1:1 (same rate in and out).
import cn.enaium.webrtc.resampler.createResampler
import cn.enaium.webrtc.resampler.createPushSincResampler
// Fixed pair, int16, 48 kHz -> 16 kHz, one 10 ms block at a time.
createResampler(48000, 16000, 1).use { resampler ->
val input = ShortArray(480) // 10 ms at 48 kHz
val output = ShortArray(160) // 10 ms at 16 kHz
val written = resampler.resample(input, output)
check(written == 160)
}
// Arbitrary ratio, float, 48 kHz -> 44.1 kHz.
createPushSincResampler(480, 441).use { resampler ->
val input = FloatArray(480)
val output = FloatArray(441)
resampler.resample(input, output)
}Every resampler is an AutoCloseable handle over native state; the block sizes are fixed at construction time and are what fixes the ratio (both sides cover the same duration, typically 10 ms). Buffers that do not match those sizes, and rate pairs the legacy resampler cannot express, are reported as IllegalArgumentException — the underlying RTC_CHECKs never fire.
fun createResampler(): Resampler
fun createResampler(inputSampleRate: Int, outputSampleRate: Int, channels: Int): Resampler
fun createSincResampler(
ioSampleRateRatio: Double,
requestFrames: Int,
source: ResamplerSource
): SincResampler
fun createPushSincResampler(sourceFrames: Int, destinationFrames: Int): PushSincResampler
fun createPushResamplerInt16(sourceFramesPerChannel: Int, destinationFramesPerChannel: Int, channels: Int): PushResamplerInt16
fun createPushResamplerFloat(sourceFramesPerChannel: Int, destinationFramesPerChannel: Int, channels: Int): PushResamplerFloat
fun resamplerVersion(): String
fun sincResamplerKernelSize(): Int
fun pushSincAlgorithmicDelaySeconds(sourceRateHz: Int): Float| Member | Description |
|---|---|
resample(input, output): Int |
Resamples interleaved ShortArray samples, returns the number written |
reset(in, out, channels): Boolean |
Reconfigures and resets all states |
resetIfNeeded(in, out, channels): Boolean |
Same, but keeps the states when nothing changed |
inputSampleRate, outputSampleRate, channels
|
The current configuration |
| Member | Description |
|---|---|
resample(source, destination): Int |
One block of ShortArray or FloatArray in, one block out |
sourceFrames, destinationFrames
|
The block sizes the ratio was fixed from |
| Member | Description |
|---|---|
resample(input, output) |
One interleaved block, sourceFramesPerChannel * channels in |
sourceFramesPerChannel, destinationFramesPerChannel, channels
|
The block geometry |
| Member | Description |
|---|---|
resample(frames, destination) |
Pulls from the ResamplerSource and writes frames samples |
chunkSize, requestFrames
|
Frames the source is asked for |
flush() |
Drops buffered data and resets the internal indices |
setRatio(ioSampleRateRatio) |
Changes the ratio and rebuilds the kernels |
fun interface ResamplerSource {
fun read(frames: Int, destination: FloatArray)
}read is called on the thread that calls SincResampler.resample and must zero pad when fewer samples are available.
Both examples drive the frozen API with 10 ms blocks, the block size WebRTC's pipeline is built around.
examples/basic — Android demoA Jetpack Compose app that records from the microphone at 48 kHz, resamples to the selected output rate and plays the result back:
Resampler (fixed WebRTC rate pairs) or PushSincResampler (arbitrary ratios, which is what makes 44.1 kHz selectable)./gradlew :examples:basic:assembleDebugexamples/waveform — resampling you can see and hearA Kotlin Multiplatform app (JVM desktop, plus macOS/Linux/Windows native executables) that draws the signal before and after resampling with Dear ImGui + ImPlot in an SDL3 window:
./gradlew :examples:waveform:jvmRun # desktop JVM
./gradlew :examples:waveform:jvmRun --args="--frames 120" # bounded headless runNative executables (./examples/waveform/build/bin/<target>/<mode>Executable/waveform.kexe) take RESAMPLER_KMP_FRAMES to bound a headless run.
The viewer is built for the JVM and as native executables; Android is covered by examples/basic, which runs the binding through the AAR/JNI path.
git clone --recursive https://github.com/Enaium/webrtc-resampler-kmp.git
cd webrtc-resampler-kmp./gradlew :resampler:publishToMavenLocal./gradlew :resampler:jvmTest # JVM (JNI)
./gradlew :resampler:macosArm64Test # macOS native (cinterop)webrtc-resampler-kmp/
├── webrtc-resampler/ # Git submodule (C++ library)
├── jni/
│ ├── CMakeLists.txt # static library + JNI shared library build
│ ├── jni_bridge.cpp # JNI bridge (C++ → JVM/Android)
│ ├── c_api/ # C API (webrtc_resampler_c.h/.cc)
│ └── jvm/ # Per-OS/arch JNI publication subprojects
│ ├── darwin-aarch64, darwin-x86_64
│ ├── linux-x86_64, linux-aarch64
│ └── windows-x86_64
├── resampler/ # Kotlin Multiplatform module
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/ # expect declarations + common interfaces
│ ├── commonTest/
│ ├── jvmMain/ # JVM actual (JNI) + NativeLoader
│ ├── androidMain/ # Android actual (JNI)
│ ├── nativeMain/ # Native actual (cinterop)
│ └── nativeInterop/cinterop/
├── examples/
│ ├── basic/ # Android demo (record → resample → play)
│ └── waveform/ # KMP ImGui/ImPlot before/after viewer
├── scripts/ # Native build helpers
└── .github/workflows/ # publish + test
MIT — see the LICENSE file. The resampler itself is BSD 3-Clause, see the submodule.