Skip to content

Commit b8bec16

Browse files
address android bugs
1 parent 726447d commit b8bec16

File tree

1 file changed

+99
-7
lines changed

1 file changed

+99
-7
lines changed

android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import java.io.ByteArrayOutputStream
1515
import java.io.File
1616
import java.io.FileOutputStream
1717
import java.io.IOException
18+
import java.nio.ByteBuffer
19+
import java.nio.ByteOrder
1820
import java.util.concurrent.atomic.AtomicBoolean
21+
import kotlin.math.max
22+
import kotlin.math.min
1923

2024

2125
class 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

Comments
 (0)