ios開發 解決用lame轉換成MP3時,播放時間變短的問題

前言

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

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

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

所以, 我今天來寫一篇文章來認真剖析這個問題, 為什么起名 ?iOS 使用 Lame 轉碼 MP3 的最正確姿勢 !是因為我在百度搜索到的各種有關于Lame轉碼的代碼, 至少很大一部分 都是不完全正確的.

概述

我將會在本篇文章分析以下幾點內容

AVAudioRecorder 配置 和 Lame 編碼壓縮配置

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

邊錄制邊轉碼的實現

測試 Demo

AVAudioRecorder 配置 和 Lame 編碼壓縮配置

AVAudioRecorder 配置的注意事項

關于 AVAudioRecorder 錄音的相關配置 和 Lame 包的編譯工作, 這里忽略不講, 主要是想說一下需要注意的地方

Lame 的轉碼壓縮, 是把錄制的 PCM 轉碼成 MP3, 所以錄制的AVFormatIDKey設置成kAudioFormatLinearPCM, 生成的文件可以是 caf 或者 wav.

caf文件是 Mac OS X 原本支持的眾多音頻格式中最新增加的一種. iPhone 短信就是這種格式, 錄制出的文件會比較大.

AVNumberOfChannelsKey必須設置為雙聲道, 不然轉碼生成的 MP3 會聲音尖銳變聲.

AVSampleRateKey必須保證和轉碼設置的相同.

Lame 編碼壓縮 的相關配置

我們需要錄音源文件路徑和生成MP3的路徑FILE *pcm和FILE *mp3,

//source 被轉換的音頻文件位置

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, 就意味著我只允許寫數據, 而如果你有對文件的讀取操作,將會出現錯誤, 這也是我被坑過的地方.

C 語言的 文件打開模式

w+以純文本方式讀寫,而wb+是以二進制方式進行讀寫。

mode說明:

w 打開只寫文件,若文件存在則文件長度清為0,即該文件內容會消失。若文件不存在則建立該文件。

w+ 打開可讀寫文件,若文件存在則文件長度清為零,即該文件內容會消失。若文件不存在則建立該文件。

wb 只寫方式打開或新建一個二進制文件,只允許寫數據。

wb+ 讀寫方式打開或建立一個二進制文件,允許讀和寫。

r 打開只讀文件,該文件必須存在,否則報錯。

r+ 打開可讀寫的文件,該文件必須存在,否則報錯。

rb+ 讀寫方式打開一個二進制文件,只允許讀寫數據。

a 以附加的方式打開只寫文件。若文件不存在,則會建立該文件,如果文件存在,寫入的數據會被加到文件尾,即文件原先的內容會被保留。(EOF符保留)

a+ 以附加方式打開可讀寫的文件。若文件不存在,則會建立該文件,如果文件存在,寫入的數據會被加到文件尾后,即文件原先的內容會被保留。 (原來的EOF符不保留)

ab+ 讀寫打開一個二進制文件,允許讀或在文件末追加數據。

加入b 字符用來告訴函數庫打開的文件為二進制文件,而非純文字文件。

然后是lame_init()來初始化,lame_set_num_channels(lame,1)默認轉碼為2雙通道, 設置單聲道會更大程度減少壓縮后文件的體積.

接下來 是執行一個 do while 的循環來反復讀取FILE* stream, 直到 read != 0 , 結束轉碼,釋放lame_close(lame); fclose(mp3);? fclose(pcm);

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

Lame 的轉碼配置網上有很多, 網上可以搜到很多相關的代碼, 作為小白 copy 使用, 由于不懂源碼實現,直接拿來用就出現了不可預料的問題. 我出現的播放時間不準確的問題, 無論是 AVPlayer 或者 AVAudioPlayer 均無法讀取正確的長度, 要么是多幾秒, 要么是少幾秒, 還可能是超過10s的的誤差, 但是播放的過程中, 定時器的計數 會和 總時間顯示不吻合, 就比如 一個顯示 2:30 的錄音, 活生生 放到了 2:50, 你能想象是多么的尷尬Bug.

問題猜測

我把錄制完成的文件, 使用 iTunes 來播放可以顯示出正確的長度, 但是使用 QuickTime Player 會出現和 AVPlayer 一樣的錯誤時長 !!!

所以分析造成這個問題的原因可能是:

AVPlayer 不能正確讀取長度

MP3的編碼出現了錯誤...

