前言
- 最近在項目中, 做有關(guān) AVAudioRecorder 的錄音開發(fā), 需要把錄制的格式轉(zhuǎn)成 MP3, 遇到了轉(zhuǎn)碼之后的MP3文件, 無法獲取正確的時長問題.
- 為了解決這個問題, 真的是反復(fù)來修改錄音配置, 浪費了不知道多少的時間來分析這個問題.
- 中間我去某某群去找大神提問問題,結(jié)果遭到了鄙視, 都統(tǒng)統(tǒng)質(zhì)疑我的錄音配置, 最后甩給我一個demo, 結(jié)果我一測試, 也是一樣的問題, 我就呵呵了.
- 所以, 我今天來寫一篇文章來認(rèn)真剖析這個問題, 為什么起名 ? iOS 使用 Lame 轉(zhuǎn)碼 MP3 的最正確姿勢 ! 是因為我在百度搜索到的各種有關(guān)于 Lame 轉(zhuǎn)碼的代碼, 至少很大一部分 都是不完全正確的.
概述
我將會在本篇文章分析以下幾點內(nèi)容
- AVAudioRecorder 配置 和 Lame 編碼壓縮配置
- 解決錄音時長讀取不正確的問題
- 邊錄制邊轉(zhuǎn)碼的實現(xiàn)
- 測試 Demo
AVAudioRecorder 配置 和 Lame 編碼壓縮配置
AVAudioRecorder 配置的注意事項
關(guān)于 AVAudioRecorder 錄音的相關(guān)配置 和 Lame 包的編譯工作, 這里忽略不講, 主要是想說一下需要注意的地方
- Lame 的轉(zhuǎn)碼壓縮, 是把錄制的 PCM 轉(zhuǎn)碼成 MP3, 所以錄制的
AVFormatIDKey
設(shè)置成kAudioFormatLinearPCM
, 生成的文件可以是 caf 或者 wav. - caf 文件是 Mac OS X 原本支持的眾多音頻格式中最新增加的一種. iPhone 短信就是這種格式, 錄制出的文件會比較大.
-
AVNumberOfChannelsKey
必須設(shè)置為雙聲道, 不然轉(zhuǎn)碼生成的 MP3 會聲音尖銳變聲. -
AVSampleRateKey
必須保證和轉(zhuǎn)碼設(shè)置的相同.
Lame 編碼壓縮 的相關(guān)配置
- 我們需要錄音源文件路徑和生成MP3的路徑
FILE *pcm
和FILE *mp3
,
//source 被轉(zhuǎn)換的音頻文件位置
FILE *pcm = fopen([cafFilePath cStringUsingEncoding:1], "rb");
//skip file header 跳過 PCM header 能保證錄音的開頭沒有噪音
fseek(pcm, 4*1024, SEEK_CUR);
//output 輸出生成的Mp3文件位置
FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:1], "wb+");
- 通過
fopen
需要注意打開文件的模式. ?? 是擴展的 的 C 語言的 文件打開模式, 為什么要說這些, 比如 我使用 wb 來打開 mp3, 就意味著我只允許寫數(shù)據(jù), 而如果你有對文件的讀取操作,將會出現(xiàn)錯誤, 這也是我被坑過的地方.
C 語言的 文件打開模式
w+以純文本方式讀寫悠菜,而wb+是以二進制方式進行讀寫姓赤。
mode說明:
w 打開只寫文件嫉你,若文件存在則文件長度清為0译仗,即該文件內(nèi)容會消失。若文件不存在則建立該文件竟稳。
w+ 打開可讀寫文件窍霞,若文件存在則文件長度清為零,即該文件內(nèi)容會消失边涕。若文件不存在則建立該文件。
wb 只寫方式打開或新建一個二進制文件褂微,只允許寫數(shù)據(jù)功蜓。
wb+ 讀寫方式打開或建立一個二進制文件,允許讀和寫宠蚂。
r 打開只讀文件式撼,該文件必須存在,否則報錯求厕。
r+ 打開可讀寫的文件著隆,該文件必須存在,否則報錯呀癣。
rb+ 讀寫方式打開一個二進制文件美浦,只允許讀寫數(shù)據(jù)。
a 以附加的方式打開只寫文件项栏。若文件不存在浦辨,則會建立該文件,如果文件存在沼沈,寫入的數(shù)據(jù)會被加到文件尾流酬,即文件原先的內(nèi)容會被保留。(EOF符保留)
a+ 以附加方式打開可讀寫的文件列另。若文件不存在康吵,則會建立該文件,如果文件存在访递,寫入的數(shù)據(jù)會被加到文件尾后晦嵌,即文件原先的內(nèi)容會被保留。 (原來的EOF符不保留)
ab+ 讀寫打開一個二進制文件,允許讀或在文件末追加數(shù)據(jù)惭载。
加入b 字符用來告訴函數(shù)庫打開的文件為二進制文件旱函,而非純文字文件。
- 然后是
lame_init()
來初始化,lame_set_num_channels(lame,1)
默認(rèn)轉(zhuǎn)碼為2雙通道, 設(shè)置單聲道會更大程度減少壓縮后文件的體積. - 接下來 是執(zhí)行一個 do while 的循環(huán)來反復(fù)讀取
FILE* stream
, 直到 read != 0 , 結(jié)束轉(zhuǎn)碼,釋放lame_close(lame); fclose(mp3); fclose(pcm);
解決錄音時長讀取不正確的問題
Lame 的轉(zhuǎn)碼配置網(wǎng)上有很多, 網(wǎng)上可以搜到很多相關(guān)的代碼, 作為小白 copy 使用, 由于不懂源碼實現(xiàn),直接拿來用就出現(xiàn)了不可預(yù)料的問題. 我出現(xiàn)的播放時間不準(zhǔn)確的問題, 無論是 AVPlayer 或者 AVAudioPlayer 均無法讀取正確的長度, 要么是多幾秒, 要么是少幾秒, 還可能是超過10s的的誤差, 但是播放的過程中, 定時器的計數(shù) 會和 總時間顯示不吻合, 就比如 一個顯示 2:30 的錄音, 活生生 放到了 2:50, 你能想象是多么的尷尬Bug.
問題猜測
我把錄制完成的文件, 使用 iTunes 來播放可以顯示出正確的長度, 但是使用 QuickTime Player 會出現(xiàn)和 AVPlayer 一樣的錯誤時長 !!!
- 所以分析造成這個問題的原因可能是:
- AVPlayer 不能正確讀取長度
- MP3的編碼出現(xiàn)了錯誤...
- 然后網(wǎng)上也有人遇到了同樣的問題,給出的解決方法是換一種 AVPlayer 讀取方法:
我總結(jié)了 AVPlayer 獲取總時長的以下方法 ,結(jié)果測試 結(jié)果都是相近,
- way 1
CMTime time = _player.currentItem.duration;
if (time.timescale == 0) {
return 0;
}
return time.value / time.timescale;
- way 2
if (self.player && self.player.currentItem && self.player.currentItem.asset) {
return CMTimeGetSeconds(self.player.currentItem.asset.duration);
} else{
return 0;
}
- way 3
AVURLAsset* audioAsset = [AVURLAsset URLAssetWithURL:self.playingURL options:nil];
CMTime audioDuration = audioAsset.duration;
float audioDurationSeconds = CMTimeGetSeconds(audioDuration);
return (NSInteger)audioDurationSeconds;
- 其中 , 使用 Asset 可以解決獲取總時間是 NA 的這種錯誤情況. 實際中我并沒有出現(xiàn)過.
- 我的測試中 AVPlayer 使用這幾個方法, 均無法得到正確的值, 所以應(yīng)該就是生成文件的問題了.
了解MP3編碼格式
然后,通過對MP3編碼格式調(diào)研, 了解到如下信息:
- MP3使用的是動態(tài)碼率方式描滔,而這種方式每一幀的長度應(yīng)該是不等的棒妨。那會不會是 AVPlayer 是把文件當(dāng)做每幀相等的方式來計算的總時間,所以才不對含长?
- 不斷輸出 AVPlayer duration來看, 每次都會有不同的結(jié)果, 而 AVPlayer 是支持Mp3 VBR格式文件播放的券腔。所以應(yīng)該還是我們的生成的文件有問題
- 了解到 MP3 VBR頭這個東西,有它記錄了整個文件的幀總數(shù)量,就能直接算出duration.所以是不是我們Lame編碼的時候,沒有寫入 VBR 頭 呢.
Lame 源碼分析
- 搜索 Lame 源碼 VBR關(guān)鍵字可以得到
/*
1 = write a Xing VBR header frame.
default = 1
this variable must have been added by a Hungarian notation Windows programmer :-)
*/
int CDECL lame_set_bWriteVbrTag(lame_global_flags *, int);
int CDECL lame_get_bWriteVbrTag(const lame_global_flags *);
源碼寫的很簡單, 就是設(shè)置了
gfp->write_lame_tag
值, 看看所有調(diào)用write_lame_tag
的地方吧拘泞。第一個就找到了lame_encode_mp3_frame(..)
函數(shù)纷纫。這不就是用來每次灌buffer給lame做MP3編碼的方法嘛!也就是說每次都會給給幀添加VBR信息陪腌,這和之前看的編碼資料描述的一樣辱魁。接下來, 就是需要找到寫入VBR頭的函數(shù), 搜索源碼可得
PutLameVBR()
被調(diào)用在lame_get_lametag_frame()
函數(shù)里, 然后我們來看看這個函數(shù):
/*
* OPTIONAL:
* lame_mp3_tags_fid will rewrite a Xing VBR tag to the mp3 file with file
* pointer fid. These calls perform forward and backwards seeks, so make
* sure fid is a real file. Make sure lame_encode_flush has been called,
* and all mp3 data has been written to the file before calling this
* function.
* NOTE:
* if VBR tags are turned off by the user, or turned off by LAME because
* the output is not a regular file, this call does nothing
* NOTE:
* LAME wants to read from the file to skip an optional ID3v2 tag, so
* make sure you opened the file for writing and reading.
* NOTE:
* You can call lame_get_lametag_frame instead, if you want to insert
* the lametag yourself.
*/
void CDECL lame_mp3_tags_fid(lame_global_flags *, FILE* fid);
- 原來這個函數(shù)是應(yīng)該在lame_encode_flush()之后調(diào), 當(dāng)所有數(shù)據(jù)都寫入完畢了再調(diào)用。仔細(xì)想想也很合理, 這時才能確定文件的總幀數(shù)诗鸭。
問題解決
- 現(xiàn)在的思路就比較清晰了, 由于在Lame編碼的過程中, 我們沒有對VBR頭進行寫入, 導(dǎo)致了 AVPlayer duration 以每幀相同的方式來計算出現(xiàn)的錯誤.
- 解決方法是, 在lame文件全部寫入之后, lame釋放之前, 使用
lame_mp3_tags_fid
寫入 VBR 頭文件, 測試通過, 讀取時間正常. - 而這行代碼
lame_mp3_tags_fid
我在 網(wǎng)上搜索的各種配置中發(fā)現(xiàn)都沒有寫.
邊錄制邊轉(zhuǎn)碼的實現(xiàn)
通常我們是在錄制結(jié)束之后, 再進行轉(zhuǎn)碼; 當(dāng)錄制的時間較長, 會消耗的時間比較長. 用戶需要等待轉(zhuǎn)碼結(jié)束后,才能操作; 但是如果我們使用邊錄制,邊轉(zhuǎn)碼的方式, 開另外一個線程同時進行轉(zhuǎn)碼,則幾乎沒有等待的時間,效率上會比較的高.
- 核心代碼實現(xiàn)
do {
curpos = ftell(pcm);
long startPos = ftell(pcm);
fseek(pcm, 0, SEEK_END);
long endPos = ftell(pcm);
long length = endPos - startPos;
fseek(pcm, curpos, SEEK_SET);
if (length > PCM_SIZE * 2 * sizeof(short int)) {
if (!isSkipPCMHeader) {
//Uump audio file header, If you do not skip file header
//you will heard some noise at the beginning!!!
fseek(pcm, 4 * 1024, SEEK_CUR);
isSkipPCMHeader = YES;
NSLog(@"skip pcm file header !!!!!!!!!!");
}
read = (int)fread(pcm_buffer, 2 * sizeof(short int), PCM_SIZE, pcm);
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
NSLog(@"read %d bytes", write);
} else {
[NSThread sleepForTimeInterval:0.05];
NSLog(@"sleep");
}
} while (! weakself.stopRecord);
- 邊錄邊轉(zhuǎn)碼, 只是我們在錄制結(jié)果后,重新開一個線程來進行文件的轉(zhuǎn)碼,
- 當(dāng)錄音進行中時, 會持續(xù)讀取到指定大小文件,進行編碼, 讀取不到,則線程休眠
- 在 while 的條件中, 我們收到 錄音結(jié)束的條件,則會結(jié)束 do while 的循環(huán).
- 我們需要在錄制結(jié)束后發(fā)送一個信號, 讓 do while 跳出循環(huán)
測試 Demo
為了讓遇到相同問題的人, 能夠更加對這些問題有一點的了解, 我會 在這里貼一個我測試的Demo 這只是一個實例程序, 并不具備完整的邏輯功能, 請熟知.
- 關(guān)于Demo, 可以在 ViewController 中
#define ENCODE_MP3 1
使用 1 和 0 , 來測試普通轉(zhuǎn)碼 和 邊錄制 邊轉(zhuǎn)碼. -
ConvertAudioFile
是錄音轉(zhuǎn)碼封裝的源碼 - 邊錄邊轉(zhuǎn)的用法
[[ConvertAudioFile sharedInstance] conventToMp3WithCafFilePath:self.cafPath
mp3FilePath:self.mp3Path
sampleRate:ETRECORD_RATE callback:^(BOOL result)
{
NSLog(@"---- 轉(zhuǎn)碼完成 --- result %d ---- ", result);
}];;
- 錄制完成轉(zhuǎn)碼的用法
[ConvertAudioFile conventToMp3WithCafFilePath:self.cafPath
mp3FilePath:self.mp3Path
sampleRate:ETRECORD_RATE callback:^(BOOL result)
{
NSLog(@"---- 轉(zhuǎn)碼完成 --- result %d ---- ", result);
}];
- Demo 見 文章底部, 如果Demo 有什么不理解 和 不準(zhǔn)確的地方,還麻煩指正...
結(jié)語
由于時間有限, 我并不會 寫太多細(xì)致的內(nèi)容, 只是對這幾天的研究做一個總結(jié),和列舉一些注意事項,如果在做音頻錄制轉(zhuǎn)碼中遇到相同的問題,則會有比較大的幫助.
總結(jié)
這次解決這個問題,讓我受益匪淺, 很多地方的收獲是超過問題本身的:
- 在使用別人的示范代碼時,如果不進行一定的剖析;當(dāng)出現(xiàn)問題的時間,會比較的難判斷問題的來源
- iOS的相關(guān)技術(shù)博客,現(xiàn)在網(wǎng)上可以搜到很多相關(guān)示范代碼, 但是由于很多人可能也是貼出了并不是很準(zhǔn)確的東西, 相關(guān)給別人帶來了錯誤的示范.
- 作為 iOS 開發(fā)者, 對很多東西,如果想要有更加深層次的理解,則需要 1. 計算機基礎(chǔ)扎實 2. iOS底層理解夠深 3.架構(gòu)設(shè)計模式理解夠深 4.代碼平時寫的必須夠優(yōu)雅
- Google 會比 Baidu 靠譜呀; 雖然我之前也是這么想的,但這次對我有幫助的文章均來自 Google, 相反Baidu 給了很多錯誤的示范.
Link
致謝
對我有幫助的文章