ios開發(fā) 解決用lame轉(zhuǎn)換成MP3時沸移,播放時間變短的問題

前言

最近在項(xiàng)目中, 做有關(guān)AVAudioRecorder的錄音開發(fā), 需要把錄制的格式轉(zhuǎn)成 MP3, 遇到了轉(zhuǎn)碼之后的MP3文件, 無法獲取正確的時長問題.

為了解決這個問題, 真的是反復(fù)來修改錄音配置, 浪費(fèi)了不知道多少的時間來分析這個問題.

中間我去某某群去找大神提問問題,結(jié)果遭到了鄙視, 都統(tǒng)統(tǒng)質(zhì)疑我的錄音配置, 最后甩給我一個demo, 結(jié)果我一測試, 也是一樣的問題, 我就呵呵了.

所以, 我今天來寫一篇文章來認(rèn)真剖析這個問題, 為什么起名 ?iOS 使用 Lame 轉(zhuǎn)碼 MP3 的最正確姿勢 !是因?yàn)槲以诎俣人阉鞯降母鞣N有關(guān)于Lame轉(zhuǎn)碼的代碼, 至少很大一部分 都是不完全正確的.

概述

我將會在本篇文章分析以下幾點(diǎn)內(nèi)容

AVAudioRecorder 配置 和 Lame 編碼壓縮配置

解決錄音時長讀取不正確的問題

邊錄制邊轉(zhuǎn)碼的實(shí)現(xiàn)

測試 Demo

AVAudioRecorder 配置 和 Lame 編碼壓縮配置

AVAudioRecorder 配置的注意事項(xiàng)

