音樂一直是我的愛好淌铐,作為一名開發(fā)俐东,同時我也想知道這些音樂是怎么播放的裁厅,音效是如何改變的堤魁,如何升降調(diào)廊蜒,一個音樂播放器是怎么實現(xiàn)的。從而開啟我的音頻學習之路
基本知識
人耳所能聽到的聲音,最低的頻率是從20Hz起一直到最高頻率20KHZ,因此音頻文件格式的最大帶寬是20KHZ耀找。根據(jù)奈奎斯特的理論,只有采樣頻率高于聲音信號最高頻率的兩倍時业崖,才能把數(shù)字信號表示的聲音還原成為原來的聲音野芒,所以音頻文件的采樣率一般在40~50KHZ,比如最常見的CD音質(zhì)采樣率44.1KHZ双炕。
對聲音進行采樣狞悲、量化過程稱為脈沖編碼調(diào)制(Pulse Code Modulation)簡稱PCM,是無損的妇斤,但是數(shù)據(jù)量過大效诅,從而產(chǎn)生各種壓縮格式,有無損壓縮(ALAC趟济、APE、FLAC)和有損壓縮(MP3咽笼、AAC顷编、OGG、WMA)兩種剑刑。
最常用的是MP3格式媳纬,其碼率代表了MP3數(shù)據(jù)的壓縮質(zhì)量双肤,常用的碼率有128kbit/s、160kbit/s钮惠、320kbit/s等等茅糜,這個值越高聲音質(zhì)量也就越高。MP3編碼方式常用的有兩種固定碼率(Constant bitrate素挽,CBR)和可變碼率(Variable bitrate蔑赘,VBR)。
MP3格式中的數(shù)據(jù)通常由兩部分組成预明,一部分為ID3用來存儲歌名缩赛、演唱者、專輯撰糠、音軌數(shù)等信息酥馍,另一部分為音頻數(shù)據(jù)。音頻數(shù)據(jù)部分以幀(frame)為單位存儲阅酪,每個音頻都有自己的幀頭旨袒,如圖所示就是一個MP3文件幀結構圖(圖片同樣來自互聯(lián)網(wǎng))。MP3中的每一個幀都有自己的幀頭术辐,其中存儲了采樣率等解碼必須的信息砚尽,所以每一個幀都可以獨立于文件存在和播放,這個特性加上高壓縮比使得MP3文件成為了音頻流播放的主流格式术吗。幀頭之后存儲著音頻數(shù)據(jù)尉辑,這些音頻數(shù)據(jù)是若干個PCM數(shù)據(jù)幀經(jīng)過壓縮算法壓縮得到的,對CBR的MP3數(shù)據(jù)來說每個幀中包含的PCM數(shù)據(jù)幀是固定的较屿,而VBR是可變的隧魄。
了解了基礎概念之后我們就可以列出一個經(jīng)典的音頻播放流程(以MP3為例):
1.讀取MP3文件
2.解析采樣率、碼率隘蝎、時長等信息购啄,分離MP3中的音頻幀
3.對分離出來的音頻幀解碼得到PCM數(shù)據(jù)
4.對PCM數(shù)據(jù)進行音效處理(均衡器、混響器等嘱么,非必須)
5.把PCM數(shù)據(jù)解碼成音頻信號
6.把音頻信號交給硬件播放
重復1-6步直到播放完成
在iOS系統(tǒng)中apple對上述流程進行了封裝和并提供不同接口
說明
Audio File Services:讀寫音頻數(shù)據(jù)狮含,可以完成播放流程中的第2步;
Audio File Stream Services:對音頻進行解碼曼振,可以完成播放流程中的第2步几迄;
Audio Converter services:音頻數(shù)據(jù)轉(zhuǎn)換,可以完成播放流程中的第3步冰评;
Audio Processing Graph Services:音效處理模塊映胁,可以完成播放流程中的第4步;
Audio Unit Services:播放音頻數(shù)據(jù):可以完成播放流程中的第5步甲雅、第6步解孙;
Extended Audio File Services:Audio File Services和Audio Converter services的結合體坑填;
AVAudioPlayer/AVPlayer(AVFoundation):高級接口,可以完成整個音頻播放的過程(包括本地文件和網(wǎng)絡流播放弛姜,第4步除外)脐瑰;
Audio Queue Services:高級接口,可以進行錄音和播放廷臼,可以完成播放流程中的第3苍在、5、6步中剩;
OpenAL:用于游戲音頻播放忌穿,暫不討論
如果你只是想實現(xiàn)音頻的播放,沒有其他需求AVFoundation會很好的滿足你的需求结啼。它的接口使用簡單掠剑、不用關心其中的細節(jié);
如果你的app需要對音頻進行流播放并且同時存儲郊愧,那么AudioFileStreamer加AudioQueue能夠幫到你朴译,你可以先把音頻數(shù)據(jù)下載到本地,一邊下載一邊用NSFileHandler等接口讀取本地音頻文件并交給AudioFileStreamer或者AudioFile解析分離音頻幀属铁,分離出來的音頻幀可以送給AudioQueue進行解碼和播放眠寿。如果是本地文件直接讀取文件解析即可。(這兩個都是比較直接的做法焦蘑,這類需求也可以用AVFoundation+本地server的方式實現(xiàn)盯拱,AVAudioPlayer會把請求發(fā)送給本地server,由本地server轉(zhuǎn)發(fā)出去例嘱,獲取數(shù)據(jù)后在本地server中存儲并轉(zhuǎn)送給AVAudioPlayer狡逢。另一個比較trick的做法是先把音頻下載到文件中,在下載到一定量的數(shù)據(jù)后把文件路徑給AVAudioPlayer播放拼卵,當然這種做法在音頻seek后就回有問題了奢浑。);
如果你正在開發(fā)一個專業(yè)的音樂播放軟件腋腮,需要對音頻施加音效(均衡器雀彼、混響器),那么除了數(shù)據(jù)的讀取和解析以外還需要用到AudioConverter來把音頻數(shù)據(jù)轉(zhuǎn)換成PCM數(shù)據(jù)即寡,再由AudioUnit+AUGraph來進行音效處理和播放(但目前多數(shù)帶音效的app都是自己開發(fā)音效模塊來坐PCM數(shù)據(jù)的處理徊哑,這部分功能自行開發(fā)在自定義性和擴展性上會比較強一些。PCM數(shù)據(jù)通過音效器處理完成后就可以使用AudioUnit播放了聪富,當然AudioQueue也支持直接使對PCM數(shù)據(jù)進行播放实柠。)。下圖描述的就是使用AudioFile + AudioConverter + AudioUnit進行音頻播放的流程(圖片引自官方文檔)善涨。
——————目前我們先來看AudioFileStream如何實現(xiàn)第2步
創(chuàng)建AudioFileStream實例
一開始窒盐,我們都要初始化一個AudioFileStream的實例,
extern OSStatus
AudioFileStreamOpen (
void * __nullable inClientData,
AudioFileStream_PropertyListenerProc inPropertyListenerProc,
AudioFileStream_PacketsProc inPacketsProc,
AudioFileTypeID inFileTypeHint,
AudioFileStreamID __nullable * __nonnull outAudioFileStream)
__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
第一個參數(shù)钢拧,是一個上下文對象蟹漓,一般為AudioFileStream的實例
第二個參數(shù),是歌曲信息解析的回調(diào)源内,一般傳入一個回調(diào)函數(shù)
第三個參數(shù)葡粒,是分離幀的回調(diào),每解析出來一部分幀就會進行回調(diào)膜钓,也是傳入一個回調(diào)函數(shù)
第四個參數(shù)嗽交,是文件類型的提示,這個參數(shù)在文件信息不完整的時候尤其有用颂斜,可以給AudioFileStream一些提示去解析我們的音頻文件夫壁,無法確認可以傳入0
一般會有以下幾種
CF_ENUM(AudioFileTypeID) {
kAudioFileAIFFType = 'AIFF',
kAudioFileAIFCType = 'AIFC',
kAudioFileWAVEType = 'WAVE',
kAudioFileRF64Type = 'RF64',
kAudioFileSoundDesigner2Type = 'Sd2f',
kAudioFileNextType = 'NeXT',
kAudioFileMP3Type = 'MPG3', // mpeg layer 3
kAudioFileMP2Type = 'MPG2', // mpeg layer 2
kAudioFileMP1Type = 'MPG1', // mpeg layer 1
kAudioFileAC3Type = 'ac-3',
kAudioFileAAC_ADTSType = 'adts',
kAudioFileMPEG4Type = 'mp4f',
kAudioFileM4AType = 'm4af',
kAudioFileM4BType = 'm4bf',
kAudioFileCAFType = 'caff',
kAudioFile3GPType = '3gpp',
kAudioFile3GP2Type = '3gp2',
kAudioFileAMRType = 'amrf',
kAudioFileFLACType = 'flac'
};
第五個參數(shù)是返回AudioFileStream實例對應的ID,通過ID我們可以得到這個實例的一些信息沃疮,返回值是OSStatus盒让,成功時會返回noErr。
解析數(shù)據(jù)
我們在得到AudioFileStream實例之后司蔬,就可以開始解析數(shù)據(jù)了邑茄,我們解析用的接口是這個
extern OSStatus
AudioFileStreamParseBytes(
AudioFileStreamID inAudioFileStream,
UInt32 inDataByteSize,
const void * __nullable inData,
AudioFileStreamParseFlags inFlags)
__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
第一個參數(shù)是AudioFileStreamID,也就是我們上面創(chuàng)建AudioFileStream實例后獲得的ID
第二個參數(shù)是inDataByteSize俊啼,本次解析數(shù)據(jù)的長度
第三個參數(shù)是inData肺缕,本次解析的數(shù)據(jù),
第四個參數(shù)是本次解析和上一次解析是否是連續(xù)的關系授帕,如果是連續(xù)的傳入0同木,否則傳入kAudioFileStreamParseFlag_Discontinuity
這里的“連續(xù)”指的是,像MP3的數(shù)據(jù)都以幀的形式存在的豪墅,解析時也是以幀作為單位去解析的泉手,但在解碼之前我們不可能知道每個幀的邊界在第幾個字節(jié),所以就會出現(xiàn)這樣的情況:我們傳給AudioFileStreamParseBytes的數(shù)據(jù)在解析完成之后會有一部分數(shù)據(jù)余下來偶器,這部分數(shù)據(jù)是接下去那一幀的前半部分斩萌,如果再次有數(shù)據(jù)輸入需要繼續(xù)解析時就必須要用到前一次解析余下來的數(shù)據(jù)才能保證幀數(shù)據(jù)完整,所以在正常播放的情況下傳入0即可屏轰。目前知道的需要傳入kAudioFileStreamParseFlag_Discontinuity
的情況有兩個颊郎,一個是在seek完畢之后顯然seek后的數(shù)據(jù)和之前的數(shù)據(jù)完全無關;另一個是開源播放器AudioStreamer的作者@Matt Gallagher曾在自己的blog中提到過的:
the Audio File Stream Services hit me with a nasty bug: AudioFileStreamParseBytes will crash when trying to parse a streaming MP3.
In this case, if we pass the kAudioFileStreamParseFlag_Discontinuity flag to AudioFileStreamParseBytes on every invocation between receiving kAudioFileStreamProperty_ReadyToProducePackets and the first successful call to MyPacketsProc, then AudioFileStreamParseBytes will be extra cautious in its approach and won't crash.
Matt發(fā)布這篇blog是在2008年霎苗,這個Bug年代相當久遠了姆吭,而且原因未知,究竟是否修復也不得而知唁盏,而且由于環(huán)境不同(比如測試用的mp3文件和所處的iOS系統(tǒng))無法重現(xiàn)這個問題内狸,所以我個人覺得還是按照Matt的work around在回調(diào)得到kAudioFileStreamProperty_ReadyToProducePackets
之后检眯,在正常解析第一幀之前都傳入kAudioFileStreamParseFlag_Discontinuity
比較好。
回過頭來昆淡,這個函數(shù)的返回值也是OSStatus锰瘸,返回的錯誤碼有以下,
CF_ENUM(OSStatus)
{
kAudioFileStreamError_UnsupportedFileType = 'typ?',
kAudioFileStreamError_UnsupportedDataFormat = 'fmt?',
kAudioFileStreamError_UnsupportedProperty = 'pty?',
kAudioFileStreamError_BadPropertySize = '!siz',
kAudioFileStreamError_NotOptimized = 'optm',
kAudioFileStreamError_InvalidPacketOffset = 'pck?',
kAudioFileStreamError_InvalidFile = 'dta?',
kAudioFileStreamError_ValueUnknown = 'unk?',
kAudioFileStreamError_DataUnavailable = 'more',
kAudioFileStreamError_IllegalOperation = 'nope',
kAudioFileStreamError_UnspecifiedError = 'wht?',
kAudioFileStreamError_DiscontinuityCantRecover = 'dsc!'
};
里面需要注意的是kAudioFileStreamError_NotOptimized
它的含義是指這個音頻文件頭不存在或者文件頭可能在文件的末尾昂灵,無法正常parse避凝,也就是這個音頻文件需要全部下載完再能播放,無法流播
同時AudioFileStreamParseBytes
方法每一次調(diào)用都需要注意返回值眨补,一旦出現(xiàn)錯誤就可以不必繼續(xù)parse
解析文件格式信息
在我們調(diào)用AudioFileStreamParseBytes
方法之后管削,之前初始化方法里面的AudioFileStream_PropertyListenerProc
也開始回調(diào),進入這個方法看一下
typedef void (*AudioFileStream_PropertyListenerProc)(
void * inClientData,
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
AudioFileStreamPropertyFlags * ioFlags);
第一個參數(shù)是我們初始化實例的上下文對象
第二個參數(shù)是實例的ID
第三個參數(shù)是此次回調(diào)解析的信息ID撑螺,表示當前PropertyID對應的信息已經(jīng)解析完成(例如數(shù)據(jù)格式含思,音頻信息的偏移量),可以通過AudioFileStreamGetProperty
來獲取這個propertyID里面對應的值
extern OSStatus
AudioFileStreamGetPropertyInfo(
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 * __nullable outPropertyDataSize,
Boolean * __nullable outWritable)
__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
第四個參數(shù)ioFlags是一個返回的參數(shù)实蓬,表示這個property是否需要緩存茸俭,如果需要的話就可以賦值kAudioFileStreamPropertyFlag_PropertyIsCached
這個回調(diào)會進行多次,但不是每一次都需要進行處理安皱,propertyID的列表如下
CF_ENUM(AudioFileStreamPropertyID)
{
kAudioFileStreamProperty_ReadyToProducePackets = 'redy',
kAudioFileStreamProperty_FileFormat = 'ffmt',
kAudioFileStreamProperty_DataFormat = 'dfmt',
kAudioFileStreamProperty_FormatList = 'flst',
kAudioFileStreamProperty_MagicCookieData = 'mgic',
kAudioFileStreamProperty_AudioDataByteCount = 'bcnt',
kAudioFileStreamProperty_AudioDataPacketCount = 'pcnt',
kAudioFileStreamProperty_MaximumPacketSize = 'psze',
kAudioFileStreamProperty_DataOffset = 'doff',
kAudioFileStreamProperty_ChannelLayout = 'cmap',
kAudioFileStreamProperty_PacketToFrame = 'pkfr',
kAudioFileStreamProperty_FrameToPacket = 'frpk',
kAudioFileStreamProperty_PacketToByte = 'pkby',
kAudioFileStreamProperty_ByteToPacket = 'bypk',
kAudioFileStreamProperty_PacketTableInfo = 'pnfo',
kAudioFileStreamProperty_PacketSizeUpperBound = 'pkub',
kAudioFileStreamProperty_AverageBytesPerPacket = 'abpp',
kAudioFileStreamProperty_BitRate = 'brat',
kAudioFileStreamProperty_InfoDictionary = 'info'
};
這里解釋幾個propertyID
1.kAudioFileStreamProperty_ReadyToProducePackets
表示解析完成调鬓,可以對音頻數(shù)據(jù)開始進行幀的分離
2.kAudioFileStreamProperty_BitRate
表示音頻數(shù)據(jù)的碼率,獲取這個property是為了計算音頻的總時長duration酌伊,而且在數(shù)據(jù)量比較小時出現(xiàn)ReadyToProducePackets
還是沒有獲取到bitRate腾窝,這時需要分離一些幀,然后計算平均bitRate
UInt32 averageBitRate = totalPackectByteCount / totalPacketCout;
2.kAudioFileStreamProperty_DataOffset
表示音頻數(shù)據(jù)在整個音頻文件的offset居砖,因為大多數(shù)音頻文件都會有一個文件頭虹脯。個值在seek時會發(fā)揮比較大的作用,音頻的seek并不是直接seek文件位置而seek時間(比如seek到2分10秒的位置)奏候,seek時會根據(jù)時間計算出音頻數(shù)據(jù)的字節(jié)offset然后需要再加上音頻數(shù)據(jù)的offset才能得到在文件中的真正offset循集。
3.kAudioFileStreamProperty_DataFormat
表示音頻文件結構信息,是一個AudioStreamBasicDescription
struct AudioStreamBasicDescription
{
Float64 mSampleRate;
AudioFormatID mFormatID;
AudioFormatFlags mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
4.kAudioFileStreamProperty_FormatList
作用和kAudioFileStreamProperty_DataFormat一樣蔗草,不過這個獲取到的是一個AudioStreamBasicDescription的數(shù)組咒彤,這個參數(shù)用來支持AAC SBR這樣包含多個文件類型的音頻格式。但是我們不知道有多少個format咒精,所以要先獲取總數(shù)據(jù)大小
AudioFormatListItem *formatList = malloc(formatListSize);
OSStatus status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);
if (status == noErr) {
UInt32 supportedFormatsSize;
status = AudioFormatGetPropertyInfo(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &supportedFormatsSize);
if (status != noErr) {
free(formatList);
return;
}
UInt32 supportedFormatCount = supportedFormatsSize / sizeof(OSType);
OSType *supportedFormats = (OSType *)malloc(supportedFormatsSize);
status = AudioFormatGetProperty(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &supportedFormatsSize, supportedFormats);
if (status != noErr) {
free(formatList);
free(supportedFormats);
return;
}
for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i++) {
AudioStreamBasicDescription format = formatList[i].mASBD;
for (UInt32 j = 0; j < supportedFormatCount; j++) {
if (format.mFormatID == supportedFormats[j]) {
_format = format;
[self calculatePacketDuration];
break;
}
}
}
free(supportedFormats);
};
free(formatList);
5.kAudioFileStreamProperty_AudioDataByteCount
表示音頻文件音頻數(shù)據(jù)的總量镶柱。這個是用來計算音頻的總時長并且可以在seek的時候計算時間對應的字節(jié)offset
UInt32 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status == noErr) {
NSLog(@"audioDataByteCount : %u, byteCountSize: %u",audioDataByteCount,byteCountSize);
}
跟bitRate一樣,在數(shù)據(jù)量比較小的時候可能獲取不到audioDataByteCount模叙,這時就需要近似計算
UInt32 dataOffset = ...; //kAudioFileStreamProperty_DataOffset
UInt32 fileLength = ...; //音頻文件大小
UInt32 audioDataByteCount = fileLength - dataOffset;
解析完音頻幀之后歇拆,我們來分離音頻幀
分離音頻幀
讀取完格式信息完成后,我們來繼續(xù)調(diào)用AudioFileStreamParseBytes
方法對幀進行分離,并進入AudioFileStream_PacketsProc
回調(diào)方法
typedef void (*AudioFileStream_PacketsProc)(
void * inClientData,
UInt32 inNumberBytes,
UInt32 inNumberPackets,
const void * inInputData,
AudioStreamPacketDescription *inPacketDescriptions);
第一個參數(shù)同樣是上下文對象
第二個參數(shù)故觅,本次處理的數(shù)據(jù)大小
第三個參數(shù)厂庇,本次共處理了多少幀,
第四個參數(shù)输吏,處理的所有數(shù)據(jù)
第五個參數(shù)宋列,AudioStreamPacketDescription
數(shù)組,存儲了每一幀數(shù)據(jù)是從第幾個字節(jié)開始的评也,這一幀總共多少字節(jié)
struct AudioStreamPacketDescription
{
SInt64 mStartOffset;
UInt32 mVariableFramesInPacket;
UInt32 mDataByteSize;
};
處理分離音頻幀
if (_discontinuous) {
_discontinuous = NO;
}
if (numberOfBytes == 0 || numberOfPackets == 0) {
return;
}
BOOL deletePackDesc = NO;
if (packetDescriptions == NULL) {
//如果packetDescriptions不存在,就按照CBR處理灭返,平均每一幀數(shù)據(jù)的數(shù)據(jù)后生成packetDescriptions
deletePackDesc = YES;
UInt32 packetSize = numberOfBytes / numberOfPackets;
AudioStreamPacketDescription *descriptions = (AudioStreamPacketDescription *)malloc(sizeof(AudioStreamPacketDescription)*numberOfPackets);
for (int i = 0; i < numberOfPackets; i++) {
UInt32 packetOffset = packetSize * i;
descriptions[i].mStartOffset = packetOffset;
descriptions[i].mVariableFramesInPacket = 0;
if (i == numberOfPackets-1) {
descriptions[i].mDataByteSize = numberOfPackets-packetOffset;
}else{
descriptions[i].mDataByteSize = packetSize;
}
}
packetDescriptions = descriptions;
}
NSMutableArray *parseDataArray = [NSMutableArray array];
for (int i = 0; i < numberOfPackets; i++) {
SInt64 packetOffset = packetDescriptions[i].mStartOffset;
//把解析出來的幀數(shù)據(jù)放進自己的buffer中
ZJParseAudioData *parsedData = [ZJParseAudioData parsedAudioDataWithBytes:packets+packetOffset packetDescription:packetDescriptions[i]];
[parseDataArray addObject:parsedData];
if (_processedPacketsCount < BitRateEstimationMaxPackets) {
_processedPacketsSizeTotal += parsedData.packetDescription.mDataByteSize;
_processedPacketsCount += 1;
[self calculateBitRate];
[self calculateDuration];
}
}
...
if (deletePackDesc) {
free(packetDescriptions);
}
inPacketDescriptions這個字段為空時需要按CBR的數(shù)據(jù)處理盗迟。但其實在解析CBR數(shù)據(jù)時inPacketDescriptions一般也有返回,因為即使是CBR數(shù)據(jù)幀的大小也不是恒定不變的熙含,例如CBR的MP3會在每一幀的數(shù)據(jù)后放1byte的填充位罚缕,這個填充位也不一定一直存在,所以幀會有1byte的浮動
Seek
這個其實就是我們拖動進度條怎静,需要到幾分幾秒邮弹,而我們實際上操作的是文件,即尋址到第幾個字節(jié)開始播放音頻數(shù)據(jù)
對于原始的PCM數(shù)據(jù)來說每一個PCM幀都是固定長度的蚓聘,對應的播放時長也是固定的腌乡,但一旦轉(zhuǎn)換成壓縮后的音頻數(shù)據(jù)就會因為編碼形式的不同而不同了。對于CBR而言每個幀中所包含的PCM數(shù)據(jù)幀是恒定的夜牡,所以每一幀對應的播放時長也是恒定的与纽;而VBR則不同,為了保證數(shù)據(jù)最優(yōu)并且文件大小最小塘装,VBR的每一幀中所包含的PCM數(shù)據(jù)幀是不固定的急迂,這就導致在流播放的情況下VBR的數(shù)據(jù)想要做seek并不容易。這里我們也只討論CBR下的seek蹦肴。
我們一般是這樣實現(xiàn)CBR的seek
1.近似地計算seek到哪個字節(jié)
double seekToTime = ...; //需要seek到哪個時間僚碎,秒為單位
UInt64 audioDataByteCount = ...; //通過kAudioFileStreamProperty_AudioDataByteCount獲取的值
SInt64 dataOffset = ...; //通過kAudioFileStreamProperty_DataOffset獲取的值
double durtion = ...; //通過公式(AudioDataByteCount * 8) / BitRate計算得到的時長
//近似seekOffset = 數(shù)據(jù)偏移 + seekToTime對應的近似字節(jié)數(shù)
SInt64 approximateSeekOffset = dataOffset + (seekToTime / duration) * audioDataByteCount;
2.計算seekToTime對應的是第幾個幀
利用之前的解析得到的音頻格式信息計算packetDuration
//首先需要計算每個packet對應的時長
AudioStreamBasicDescription asbd = ...; ////通過kAudioFileStreamProperty_DataFormat或者kAudioFileStreamProperty_FormatList獲取的值
double packetDuration = asbd.mFramesPerPacket / asbd.mSampleRate
//然后計算packet位置
SInt64 seekToPacket = floor(seekToTime / packetDuration);
3.使用AudioFileStreamSeek計算精確的字節(jié)偏移時間
AudioFileStreamSeek
可以用來尋找某一個幀(Packet)對應的字節(jié)偏移(byte offset):
- 如果ioFlags里有
kAudioFileStreamSeekFlag_OffsetIsEstimated
說明給出的outDataByteOffset是估算的,并不準確阴幌,那么還是應該用第1步計算出來的approximateSeekOffset來做seek勺阐; - 如果ioFlags里沒有
kAudioFileStreamSeekFlag_OffsetIsEstimated
說明給出了準確的outDataByteOffset,就是輸入的seekToPacket對應的字節(jié)偏移量裂七,我們可以根據(jù)outDataByteOffset來計算出精確的seekOffset和seekToTime皆看;
4.按照seekByteOffset讀取對應的數(shù)據(jù)繼續(xù)使用AudioFileStreamParseByte
進行解析
計算duration
獲取時長的最佳方法是從ID3信息中去讀取,那樣是最準確的背零。如果ID3信息中沒有存腰吟,那就依賴于文件頭中的信息去計算了。
計算duration的公式如下:
double duration = (audioDataByteCount * 8) / bitRate
音頻數(shù)據(jù)的字節(jié)總量audioDataByteCount
可以通過kAudioFileStreamProperty_AudioDataByteCount
獲取,碼率bitRate可以通過kAudioFileStreamProperty_BitRate
獲取也可以通過Parse一部分數(shù)據(jù)后計算平均碼率來得到毛雇。
對于CBR數(shù)據(jù)來說用這樣的計算方法的duration會比較準確嫉称,對于VBR數(shù)據(jù)就不好說了。所以對于VBR數(shù)據(jù)來說灵疮,最好是能夠從ID3信息中獲取到duration织阅,獲取不到再想辦法通過計算平均碼率的途徑來計算duration。
最后需要關閉AudioFileStream
extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream);
小結
1.使用AudioFileStream
需要先調(diào)用AudioFileStreamOpen
震捣,最好提供文件類型幫助解析
2.當有數(shù)據(jù)時調(diào)用AudioFileStreamParseBytes
進行解析荔棉,當出現(xiàn)noErr以外的值則代表解析出錯,kAudioFileStreamError_NotOptimized
則代表文件缺少頭信息或者在文件尾部不適合流播放
3.在調(diào)用AudioFileStreamParseBytes
之后會先進入AudioFileStream_PropertyListenerProc
蒿赢,當回調(diào)得到kAudioFileStreamProperty_ReadyToProducePackets
則再進入MyAudioFileStreamPacketsCallBack
分離幀信息润樱。
4.使用后需關閉AudioFileStream
最后demo地址 : https://github.com/chanbendong/ZJAudioFileStream