Jetpack Compose向けにリアルタイムバッファ処理用途での録音実装したことなかったので実装してみました
callback頻度を100ms程度としてます
<uses-permission android:name="android.permission.RECORD_AUDIO" />class AudioRecorder {
companion object {
const val AUDIO_SOURCE = MediaRecorder.AudioSource.MIC
const val SAMPLE_RATE = 16000
const val CHANNEL = AudioFormat.CHANNEL_IN_MONO
const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
}
lateinit var audioRecord: AudioRecord
private set
val _isRecording = MutableStateFlow(false)
val isRecording = _isRecording.asStateFlow()
private val bufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE,
CHANNEL,
AUDIO_FORMAT
)
// 100ms/1frameとする
private val frameSize = SAMPLE_RATE * Short.SIZE_BYTES / 10
private var frame = ByteArray(0)
private var index = 0L
fun start(context: Context, callback: (buffer: ByteArray, size: Int, index: Long) -> Unit): Boolean {
stop()
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
audioRecord = AudioRecord(
AUDIO_SOURCE,
SAMPLE_RATE,
CHANNEL,
AUDIO_FORMAT,
bufferSize
)
CoroutineScope(Job() + Dispatchers.IO).launch {
capture(callback)
}
return true
}
return false
}
fun stop() {
if (isRecording.value) {
audioRecord.stop()
audioRecord.release()
}
}
private suspend fun capture(callback: (buffer: ByteArray, size: Int, index: Long) -> Unit) = coroutineScope {
audioRecord.startRecording()
_isRecording.update { true }
index = 0L
frame = ByteArray(0)
while (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
val buffer = ByteArray(bufferSize)
val size = audioRecord.read(buffer, 0, bufferSize)
if (size == 0) continue
val copy = if (frameSize - frame.size > size) size else frameSize - frame.size
frame = frame + buffer.copyOfRange(0, copy)
if (frame.size < frameSize) continue
callback(frame, frame.size, index++)
frame = if (copy >= size) ByteArray(0) else buffer.copyOfRange(copy, size)
}
if (frame.isNotEmpty()) callback(frame, frame.size, index)
_isRecording.update { false }
}
}@Composable
fun Screen(modifier: Modifier = Modifier) {
val recorder = AudioRecorder()
val context = LocalContext.current
val isRecording by recorder.isRecording.collectAsState()
Row(modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Button(onClick = {
if (!recorder.isRecording.value) {
recorder.start(context) { buffer, size, index ->
Log.d("AudioRecorder", "$index $size")
}
}
else {
recorder.stop()
}
}) {
Text(if (isRecording) "stop" else "start")
}
}
}ぱーみっしょんちぇっく こぴぺこーど
※ Manifest.permission.RECORD_AUDIOではなくandroid.Manifest.permission.RECORD_AUDIOを参照
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
Log.d("RequestPermission", "$isGranted")
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
Log.d("checkSelfPermission", "ok!")
} else {
requestPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
}Android Studio Koala 2024.1.1 Patch 1 built on July 11, 2024
