MediaCodec進行音頻合成
1.前言
音頻合成在現實生活中應用廣泛,在網上可以搜索到不少相關的講解和代碼實現,但個人感覺在網上搜索到的音頻合成相關文章的講解都並非十分透徹,故而寫下本篇博文,計劃通過講解如何使用代碼實現音頻合成功能從而將本人對音頻合成的理解闡述給各位,力圖讀完的各位可以對音頻合成整體過程有一個清晰的了解。
本篇博文以Java為示例語言,以Android為示例平台。
本篇博文著力於講解音頻合成實現原理與過程中的細節和潛在問題,目的是讓各位不被編碼語言所限制,在本質上理解如何實現音頻合成的功能。
2.音頻合成
2.1.功能簡介
本次實現的音頻合成功能參考"唱吧"的音頻合成,功能流程是:錄音生成PCM文件,接著根據錄音時長對背景音樂文件進行解碼加裁剪,同時將解碼後的音頻調製到與錄音文件相同的採樣率,採樣點位元組數,聲道數,接著根據指定係數對兩個音頻文件進行音量調節併合成為PCM文件,最後進行壓縮編碼生成MP3文件。
2.2.功能實現
2.2.1.錄音
錄音功能生成的目標音頻格式是PCM格式,對於PCM的定義,維基百科上是這麼寫到的:"Pulse-code modulation (PCM) is a method used to digitally represent sampled analog signals. It is the standard form of digital audio in computers, Compact Discs, digital telephony and other digital audio applications. In a PCM stream, the amplitude of the analog signal is sampled regularly at uniform intervals, and each sample is quantized to the nearest value within a range of digital steps.",大致意思是PCM是用來採樣模擬信號的一種方法,是現在數字音頻應用中數字音頻的標準格式,而PCM採樣的原理,是均勻間隔的將模擬信號的振幅量化成指定數據範圍內最貼近的數值。
PCM文件存儲的數據是不經壓縮的純音頻數據,當然只是這麼說可能有些抽象,我們拉上大家熟知的MP3文件進行對比,MP3文件存儲的是壓縮後的音頻,PCM與MP3兩者之間的關係簡單說就是:PCM文件經過MP3壓縮演算法處理後生成的文件就是MP3文件。我們簡單比較一下雙方存儲所消耗的空間,1分鐘的每採樣點16位的雙聲道的44.1kHz採樣率PCM文件大小為:16016/8244100/1024=10335.9375KB,約為10MB,而對應的128kps的MP3文件大小僅為1MB左右,既然PCM文件佔用存儲空間這麼大,我們是不是應該放棄使用PCM格式存儲錄音,恰恰相反,注意第一句話:"PCM文件存儲的數據是不經壓縮的純音頻數據",這意味只有PCM格式的音頻數據是可以用來直接進行聲音處理,例如進行音量調節,聲音濾鏡等操作,相對的其他的音頻編碼格式都是必須解碼後才能進行處理(PCM編碼的WAV文件也得先讀取文件頭),當然這不代表PCM文件就好用,因為沒有文件頭,所以進行處理或者播放之前我們必須事先知道PCM文件的聲道數,採樣點位元組數,採樣率,編碼大小端,這在大多數情況下都是不可能的,事實上就我所知沒有播放器是直接支持PCM文件的播放。不過現在錄音的各項係數都是我們定義的,所以我們就不用擔心這個問題。
背景知識了解這些就足夠了,下面我給出實現代碼,綜合代碼講解實現過程。
if(recordVoice) {
audioRecord =newAudioRecord(MediaRecorder.AudioSource.MIC,
Constant.RecordSampleRate, AudioFormat.CHANNEL_IN_MONO,
pcmFormat.getAudioFormat(), audioRecordBufferSize);
try{
audioRecord.startRecording();
}catch(Exception e) {
NoRecordPermission();
continue;
}
BufferedOutputStream bufferedOutputStream = FileFunction
.GetBufferedOutputStreamFromFile(recordFileUrl);
while(recordVoice) {
intaudioRecordReadDataSize =
audioRecord.read(audioRecordBuffer,, audioRecordBufferSize);
if(audioRecordReadDataSize >) {
calculateRealVolume(audioRecordBuffer, audioRecordReadDataSize);
if(bufferedOutputStream !=null) {
try{
byte[] outputByteArray = CommonFunction
.GetByteBuffer(audioRecordBuffer,
audioRecordReadDataSize, Variable.isBigEnding);
bufferedOutputStream.write(outputByteArray);
}catch(IOException e) {
e.printStackTrace();
}
}
}else{
NoRecordPermission();
continue;
}
}
if(bufferedOutputStream !=null) {
try{
bufferedOutputStream.close();
}catch(Exception e) {
LogFunction.error("關閉錄音輸出數據流異常", e);
}
}
audioRecord.stop();
audioRecord.release();
audioRecord =null;
}
錄音的實際實現和控制代碼較多,在此僅抽出核心的錄音代碼進行講解。在此為獲取錄音的原始數據,我使用了Android原生的AudioRecord,其他的平台基本也會提供類似的工具類。這段代碼實現的功能是當錄音開始後,應用會根據設定的採樣率和聲道數以及採樣位元組數來不斷從MIC中獲取原始的音頻數據,然後將獲取的音頻數據寫入到指定文件中,直至錄音結束。這段代碼邏輯比較清晰的,我就不過多講解了。
潛在問題的話,手機平台上是需要申請錄音許可權的,如果沒有錄音許可權就無法生成正確的錄音文件。
2.2.2.解碼與裁剪背景音樂
如前文所說,除了PCM格式以外的所有音頻編碼格式的音頻都必須解碼後才可以處理,因此要讓背景音樂參與合成必須事先對背景音樂進行解碼,同時為減少合成的MP3文件的大小,需要根據錄音時長對解碼的音頻文件進行裁剪。本節不會詳細解釋解碼演算法,因為每個平台都會有對應封裝的工具類,直接使用即可。
背景知識先講這些,本次功能實現過程中的潛在問題較多,下面我給出實現代碼,綜合代碼講解實現過程。
privatebooleandecodeMusicFile(String musicFileUrl, String decodeFileUrl,intstartSecond,
intendSecond,
Handler handler,
DecodeOperateInterface decodeOperateInterface){
intsampleRate =;
intchannelCount =;
longduration =;
String mime =null;
MediaExtractor mediaExtractor =newMediaExtractor();
MediaFormat mediaFormat =null;
MediaCodec mediaCodec =null;
try{
mediaExtractor.setDataSource(musicFileUrl);
}catch(Exception e) {
LogFunction.error("設置解碼音頻文件路徑錯誤", e);
returnfalse;
}
mediaFormat = mediaExtractor.getTrackFormat();
sampleRate = mediaFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ?
mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) :44100;
channelCount = mediaFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ?
mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) :1;
duration = mediaFormat.containsKey(MediaFormat.KEY_DURATION) ? mediaFormat.getLong
(MediaFormat.KEY_DURATION)
:;
mime = mediaFormat.containsKey(MediaFormat.KEY_MIME) ? mediaFormat.getString(MediaFormat
.KEY_MIME) :"";
LogFunction.log("歌曲信息",
"Track info: mime:"+ mime +" 採樣率sampleRate:"+ sampleRate +" channels:"+
channelCount +" duration:"+ duration);
if(CommonFunction.isEmpty(mime) || !mime.startsWith("audio/")) {
LogFunction.error("解碼文件不是音頻文件","mime:"+ mime);
returnfalse;
}
if(mime.equals("audio/ffmpeg")) {
mime ="audio/mpeg";
mediaFormat.setString(MediaFormat.KEY_MIME, mime);
}
try{
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(mediaFormat,null,null,);
}catch(Exception e) {
LogFunction.error("解碼器configure出錯", e);
returnfalse;
}
getDecodeData(mediaExtractor, mediaCodec, decodeFileUrl, sampleRate, channelCount,
startSecond,
endSecond, handler, decodeOperateInterface);
returntrue;
}
decodeMusicFile方法的代碼主要功能是獲取背景音樂信息,初始化解碼器,最後調用getDecodeData方法正式開始對背景音樂進行處理。
代碼中使用了Android原生工具類作為解碼器,事實上作為原生的解碼器,我也遇到過兼容性問題不得不做了一些相應的處理,不得不抱怨一句不同的Android定製系統實在是導致了太多的兼容性問題。
privatevoidgetDecodeData(MediaExtractor mediaExtractor, MediaCodec mediaCodec,
String decodeFileUrl,intsampleRate,
intchannelCount,intstartSecond,intendSecond,
Handler handler,
finalDecodeOperateInterface decodeOperateInterface){
booleandecodeInputEnd =false;
booleandecodeOutputEnd =false;
intsampleDataSize;
intinputBufferIndex;
intoutputBufferIndex;
intbyteNumber;
longdecodeNoticeTime = System.currentTimeMillis();
longdecodeTime;
longpresentationTimeUs =;
finallongtimeOutUs =100;
finallongstartMicroseconds = startSecond *1000*1000;
finallongendMicroseconds = endSecond *1000*1000;
ByteBuffer[] inputBuffers;
ByteBuffer[] outputBuffers;
ByteBuffer sourceBuffer;
ByteBuffer targetBuffer;
MediaFormat outputFormat = mediaCodec.getOutputFormat();
MediaCodec.BufferInfo bufferInfo;
byteNumber =
(outputFormat.containsKey("bit-width") ? outputFormat.getInteger("bit-width") :
) /8;
mediaCodec.start();
inputBuffers = mediaCodec.getInputBuffers();
outputBuffers = mediaCodec.getOutputBuffers();
mediaExtractor.selectTrack();
bufferInfo =newMediaCodec.BufferInfo();
BufferedOutputStream bufferedOutputStream = FileFunction
.GetBufferedOutputStreamFromFile(decodeFileUrl);
while(!decodeOutputEnd) {
if(decodeInputEnd) {
return;
}
decodeTime = System.currentTimeMillis();
if(decodeTime - decodeNoticeTime > Constant.OneSecond) {
finalintdecodeProgress =
(int) ((presentationTimeUs - startMicroseconds) * Constant
.NormalMaxProgress /
endMicroseconds);
if(decodeProgress >) {
handler.post(newRunnable() {
@Override
publicvoidrun(){
decodeOperateInterface.updateDecodeProgress(decodeProgress);
}
});
}
decodeNoticeTime = decodeTime;
}
try{
inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOutUs);
if(inputBufferIndex >=) {
sourceBuffer = inputBuffers[inputBufferIndex];
sampleDataSize = mediaExtractor.readSampleData(sourceBuffer,);
if(sampleDataSize
decodeInputEnd =true;
sampleDataSize =;
}else{
presentationTimeUs = mediaExtractor.getSampleTime();
}
mediaCodec.queueInputBuffer(inputBufferIndex,, sampleDataSize,
presentationTimeUs,
decodeInputEnd ? MediaCodec.BUFFER_FLAG_END_OF_STREAM :);
if(!decodeInputEnd) {
mediaExtractor.advance();
}
}else{
LogFunction.error("inputBufferIndex",""+ inputBufferIndex);
}
// decode to PCM and push it to the AudioTrack player
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOutUs);
if(outputBufferIndex
switch(outputBufferIndex) {
caseMediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
outputBuffers = mediaCodec.getOutputBuffers();
LogFunction.error("MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED",
"[AudioDecoder]output buffers have changed.");
break;
caseMediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
outputFormat = mediaCodec.getOutputFormat();
sampleRate = outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ?
outputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) :
sampleRate;
channelCount = outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ?
outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) :
channelCount;
byteNumber = (outputFormat.containsKey("bit-width") ? outputFormat
.getInteger
("bit-width") /8;
LogFunction.error("MediaCodec.INFO_OUTPUT_FORMAT_CHANGED",
"[AudioDecoder]output format has changed to "+
mediaCodec.getOutputFormat());
break;
default:
LogFunction.error("error",
"[AudioDecoder] dequeueOutputBuffer returned "+
outputBufferIndex);
break;
}
continue;
}
targetBuffer = outputBuffers[outputBufferIndex];
byte[] sourceByteArray =newbyte[bufferInfo.size];
targetBuffer.get(sourceByteArray);
targetBuffer.clear();
mediaCodec.releaseOutputBuffer(outputBufferIndex,false);
if((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) !=) {
decodeOutputEnd =true;
}
if(sourceByteArray.length >&& bufferedOutputStream !=null) {
if(presentationTimeUs
continue;
}
byte[] convertByteNumberByteArray = ConvertByteNumber(byteNumber, Constant
.RecordByteNumber,
sourceByteArray);
byte[] resultByteArray =
ConvertChannelNumber(channelCount, Constant.RecordChannelNumber,
Constant.RecordByteNumber,
convertByteNumberByteArray);
try{
bufferedOutputStream.write(resultByteArray);
}catch(Exception e) {
LogFunction.error("輸出解壓音頻數據異常", e);
}
}
if(presentationTimeUs > endMicroseconds) {
break;
}
}catch(Exception e) {
LogFunction.error("getDecodeData異常", e);
}
}
if(bufferedOutputStream !=null) {
try{
bufferedOutputStream.close();
}catch(IOException e) {
LogFunction.error("關閉bufferedOutputStream異常", e);
}
}
if(sampleRate != Constant.RecordSampleRate) {
Resample(sampleRate, decodeFileUrl);
}
if(mediaCodec !=null) {
mediaCodec.stop();
mediaCodec.release();
}
if(mediaExtractor !=null) {
mediaExtractor.release();
}
}
getDecodeData方法是此次的進行解碼和裁剪的核心,方法的傳入參數中mediaExtractor,mediaCodec用以實際控制處理背景音樂的音頻數據,decodeFileUrl用以指明解碼和裁剪後的PCM文件的存儲地址,sampleRate,channelCount分別用以指明背景音樂的採樣率,聲道數,startSecond用以指明裁剪背景音樂的開始時間,目前功能中默認為0,endSecond用以指明裁剪背景音樂的結束時間,數值大小由錄音時長直接決定。
getDecodeData方法中通過不斷通過mediaCodec讀入背景音樂原始數據進行處理,然後解碼輸出到buffer從而獲取解碼後的數據,因為mediaCodec的讀取解碼方法和平台相關就不過多描述,在解碼過程中通過startSecond與endSecond來控制解碼後音頻數據輸出的開始與結束。
解碼和裁剪根據上文的描述是比較簡單的,通過平台提供的工具類解碼背景音樂數據,然後通過變數裁剪出指定長度的解碼後音頻數據輸出到外文件,這一個流程結束功能就實現了,但在過程中存在幾個潛在問題點。
首先,要進行合成處理的話,我們必須要保證錄音文件和解碼後文件的採樣率,採樣點位元組數,以及聲道數相同,因為錄音文件的這三項係數已經固定,所以我們必須對解碼的音頻數據進行處理以保證最終生成的解碼文件三項係數和錄音文件一致。在http://blog.csdn.net/ownwell/article/details/8114121/,我們可以了解PCM文件常見的四種存儲格式。我們就可以知道如何編碼以將已知格式的音頻數據轉化到另一採樣點位元組數和聲道數。
getDecodeData方法中146行調用的ConvertByteNumber方法是通過處理音頻數據以保證解碼後音頻文件和錄音文件採樣點位元組數相同。
privatestaticbyte[]ConvertByteNumber(intsourceByteNumber,intoutputByteNumber,byte[]
sourceByteArray){
if(sourceByteNumber == outputByteNumber) {
returnsourceByteArray;
}
intsourceByteArrayLength = sourceByteArray.length;
byte[] byteArray;
switch(sourceByteNumber) {
case1:
switch(outputByteNumber) {
case2:
byteArray =newbyte[sourceByteArrayLength *2];
byteresultByte[];
for(intindex =; index
resultByte = CommonFunction.GetBytes((short) (sourceByteArray[index]
*256), Variable
.isBigEnding);
byteArray[2* index] = resultByte[];
byteArray[2* index +1] = resultByte[1];
}
returnbyteArray;
}
break;
case2:
switch(outputByteNumber) {
case1:
intoutputByteArrayLength = sourceByteArrayLength /2;
byteArray =newbyte[outputByteArrayLength];
for(intindex =; index
byteArray[index] = (byte) (CommonFunction.GetShort(sourceByteArray[2
* index],
sourceByteArray[2* index +1], Variable.isBigEnding) /256);
}
returnbyteArray;
}
break;
}
returnsourceByteArray;
}
ConvertByteNumber方法的參數中sourceByteNumber代表背景音樂文件採樣點位元組數,outputByteNumber代表錄音文件採樣點位元組數,兩者如果相同就不處理,不相同則根據背景音樂文件採樣點位元組數進行不同的處理,本方法只對單位元組存儲和雙位元組存儲進行了處理,歡迎在各位Github上填充其他採樣點位元組數的處理方法,
getDecodeData方法中149行調用的ConvertChannelNumber方法是通過處理音頻數據以保證解碼後音頻文件和錄音文件聲道數相同。
privatestaticbyte[]ConvertChannelNumber(intsourceChannelCount,intoutputChannelCount,
intbyteNumber,
byte[] sourceByteArray){
if(sourceChannelCount == outputChannelCount) {
returnsourceByteArray;
}
switch(byteNumber) {
case1:
case2:
break;
default:
returnsourceByteArray;
}
intsourceByteArrayLength = sourceByteArray.length;
byte[] byteArray;
switch(sourceChannelCount) {
case1:
switch(outputChannelCount) {
case2:
byteArray =newbyte[sourceByteArrayLength *2];
bytefirstByte;
bytesecondByte;
switch(byteNumber) {
case1:
for(intindex =; index
firstByte = sourceByteArray[index];
byteArray[2* index] = firstByte;
byteArray[2* index +1] = firstByte;
}
break;
case2:
for(intindex =; index
firstByte = sourceByteArray[index];
secondByte = sourceByteArray[index +1];
byteArray[2* index] = firstByte;
byteArray[2* index +1] = secondByte;
byteArray[2* index +2] = firstByte;
byteArray[2* index +3] = secondByte;
}
break;
}
returnbyteArray;
}
break;
case2:
switch(outputChannelCount) {
case1:
intoutputByteArrayLength = sourceByteArrayLength /2;
byteArray =newbyte[outputByteArrayLength];
switch(byteNumber) {
case1:
for(intindex =; index
shortaverageNumber =
(short) ((short) sourceByteArray[2* index] + (short)
sourceByteArray[2*
index +1]);
byteArray[index] = (byte) (averageNumber >>1);
}
break;
case2:
for(intindex =; index
byteresultByte[] = CommonFunction.AverageShortByteArray
(sourceByteArray[2* index],
sourceByteArray[2* index +1],
sourceByteArray[2*
index +2],
sourceByteArray[2* index +3], Variable
.isBigEnding);
byteArray[index] = resultByte[];
byteArray[index +1] = resultByte[1];
}
break;
}
returnbyteArray;
}
break;
}
returnsourceByteArray;
}
ConvertChannelNumber方法的參數中sourceChannelCount代表背景音樂文件聲道數,outputByteNumber代表錄音文件聲道數,兩者如果相同就不處理,不相同則根據聲道數和採樣點位元組數進行不同的處理,本方法只對單雙通道進行了處理,歡迎在Github上填充立體聲等聲道的處理方法。
getDecodeData方法中176行調用的Resample方法是用以處理音頻數據以保證解碼後音頻文件和錄音文件採樣率相同。
privatestaticvoidResample(intsampleRate, String decodeFileUrl){
String newDecodeFileUrl = decodeFileUrl +"new";
try{
FileInputStream fileInputStream =
newFileInputStream(newFile(decodeFileUrl));
FileOutputStream fileOutputStream =
newFileOutputStream(newFile(newDecodeFileUrl));
newSSRC(fileInputStream, fileOutputStream, sampleRate, Constant.RecordSampleRate,
Constant.RecordByteNumber, Constant.RecordByteNumber,1, Integer.MAX_VALUE,
,,true);
fileInputStream.close();
fileOutputStream.close();
FileFunction.RenameFile(newDecodeFileUrl, decodeFileUrl);
}catch(IOException e) {
LogFunction.error("關閉bufferedOutputStream異常", e);
}
}
為了修改採樣率,在此使用了SSRC在Java端的實現,在網上可以搜到一份關於SSRC的介紹:"SSRC = Synchronous Sample Rate Converter,同步採樣率轉換,直白地說就是只能做整數倍頻,不支持任意頻率之間的轉換,比如44.1KHz48KHz。",但不同的SSRC實現原理有所不同,我是用的是來自https://github.com/shibatch/SSRC在Java端的實現,簡單讀了此SSRC在Java端實現的源碼,其代碼實現中通過判別重採樣前後採樣率的最大公約數是否滿足設定條件作為是否可重採樣的依據,可以支持常見的非整數倍頻率的採樣率轉化,如44.1khz48khz,但如果目標採樣率是比較特殊的採樣率如某一較大的質數,那就無法支持重採樣。
至此,Resample,ConvertByteNumber,ConvertChannelNumber三個方法的處理保證了解碼後文件和錄音文件的採樣率,採樣點位元組數,以及聲道數相同。
接著,此處潛在的第二個問題就是大小端存儲。 對計算機體系結構有所了解的同學肯定了解"大小端"這個概念,大小端分別代表了多位元組數據在內存中組織的兩種不同順序,如果對於"大小端"不是太了解,可以瀏覽http://blog.jobbole.com/102432/的闡述,在處理音頻數據的方法中,我們可以看到"Variable.isBigEnding"這個參數,這個參數的含義就是當前平台是否使用大端編碼,這裡大家肯定會有疑問,內存中多位元組數據的組織順序為什麼會影響我們對音頻數據的處理,舉個例子,如果我們在將採樣點8位的音頻數據轉化為採樣點16位,目前的做法是將原始數據乘以256,相當於每一個byte轉化為short,同時short的高位元組為原byte的內容,低位元組為0,那現在問題來了,那就是高位元組放到高地址還是低地址,這就和平台採用的大小端存儲格式息息相關了,當然如果我們輸出的數據類型是short那就不用關心,Java會幫我們處理掉,但我們輸出的是byte數組,這就需要我們自己對數據進行處理了。
這是一個很容易忽視的問題,因為正常情況下的軟體開發過程中我們基本是不用關心大小端的問題的,但在這裡必須對大小端的情況進行處理,不然會出現在某些平台合成的音頻無法播放的情況。
2.2.3.合成與輸出
錄音和對背景音樂的處理結束了,接下來就是最後的合成了,對於合成我們腦海中浮現最多的會是什麼?相加,對沒錯,音頻合成並不神秘,音頻合成的本質就是相同係數的音頻文件之間數據的加和,當然現實中的合成往往並非如此簡單,在網上搜索"混音演算法",我們可以看到大量高深的音頻合成演算法,但就目前而言,我們沒必要實現複雜的混音演算法,只要讓兩個音頻文件的原始音頻數據相加即可,不過為了讓我們的合成看上去稍微有一些技術含量,此次提供的音頻合成方法中允許任意音頻文件相對於另一音頻文件進行時間上的偏移,並可以通過兩個權重數據進行音量調節。
ComposeAudio方法是此次的進行合成的具體代碼實現,方法的傳入參數中firstAudioFilePath, secondAudioFilePath是用以合成的音頻文件地址,composeAudioFilePath用以指明合成後輸出的MP3文件的存儲地址,firstAudioWeight,secondAudioWeight分別用以指明合成的兩個音頻文件在合成過程中的音量權重,audioOffset用以指明第一個音頻文件相對於第二個音頻文件合成過程中的數據偏移,如為負數,則合成過程中先輸出audioOffset個位元組長度的第二個音頻文件數據,如為正數,則合成過程中先輸出audioOffset個位元組長度的第一個音頻文件數據,audioOffset在另一程度上也代表著時間的偏移,目前我們合成的兩個音頻文件參數為16位單通道44.1khz採樣率,那麼audioOffset如果為116/81*44100=88200位元組,那麼最終合成出的MP3文件中會先播放1s的第一個音頻文件的音頻接著再播放兩個音頻文件加和的音頻。
整體合成代碼是很清晰的,因為加入了時間偏移,所以合成過程中是有可能有一個文件先輸出完的,在代碼中針對性的進行處理即可,當然即使沒有時間偏移也是可能出現類似情況的,比如音樂時長2分鐘,錄音3分鐘,音樂輸出結束後那就只應該輸出錄音音頻了,另外在代碼中將PCM數據編碼為MP3文件使用了LAME的MP3編碼庫,除此以外代碼中就沒有比較複雜的模塊了。
TAG:何俊林 |