diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9382be0..fde33c7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + Unit +) { + companion object { + private const val TAG = "VoiceRecorder" + private const val SAMPLE_RATE = 16000 + private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO + private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT + private const val CHUNK_SIZE = 3200 // ~100ms at 16kHz mono 16-bit + } + + private var audioRecord: AudioRecord? = null + private var recordingJob: Job? = null + + val isRecording: Boolean get() = recordingJob?.isActive == true + + fun hasPermission(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, Manifest.permission.RECORD_AUDIO + ) == PackageManager.PERMISSION_GRANTED + } + + fun start(): Boolean { + if (isRecording) return false + + val bufferSize = maxOf( + AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT), + CHUNK_SIZE * 2 + ) + + val record = try { + AudioRecord( + MediaRecorder.AudioSource.MIC, + SAMPLE_RATE, + CHANNEL_CONFIG, + AUDIO_FORMAT, + bufferSize + ) + } catch (e: SecurityException) { + Log.e(TAG, "Missing RECORD_AUDIO permission", e) + return false + } + + if (record.state != AudioRecord.STATE_INITIALIZED) { + Log.e(TAG, "AudioRecord failed to initialize") + record.release() + return false + } + + audioRecord = record + record.startRecording() + + recordingJob = scope.launch(Dispatchers.IO) { + val buffer = ByteArray(CHUNK_SIZE) + while (isActive) { + val bytesRead = record.read(buffer, 0, CHUNK_SIZE) + if (bytesRead > 0) { + val base64 = Base64.encodeToString( + buffer.copyOf(bytesRead), + Base64.NO_WRAP + ) + onChunk(base64) + } + } + } + + Log.i(TAG, "Recording started") + return true + } + + fun stop() { + recordingJob?.cancel() + recordingJob = null + audioRecord?.let { + try { + it.stop() + it.release() + } catch (e: Exception) { + Log.w(TAG, "Error stopping AudioRecord", e) + } + } + audioRecord = null + Log.i(TAG, "Recording stopped") + } +}