1

I'm looking for a way, in Android 10+ to record/get stream from speakers directly. I have started with this code here that uses MediaRecorder to record MIC(attempted to use VOICE_DOWNLINK, VOICE_UPLINK and more with no luck). I'm interested in direct speaker output recording/stream. Is there anyway/trick to do so?

Aviel Fedida
  • 4,004
  • 9
  • 54
  • 88

1 Answers1

1

I am using a direct uncompressed recording of the audio stream (pcm-format) from an Android microphone and convert it to a ByteBuffer in wav-format (also uncompressed) with this PcmAudioManager (Android SDK 23+, but I suupose it works for lower SDKs too), class:

package com.jetico.messenger.ui.messaging.message.audio.pcm;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class PcmAudioManager {
    public static String EXTENSION_WAV = "wav";

    private static final String TAG = "VoiceRecord";
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
    private static final int
            RECORDER_SAMPLE_RATE = 22050,
            RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT,
            NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(),
            RECORDER_CHANNELS_IN = AudioFormat.CHANNEL_IN_MONO,
            bufferSize = AudioRecord.getMinBufferSize(RECORDER_SAMPLE_RATE, RECORDER_CHANNELS_IN, RECORDER_AUDIO_ENCODING);

    private volatile boolean
            isRecording = false,
            isSpeakButtonLongPressed = false;
    private volatile List<Future<ByteBuffer>> mRunningTaskList;
    private AudioRecord recorder = null;

    public void startRecording(int jingleEndPosition) {
        if (!isRecording) {
            BlockingQueue<Runnable> mTaskQueue = new LinkedBlockingQueue<>();
            mRunningTaskList = new ArrayList<>();
            int KEEP_ALIVE_TIME = 1;
            ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                    NUMBER_OF_CORES,
                    NUMBER_OF_CORES * 2,
                    KEEP_ALIVE_TIME,
                    KEEP_ALIVE_TIME_UNIT,
                    mTaskQueue,
                    runnable -> {
                        Thread thread = new Thread(runnable);
                        thread.setName("CustomThread");
                        thread.setPriority(Thread.NORM_PRIORITY);

                        thread.setUncaughtExceptionHandler((t, ex) -> ex.printStackTrace()); // An exception handler is created to log the exception from threads
                        return thread;
                    });

            Future<ByteBuffer> future = executorService.submit(getRecordingCallable(jingleEndPosition));
            mRunningTaskList.add(future);
            isSpeakButtonLongPressed = true;
        }
    }

    public void stopRecording() {
        if (isSpeakButtonLongPressed) {// We're only interested in anything if our speak button is currently pressed.
            if (recorder != null) {
                isRecording = false;
                recorder.stop();
                recorder.release();
                recorder = null;
                Log.w(TAG, "Recorder stopped!");
            }
            isSpeakButtonLongPressed = false;
        }
    }

    public Future<ByteBuffer> getRecorded() {
        return mRunningTaskList.isEmpty() ? null : mRunningTaskList.iterator().next();
    }

    public void cancelRecorded() {
        if (!mRunningTaskList.isEmpty())
            mRunningTaskList.clear();
    }

    private Callable<ByteBuffer> getRecordingCallable(int jingleEndPosition) {
        if( bufferSize == AudioRecord.ERROR_BAD_VALUE)
            Log.e( TAG, "Bad Value for bufferSize, recording parameters are not supported by the hardware");

        if( bufferSize == AudioRecord.ERROR )
            Log.e( TAG, "Bad Value for bufferSize, implementation was unable to query the hardware for its output properties");

        Log.e( TAG, "bufferSize=" + bufferSize);

        recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDER_SAMPLE_RATE, RECORDER_CHANNELS_IN, RECORDER_AUDIO_ENCODING, bufferSize);
        recorder.startRecording();
        isRecording = true;
        return () -> writeAudioDataToArray(jingleEndPosition);
    }

    private ByteBuffer writeAudioDataToArray(int jingleEndPosition) {
        int firstMemoryAllocationInSec = 5;
        int nextMemoryAllocationInSec = 5;
        int sampleRate = 22050;
        ByteBuffer decodedBytes = ByteBuffer.allocateDirect(firstMemoryAllocationInSec * sampleRate * 2);// Allocate memory for firstMemoryAllocationInSec seconds first. Reallocate later if more is needed.
        decodedBytes.rewind();
        decodedBytes.order(ByteOrder.LITTLE_ENDIAN);
        byte[] audioBuffer = new byte[bufferSize];
        while (isRecording) {
            if (decodedBytes.remaining() < 2048) {// check if mDecodedSamples can contain 1024 additional samples.
                int newCapacity = decodedBytes.capacity() + nextMemoryAllocationInSec * sampleRate * 2;// Try to allocate memory for nextMemoryAllocationInSec additional seconds.
                ByteBuffer newDecodedBytes;
                try {
                    newDecodedBytes = ByteBuffer.allocateDirect(newCapacity);
                    decodedBytes.rewind();
                    newDecodedBytes.put(decodedBytes);
                    decodedBytes = newDecodedBytes;
                    decodedBytes.order(ByteOrder.LITTLE_ENDIAN);
                } catch (OutOfMemoryError oome) {
                    oome.printStackTrace();
                    break;
                }
            }
            recorder.read(audioBuffer, 0, bufferSize);
            decodedBytes.put(audioBuffer);
        }

        int endOffset = decodedBytes.capacity() - decodedBytes.position();
        ByteBuffer soundBufferTrimmed = trim(decodedBytes, jingleEndPosition, endOffset);
        return pcmToWav(soundBufferTrimmed, RECORDER_SAMPLE_RATE, 1, 16);
    }

    public static int getDurationInSec(int length) {
        int bitDepth = 16; //AudioFormat.ENCODING_PCM_16BIT?
        int channelCount = 1; //AudioFormat.CHANNEL_OUT_MONO?
        return length / (RECORDER_SAMPLE_RATE * (bitDepth / 8) * channelCount);
    }

    public static String getFileName(){
        return "voice_".concat(getTimeMark()).concat( ".").concat(EXTENSION_WAV);
    }

    private static ByteBuffer pcmToWav(ByteBuffer pcmData, int soundRate, int channelsNumber, int format) {//https://stackoverflow.com/questions/4440015/java-pcm-to-wav
        byte[] header = new byte[44];

        long totalDataLen = pcmData.capacity() + 36;
        long bitrate = soundRate * channelsNumber * format;

        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = (byte) format;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channelsNumber;
        header[23] = 0;
        header[24] = (byte) (soundRate & 0xff);
        header[25] = (byte) ((soundRate >> 8) & 0xff);
        header[26] = (byte) ((soundRate >> 16) & 0xff);
        header[27] = (byte) ((soundRate >> 24) & 0xff);
        header[28] = (byte) ((bitrate / 8) & 0xff);
        header[29] = (byte) (((bitrate / 8) >> 8) & 0xff);
        header[30] = (byte) (((bitrate / 8) >> 16) & 0xff);
        header[31] = (byte) (((bitrate / 8) >> 24) & 0xff);
        header[32] = (byte) ((channelsNumber * format) / 8);
        header[33] = 0;
        header[34] = 16;
        header[35] = 0;
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (pcmData.capacity()  & 0xff);
        header[41] = (byte) ((pcmData.capacity() >> 8) & 0xff);
        header[42] = (byte) ((pcmData.capacity() >> 16) & 0xff);
        header[43] = (byte) ((pcmData.capacity() >> 24) & 0xff);

        ByteBuffer headerBuffer = ByteBuffer.wrap(header);
        ByteBuffer fullRecordBuffer = ByteBuffer.allocateDirect(44 + pcmData.capacity()).put(headerBuffer).put(pcmData);
        fullRecordBuffer.rewind();
        return fullRecordBuffer;
    }

    private static ByteBuffer trim(ByteBuffer src, int startOffset, int endOffset) {
        ByteBuffer duplicated = src.duplicate();
        duplicated.position(startOffset);
        duplicated.limit(src.capacity() - endOffset);
        ByteBuffer dest;
        if (src.isDirect())
            dest = ByteBuffer.allocateDirect(src.capacity() - startOffset - endOffset).order(ByteOrder.BIG_ENDIAN);
        else
            dest = ByteBuffer.allocate(src.capacity() - startOffset - endOffset).order(ByteOrder.BIG_ENDIAN);
        dest.put(duplicated);
        dest.rewind();
        return dest;
    }

    private static String getTimeMark() {
        return new SimpleDateFormat("yyyyMMddHHmmss").format(Calendar.getInstance().getTime());
    }
}
isabsent
  • 3,683
  • 3
  • 25
  • 46
  • Hi, thanks, it might be useful but currently as I've said, I'm looking for direct stream/recording from the speaker and not the MIC – Aviel Fedida Feb 21 '21 at 15:25