@@ -15,7 +15,11 @@ import java.io.ByteArrayOutputStream
1515import java.io.File
1616import java.io.FileOutputStream
1717import java.io.IOException
18+ import java.nio.ByteBuffer
19+ import java.nio.ByteOrder
1820import java.util.concurrent.atomic.AtomicBoolean
21+ import kotlin.math.max
22+ import kotlin.math.min
1923
2024
2125class AudioRecorderManager (
@@ -43,6 +47,7 @@ class AudioRecorderManager(
4347 private val mainHandler = Handler (Looper .getMainLooper())
4448 private val audioRecordLock = Any ()
4549 private var audioFileHandler: AudioFileHandler = AudioFileHandler (filesDir)
50+ private var hardwareSampleRate: Int = 0
4651
4752 // Flag to control whether actual audio data or silence is sent
4853 private var isSilent = false
@@ -132,10 +137,19 @@ class AudioRecorderManager(
132137 if (audioRecord == null || ! isPaused.get()) {
133138 Log .d(Constants .TAG , " AudioFormat: $audioFormat , BufferSize: $bufferSizeInBytes " )
134139
135- audioRecord = createAudioRecord(tempRecordingConfig , audioFormat, promise)
140+ audioRecord = createAudioRecord(recordingConfig , audioFormat, promise)
136141 if (audioRecord == null ) {
137142 return
138143 }
144+ hardwareSampleRate = audioRecord?.sampleRate ? : tempRecordingConfig.sampleRate
145+ if (hardwareSampleRate != recordingConfig.sampleRate) {
146+ Log .w(
147+ Constants .TAG ,
148+ " Hardware sample rate $hardwareSampleRate Hz differs from requested ${recordingConfig.sampleRate} Hz. Resampling will be applied."
149+ )
150+ } else {
151+ Log .d(Constants .TAG , " Hardware sample rate matches requested: ${recordingConfig.sampleRate} Hz" )
152+ }
139153 }
140154 // Create the audio file and write WAV header
141155 audioFile = createAndPrepareAudioFile(formatConfig.fileExtension, recordingConfig)
@@ -212,6 +226,7 @@ class AudioRecorderManager(
212226 streamUuid = null
213227 lastEmitTime = SystemClock .elapsedRealtime()
214228 lastEmittedSize = 0
229+ hardwareSampleRate = 0
215230
216231 Log .d(Constants .TAG , " Audio resources cleaned up" )
217232 } catch (e: Exception ) {
@@ -233,7 +248,10 @@ class AudioRecorderManager(
233248 val bytesRead = audioRecord?.read(audioData, 0 , bufferSizeInBytes) ? : - 1
234249 Log .d(Constants .TAG , " Last Read $bytesRead bytes" )
235250 if (bytesRead > 0 ) {
236- emitAudioData(audioData, bytesRead)
251+ val processedData = processAudioChunk(audioData, bytesRead)
252+ if (processedData.isNotEmpty()) {
253+ emitAudioData(processedData, processedData.size)
254+ }
237255 }
238256
239257 // Generate result before cleanup
@@ -370,9 +388,14 @@ class AudioRecorderManager(
370388 } ? : - 1 // Handle null case
371389 }
372390 if (bytesRead > 0 ) {
373- fos.write(audioData, 0 , bytesRead)
374- totalDataSize + = bytesRead
375- accumulatedAudioData.write(audioData, 0 , bytesRead)
391+ val processedData = processAudioChunk(audioData, bytesRead)
392+ if (processedData.isEmpty()) {
393+ continue
394+ }
395+
396+ fos.write(processedData)
397+ totalDataSize + = processedData.size
398+ accumulatedAudioData.write(processedData)
376399
377400 // Emit audio data at defined intervals
378401 if (SystemClock .elapsedRealtime() - lastEmitTime >= interval) {
@@ -384,7 +407,7 @@ class AudioRecorderManager(
384407 accumulatedAudioData.reset() // Clear the accumulator
385408 }
386409
387- Log .d(Constants .TAG , " Bytes written to file: $bytesRead " )
410+ Log .d(Constants .TAG , " Bytes written to file (processed) : ${processedData.size} " )
388411 }
389412 }
390413 }
@@ -394,6 +417,57 @@ class AudioRecorderManager(
394417 }
395418 }
396419
420+ private fun processAudioChunk (rawData : ByteArray , bytesRead : Int ): ByteArray {
421+ if (bytesRead <= 0 ) {
422+ return ByteArray (0 )
423+ }
424+
425+ if (hardwareSampleRate == 0 || hardwareSampleRate == recordingConfig.sampleRate) {
426+ return rawData.copyOfRange(0 , bytesRead)
427+ }
428+
429+ if (audioFormat != AudioFormat .ENCODING_PCM_16BIT ) {
430+ Log .w(
431+ Constants .TAG ,
432+ " Resampling currently supports only 16-bit PCM. Skipping conversion for format $audioFormat "
433+ )
434+ return rawData.copyOfRange(0 , bytesRead)
435+ }
436+
437+ val sourceSampleCount = bytesRead / 2
438+ if (sourceSampleCount <= 0 ) {
439+ return ByteArray (0 )
440+ }
441+
442+ val sourceSamples = ShortArray (sourceSampleCount)
443+ ByteBuffer .wrap(rawData, 0 , bytesRead)
444+ .order(ByteOrder .LITTLE_ENDIAN )
445+ .asShortBuffer()
446+ .get(sourceSamples)
447+
448+ val targetSampleCount = ((sourceSampleCount.toLong() * recordingConfig.sampleRate + hardwareSampleRate / 2 ) / hardwareSampleRate).toInt()
449+ if (targetSampleCount <= 0 ) {
450+ return ByteArray (0 )
451+ }
452+
453+ val targetSamples = ShortArray (targetSampleCount)
454+ val step = sourceSampleCount.toDouble() / targetSampleCount
455+ var position = 0.0
456+
457+ for (i in 0 until targetSampleCount) {
458+ val index = position.toInt().coerceAtMost(sourceSampleCount - 1 )
459+ val fraction = position - index
460+ val nextIndex = min(index + 1 , sourceSampleCount - 1 )
461+ val interpolated = ((1 - fraction) * sourceSamples[index] + fraction * sourceSamples[nextIndex]).toInt()
462+ targetSamples[i] = interpolated.toShort()
463+ position + = step
464+ }
465+
466+ val outBuffer = ByteBuffer .allocate(targetSampleCount * 2 ).order(ByteOrder .LITTLE_ENDIAN )
467+ outBuffer.asShortBuffer().put(targetSamples)
468+ return outBuffer.array()
469+ }
470+
397471 private fun emitAudioData (audioData : ByteArray , length : Int ) {
398472 // If silent mode is active, replace audioData with zeros (using concise expression)
399473 val dataToEncode = if (isSilent) ByteArray (length) else audioData
@@ -493,10 +567,28 @@ class AudioRecorderManager(
493567 // Always use VOICE_COMMUNICATION for better echo cancellation
494568 val audioSource = MediaRecorder .AudioSource .VOICE_COMMUNICATION
495569
570+ val channelConfig = if (config.channels == 1 ) {
571+ AudioFormat .CHANNEL_IN_MONO
572+ } else {
573+ AudioFormat .CHANNEL_IN_STEREO
574+ }
575+
576+ val minBufferSize = AudioRecord .getMinBufferSize(config.sampleRate, channelConfig, audioFormat)
577+ if (minBufferSize <= 0 ) {
578+ promise.reject(
579+ " INITIALIZATION_FAILED" ,
580+ " Unable to acquire minimum buffer size for sample rate ${config.sampleRate} " ,
581+ null
582+ )
583+ return null
584+ }
585+
586+ bufferSizeInBytes = max(bufferSizeInBytes, minBufferSize)
587+
496588 val record = AudioRecord (
497589 audioSource, // Using VOICE_COMMUNICATION for built-in echo cancellation
498590 config.sampleRate,
499- if (config.channels == 1 ) AudioFormat . CHANNEL_IN_MONO else AudioFormat . CHANNEL_IN_STEREO ,
591+ channelConfig ,
500592 audioFormat,
501593 bufferSizeInBytes
502594 )
0 commit comments