I am working on an Android application that involves real-time audio recording from the microphone and immediate playback through a Bluetooth speaker. However, I am facing latency issues between the audio input from the microphone and the audio output through the Bluetooth speaker. The latency seems to increase as the recording duration gets longer.
I have been trying various approaches to minimize this latency, but I have not been successful so far. I'm reaching out to the Stack Overflow community for assistance and guidance on how to effectively reduce this latency.
Here is the relevant code snippets from my RecordFragment and AudioRecordingThread classes:
AudioRecordingThread.java :
public void run() {
//change
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
FileOutputStream out = prepareWriting();
if (out == null) {
return;
}
intRecordSampleRate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
// bufferSize = AudioRecord.getMinBufferSize(SAMPLING_RATE,
// AudioFormat.CHANNEL_IN_MONO,
// AudioFormat.ENCODING_PCM_16BIT);
//change 1
bufferSize = 441 * SAMPLING_RATE / 1000; //change 1
audioBuffer = new byte[bufferSize];
shortAudioData = new short[bufferSize];
record = new AudioRecord(AudioSource.DEFAULT, /*AudioSource.MIC*/
SAMPLING_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
BUFFER_SIZE_IN_BYTES);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC
, SAMPLING_RATE
, AudioFormat.CHANNEL_OUT_MONO
, AudioFormat.ENCODING_PCM_16BIT
, BUFFER_SIZE_IN_BYTES
, AudioTrack.MODE_STREAM);
audioTrack.setPlaybackRate(SAMPLING_RATE);
record.startRecording();
audioTrack.play();
int read = 0;
while (isRecording) {
//change 2
long startTime = System.currentTimeMillis();
read = record.read(audioBuffer, 0, bufferSize);
long endTime = System.currentTimeMillis();
// Send the audio buffer to the BT speaker
audioTrackWriteTime = System.nanoTime();
audioTrack.write(audioBuffer, 0, read);
audioRecordReadTime = System.nanoTime();
// Sleep for the remaining time in the 10 ms period
long sleepTime = 10 - (endTime - startTime);
if (sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// read = record.read(audioBuffer, 0, bufferSize);
// audioTrack.write(audioBuffer, 0, bufferSize);
if ((read == AudioRecord.ERROR_INVALID_OPERATION) ||
(read == AudioRecord.ERROR_BAD_VALUE) ||
(read == AudioRecord.ERROR_BAD_VALUE) ||
(read <= 0)) {
continue;
}
if (RecordFragment.checkSound==1) {
stopRecording();
long latencyInMilliseconds = getLatencyInMilliseconds();
Log.d("AudioLatency", "Latency: " + latencyInMilliseconds + " ms");
finishWriting(out);
convertRawToWav();
//proceed();
write(out);
}
}
if (record != null) {
record.stop();
record.release();
}
if (RecordFragment.checkSound==1) {
finishWriting(out);
convertRawToWav();
}
}
Record Fragment :
btn_testSound.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Log.e("Record Fragment","Inside Record Fragment");
if (requestAudioPermissions()) {
if (MyBroadcastReceiver.connectedBluetooth || welcomePref.isConnectedDevice()) {
testLoudSpeakerSound();
changeColor();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage("Please go to Settings to connect a bluetooth speaker")
.setCancelable(true)
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
//finish();
/* startActivity(new Intent(getActivity(), DashBoardActivity.class)
.putExtra("tag", "speaker"));*/
}
});
AlertDialog alertdialog = builder.create();
alertdialog.setTitle("Bluetooth Speaker not connected");
alertdialog.show();
}
}
}
});
public void testLoudSpeakerSound() {
checkSound = 0;
if (isLoudSpeakerOn || !this.isPermit) {
if (SDK_INT >= 23) {
voiceFromBuiltinMic();
}
toStopSpeaker();
btn_testSound.setText("Test Audio Out");
return;
}
isLoudSpeakerOn = true;
loudspeaker_status = 1;
btn_testSound.setText("Off");
testAudio=true;
new AsyncTask<Void, Void, Void>() {
/* access modifiers changed from: protected */
public Void doInBackground(Void... voidArr) {
loudSpeaker();
return null;
}
}.execute();
}
public void voiceFromBuiltinMic() {
//bottomNavigationView.getMenu().findItem(nav_home).setEnabled(false);
try {
Thread.sleep(10);
} catch (Exception unused) {
}
if (am == null) {
am = (AudioManager) getActivity().getSystemService("audio");
}
int i = loudspeaker_status;
if (i == 1 || i == 0) {
am.setSpeakerphoneOn(false);
am.setMode(3);
} else if (i == 2) {
am.setSpeakerphoneOn(false);
am.setMode(2);
}
if (this.recorder != null) {
AudioDeviceInfo myAudioDeviceInfo_builtinMic = getMyAudioDeviceInfo_builtinMic();
if (myAudioDeviceInfo_builtinMic != null) {
if (SDK_INT >= Build.VERSION_CODES.M) {
this.recorder.setPreferredDevice(myAudioDeviceInfo_builtinMic);
}
if (this.audioTrack != null) {
AudioDeviceInfo myAudioDeviceInfo_BL_a2dp = getMyAudioDeviceInfo_BL_a2dp();
if (SDK_INT >= Build.VERSION_CODES.M) {
if (myAudioDeviceInfo_BL_a2dp != null && this.audioTrack.setPreferredDevice(myAudioDeviceInfo_BL_a2dp)) {
// setMaxVol();
}
}
}
}
}
}
public void loudSpeaker() {
try {
if (am == null) {
am = (AudioManager) getActivity().getSystemService("audio");
}
int i = loudspeaker_status;
am.setSpeakerphoneOn(false);
am.setMode(3);
if (i == 1) {
am.setSpeakerphoneOn(false);
am.setMode(3);
} else if (i == 2) {
am.setSpeakerphoneOn(false);
am.setMode(2);
}
if (SDK_INT >= 23) {
if (findHeadset()) {
voiceFromHeadset();
} else if (loudspeaker_status == 1) {
voiceFromBuiltinMic();
}
}
//initialize filename variable with date and time at the end to ensure the new file wont overwrite previous file
directory = ContextCompat.getExternalFilesDirs(getActivity(), Environment.DIRECTORY_MUSIC)[0];
fileName = directory + "/" + recordFileName + ".mp3";
// recordFileName = "Recording_" + formatter.format(now) + ".mp3";
audioInputStartTimestamp = System.currentTimeMillis();
recordingThread = new AudioRecordingThread(fileName, new AudioRecordingHandler() {
@Override
public void onFftDataCapture(final byte[] bytes) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
}
});
}
@Override
public void onRecordSuccess() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Log.e("filename", ">>>>>>>" + fileName);
byte[] audio = converwav(fileName);
if (audio != null) {
// callSaveRecordingAPI();
// MyToast.display(getActivity(), "File Saved");
Log.e("filebyte", ">>>>>>>>>>" + audio);
long audioLatency = audioOutputStartTimestamp - audioInputStartTimestamp;
Log.e("AudioLatency", "Audio Latency (ms): " + audioLatency);
}
}
});
}
@Override
public void onRecordingError() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
}
});
}
@Override
public void onRecordSaveError() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
}
});
}
});
recordingThread.start();
audioOutputStartTimestamp = System.currentTimeMillis();
} catch (Exception unused) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
isLoudSpeakerOn = false;
isRecording = false;
isRecordOn = false;
Log.e("TAG", ": >>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
}
});
}
}
I have already tried adjusting the buffer size and experimenting with different thread priorities, but the latency issue remains. I suspect that there might be a more effective approach to manage audio recording and playback simultaneously to reduce the latency.
Could anyone provide guidance or suggestions on how to further reduce audio latency in this scenario? Are there any specific optimizations I should consider? I would greatly appreciate any insights or solutions that can help improve the real-time audio experience in my application.