Implement Audio Capture Infrastructure for Microphone Input
Resolved 💬 2 comments Opened Oct 20, 2025 by sanchitmonga22 Closed Oct 20, 2025
Priority
CRITICAL - Production Blocker
Context
Audio capture infrastructure is the foundation for all voice features. It provides a unified API for capturing microphone input across platforms. The iOS SDK has a complete AudioCaptureService with real-time streaming. The Kotlin SDK is missing this critical infrastructure.
Current State
- Status: Infrastructure does not exist
- Impact: Cannot capture audio from microphone for VAD, STT, or Wake Word components
- iOS Has: Complete AudioCaptureService with AVAudioEngine integration
iOS Reference Implementation
- Service:
sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Audio/AudioCaptureService.swift - Models:
sdk/runanywhere-swift/Sources/RunAnywhere/Infrastructure/Audio/Models/
iOS Architecture Pattern
public protocol AudioCaptureService: Sendable {
func startCapture() async throws
func stopCapture() async
var audioStream: AsyncStream<Data> { get }
var audioFormat: AudioFormat { get }
}
public final class AVAudioCaptureService: AudioCaptureService {
private let audioEngine = AVAudioEngine()
public func startCapture() async throws {
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, time in
// Emit audio data
}
try audioEngine.start()
}
}
Implementation Plan
1. Define Common Interfaces (commonMain)
// commonMain/kotlin/com/runanywhere/sdk/infrastructure/audio/AudioCaptureService.kt
interface AudioCaptureService {
suspend fun startCapture()
suspend fun stopCapture()
val audioStream: Flow<ByteArray>
val audioFormat: AudioFormat
val isCapturing: Boolean
}
// commonMain/kotlin/com/runanywhere/sdk/infrastructure/audio/models/AudioFormat.kt
data class AudioFormat(
val sampleRate: Int = 16000, // 16kHz default
val channels: Int = 1, // Mono
val encoding: AudioEncoding = AudioEncoding.PCM_16BIT
)
enum class AudioEncoding {
PCM_8BIT,
PCM_16BIT,
PCM_24BIT,
PCM_32BIT,
FLOAT
}
2. Platform Implementations
Android (androidMain)
// androidMain/kotlin/com/runanywhere/sdk/infrastructure/audio/AndroidAudioCaptureService.kt
class AndroidAudioCaptureService(
private val context: Context,
override val audioFormat: AudioFormat = AudioFormat()
) : AudioCaptureService {
private var audioRecord: AudioRecord? = null
private var captureJob: Job? = null
private val _audioStream = MutableSharedFlow<ByteArray>()
override val audioStream: SharedFlow<ByteArray> = _audioStream.asSharedFlow()
override var isCapturing: Boolean = false
private set
override suspend fun startCapture() {
if (isCapturing) return
// Check permission
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
throw SecurityException("RECORD_AUDIO permission not granted")
}
val bufferSize = AudioRecord.getMinBufferSize(
audioFormat.sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
audioFormat.sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
)
audioRecord?.startRecording()
isCapturing = true
// Start capture loop
captureJob = CoroutineScope(Dispatchers.IO).launch {
val buffer = ByteArray(bufferSize)
while (isCapturing) {
val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0
if (bytesRead > 0) {
_audioStream.emit(buffer.copyOf(bytesRead))
}
}
}
}
override suspend fun stopCapture() {
isCapturing = false
captureJob?.cancel()
audioRecord?.stop()
audioRecord?.release()
audioRecord = null
}
}
JVM (jvmMain)
// jvmMain/kotlin/com/runanywhere/sdk/infrastructure/audio/JvmAudioCaptureService.kt
class JvmAudioCaptureService(
override val audioFormat: AudioFormat = AudioFormat()
) : AudioCaptureService {
private var targetDataLine: TargetDataLine? = null
private var captureJob: Job? = null
private val _audioStream = MutableSharedFlow<ByteArray>()
override val audioStream: SharedFlow<ByteArray> = _audioStream.asSharedFlow()
override var isCapturing: Boolean = false
private set
override suspend fun startCapture() {
if (isCapturing) return
val format = javax.sound.sampled.AudioFormat(
audioFormat.sampleRate.toFloat(),
16, // 16-bit
audioFormat.channels,
true, // signed
false // little-endian
)
val info = DataLine.Info(TargetDataLine::class.java, format)
if (!AudioSystem.isLineSupported(info)) {
throw UnsupportedOperationException("Audio format not supported")
}
targetDataLine = (AudioSystem.getLine(info) as TargetDataLine).apply {
open(format)
start()
}
isCapturing = true
// Start capture loop
captureJob = CoroutineScope(Dispatchers.IO).launch {
val buffer = ByteArray(4096)
while (isCapturing) {
val bytesRead = targetDataLine?.read(buffer, 0, buffer.size) ?: 0
if (bytesRead > 0) {
_audioStream.emit(buffer.copyOf(bytesRead))
}
}
}
}
override suspend fun stopCapture() {
isCapturing = false
captureJob?.cancel()
targetDataLine?.stop()
targetDataLine?.close()
targetDataLine = null
}
}
3. Service Registration
// commonMain/kotlin/com/runanywhere/sdk/foundation/ServiceContainer.kt
class ServiceContainer {
val audioCaptureService: AudioCaptureService by lazy {
createAudioCaptureService()
}
private fun createAudioCaptureService(): AudioCaptureService {
return platformAudioCaptureService()
}
}
// expect/actual pattern
expect fun platformAudioCaptureService(): AudioCaptureService
Files to Create/Modify
New Files (commonMain)
- [ ]
infrastructure/audio/AudioCaptureService.kt - [ ]
infrastructure/audio/models/AudioFormat.kt - [ ]
infrastructure/audio/models/AudioEncoding.kt
New Files (androidMain)
- [ ]
infrastructure/audio/AndroidAudioCaptureService.kt
New Files (jvmMain)
- [ ]
infrastructure/audio/JvmAudioCaptureService.kt
Files to Modify
- [ ]
foundation/ServiceContainer.kt(add audio capture service)
Testing Requirements
- [ ] Unit tests for AudioFormat
- [ ] Android platform tests (AudioRecord integration)
- [ ] JVM platform tests (javax.sound integration)
- [ ] Stream backpressure tests
- [ ] Permission handling tests (Android)
- [ ] Multiple start/stop cycle tests
- [ ] Audio format conversion tests
Success Criteria
- [ ] Service captures audio on Android using AudioRecord
- [ ] Service captures audio on JVM using javax.sound
- [ ] Audio stream emits PCM data at configured sample rate
- [ ] Proper permission handling on Android
- [ ] Graceful start/stop lifecycle
- [ ] Low latency audio capture (<100ms)
- [ ] Thread-safe implementation
- [ ] Integrated into ServiceContainer
Estimated Effort
1-2 weeks (80-100 hours)
- Common interface design: 1-2 days
- Android implementation: 3-4 days
- JVM implementation: 3-4 days
- Testing: 2-3 days
- Documentation: 1 day
Dependencies
- Depends on: #152 (Voice Capability Service for permission checks)
- Depends on: #147 (privacy-first logging)
- Blocks: VADComponent (needs audio stream)
- Blocks: STTComponent (needs audio stream)
- Blocks: #150 (Wake Word Detection)
Related Issues
- Part of audio infrastructure foundation
- Required for all voice input features
- Aligns with iOS AudioCaptureService architecture
🤖 Generated with Claude Code
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