iOS 使用 Lame 轉(zhuǎn)碼 MP3 的最正確姿勢

前言

  • 最近在項目中, 做有關(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 *pcmFILE *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 一樣的錯誤時長 !!!

  • 所以分析造成這個問題的原因可能是:
    1. AVPlayer 不能正確讀取長度
    1. 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

致謝

對我有幫助的文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末染簇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子强岸,更是在濱河造成了極大的恐慌锻弓,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝌箍,死亡現(xiàn)場離奇詭異青灼,居然都是意外死亡,警方通過查閱死者的電腦和手機十绑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門聚至,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酷勺,“玉大人本橙,你說我怎么就攤上這事〈嗨撸” “怎么了甚亭?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長击胜。 經(jīng)常有香客問我亏狰,道長,這世上最難降的妖魔是什么偶摔? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任暇唾,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘策州。我一直安慰自己瘸味,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布够挂。 她就那樣靜靜地躺著旁仿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪孽糖。 梳的紋絲不亂的頭發(fā)上枯冈,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音办悟,去河邊找鬼尘奏。 笑死,一個胖子當(dāng)著我的面吹牛誉尖,可吹牛的內(nèi)容都是我干的罪既。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼铡恕,長吁一口氣:“原來是場噩夢啊……” “哼琢感!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起探熔,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤驹针,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后诀艰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柬甥,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年其垄,在試婚紗的時候發(fā)現(xiàn)自己被綠了苛蒲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡绿满,死狀恐怖臂外,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喇颁,我是刑警寧澤漏健,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站橘霎,受9級特大地震影響蔫浆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姐叁,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一瓦盛、第九天 我趴在偏房一處隱蔽的房頂上張望洗显。 院中可真熱鬧,春花似錦原环、人聲如沸墙懂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽损搬。三九已至,卻和暖如春柜与,著一層夾襖步出監(jiān)牢的瞬間巧勤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工弄匕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留颅悉,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓迁匠,卻偏偏與公主長得像剩瓶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子城丧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容