然后網上也有人遇到了同樣的問題,給出的解決方法是換一種 AVPlayer 讀取方法:

我總結了 AVPlayer 獲取總時長的以下方法 ,結果測試 結果都是相近,

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 的這種錯誤情況. 實際中我并沒有出現過.

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

了解MP3編碼格式

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

MP3使用的是動態碼率方式,而這種方式每一幀的長度應該是不等的。那會不會是AVPlayer是把文件當做每幀相等的方式來計算的總時間,所以才不對?

不斷輸出 AVPlayer duration來看, 每次都會有不同的結果, 而 AVPlayer 是支持Mp3 VBR格式文件播放的。所以應該還是我們的生成的文件有問題

了解到 MP3 VBR頭這個東西,有它記錄了整個文件的幀總數量,就能直接算出duration.所以是不是我們Lame編碼的時候,沒有寫入 VBR 頭 呢.

Lame 源碼分析

搜索 Lame 源碼VBR關鍵字可以得到

/*

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 *);

源碼寫的很簡單, 就是設置了gfp->write_lame_tag值, 看看所有調用write_lame_tag的地方吧。第一個就找到了lame_encode_mp3_frame(..)函數。這不就是用來每次灌buffer給lame做MP3編碼的方法嘛!也就是說每次都會給給幀添加VBR信息,這和之前看的編碼資料描述的一樣。

接下來, 就是需要找到寫入VBR頭的函數, 搜索源碼可得PutLameVBR()被調用在lame_get_lametag_frame()函數里, 然后我們來看看這個函數:

/*

* 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);

原來這個函數是應該在lame_encode_flush()之后調, 當所有數據都寫入完畢了再調用。仔細想想也很合理, 這時才能確定文件的總幀數。

問題解決

現在的思路就比較清晰了, 由于在Lame編碼的過程中, 我們沒有對VBR頭進行寫入, 導致了 AVPlayer duration 以每幀相同的方式來計算出現的錯誤.

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

而這行代碼lame_mp3_tags_fid我在 網上搜索的各種配置中發現都沒有寫.

邊錄制邊轉碼的實現

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

核心代碼實現

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);

邊錄邊轉碼, 只是我們在錄制結果后,重新開一個線程來進行文件的轉碼,

當錄音進行中時, 會持續讀取到指定大小文件,進行編碼, 讀取不到,則線程休眠

在 while 的條件中, 我們收到 錄音結束的條件,則會結束 do while 的循環.

我們需要在錄制結束后發送一個信號, 讓 do while 跳出循環

測試 Demo

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

關于Demo, 可以在 ViewController 中#define ENCODE_MP3 1使用 1 和 0 , 來測試普通轉碼 和 邊錄制 邊轉碼.

ConvertAudioFile是錄音轉碼封裝的源碼

邊錄邊轉的用法

[[ConvertAudioFile sharedInstance] conventToMp3WithCafFilePath:self.cafPath

mp3FilePath:self.mp3Path

sampleRate:ETRECORD_RATE callback:^(BOOL result)

{

NSLog(@"---- 轉碼完成? --- result %d? ---- ", result);

}];;

錄制完成轉碼的用法

[ConvertAudioFile conventToMp3WithCafFilePath:self.cafPath

mp3FilePath:self.mp3Path

sampleRate:ETRECORD_RATE callback:^(BOOL result)

{

NSLog(@"---- 轉碼完成? --- result %d? ---- ", result);

}];

Demo 見 文章底部, 如果Demo 有什么不理解 和 不準確的地方,還麻煩指正...

結語

由于時間有限, 我并不會 寫太多細致的內容, 只是對這幾天的研究做一個總結,和列舉一些注意事項,如果在做音頻錄制轉碼中遇到相同的問題,則會有比較大的幫助.

總結

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

在使用別人的示范代碼時,如果不進行一定的剖析;當出現問題的時間,會比較的難判斷問題的來源

iOS的相關技術博客,現在網上可以搜到很多相關示范代碼, 但是由于很多人可能也是貼出了并不是很準確的東西, 相關給別人帶來了錯誤的示范.

作為 iOS 開發者, 對很多東西,如果想要有更加深層次的理解,則需要 1. 計算機基礎扎實 2. iOS底層理解夠深 3.架構設計模式理解夠深 4.代碼平時寫的必須夠優雅

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

Link

Demo - GitHub

簡書

個人博客

致謝

對我有幫助的文章

https://itony.me/365.html

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容