PcmSource.kt
package com.shengzhiai.yugu.audio
import android.annotation.SuppressLint
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import com.shengzhiai.yugu.YuguErrors
/** Microphone abstraction so the recorder state machine is testable without hardware. */
internal interface PcmSource {
/** Acquires and starts the microphone. Throws YuguException 90201 or 90202. */
fun start()
/** Blocking read; returns bytes read, 0 when stopped, a negative AudioRecord error code on failure. */
fun read(buffer: ByteArray, offset: Int, length: Int): Int
/** Stops capturing, keeps the instance (pause). */
fun stop()
/** Releases the microphone. Idempotent. */
fun release()
}
/** AudioRecord 16 kHz mono PCM16 source. */
internal class AudioRecordSource(
private val sampleRate: Int,
private val frameBytes: Int,
private val audioSource: Int = MediaRecorder.AudioSource.MIC,
) : PcmSource {
@Volatile
private var record: AudioRecord? = null
@Synchronized
@SuppressLint("MissingPermission")
override fun start() {
val rec = record ?: create().also { record = it }
try {
rec.startRecording()
} catch (e: IllegalStateException) {
release()
throw YuguErrors.local(90202, "麦克风无法开始录音:${e.message}", e)
} catch (e: SecurityException) {
release()
throw YuguErrors.local(90201, cause = e)
}
if (rec.recordingState != AudioRecord.RECORDSTATE_RECORDING) {
release()
throw YuguErrors.local(90202, "麦克风被占用,未进入录音状态")
}
}
@SuppressLint("MissingPermission")
private fun create(): AudioRecord {
val minBuf = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
if (minBuf <= 0) throw YuguErrors.local(90202, "设备不支持 $sampleRate Hz 单声道 16 位录音,getMinBufferSize=$minBuf")
val rec = try {
AudioRecord(audioSource, sampleRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, maxOf(minBuf, frameBytes * 8))
} catch (e: SecurityException) {
throw YuguErrors.local(90201, cause = e)
} catch (e: IllegalArgumentException) {
throw YuguErrors.local(90202, "AudioRecord 参数不被支持:${e.message}", e)
}
if (rec.state != AudioRecord.STATE_INITIALIZED) {
rec.release()
throw YuguErrors.local(90202, "AudioRecord 初始化失败,麦克风可能被占用或没有权限")
}
return rec
}
override fun read(buffer: ByteArray, offset: Int, length: Int): Int =
record?.read(buffer, offset, length) ?: AudioRecord.ERROR_INVALID_OPERATION
@Synchronized
override fun stop() {
try {
record?.stop()
} catch (_: IllegalStateException) {
// not recording
}
}
@Synchronized
override fun release() {
val rec = record ?: return
record = null
try {
rec.stop()
} catch (_: IllegalStateException) {
// not recording
}
rec.release()
}
}