kpal
0.4.0indexedAutomates Maven Central releases through a dedicated Gradle publishing build, creating signed upload bundles, handling Sonatype/GPG secrets and customizable POM license metadata.
Automates Maven Central releases through a dedicated Gradle publishing build, creating signed upload bundles, handling Sonatype/GPG secrets and customizable POM license metadata.
kpal gives you a cross-platform Kotlin API for device features, then hardens that API with manual QA apps and a device simulator. Use the QA apps when behavior must be proven on real Android and iOS hardware, and use the simulator for cheap integration tests that run quickly in coding-agent feedback loops without a real device or human intervention.
Use the device module for production code:
dependencies:
- ./device
Use the simulator module from tests or test-only tools:
dependencies:
- ./simulator
Published artifacts use this group:
implementation("io.github.leon-jakob-schneider.kpal:device:<version>")
testImplementation("io.github.leon-jakob-schneider.kpal:simulator:<version>")
Create a platform device and request an audio engine:
import app.miso.audio.AudioSessionConfig
import app.miso.device.DeviceConfig
import app.miso.device.DeviceImpl
import kotlinx.coroutines.runBlocking
val device = DeviceImpl(
platformContext = platformContext,
config = DeviceConfig(
audio = AudioSessionConfig(
sampleRate = 24_000,
ioBufferFrames = 1_024,
preferSpeaker = true,
voiceProcessing = true,
),
),
)
val request = device.audio.requestEngine()
engine = request.engine ?:
engine.useDuplex { duplex ->
runBlocking {
inputChunk = duplex.takeNextInputPcm16()
duplex.playPcm16(inputChunk)
}
}
useDuplex calls its block only after the platform audio graph is ready. AudioDuplex functions are suspending and cancellation-aware, so bridge into a coroutine from the block before reading, writing, or restarting. takeNextInputPcm16() suspends until a PCM16 input chunk is available and throws if the duplex session fails; it does not use null to represent an empty queue.
On Android, pass an Android Context as platformContext. On iOS and JVM desktop, platformContext can stay null.
Pass an AudioSessionObserver when you need route, level, byte-count, or error updates:
import app.miso.audio.AudioError
import app.miso.audio.AudioSessionObserver
app.miso.audio.AudioSessionState
app.miso.device.DeviceImpl
observer = : AudioSessionObserver {
{
println()
println()
}
{
println(error.message)
}
}
device = DeviceImpl(
platformContext = platformContext,
audioObserver = observer,
)
Use the shared tone generator for simple playback checks:
import app.miso.audio.Pcm16ToneGenerator
val tone = Pcm16ToneGenerator.sine(
frequencyHz = 440.0,
durationMillis = 1_500,
sampleRate = 24_000,
)
Use DeviceSimulator when the code under test should exercise the same Device surface without opening a real microphone or speaker:
import app.miso.audio.Pcm16ToneGenerator
import app.miso.simulator.DeviceSimulator
import kotlinx.coroutines.runBlocking
val device = DeviceSimulator()
val input = Pcm16ToneGenerator.sine(durationMillis = 200)
device.setAudioInputPcm16(input)
val engine = device.audio.requestEngine().engine ?: error("No audio engine")
engine.useDuplex { duplex ->
runBlocking {
val captured = duplex.takeNextInputPcm16()
check(captured.contentEquals(input))
duplex.playPcm16(captured)
}
}
val output = device.drainAudioOutputPcm16()
check(output.contentEquals(input))
Useful simulator controls:
setAudioInputPcm16(bytes): replace pending simulated microphone input.appendAudioInputPcm16(bytes): queue more simulated microphone input.Build the app:
./amper build -m device-qa-android-app -p android
Run it on a connected Android device or emulator:
adb devices
./amper run -m device-qa-android-app -p android -d <device-id>
Use the app to start capture, play a 440 Hz tone, play captured audio, and inspect route, input level, captured bytes, and played bytes.
Build the app:
./amper build -m device-qa-ios-app -p iosSimulatorArm64
Run it on a simulator or connected iOS device:
xcrun simctl list devices
./amper run -m device-qa-ios-app -p iosSimulatorArm64 -d <device-id>
For real-device QA, open device-qa-ios-app/module.xcodeproj in Xcode and run the app on an iPhone.
Use the iOS QA suite to validate built-in speaker playback, built-in mic loopback, recording coverage, AirPods playback, and AirPods mic loopback. Export the report at the end of a manual run when you need evidence for a device-specific change.
List modules:
./amper show modules
Build all modules:
./amper build
Build the shared device library:
./amper build -m device
Build the JVM desktop device library:
./amper build -m device -p jvm
Build the simulator:
./amper build -m simulator
takeNextAudioOutputPcm16()audioOutputPcm16(): snapshot all captured speaker output.drainAudioOutputPcm16(): read and clear speaker output.clearAudioInput() / clearAudioOutput(): reset simulator buffers.Surfaced from shared tags and platforms — no rankings paid for.