mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
271 lines
9.2 KiB
Java
271 lines
9.2 KiB
Java
package org.thoughtcrime.securesms.audio;
|
|
|
|
import android.content.Context;
|
|
import android.media.AudioFormat;
|
|
import android.media.AudioRecord;
|
|
import android.media.MediaCodec;
|
|
import android.media.MediaCodecInfo;
|
|
import android.media.MediaFormat;
|
|
import android.media.MediaMuxer;
|
|
import android.media.MediaRecorder;
|
|
import android.util.Log;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import org.thoughtcrime.securesms.util.Prefs;
|
|
|
|
public class AudioCodec {
|
|
|
|
private static final String TAG = "AudioCodec";
|
|
|
|
private static final int SAMPLE_RATE = 44100;
|
|
private static final int CHANNELS = 1;
|
|
private static final int BIT_RATE_BALANCED = 32000;
|
|
private static final int BIT_RATE_WORSE = 24000;
|
|
|
|
private final int bufferSize;
|
|
private final MediaCodec mediaCodec;
|
|
private final AudioRecord audioRecord;
|
|
private final String outputPath;
|
|
|
|
private volatile boolean running = true;
|
|
private volatile boolean finished = false;
|
|
private long startTime = 0;
|
|
|
|
public AudioCodec(Context context, String outputPath) throws IOException {
|
|
this.outputPath = outputPath;
|
|
this.bufferSize =
|
|
AudioRecord.getMinBufferSize(
|
|
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
|
this.audioRecord = createAudioRecord(this.bufferSize);
|
|
this.mediaCodec = createMediaCodec(context, this.bufferSize);
|
|
|
|
this.mediaCodec.start();
|
|
|
|
try {
|
|
audioRecord.startRecording();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Failed to start recording", e);
|
|
mediaCodec.release();
|
|
throw new IOException(e);
|
|
}
|
|
}
|
|
|
|
public synchronized void stop() {
|
|
running = false;
|
|
while (!finished) {
|
|
try {
|
|
wait(5000);
|
|
if (!finished) {
|
|
Log.w(TAG, "Timeout waiting for recording to finish");
|
|
break;
|
|
}
|
|
} catch (InterruptedException ie) {
|
|
Log.w(TAG, "Interrupted while waiting for recording to finish", ie);
|
|
Thread.currentThread().interrupt();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void start() {
|
|
new Thread(
|
|
() -> {
|
|
this.startTime = System.nanoTime();
|
|
|
|
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
|
byte[] audioRecordData = new byte[bufferSize];
|
|
MediaMuxer muxer = null;
|
|
int audioTrackIndex = -1;
|
|
boolean muxerStarted = false;
|
|
|
|
try {
|
|
muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
|
|
|
while (true) {
|
|
boolean running = isRunning();
|
|
|
|
handleCodecInput(audioRecord, audioRecordData, mediaCodec, running);
|
|
int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 2000);
|
|
|
|
while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
|
if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
// Get the format to add track to muxer
|
|
if (muxerStarted) {
|
|
Log.w(TAG, "Output format changed after muxer started, ignoring");
|
|
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
|
continue;
|
|
}
|
|
MediaFormat newFormat = mediaCodec.getOutputFormat();
|
|
Log.d(TAG, "Output format changed: " + newFormat);
|
|
audioTrackIndex = muxer.addTrack(newFormat);
|
|
muxer.start();
|
|
muxerStarted = true;
|
|
Log.d(TAG, "Muxer started");
|
|
} else if (codecOutputBufferIndex >= 0) {
|
|
ByteBuffer encodedData = mediaCodec.getOutputBuffer(codecOutputBufferIndex);
|
|
|
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
// Codec config info, not actual data, skip it
|
|
Log.d(TAG, "Ignoring CODEC_CONFIG buffer");
|
|
bufferInfo.size = 0;
|
|
}
|
|
|
|
if (bufferInfo.size != 0 && encodedData != null) {
|
|
if (!muxerStarted) {
|
|
Log.w(TAG, "Muxer not started, dropping encoded data");
|
|
} else {
|
|
// Adjust ByteBuffer to match BufferInfo
|
|
encodedData.position(bufferInfo.offset);
|
|
encodedData.limit(bufferInfo.offset + bufferInfo.size);
|
|
|
|
// Write sample data to muxer
|
|
muxer.writeSampleData(audioTrackIndex, encodedData, bufferInfo);
|
|
}
|
|
}
|
|
|
|
mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false);
|
|
|
|
// Check for end of stream
|
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
Log.d(TAG, "End of stream reached");
|
|
break;
|
|
}
|
|
}
|
|
|
|
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
|
}
|
|
|
|
if (!running && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
break;
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error during encoding", e);
|
|
} finally {
|
|
try {
|
|
if (muxerStarted && muxer != null) {
|
|
try {
|
|
muxer.stop();
|
|
Log.d(TAG, "Muxer stopped");
|
|
} catch (IllegalStateException e) {
|
|
Log.w(TAG, "Muxer already stopped", e);
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error stopping muxer", e);
|
|
}
|
|
|
|
if (muxer != null) {
|
|
try {
|
|
muxer.release();
|
|
Log.d(TAG, "Muxer released");
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error releasing muxer", e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
mediaCodec.stop();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error stopping codec", e);
|
|
}
|
|
|
|
try {
|
|
audioRecord.stop();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error stopping audio record", e);
|
|
}
|
|
|
|
try {
|
|
mediaCodec.release();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error releasing codec", e);
|
|
}
|
|
|
|
try {
|
|
audioRecord.release();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Error releasing audio record", e);
|
|
}
|
|
|
|
setFinished();
|
|
}
|
|
},
|
|
"AudioCodec")
|
|
.start();
|
|
}
|
|
|
|
private synchronized boolean isRunning() {
|
|
return running;
|
|
}
|
|
|
|
private synchronized void setFinished() {
|
|
finished = true;
|
|
notifyAll();
|
|
}
|
|
|
|
private void handleCodecInput(
|
|
AudioRecord audioRecord, byte[] audioRecordData, MediaCodec mediaCodec, boolean running) {
|
|
int length = audioRecord.read(audioRecordData, 0, audioRecordData.length);
|
|
|
|
if (length < 0) {
|
|
Log.w(TAG, "Error reading from AudioRecord: " + length);
|
|
return;
|
|
}
|
|
|
|
int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000);
|
|
|
|
if (codecInputBufferIndex >= 0) {
|
|
ByteBuffer codecBuffer = mediaCodec.getInputBuffer(codecInputBufferIndex);
|
|
|
|
if (codecBuffer != null) {
|
|
codecBuffer.clear();
|
|
codecBuffer.put(audioRecordData, 0, length);
|
|
long presentationTimeUs = getPresentationTimeUs();
|
|
mediaCodec.queueInputBuffer(
|
|
codecInputBufferIndex,
|
|
0,
|
|
length,
|
|
presentationTimeUs,
|
|
running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
|
}
|
|
}
|
|
}
|
|
|
|
private long getPresentationTimeUs() {
|
|
return (System.nanoTime() - startTime) / 1000;
|
|
}
|
|
|
|
private AudioRecord createAudioRecord(int bufferSize) {
|
|
return new AudioRecord(
|
|
MediaRecorder.AudioSource.MIC,
|
|
SAMPLE_RATE,
|
|
AudioFormat.CHANNEL_IN_MONO,
|
|
AudioFormat.ENCODING_PCM_16BIT,
|
|
bufferSize * 10);
|
|
}
|
|
|
|
private MediaCodec createMediaCodec(Context context, int bufferSize) throws IOException {
|
|
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
|
|
|
|
MediaFormat mediaFormat =
|
|
MediaFormat.createAudioFormat("audio/mp4a-latm", SAMPLE_RATE, CHANNELS);
|
|
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize);
|
|
mediaFormat.setInteger(
|
|
MediaFormat.KEY_BIT_RATE,
|
|
Prefs.isHardCompressionEnabled(context) ? BIT_RATE_WORSE : BIT_RATE_BALANCED);
|
|
mediaFormat.setInteger(
|
|
MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
|
|
|
|
try {
|
|
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
|
} catch (Exception e) {
|
|
Log.w(TAG, e);
|
|
mediaCodec.release();
|
|
throw new IOException(e);
|
|
}
|
|
|
|
return mediaCodec;
|
|
}
|
|
}
|