11package com.whispertranscriber.audio
22
3+ import android.annotation.SuppressLint
4+ import android.content.Context
5+ import android.media.AudioDeviceInfo
36import android.media.AudioFormat
47import android.media.AudioRecord
8+ import android.media.AudioManager
59import android.media.MediaRecorder
10+ import android.os.Build
611import android.util.Log
712import java.io.ByteArrayOutputStream
813import java.nio.ByteBuffer
914import java.nio.ByteOrder
1015
11- class AudioRecorder {
16+ class AudioRecorder ( private val context : Context ) {
1217
1318 companion object {
1419 private const val TAG = " AudioRecorder"
@@ -22,9 +27,14 @@ class AudioRecorder {
2227 private var recordingThread: Thread ? = null
2328 private val audioBuffer = ByteArrayOutputStream ()
2429 private var sampleRate = SAMPLE_RATE_MEDIUM
30+ private var audioManager: AudioManager ? = null
31+ private var previousAudioMode: Int? = null
32+ private var communicationDeviceActive = false
33+ private var bluetoothScoStarted = false
2534
2635 fun getSampleRate (): Int = sampleRate
2736
37+ @SuppressLint(" MissingPermission" )
2838 fun startRecording (quality : String = "medium", onPcmChunk : ((ByteArray ) -> Unit )? = null) {
2939 if (isRecording) return
3040
@@ -44,21 +54,32 @@ class AudioRecorder {
4454 }
4555
4656 try {
47- audioRecord = AudioRecord (
48- MediaRecorder .AudioSource .MIC ,
49- sampleRate,
50- channelConfig,
51- audioFormat,
52- bufferSize * 2
53- )
57+ val preferredInput = prepareAudioRouting()
58+ audioRecord = AudioRecord .Builder ()
59+ .setAudioSource(MediaRecorder .AudioSource .VOICE_COMMUNICATION )
60+ .setAudioFormat(
61+ AudioFormat .Builder ()
62+ .setSampleRate(sampleRate)
63+ .setEncoding(audioFormat)
64+ .setChannelMask(channelConfig)
65+ .build()
66+ )
67+ .setBufferSizeInBytes(bufferSize * 2 )
68+ .build()
5469
5570 if (audioRecord?.state != AudioRecord .STATE_INITIALIZED ) {
5671 Log .e(TAG , " AudioRecord failed to initialize" )
5772 audioRecord?.release()
5873 audioRecord = null
74+ restoreAudioRouting()
5975 return
6076 }
6177
78+ preferredInput?.let { device ->
79+ val routed = audioRecord?.setPreferredDevice(device) == true
80+ Log .d(TAG , " Preferred input ${device.productName} (${device.type} ) set: $routed " )
81+ }
82+
6283 audioBuffer.reset()
6384 isRecording = true
6485 audioRecord?.startRecording()
@@ -83,6 +104,10 @@ class AudioRecorder {
83104 Log .d(TAG , " Recording started at ${sampleRate} Hz" )
84105 } catch (e: SecurityException ) {
85106 Log .e(TAG , " Missing RECORD_AUDIO permission" , e)
107+ restoreAudioRouting()
108+ } catch (e: Exception ) {
109+ Log .e(TAG , " Recording failed to start" , e)
110+ restoreAudioRouting()
86111 }
87112 }
88113
@@ -94,6 +119,7 @@ class AudioRecorder {
94119 audioRecord?.stop()
95120 audioRecord?.release()
96121 audioRecord = null
122+ restoreAudioRouting()
97123
98124 val pcmData = synchronized(audioBuffer) {
99125 audioBuffer.toByteArray()
@@ -109,9 +135,112 @@ class AudioRecorder {
109135 recordingThread?.join(1000 )
110136 audioRecord?.release()
111137 audioRecord = null
138+ restoreAudioRouting()
112139 audioBuffer.reset()
113140 }
114141
142+ @SuppressLint(" MissingPermission" )
143+ private fun prepareAudioRouting (): AudioDeviceInfo ? {
144+ val manager = context.getSystemService(AudioManager ::class .java) ? : return null
145+ audioManager = manager
146+ previousAudioMode = manager.mode
147+ try {
148+ manager.mode = AudioManager .MODE_IN_COMMUNICATION
149+ } catch (e: Exception ) {
150+ Log .w(TAG , " Unable to enter communication audio mode" , e)
151+ }
152+
153+ val inputDevices = manager.getDevices(AudioManager .GET_DEVICES_INPUTS ).toList()
154+ val preferred = AudioInputDeviceSelector .choosePreferredInput(
155+ inputDevices.map { device ->
156+ AudioInputDevice (
157+ id = device.id,
158+ type = device.type,
159+ name = device.productName?.toString().orEmpty()
160+ )
161+ }
162+ )
163+ val preferredInput = preferred?.let { selected ->
164+ inputDevices.firstOrNull { it.id == selected.id }
165+ }
166+ if (preferred != null ) {
167+ Log .d(TAG , " Selected input ${preferred.name.ifBlank { preferred.id.toString() }} (${preferred.type} )" )
168+ routeCommunicationDevice(manager, preferred.type)
169+ }
170+ return preferredInput
171+ }
172+
173+ @SuppressLint(" MissingPermission" )
174+ private fun routeCommunicationDevice (manager : AudioManager , preferredInputType : Int ) {
175+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
176+ val communicationDevice = manager.availableCommunicationDevices.firstOrNull { device ->
177+ device.type == preferredInputType
178+ } ? : manager.availableCommunicationDevices.firstOrNull { device ->
179+ isBluetoothType(preferredInputType) && isBluetoothType(device.type)
180+ }
181+ if (communicationDevice != null ) {
182+ try {
183+ communicationDeviceActive = manager.setCommunicationDevice(communicationDevice)
184+ Log .d(TAG , " Communication device ${communicationDevice.productName} (${communicationDevice.type} ) set: $communicationDeviceActive " )
185+ } catch (e: SecurityException ) {
186+ Log .w(TAG , " Bluetooth routing permission denied" , e)
187+ } catch (e: Exception ) {
188+ Log .w(TAG , " Unable to set communication device" , e)
189+ }
190+ }
191+ } else if (preferredInputType == AudioDeviceInfo .TYPE_BLUETOOTH_SCO ) {
192+ try {
193+ @Suppress(" DEPRECATION" )
194+ manager.startBluetoothSco()
195+ @Suppress(" DEPRECATION" )
196+ manager.isBluetoothScoOn = true
197+ bluetoothScoStarted = true
198+ Log .d(TAG , " Bluetooth SCO routing requested" )
199+ } catch (e: Exception ) {
200+ Log .w(TAG , " Unable to start Bluetooth SCO" , e)
201+ }
202+ }
203+ }
204+
205+ @SuppressLint(" MissingPermission" )
206+ private fun restoreAudioRouting () {
207+ val manager = audioManager ? : return
208+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S && communicationDeviceActive) {
209+ try {
210+ manager.clearCommunicationDevice()
211+ } catch (e: SecurityException ) {
212+ Log .w(TAG , " Bluetooth routing permission denied while clearing route" , e)
213+ } catch (e: Exception ) {
214+ Log .w(TAG , " Unable to clear communication device" , e)
215+ }
216+ }
217+ if (bluetoothScoStarted) {
218+ try {
219+ @Suppress(" DEPRECATION" )
220+ manager.isBluetoothScoOn = false
221+ @Suppress(" DEPRECATION" )
222+ manager.stopBluetoothSco()
223+ } catch (e: Exception ) {
224+ Log .w(TAG , " Unable to stop Bluetooth SCO" , e)
225+ }
226+ }
227+ previousAudioMode?.let { mode ->
228+ try {
229+ manager.mode = mode
230+ } catch (e: Exception ) {
231+ Log .w(TAG , " Unable to restore audio mode" , e)
232+ }
233+ }
234+ communicationDeviceActive = false
235+ bluetoothScoStarted = false
236+ previousAudioMode = null
237+ audioManager = null
238+ }
239+
240+ private fun isBluetoothType (type : Int ): Boolean =
241+ type == AudioDeviceInfo .TYPE_BLUETOOTH_SCO ||
242+ type == AudioDeviceInfo .TYPE_BLE_HEADSET
243+
115244 private fun createWavFile (pcmData : ByteArray , sampleRate : Int ): ByteArray {
116245 val channels = 1
117246 val bitsPerSample = 16
0 commit comments