前言
從視頻文件中分離出音頻文件
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的具體取值範圍需要自己多去測試,當然
這是最簡單的一種實現方式。