關(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需要注意打開文件的模式. ?? 是擴(kuò)展的 的 C 語言的 文件打開模式, 為什么要說這些, 比如 我使用 wb 來打開 mp3, 就意味著我只允許寫數(shù)據(jù), 而如果你有對文件的讀取操作,將會出現(xiàn)錯誤, 這也是我被坑過的地方.

C 語言的 文件打開模式

w+以純文本方式讀寫,而wb+是以二進(jìn)制方式進(jìn)行讀寫荣回。

mode說明:

w 打開只寫文件遭贸,若文件存在則文件長度清為0,即該文件內(nèi)容會消失心软。若文件不存在則建立該文件壕吹。

w+ 打開可讀寫文件,若文件存在則文件長度清為零删铃,即該文件內(nèi)容會消失耳贬。若文件不存在則建立該文件。

wb 只寫方式打開或新建一個二進(jìn)制文件猎唁,只允許寫數(shù)據(jù)咒劲。

wb+ 讀寫方式打開或建立一個二進(jìn)制文件,允許讀和寫诫隅。

r 打開只讀文件腐魂,該文件必須存在,否則報(bào)錯阎肝。

r+ 打開可讀寫的文件挤渔,該文件必須存在,否則報(bào)錯风题。

rb+ 讀寫方式打開一個二進(jìn)制文件,只允許讀寫數(shù)據(jù)嫉父。

a 以附加的方式打開只寫文件沛硅。若文件不存在,則會建立該文件绕辖,如果文件存在摇肌,寫入的數(shù)據(jù)會被加到文件尾,即文件原先的內(nèi)容會被保留仪际。(EOF符保留)

a+ 以附加方式打開可讀寫的文件围小。若文件不存在,則會建立該文件树碱,如果文件存在肯适,寫入的數(shù)據(jù)會被加到文件尾后,即文件原先的內(nèi)容會被保留成榜。 (原來的EOF符不保留)

ab+ 讀寫打開一個二進(jìn)制文件框舔,允許讀或在文件末追加數(shù)據(jù)。

加入b 字符用來告訴函數(shù)庫打開的文件為二進(jìn)制文件,而非純文字文件刘绣。

然后是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 使用, 由于不懂源碼實(shí)現(xiàn),直接拿來用就出現(xiàn)了不可預(yù)料的問題. 我出現(xiàn)的播放時間不準(zhǔn)確的問題, 無論是 AVPlayer 或者 AVAudioPlayer 均無法讀取正確的長度, 要么是多幾秒, 要么是少幾秒, 還可能是超過10s的的誤差, 但是播放的過程中, 定時器的計(jì)數(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 的這種錯誤情況. 實(shí)際中我并沒有出現(xiàn)過.

我的測試中 AVPlayer 使用這幾個方法, 均無法得到正確的值, 所以應(yīng)該就是生成文件的問題了.

了解MP3編碼格式

然后,通過對MP3編碼格式調(diào)研, 了解到如下信息:

MP3使用的是動態(tài)碼率方式樱溉,而這種方式每一幀的長度應(yīng)該是不等的。那會不會是AVPlayer是把文件當(dāng)做每幀相等的方式來計(jì)算的總時間纬凤,所以才不對福贞?

不斷輸出 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頭進(jìn)行寫入, 導(dǎo)致了 AVPlayer duration 以每幀相同的方式來計(jì)算出現(xiàn)的錯誤.

解決方法是, 在lame文件全部寫入之后, lame釋放之前, 使用lame_mp3_tags_fid寫入 VBR 頭文件, 測試通過, 讀取時間正常.

而這行代碼lame_mp3_tags_fid我在 網(wǎng)上搜索的各種配置中發(fā)現(xiàn)都沒有寫.

邊錄制邊轉(zhuǎn)碼的實(shí)現(xiàn)

通常我們是在錄制結(jié)束之后, 再進(jìn)行轉(zhuǎn)碼; 當(dāng)錄制的時間較長, 會消耗的時間比較長. 用戶需要等待轉(zhuǎn)碼結(jié)束后,才能操作; 但是如果我們使用邊錄制,邊轉(zhuǎn)碼的方式, 開另外一個線程同時進(jìn)行轉(zhuǎn)碼,則幾乎沒有等待的時間,效率上會比較的高.

核心代碼實(shí)現(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é)果后,重新開一個線程來進(jìn)行文件的轉(zhuǎn)碼,

當(dāng)錄音進(jìn)行中時, 會持續(xù)讀取到指定大小文件,進(jìn)行編碼, 讀取不到,則線程休眠

在 while 的條件中, 我們收到 錄音結(jié)束的條件,則會結(jié)束 do while 的循環(huán).

我們需要在錄制結(jié)束后發(fā)送一個信號, 讓 do while 跳出循環(huán)

測試 Demo

為了讓遇到相同問題的人, 能夠更加對這些問題有一點(diǎn)的了解, 我會 在這里貼一個我測試的Demo 這只是一個實(shí)例程序, 并不具備完整的邏輯功能, 請熟知.

關(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é),和列舉一些注意事項(xiàng),如果在做音頻錄制轉(zhuǎn)碼中遇到相同的問題,則會有比較大的幫助.

總結(jié)

這次解決這個問題,讓我受益匪淺, 很多地方的收獲是超過問題本身的:

在使用別人的示范代碼時,如果不進(jìn)行一定的剖析;當(dāng)出現(xiàn)問題的時間,會比較的難判斷問題的來源

iOS的相關(guān)技術(shù)博客,現(xiàn)在網(wǎng)上可以搜到很多相關(guān)示范代碼, 但是由于很多人可能也是貼出了并不是很準(zhǔn)確的東西, 相關(guān)給別人帶來了錯誤的示范.

作為 iOS 開發(fā)者, 對很多東西,如果想要有更加深層次的理解,則需要 1. 計(jì)算機(jī)基礎(chǔ)扎實(shí) 2. iOS底層理解夠深 3.架構(gòu)設(shè)計(jì)模式理解夠深 4.代碼平時寫的必須夠優(yōu)雅

Google 會比 Baidu 靠譜呀; 雖然我之前也是這么想的,但這次對我有幫助的文章均來自 Google, 相反Baidu 給了很多錯誤的示范.

Link

Demo - GitHub

簡書

個人博客

致謝

對我有幫助的文章

https://itony.me/365.html

http://www.reibang.com/p/57f38f075ba0

原文鏈接:http://www.reibang.com/p/971fff236881

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市朱躺,隨后出現(xiàn)的幾起案子刁赖,更是在濱河造成了極大的恐慌,老刑警劉巖长搀,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宇弛,死亡現(xiàn)場離奇詭異,居然都是意外死亡源请,警方通過查閱死者的電腦和手機(jī)枪芒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谁尸,“玉大人舅踪,你說我怎么就攤上這事×悸” “怎么了抽碌?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長决瞳。 經(jīng)常有香客問我货徙,道長,這世上最難降的妖魔是什么皮胡? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任痴颊,我火速辦了婚禮,結(jié)果婚禮上胸囱,老公的妹妹穿的比我還像新娘祷舀。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布裳扯。 她就那樣靜靜地躺著抛丽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪饰豺。 梳的紋絲不亂的頭發(fā)上亿鲜,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音冤吨,去河邊找鬼蒿柳。 笑死,一個胖子當(dāng)著我的面吹牛漩蟆,可吹牛的內(nèi)容都是我干的垒探。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼怠李,長吁一口氣:“原來是場噩夢啊……” “哼圾叼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捺癞,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夷蚊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后髓介,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惕鼓,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年唐础,在試婚紗的時候發(fā)現(xiàn)自己被綠了箱歧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡彻犁,死狀恐怖叫胁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情汞幢,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布微谓,位于F島的核電站森篷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏豺型。R本人自食惡果不足惜仲智,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望姻氨。 院中可真熱鬧钓辆,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至似嗤,卻和暖如春啸臀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烁落。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工乘粒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伤塌。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓灯萍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親每聪。 傳聞我的和親對象是個殘疾皇子旦棉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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