
High-performance audio time-stretching and pitch-shifting using a phase-locked phase vocoder; zero-allocation real-time streaming, multichannel phase coherence, and independent formant scaling.
Kempo is a high-performance, pure Kotlin Multiplatform (KMP) audio time-stretching and pitch-shifting library powered by an advanced phase-locked phase vocoder.
Engineered from the ground up for professional digital signal processing (DSP), sub-millisecond streaming latency, and pristine acoustic transparency, Kempo operates natively across JVM, Android, iOS, and WebAssembly (Wasm/JS) without C/C++ native toolchains, dynamic libraries, or JNI runtime overhead.
FastFft: In-place mixed-radix FFT engine supporting radices 2, 3, 4, and composite factorization.ModifiedRealFFT: Real-to-complex transform evaluated on half-bin shifted frequency points (k + 0.5) / N
to eliminate DC/Nyquist edge singularities.Windows: Parametric Kaiser windows with Weighted Overlap-Add (WOLA) normalization for perfect
reconstruction.MultiChannelBuffer): Circular multichannel ring buffer backed by a single flat
array, accelerated with unrolled vector accumulators and split-copy boundary handling.┌─────────────────────────────────────────────────────────────────────────┐
│ Audio Ingestion │
│ AudioTrack (32-bit Float) / Raw Multi-Channel Float Arrays │
│ │ │
│ PcmConverter.fromPcm16 ◄─────┴─────► PcmConverter.toPcm16 │
└───────────────────────────────────┬─────────────────────────────────────┘
│ Feed Blocks
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Dynamic STFT Analysis │
│ MultiChannelBuffer (Input Ring) ──► WindowedFFT (WOLA Kaiser Window) │
│ │ │
│ ▼ │
│ ModifiedRealFFT │
│ [Half-bin grid: (k + 0.5) / N] │
└───────────────────────────────────┬─────────────────────────────────────┘
│ Spectral Frames
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase-Locked Vocoder Processing Core │
│ • Spectral Peak Detection ──► Slew-Smoothed Energy Contours │
│ • Vertical Phase-Locking ──► Harmonic Alignment Across Bins │
│ • Horizontal Phase Progression ──► Instantaneous Phase Accumulator │
│ • Inter-Channel Coherence ──► Locked Inter-channel Phase (IPD) │
│ • Formant Filtering ──► Cepstral-like Spectral Envelope │
└───────────────────────────────────┬─────────────────────────────────────┘
│ Modified Spectrum
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Resynthesis & Overlap-Add │
│ Inverse ModifiedRealFFT ──► WOLA Synthesis Window Normalization │
│ │ │
│ ▼ │
│ MultiChannelBuffer (Output Ring) │
└─────────────────────────────────────────────────────────────────────────┘
MultiChannelBuffer. Centered circular rotations align windowed
frames before computing half-bin shifted real spectra via ModifiedRealFFT.The codebase is partitioned into targeted modules separating mathematical DSP foundations from interactive UI tooling:
| Module | Description |
|---|---|
:library |
Core KMP library containing KempoStretch, DynamicSTFT, FastFft, ModifiedRealFFT, MultiChannelBuffer, AudioTrack, and PcmConverter. Published to Maven Central. |
:benchmark |
JMH and kotlinx-benchmark test harness measuring execution throughput, GC overhead, and latency profiles across JVM/KMP targets. |
:example |
Desktop Compose Studio application featuring real-time interactive waveform rendering, drag-and-drop WAV audio decoding, and live parameter control cards. |
Kempo targets all major Kotlin Multiplatform platforms without external C/C++ runtime requirements:
iosX64, iosArm64, iosSimulatorArm64)wasmJs) for browser and Node.js runtimesAdd the dependency to your module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.numq:kempo:1.0.0")
}
}
}
Or for single-target JVM / Android projects:
dependencies {
implementation("io.github.numq:kempo:1.0.0")
}
For batch audio workflows or high-level offline rendering:
import io.github.numq.kempo.AudioTrack
fun processAudio(inputTrack: AudioTrack): AudioTrack {
// 25% faster (1.25x), transposed up by +3 semitones, preserving natural vocal timbre
return inputTrack.stretch(
timeRatio = 1.25f,
pitchSemitones = 3.0f,
formantSemitones = 0.0f
)
}
For real-time audio threads (e.g., audio callbacks, synthesizers, DAW engines):
import io.github.numq.kempo.KempoStretch
import kotlin.math.max
class RealtimeStreamEngine(channels: Int = 2, sampleRate: Float = 44100f) {
private val stretch = KempoStretch().apply {
presetDefault(channels, sampleRate)
setTransposeSemitones(semitones = 2.0f) // Transpose +2 semitones
setFormantSemitones(semitones = 0.0f) // Keep natural formants
}
private var timeRatio = 1.25f // 1.25x speed
val outBlockSize: Int = stretch.defaultInterval
val inBlockSize: Int get() = max(1, (outBlockSize / timeRatio).toInt())
// Called inside the real-time audio callback: ZERO heap allocations
fun processBlock(inputChannels: Array<FloatArray>, outputChannels: Array<FloatArray>) {
stretch.process(
inputs = inputChannels,
inputSamples = inBlockSize,
outputs = outputChannels,
outputSamples = outBlockSize
)
}
}
For deterministic whole-file rendering with automated pre-roll warming, latency compensation, and tail overlap-add flushing:
import io.github.numq.kempo.KempoStretch
import kotlin.math.roundToInt
fun renderTrack(
rawInput: Array<FloatArray>,
sampleRate: Float,
timeRatio: Float = 1.0f,
pitchSemitones: Float = -3.0f
): Array<FloatArray> {
val channels = rawInput.size
val totalInSamples = rawInput[0].size
val totalOutSamples = (totalInSamples / timeRatio).roundToInt().coerceAtLeast(1)
val rawOutput = Array(channels) { FloatArray(totalOutSamples) }
val stretch = KempoStretch()
stretch.presetDefault(channels, sampleRate)
stretch.setTransposeSemitones(pitchSemitones)
val success = stretch.exact(
inputs = rawInput,
inputSamples = totalInSamples,
outputs = rawOutput,
outputSamples = totalOutSamples
)
return rawOutput
}
Convert raw platform audio bytes (e.g., from WAV files or audio recorders) into normalized AudioTrack representations:
import io.github.numq.kempo.PcmConverter
// 1. Decode raw interleaved PCM bytes into [-1.0f, 1.0f] float buffers
val track = PcmConverter.fromPcm16(
bytes = wavByteArray,
channels = 2,
sampleRate = 44100f,
isLittleEndian = true
)
// 2. Perform transformation
val processedTrack = track.stretch(timeRatio = 1.0f, pitchSemitones = 4.0f)
// 3. Serialize back to 16-bit PCM bytes
val outputBytes = PcmConverter.toPcm16(processedTrack, isLittleEndian = true)
Classical resampling couples pitch and duration ($f_{\text{new}} = f_{\text{old}} \cdot r$). Kempo allows adjusting duration, musical pitch, and spectral timbre completely independently:
setTransposeSemitones(semitones): Transposes the perceived fundamental frequency and harmonics without modifying
playback duration.setFormantSemitones(semitones, compensatePitch): Modifies the spectral envelope peak positions. Passing
compensatePitch = true automatically shifts the formant envelope inversely to pitch transposition, preserving
original vocal identities during large pitch shifts.For experimental transformations, speech conditioning, or microtonal scales:
tonalityLimit: Frequencies above this threshold transition from harmonic multiplicative transposition to linear
shifting, preserving high-frequency noise and transient clarity.setFreqMap(fn: (Float) -> Float): Injects a custom transfer function mapping normalized frequency bins
[0.0, 0.5] to arbitrary target frequencies.stretch.inputLatency() and stretch.outputLatency()
to maintain sample-accurate sync across mixer tracks.configure() / presetDefault(). Calling process() inside real-time loops performs zero memory allocation.Kempo is channel-layout agnostic and accepts arbitrary channel counts (Mono, Stereo, 5.1, 7.1) via flat channel buffers:
5.1 -> Stereo Downmix -> Kempo) is strongly recommended. It reduces CPU workload
by ~3x (processing 2 FFTs instead of 6) and preserves battery on mobile devices while folding dialogue linearly.Measured using JMH 1.37 on OpenJDK 17 (HotSpot 64-Bit Server VM, 17.0.14) with 3 warmup iterations and 5 measurement iterations:
| Component | Benchmark | Configuration | Throughput |
|---|---|---|---|
FastFft |
Forward Complex FFT | N = 256 / 1024 / 4096 | 427.1 / 100.6 / 21.4 ops/ms |
FastFft |
Inverse Complex FFT | N = 256 / 1024 / 4096 | 454.3 / 84.9 / 21.0 ops/ms |
ModifiedRealFFT |
Forward Real FFT | N = 256 / 1024 / 4096 | 569.8 / 139.8 / 27.3 ops/ms |
ModifiedRealFFT |
Inverse Real FFT | N = 256 / 1024 / 4096 | 589.8 / 135.8 / 29.4 ops/ms |
WindowedFFT |
Forward Windowed FFT | N = 256 / 1024 / 4096 | 613.5 / 138.4 / 33.7 ops/ms |
WindowedFFT |
Inverse Windowed FFT | N = 256 / 1024 / 4096 | 520.6 / 155.2 / 32.9 ops/ms |
WindowedFFT |
Full Cycle (WOLA + FFT + IFFT) | N = 256 / 1024 / 4096 | 303.9 / 75.3 / 15.4 ops/ms |
MultiChannelBuffer |
Block Write | 1024 samples, 2 Ch | 13 976.6 ops/ms |
MultiChannelBuffer |
Block Read | 1024 samples, 2 Ch | 10 664.4 ops/ms |
MultiChannelBuffer |
Block Add | 1024 samples, 2 Ch | 1 938.9 ops/ms |
MultiChannelBuffer |
Block Clear | 1024 samples, 2 Ch | 27 084.5 ops/ms |
MultiChannelBuffer |
Wrap-Around Ring Copy | 1024 samples, 2 Ch | 9 851.9 ops/ms |
PcmConverter |
fromPcm16 Deserialization |
44 100 frames, Stereo | 3.70 ops/ms |
PcmConverter |
toPcm16 Serialization |
44 100 frames, Stereo | 3.11 ops/ms |
| Component | Benchmark Mode | Channels | Ratio | Avg Time | Real-Time Factor (RTF) |
|---|---|---|---|---|---|
KempoStretch |
Streaming Block Latency | 1 (Mono) | 1.0x | 221.0 μs | — |
KempoStretch |
Streaming Block Latency | 1 (Mono) | 0.75x / 1.25x | 271.4 μs / 266.2 μs | — |
KempoStretch |
Streaming Block Latency | 2 (Stereo) | 1.0x | 484.6 μs | — |
KempoStretch |
Streaming Block Latency | 2 (Stereo) | 0.75x / 1.25x | 566.7 μs / 501.7 μs | — |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 1 (Mono) | 1.0x | 7.77 ms | 128.7x RT |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 2 (Stereo) | 1.0x | 14.15 ms | 70.7x RT |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 2 (Stereo) | 1.25x | 13.55 ms | 73.8x RT |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 2 (Stereo) | 0.75x | 25.08 ms | 39.9x RT |
AudioTrack.stretch |
Convenience High-Level API | 1 (Mono) | 1.0x | 8.63 ms | 115.9x RT |
AudioTrack.stretch |
Convenience High-Level API | 2 (Stereo) | 1.0x | 14.91 ms | 67.1x RT |
Kempo's core DSP architecture, modified real FFT, and phase-locking vocoder algorithms are directly based on the research and reference implementation of signalsmith-stretch by Geraint Luff (Signalsmith Audio).
Special thanks to Geraint Luff for designing and publishing the mathematical foundation of this phase vocoder.
Apache-2.0 License - see LICENSE file for details.
Kempo is a high-performance, pure Kotlin Multiplatform (KMP) audio time-stretching and pitch-shifting library powered by an advanced phase-locked phase vocoder.
Engineered from the ground up for professional digital signal processing (DSP), sub-millisecond streaming latency, and pristine acoustic transparency, Kempo operates natively across JVM, Android, iOS, and WebAssembly (Wasm/JS) without C/C++ native toolchains, dynamic libraries, or JNI runtime overhead.
FastFft: In-place mixed-radix FFT engine supporting radices 2, 3, 4, and composite factorization.ModifiedRealFFT: Real-to-complex transform evaluated on half-bin shifted frequency points (k + 0.5) / N
to eliminate DC/Nyquist edge singularities.Windows: Parametric Kaiser windows with Weighted Overlap-Add (WOLA) normalization for perfect
reconstruction.MultiChannelBuffer): Circular multichannel ring buffer backed by a single flat
array, accelerated with unrolled vector accumulators and split-copy boundary handling.┌─────────────────────────────────────────────────────────────────────────┐
│ Audio Ingestion │
│ AudioTrack (32-bit Float) / Raw Multi-Channel Float Arrays │
│ │ │
│ PcmConverter.fromPcm16 ◄─────┴─────► PcmConverter.toPcm16 │
└───────────────────────────────────┬─────────────────────────────────────┘
│ Feed Blocks
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Dynamic STFT Analysis │
│ MultiChannelBuffer (Input Ring) ──► WindowedFFT (WOLA Kaiser Window) │
│ │ │
│ ▼ │
│ ModifiedRealFFT │
│ [Half-bin grid: (k + 0.5) / N] │
└───────────────────────────────────┬─────────────────────────────────────┘
│ Spectral Frames
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase-Locked Vocoder Processing Core │
│ • Spectral Peak Detection ──► Slew-Smoothed Energy Contours │
│ • Vertical Phase-Locking ──► Harmonic Alignment Across Bins │
│ • Horizontal Phase Progression ──► Instantaneous Phase Accumulator │
│ • Inter-Channel Coherence ──► Locked Inter-channel Phase (IPD) │
│ • Formant Filtering ──► Cepstral-like Spectral Envelope │
└───────────────────────────────────┬─────────────────────────────────────┘
│ Modified Spectrum
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Resynthesis & Overlap-Add │
│ Inverse ModifiedRealFFT ──► WOLA Synthesis Window Normalization │
│ │ │
│ ▼ │
│ MultiChannelBuffer (Output Ring) │
└─────────────────────────────────────────────────────────────────────────┘
MultiChannelBuffer. Centered circular rotations align windowed
frames before computing half-bin shifted real spectra via ModifiedRealFFT.The codebase is partitioned into targeted modules separating mathematical DSP foundations from interactive UI tooling:
| Module | Description |
|---|---|
:library |
Core KMP library containing KempoStretch, DynamicSTFT, FastFft, ModifiedRealFFT, MultiChannelBuffer, AudioTrack, and PcmConverter. Published to Maven Central. |
:benchmark |
JMH and kotlinx-benchmark test harness measuring execution throughput, GC overhead, and latency profiles across JVM/KMP targets. |
:example |
Desktop Compose Studio application featuring real-time interactive waveform rendering, drag-and-drop WAV audio decoding, and live parameter control cards. |
Kempo targets all major Kotlin Multiplatform platforms without external C/C++ runtime requirements:
iosX64, iosArm64, iosSimulatorArm64)wasmJs) for browser and Node.js runtimesAdd the dependency to your module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.numq:kempo:1.0.0")
}
}
}
Or for single-target JVM / Android projects:
dependencies {
implementation("io.github.numq:kempo:1.0.0")
}
For batch audio workflows or high-level offline rendering:
import io.github.numq.kempo.AudioTrack
fun processAudio(inputTrack: AudioTrack): AudioTrack {
// 25% faster (1.25x), transposed up by +3 semitones, preserving natural vocal timbre
return inputTrack.stretch(
timeRatio = 1.25f,
pitchSemitones = 3.0f,
formantSemitones = 0.0f
)
}
For real-time audio threads (e.g., audio callbacks, synthesizers, DAW engines):
import io.github.numq.kempo.KempoStretch
import kotlin.math.max
class RealtimeStreamEngine(channels: Int = 2, sampleRate: Float = 44100f) {
private val stretch = KempoStretch().apply {
presetDefault(channels, sampleRate)
setTransposeSemitones(semitones = 2.0f) // Transpose +2 semitones
setFormantSemitones(semitones = 0.0f) // Keep natural formants
}
private var timeRatio = 1.25f // 1.25x speed
val outBlockSize: Int = stretch.defaultInterval
val inBlockSize: Int get() = max(1, (outBlockSize / timeRatio).toInt())
// Called inside the real-time audio callback: ZERO heap allocations
fun processBlock(inputChannels: Array<FloatArray>, outputChannels: Array<FloatArray>) {
stretch.process(
inputs = inputChannels,
inputSamples = inBlockSize,
outputs = outputChannels,
outputSamples = outBlockSize
)
}
}
For deterministic whole-file rendering with automated pre-roll warming, latency compensation, and tail overlap-add flushing:
import io.github.numq.kempo.KempoStretch
import kotlin.math.roundToInt
fun renderTrack(
rawInput: Array<FloatArray>,
sampleRate: Float,
timeRatio: Float = 1.0f,
pitchSemitones: Float = -3.0f
): Array<FloatArray> {
val channels = rawInput.size
val totalInSamples = rawInput[0].size
val totalOutSamples = (totalInSamples / timeRatio).roundToInt().coerceAtLeast(1)
val rawOutput = Array(channels) { FloatArray(totalOutSamples) }
val stretch = KempoStretch()
stretch.presetDefault(channels, sampleRate)
stretch.setTransposeSemitones(pitchSemitones)
val success = stretch.exact(
inputs = rawInput,
inputSamples = totalInSamples,
outputs = rawOutput,
outputSamples = totalOutSamples
)
return rawOutput
}
Convert raw platform audio bytes (e.g., from WAV files or audio recorders) into normalized AudioTrack representations:
import io.github.numq.kempo.PcmConverter
// 1. Decode raw interleaved PCM bytes into [-1.0f, 1.0f] float buffers
val track = PcmConverter.fromPcm16(
bytes = wavByteArray,
channels = 2,
sampleRate = 44100f,
isLittleEndian = true
)
// 2. Perform transformation
val processedTrack = track.stretch(timeRatio = 1.0f, pitchSemitones = 4.0f)
// 3. Serialize back to 16-bit PCM bytes
val outputBytes = PcmConverter.toPcm16(processedTrack, isLittleEndian = true)
Classical resampling couples pitch and duration ($f_{\text{new}} = f_{\text{old}} \cdot r$). Kempo allows adjusting duration, musical pitch, and spectral timbre completely independently:
setTransposeSemitones(semitones): Transposes the perceived fundamental frequency and harmonics without modifying
playback duration.setFormantSemitones(semitones, compensatePitch): Modifies the spectral envelope peak positions. Passing
compensatePitch = true automatically shifts the formant envelope inversely to pitch transposition, preserving
original vocal identities during large pitch shifts.For experimental transformations, speech conditioning, or microtonal scales:
tonalityLimit: Frequencies above this threshold transition from harmonic multiplicative transposition to linear
shifting, preserving high-frequency noise and transient clarity.setFreqMap(fn: (Float) -> Float): Injects a custom transfer function mapping normalized frequency bins
[0.0, 0.5] to arbitrary target frequencies.stretch.inputLatency() and stretch.outputLatency()
to maintain sample-accurate sync across mixer tracks.configure() / presetDefault(). Calling process() inside real-time loops performs zero memory allocation.Kempo is channel-layout agnostic and accepts arbitrary channel counts (Mono, Stereo, 5.1, 7.1) via flat channel buffers:
5.1 -> Stereo Downmix -> Kempo) is strongly recommended. It reduces CPU workload
by ~3x (processing 2 FFTs instead of 6) and preserves battery on mobile devices while folding dialogue linearly.Measured using JMH 1.37 on OpenJDK 17 (HotSpot 64-Bit Server VM, 17.0.14) with 3 warmup iterations and 5 measurement iterations:
| Component | Benchmark | Configuration | Throughput |
|---|---|---|---|
FastFft |
Forward Complex FFT | N = 256 / 1024 / 4096 | 427.1 / 100.6 / 21.4 ops/ms |
FastFft |
Inverse Complex FFT | N = 256 / 1024 / 4096 | 454.3 / 84.9 / 21.0 ops/ms |
ModifiedRealFFT |
Forward Real FFT | N = 256 / 1024 / 4096 | 569.8 / 139.8 / 27.3 ops/ms |
ModifiedRealFFT |
Inverse Real FFT | N = 256 / 1024 / 4096 | 589.8 / 135.8 / 29.4 ops/ms |
WindowedFFT |
Forward Windowed FFT | N = 256 / 1024 / 4096 | 613.5 / 138.4 / 33.7 ops/ms |
WindowedFFT |
Inverse Windowed FFT | N = 256 / 1024 / 4096 | 520.6 / 155.2 / 32.9 ops/ms |
WindowedFFT |
Full Cycle (WOLA + FFT + IFFT) | N = 256 / 1024 / 4096 | 303.9 / 75.3 / 15.4 ops/ms |
MultiChannelBuffer |
Block Write | 1024 samples, 2 Ch | 13 976.6 ops/ms |
MultiChannelBuffer |
Block Read | 1024 samples, 2 Ch | 10 664.4 ops/ms |
MultiChannelBuffer |
Block Add | 1024 samples, 2 Ch | 1 938.9 ops/ms |
MultiChannelBuffer |
Block Clear | 1024 samples, 2 Ch | 27 084.5 ops/ms |
MultiChannelBuffer |
Wrap-Around Ring Copy | 1024 samples, 2 Ch | 9 851.9 ops/ms |
PcmConverter |
fromPcm16 Deserialization |
44 100 frames, Stereo | 3.70 ops/ms |
PcmConverter |
toPcm16 Serialization |
44 100 frames, Stereo | 3.11 ops/ms |
| Component | Benchmark Mode | Channels | Ratio | Avg Time | Real-Time Factor (RTF) |
|---|---|---|---|---|---|
KempoStretch |
Streaming Block Latency | 1 (Mono) | 1.0x | 221.0 μs | — |
KempoStretch |
Streaming Block Latency | 1 (Mono) | 0.75x / 1.25x | 271.4 μs / 266.2 μs | — |
KempoStretch |
Streaming Block Latency | 2 (Stereo) | 1.0x | 484.6 μs | — |
KempoStretch |
Streaming Block Latency | 2 (Stereo) | 0.75x / 1.25x | 566.7 μs / 501.7 μs | — |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 1 (Mono) | 1.0x | 7.77 ms | 128.7x RT |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 2 (Stereo) | 1.0x | 14.15 ms | 70.7x RT |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 2 (Stereo) | 1.25x | 13.55 ms | 73.8x RT |
KempoStretch |
Exact Offline (1.0 s @ 44.1 kHz) | 2 (Stereo) | 0.75x | 25.08 ms | 39.9x RT |
AudioTrack.stretch |
Convenience High-Level API | 1 (Mono) | 1.0x | 8.63 ms | 115.9x RT |
AudioTrack.stretch |
Convenience High-Level API | 2 (Stereo) | 1.0x | 14.91 ms | 67.1x RT |
Kempo's core DSP architecture, modified real FFT, and phase-locking vocoder algorithms are directly based on the research and reference implementation of signalsmith-stretch by Geraint Luff (Signalsmith Audio).
Special thanks to Geraint Luff for designing and publishing the mathematical foundation of this phase vocoder.
Apache-2.0 License - see LICENSE file for details.