|
|
@ -31,24 +31,6 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
private static final String TAG = AudioMediaEncoder.class.getSimpleName(); |
|
|
|
private static final String TAG = AudioMediaEncoder.class.getSimpleName(); |
|
|
|
private static final CameraLogger LOG = CameraLogger.create(TAG); |
|
|
|
private static final CameraLogger LOG = CameraLogger.create(TAG); |
|
|
|
|
|
|
|
|
|
|
|
private static final String MIME_TYPE = "audio/mp4a-latm"; |
|
|
|
|
|
|
|
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; // Determines the SAMPLE_SIZE
|
|
|
|
|
|
|
|
private static final int CHANNELS = AudioFormat.CHANNEL_IN_MONO; // AudioFormat.CHANNEL_IN_STEREO;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The 44.1KHz frequency is the only setting guaranteed to be available on all devices.
|
|
|
|
|
|
|
|
private static final int SAMPLING_FREQUENCY = 44100; // samples/sec
|
|
|
|
|
|
|
|
private static final int CHANNELS_COUNT = 1; // 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static final int SAMPLE_SIZE = 2; // byte/sample/channel
|
|
|
|
|
|
|
|
private static final int BYTE_RATE_PER_CHANNEL = SAMPLING_FREQUENCY * SAMPLE_SIZE; // byte/sec/channel
|
|
|
|
|
|
|
|
private static final int BYTE_RATE = BYTE_RATE_PER_CHANNEL * CHANNELS_COUNT; // byte/sec
|
|
|
|
|
|
|
|
@SuppressWarnings("unused") |
|
|
|
|
|
|
|
private static final int BIT_RATE = BYTE_RATE * 8; // bit/sec
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We call FRAME here the chunk of data that we want to read at each loop cycle
|
|
|
|
|
|
|
|
private static final int FRAME_SIZE_PER_CHANNEL = 1024; // bytes/frame/channel [AAC constant]
|
|
|
|
|
|
|
|
private static final int FRAME_SIZE = FRAME_SIZE_PER_CHANNEL * CHANNELS_COUNT; // bytes/frame
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We allocate buffers of 1KB each, which is not so much. This value indicates the maximum
|
|
|
|
// We allocate buffers of 1KB each, which is not so much. This value indicates the maximum
|
|
|
|
// number of these buffers that we can allocate at a given instant.
|
|
|
|
// number of these buffers that we can allocate at a given instant.
|
|
|
|
// This value is the number of runnables that the encoder thread is allowed to be 'behind'
|
|
|
|
// This value is the number of runnables that the encoder thread is allowed to be 'behind'
|
|
|
@ -64,14 +46,48 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
private Config mConfig; |
|
|
|
private Config mConfig; |
|
|
|
|
|
|
|
|
|
|
|
public static class Config { |
|
|
|
public static class Config { |
|
|
|
|
|
|
|
|
|
|
|
public int bitRate; |
|
|
|
public int bitRate; |
|
|
|
|
|
|
|
public int channels = 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Not configurable options (for now)
|
|
|
|
|
|
|
|
private final String mimeType = "audio/mp4a-latm"; |
|
|
|
|
|
|
|
private final int encoding = AudioFormat.ENCODING_PCM_16BIT; // Determines the SAMPLE_SIZE
|
|
|
|
|
|
|
|
// The 44.1KHz frequency is the only setting guaranteed to be available on all devices.
|
|
|
|
|
|
|
|
private final int samplingFrequency = 44100; // samples/sec
|
|
|
|
|
|
|
|
private final int sampleSize = 2; // byte/sample/channel
|
|
|
|
|
|
|
|
private final int byteRatePerChannel = samplingFrequency * sampleSize; // byte/sec/channel
|
|
|
|
|
|
|
|
private final int frameSizePerChannel = 1024; // bytes/frame/channel [AAC constant]
|
|
|
|
|
|
|
|
|
|
|
|
@NonNull |
|
|
|
@NonNull |
|
|
|
private Config copy() { |
|
|
|
private Config copy() { |
|
|
|
Config config = new Config(); |
|
|
|
Config config = new Config(); |
|
|
|
config.bitRate = this.bitRate; |
|
|
|
config.bitRate = this.bitRate; |
|
|
|
|
|
|
|
config.channels = this.channels; |
|
|
|
return config; |
|
|
|
return config; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private int byteRate() { |
|
|
|
|
|
|
|
return byteRatePerChannel * channels; // byte/sec
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private int bitRate() { |
|
|
|
|
|
|
|
return byteRate() * 8; // bit/sec
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private int frameSize() { |
|
|
|
|
|
|
|
// We call FRAME here the chunk of data that we want to read at each loop cycle
|
|
|
|
|
|
|
|
return frameSizePerChannel * channels; // bytes/frame
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private int audioFormatChannels() { |
|
|
|
|
|
|
|
if (channels == 1) { |
|
|
|
|
|
|
|
return AudioFormat.CHANNEL_IN_MONO; |
|
|
|
|
|
|
|
} else if (channels == 2) { |
|
|
|
|
|
|
|
return AudioFormat.CHANNEL_IN_STEREO; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
throw new RuntimeException("Invalid number of channels: " + channels); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public AudioMediaEncoder(@NonNull Config config) { |
|
|
|
public AudioMediaEncoder(@NonNull Config config) { |
|
|
@ -87,19 +103,19 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
@EncoderThread |
|
|
|
@EncoderThread |
|
|
|
@Override |
|
|
|
@Override |
|
|
|
protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { |
|
|
|
protected void onPrepare(@NonNull MediaEncoderEngine.Controller controller, long maxLengthMillis) { |
|
|
|
final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLING_FREQUENCY, CHANNELS_COUNT); |
|
|
|
final MediaFormat audioFormat = MediaFormat.createAudioFormat(mConfig.mimeType, mConfig.samplingFrequency, mConfig.channels); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, CHANNELS); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, mConfig.audioFormatChannels()); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS_COUNT); |
|
|
|
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, mConfig.channels); |
|
|
|
try { |
|
|
|
try { |
|
|
|
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
|
|
|
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); |
|
|
|
} catch (IOException e) { |
|
|
|
} catch (IOException e) { |
|
|
|
throw new RuntimeException(e); |
|
|
|
throw new RuntimeException(e); |
|
|
|
} |
|
|
|
} |
|
|
|
mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
|
|
|
mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
|
|
|
mMediaCodec.start(); |
|
|
|
mMediaCodec.start(); |
|
|
|
mByteBufferPool = new ByteBufferPool(FRAME_SIZE, BUFFER_POOL_MAX_SIZE); |
|
|
|
mByteBufferPool = new ByteBufferPool(mConfig.frameSize(), BUFFER_POOL_MAX_SIZE); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@EncoderThread |
|
|
|
@EncoderThread |
|
|
@ -141,13 +157,13 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
private long mFirstTimeUs = Long.MIN_VALUE; |
|
|
|
private long mFirstTimeUs = Long.MIN_VALUE; |
|
|
|
|
|
|
|
|
|
|
|
private AudioRecordingThread() { |
|
|
|
private AudioRecordingThread() { |
|
|
|
final int minBufferSize = AudioRecord.getMinBufferSize(SAMPLING_FREQUENCY, CHANNELS, ENCODING); |
|
|
|
final int minBufferSize = AudioRecord.getMinBufferSize(mConfig.samplingFrequency, mConfig.channels, mConfig.encoding); |
|
|
|
int bufferSize = FRAME_SIZE * 25; // Make this bigger so we don't skip frames.
|
|
|
|
int bufferSize = mConfig.frameSize() * 25; // Make this bigger so we don't skip frames.
|
|
|
|
while (bufferSize < minBufferSize) { |
|
|
|
while (bufferSize < minBufferSize) { |
|
|
|
bufferSize += FRAME_SIZE; // Unlikely I think.
|
|
|
|
bufferSize += mConfig.frameSize(); // Unlikely I think.
|
|
|
|
} |
|
|
|
} |
|
|
|
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.CAMCORDER, |
|
|
|
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.CAMCORDER, |
|
|
|
SAMPLING_FREQUENCY, CHANNELS, ENCODING, bufferSize); |
|
|
|
mConfig.samplingFrequency, mConfig.channels, mConfig.encoding, bufferSize); |
|
|
|
setPriority(Thread.MAX_PRIORITY); |
|
|
|
setPriority(Thread.MAX_PRIORITY); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -185,7 +201,7 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
mCurrentBuffer.clear(); |
|
|
|
mCurrentBuffer.clear(); |
|
|
|
mReadBytes = mAudioRecord.read(mCurrentBuffer, FRAME_SIZE); |
|
|
|
mReadBytes = mAudioRecord.read(mCurrentBuffer, mConfig.frameSize()); |
|
|
|
LOG.i("read thread - eos:", endOfStream, "- Read new audio frame. Bytes:", mReadBytes); |
|
|
|
LOG.i("read thread - eos:", endOfStream, "- Read new audio frame. Bytes:", mReadBytes); |
|
|
|
if (mReadBytes > 0) { // Good read: increase PTS.
|
|
|
|
if (mReadBytes > 0) { // Good read: increase PTS.
|
|
|
|
increaseTime(mReadBytes, endOfStream); |
|
|
|
increaseTime(mReadBytes, endOfStream); |
|
|
@ -206,7 +222,9 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
private void sleep() { |
|
|
|
private void sleep() { |
|
|
|
try { |
|
|
|
try { |
|
|
|
Thread.sleep(AudioTimestamp.bytesToUs(FRAME_SIZE * 6, BYTE_RATE) / 1000); |
|
|
|
Thread.sleep(AudioTimestamp.bytesToUs( |
|
|
|
|
|
|
|
mConfig.frameSize() * 6, |
|
|
|
|
|
|
|
mConfig.byteRate()) / 1000); |
|
|
|
} catch (InterruptedException ignore) {} |
|
|
|
} catch (InterruptedException ignore) {} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -219,12 +237,12 @@ public class AudioMediaEncoder extends MediaEncoder { |
|
|
|
* @param endOfStream end of stream? |
|
|
|
* @param endOfStream end of stream? |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
private void increaseTime(int readBytes, boolean endOfStream) { |
|
|
|
private void increaseTime(int readBytes, boolean endOfStream) { |
|
|
|
mLastTimeUs = mTimestamp.increaseUs(readBytes, BYTE_RATE); |
|
|
|
mLastTimeUs = mTimestamp.increaseUs(readBytes, mConfig.byteRate()); |
|
|
|
if (mFirstTimeUs == Long.MIN_VALUE) { |
|
|
|
if (mFirstTimeUs == Long.MIN_VALUE) { |
|
|
|
mFirstTimeUs = mLastTimeUs; |
|
|
|
mFirstTimeUs = mLastTimeUs; |
|
|
|
// Compute the first frame milliseconds as well.
|
|
|
|
// Compute the first frame milliseconds as well.
|
|
|
|
notifyFirstFrameMillis(System.currentTimeMillis() |
|
|
|
notifyFirstFrameMillis(System.currentTimeMillis() |
|
|
|
- AudioTimestamp.bytesToUs(readBytes, BYTE_RATE) / 1000L); |
|
|
|
- AudioTimestamp.bytesToUs(readBytes, mConfig.byteRate()) / 1000L); |
|
|
|
} |
|
|
|
} |
|
|
|
boolean didReachMaxLength = (mLastTimeUs - mFirstTimeUs) > getMaxLengthMillis() * 1000L; |
|
|
|
boolean didReachMaxLength = (mLastTimeUs - mFirstTimeUs) > getMaxLengthMillis() * 1000L; |
|
|
|
if (didReachMaxLength && !endOfStream) { |
|
|
|
if (didReachMaxLength && !endOfStream) { |
|
|
|