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?
Asked
Active
Viewed 103 times
1 Answers
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