Reduce Latency in Android Audio Recording and Playback in Java Android

106 views Asked by At

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.

0

There are 0 answers