前言
从视频文件中分离出音频文件
MediaExtractor extractor = new MediaExtractor();
int audioTrack = -1;
boolean hasAudio = false;
try {
extractor.setDataSource(videoPath);
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat trackFormat = extractor.getTrackFormat(i);
String mime = trackFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
audioTrack = i;
hasAudio = true;
break;
}
}
上段代码的意思,就是初始化了一个音视频分离器,然后通过setDataSource给他设置了数据源,然后遍历该数据源的所有信道,如果他KEY_MIME是“audio/”开头的,说明他是音频的信道,也就是我们所需要的音频数据所在的信道。然后我们记录下audioTrack。然后我们需要选中音频信道 if (hasAudio) {
extractor.selectTrack(audioTrack);
...
}
接下来,我们就需要初始化一个混合器MediaMuxer了 MediaMuxer mediaMuxer = new MediaMuxer(audioSavePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
MediaFormat trackFormat = extractor.getTrackFormat(audioTrack);
int writeAudioIndex = mediaMuxer.addTrack(trackFormat);
mediaMuxer.start();
ByteBuffer byteBuffer = ByteBuffer.allocate(trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
上面的代码中,我们就初始化了一个MediaMuxer,并且设置了输出文件的位置和输出的格式,并且初始化了一个buffer缓冲区和一个用来保存视频信息的BufferInfo,然后我们可以开始读取和写入数据了 extractor.readSampleData(byteBuffer, 0);
if (extractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC) {
extractor.advance();
}
while (true) {
int readSampleSize = extractor.readSampleData(byteBuffer, 0);
Log.e("hero","---读取音频数据,当前读取到的大小-----:::"+readSampleSize);
if (readSampleSize < 0) {
break;
}
bufferInfo.size = readSampleSize;
bufferInfo.flags = extractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.presentationTimeUs = extractor.getSampleTime();
Log.e("hero","----写入音频数据---当前的时间戳:::"+extractor.getSampleTime());
mediaMuxer.writeSampleData(writeAudioIndex, byteBuffer, bufferInfo);
extractor.advance();//移动到下一帧
}
mediaMuxer.release();
extractor.release();
上面的代码,简单的说就是通过readSampleData从分离器中读取数据,如果没有读到说明分离完成了,就break掉,如果有数据,就设置当前帧的bufferInfo,比如偏移量,标志,时间戳,当前帧大小等,然后把当前帧数据和帧信息写入到混合器MediaMuxer中,然后开始读取下一帧。
音频文件转PCM数据
MediaExtractor extractor = new MediaExtractor();
int audioTrack = -1;
boolean hasAudio = false;
try {
extractor.setDataSource(audioPath);
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat trackFormat = extractor.getTrackFormat(i);
String mime = trackFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
audioTrack = i;
hasAudio = true;
break;
}
}
if (hasAudio) {
extractor.selectTrack(audioTrack);
...
}
同样,因为我们是要解码音频数据,所以就拿到音频的信道。然后,接下来就是音频解码中非常重要的一些知识了,首先初始化音频的解码器
MediaFormat trackFormat = extractor.getTrackFormat(audioTrack);
//初始化音频的解码器
MediaCodec audioCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
audioCodec.configure(trackFormat, null, null, 0);
audioCodec.start();
ByteBuffer[] inputBuffers = audioCodec.getInputBuffers();
ByteBuffer[] outputBuffers = audioCodec.getOutputBuffers();
MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();
MediaCodec.BufferInfo inputInfo = new MediaCodec.BufferInfo();
上面的代码,就是从分离器中拿到音频信道的MediaFormat,然后通过MediaCodec.createDecoderbyType,初始化一个相应音频格式的解码器,MediaFormat.KEY_MIME的值,其实在MediaFormat中已经列举出来了。 for (int i = 0; i < inputBuffers.length; i++) {
//遍历所以的编码器 然后将数据传入之后 再去输出端取数据
int inputIndex = audioCodec.dequeueInputBuffer(TIMEOUT_USEC);
if (inputIndex >= 0) {
/**从分离器中拿到数据 写入解码器 */
ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到inputBuffer
inputBuffer.clear();//清空之前传入inputBuffer内的数据
int sampleSize = extractor.readSampleData(inputBuffer, 0);//MediaExtractor读取数据到inputBuffer中
if (sampleSize < 0) {
audioCodec.queueInputBuffer(inputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
inputInfo.offset = 0;
inputInfo.size = sampleSize;
inputInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
inputInfo.presentationTimeUs = extractor.getSampleTime();
audioCodec.queueInputBuffer(inputIndex, inputInfo.offset, sampleSize, inputInfo.presentationTimeUs, 0);//通知MediaDecode解码刚刚传入的数据
extractor.advance();//MediaExtractor移动到下一取样处
}
}
}
如果dequeueInputbuffer拿到的inputIndex = -1,说明这个输入流不可用,如果可以用,我们就从输入流的数组中取到相应位置的输入流,然后清空之前遗留的数据,通过分离器的readSampleData读取音频数据,如果读到的数据大小小于0,说明数据已经读取完毕了,就将对应的输入流插入一个流已经结束的标志位MediaCodec.BUFFER_FLAG_END_OF_STREAM。大于0的话,就初始化输入流的InputInfo,包括当前数据的时间戳,偏移量等等,然后通知解码器对数据进行解码audioCodec.queueInputBuffer。并且通过extactor.advance()将分离器移动到下一个取数据的地方。
while (!decodeOutputDone) {
int outputIndex = audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
/**没有可用的解码器output*/
decodeOutputDone = true;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = audioCodec.getOutputBuffers();
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = audioCodec.getOutputFormat();
} else if (outputIndex < 0) {
} else {
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = audioCodec.getOutputBuffer(outputIndex);
} else {
outputBuffer = outputBuffers[outputIndex];
}
chunkPCM = new byte[decodeBufferInfo.size];
outputBuffer.get(chunkPCM);
outputBuffer.clear();
fos.write(chunkPCM);//数据写入文件中
fos.flush();
}
audioCodec.releaseOutputBuffer(outputIndex, false);
if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
/**
* 解码结束,释放分离器和解码器
* */
extractor.release();
audioCodec.stop();
audioCodec.release();
codeOver = true;
decodeOutputDone = true;
}
}
下面,我们来大致解释一下上面的这些代码,通过dequeueOutputBuffer遍历,如果当前有可以使用的输出流,就通过outputIndex拿到对应位置的OutputBuffer,并且从该输出流缓冲区,读取到byte数据,这个byte数据就是音频解码出来的最原始的音频数据,其实就是一个byte数组。现实世界的声音转化成电脑或者手机系统里面的数据的时候,其实大致过程就是通过取样以及离散化,对脉冲信号进行编码调制,最终转化成PCM格式的数据,也就是一个byte的数组,这就是所有mp3或者aac等音频格式解码出来的在系统中的最原始数据,所谓的PCM格式的音频数据其实可以简单的理解为一个byte数组,而为什么又会有mp3或者是aac的区别呢,因为原始的PCM数据的数据量是非常大的,为了便于保存和传输,就制定了许多不同的编码格式,对一些杂音或者是不重要的数据进行分离,常见的就有mp3、AAC、wav等等,而这样的编码或多或少都是有损的编码,而音质越好,他的数据量就越大,比如wav这种无损的编码格式,同样一首歌,wav格式的大小就远远大于了mp3或者是AAC。言归正传,在我们读取到界面后的byte数据之后,我们就可以通过io,写入到一个本地文件中,从输出流中读取到数据后,一定记得释放到当前的输出缓冲区 audioCodec.releaseOutputBuffer(outputIndex, false);
PCM数据转音频文件
FileInputStream fis = new FileInputStream(pcmPath);
然后,初始化编码器相关的东西 int inputIndex;
ByteBuffer inputBuffer;
int outputIndex;
ByteBuffer outputBuffer;
byte[] chunkAudio;
int outBitSize;
int outPacketSize;
//初始化编码器
MediaFormat encodeFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 2);//mime type 采样率 声道数
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024);
MediaCodec mediaEncode = MediaCodec.createEncoderByType("audio/mp4a-latm");
mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaEncode.start();
ByteBuffer[] encodeInputBuffers = mediaEncode.getInputBuffers();
ByteBuffer[] encodeOutputBuffers = mediaEncode.getOutputBuffers();
MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();
上面的代码可以看到,我们这里的输出音频格式和采样率,比特率等等都是写死的,但是原始的音频并不一定就是同样的配置,所以可能会存在问题,比如你解码的音频是mp3格式,而我们这里保存的是aac格式的文件,很明显的一个差别就是aac文件的大小会比原始的mp3格式的音频更小。 FileOutputStream fos = new FileOutputStream(new File(audioPath));
BufferedOutputStream bos = new BufferedOutputStream(fos, 500 * 1024);
肯定,下面就是最核心的读取数据—>给到编码器—>编码器的输出流拿数据—>通过io写入到文件中,这样一个循环中 boolean isReadEnd = false;
while (!isReadEnd) {
for (int i = 0; i < encodeInputBuffers.length - 1; i++) {
if (fis.read(buffer) != -1) {
allAudioBytes = Arrays.copyOf(buffer, buffer.length);
} else {
Log.e("hero", "---文件读取完成---");
isReadEnd = true;
break;
}
Log.e("hero", "---io---读取文件-写入编码器--" + allAudioBytes.length);
inputIndex = mediaEncode.dequeueInputBuffer(-1);
inputBuffer = encodeInputBuffers[inputIndex];
inputBuffer.clear();//同解码器
inputBuffer.limit(allAudioBytes.length);
inputBuffer.put(allAudioBytes);//PCM数据填充给inputBuffer
mediaEncode.queueInputBuffer(inputIndex, 0, allAudioBytes.length, 0, 0);//通知编码器 编码
}
outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);//同解码器
while (outputIndex >= 0) {
//从编码器中取出数据
outBitSize = encodeBufferInfo.size;
outPacketSize = outBitSize + 7;//7为ADTS头部的大小
outputBuffer = encodeOutputBuffers[outputIndex];//拿到输出Buffer
outputBuffer.position(encodeBufferInfo.offset);
outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
chunkAudio = new byte[outPacketSize];
AudioCodec.addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代码后面会贴上
outputBuffer.get(chunkAudio, 7, outBitSize);//将编码得到的AAC数据 取出到byte[]中 偏移量offset=7 你懂得
outputBuffer.position(encodeBufferInfo.offset);
Log.e("hero", "--编码成功-写入文件----" + chunkAudio.length);
bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 将文件保存到内存卡中 *.aac
bos.flush();
mediaEncode.releaseOutputBuffer(outputIndex, false);
outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);
}
}
mediaEncode.stop();
mediaEncode.release();
fos.close();
上面的代码还是比较容易懂的,但是有个值得注意的点就是,首先我们每次从文件中读取的byte的大小是有限制的,你不能一下子读太多数据,如果一下子给太多数据编码器会卡死,我们这里限制的是8 * 1024 ,然后有一个就是addADTStopacket方法。
/**
* 写入ADTS头部数据
* */
public static void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = 4; // 44.1KHz
int chanCfg = 2; // CPE
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
这是音频编码中非常重要的一个写入ADTS头部数据的操作,具体的原因,请查阅相关资料,我们这里就不多说了,其实大概就是用几个字符来表示我们音频的格式,采样率等等信息。通过上面几个步骤,我们就完成了将一个很大的音频的原始数据PCM进行编码,然后写入到一个音频文件里面的操作。
音频的混音(单声道和双声道的区别)
C = A + B - A * B / (数据类型的最大值);
byte数据就是
C = A + B - A * B / 127;
short数据就是
C = A + B - A * B / 32767;
为什么要进行 后面的减去操作呢?如果你使用byte进行数据操作的话,非常容易就出现数据大于最大值,而这样的公式就是为了避免这样的情况出现,另外无法避免时,需要进行归一化,也就是如果超出了范围,就取最大值,如果小于了范围就取最小值具体的实现,如下
/**
* 归一化混音
* */
public static byte[] normalizationMix(byte[][] allAudioBytes){
if (allAudioBytes == null || allAudioBytes.length == 0)
return null;
byte[] realMixAudio = allAudioBytes[0];
//如果只有一个音频的话,就返回这个音频数据
if(allAudioBytes.length == 1)
return realMixAudio;
//row 有几个音频要混音
int row = realMixAudio.length /2;
//
short[][] sourecs = new short[allAudioBytes.length][row];
for (int r = 0; r < 2; ++r) {
for (int c = 0; c < row; ++c) {
sourecs[r][c] = (short) ((allAudioBytes[r][c * 2] & 0xff) | (allAudioBytes[r][c * 2 + 1] & 0xff) << 8);
}
}
//coloum第一个音频长度 / 2
short[] result = new short[row];
//转成short再计算的原因是,提供精确度,高端的混音软件据说都是这样做的,可以测试一下不转short直接计算的混音结果
for (int i = 0; i < row; i++) {
int a = sourecs[0][i] ;
int b = sourecs[1][i] ;
if (a <0 && b<0){
int i1 = a + b - a * b / (-32768);
if (i1 > 32767){
result[i] = 32767;
}else if (i1 < - 32768){
result[i] = -32768;
}else {
result[i] = (short) i1;
}
}else if (a > 0 && b> 0){
int i1 = a + b - a * b / 32767;
if (i1 > 32767){
result[i] = 32767;
}else if (i1 < - 32768){
result[i] = -32768;
}else {
result[i] = (short) i1;
}
}else {
int i1 = a + b ;
if (i1 > 32767){
result[i] = 32767;
}else if (i1 < - 32768){
result[i] = -32768;
}else {
result[i] = (short) i1;
}
}
}
return toByteArray(result);
}
public static byte[] toByteArray(short[] src) {
int count = src.length;
byte[] dest = new byte[count << 1];
for (int i = 0; i < count; i++) {
dest[i * 2 +1] = (byte) ((src[i] & 0xFF00) >> 8);
dest[i * 2] = (byte) ((src[i] & 0x00FF));
}
return dest;
}
for (int i = 0; i < monoBytes.length; i += 2) {
stereoBytes[i*2+0] = monoBytes[i];
stereoBytes[i*2+1] = monoBytes[i+1];
stereoBytes[i*2+2] = monoBytes[i];
stereoBytes[i*2+3] = monoBytes[i+1];
}
音频的音量调节
在上面,我们实现了音频的混音,但是如果我们想要调节音频原始音量的大小(不是通过手机音量键调节),我们应该怎么做呢,比如如果要让你实现给视频增加bgm的时候,你bgm的音量不能太大以至于原视频声音听不见了。其实所谓音量调节,还是很简单,就是原始byte数据 乘上一定范围内的数值,即可实现该功能。 C = A * vol;//vol的取值范围 通常是小于10,大于0的,如果是0的话,就没有声音了,如果太大了就会出现杂音
vol的具体取值范围需要自己多去测试,当然
这是最简单的一种实现方式。