iOS音頻學習一之AudioFileStream

音樂一直是我的愛好淌铐,作為一名開發(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是可變的隧魄。

MP3幀

了解了基礎概念之后我們就可以列出一個經(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對上述流程進行了封裝和并提供不同接口


CoreAudio

說明

  • 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

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市羡棵,隨后出現(xiàn)的幾起案子壹若,更是在濱河造成了極大的恐慌,老刑警劉巖皂冰,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件店展,死亡現(xiàn)場離奇詭異,居然都是意外死亡秃流,警方通過查閱死者的電腦和手機赂蕴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剔应,“玉大人睡腿,你說我怎么就攤上這事【” “怎么了席怪?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長纤控。 經(jīng)常有香客問我挂捻,道長,這世上最難降的妖魔是什么船万? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任刻撒,我火速辦了婚禮,結果婚禮上耿导,老公的妹妹穿的比我還像新娘声怔。我一直安慰自己,他們只是感情好舱呻,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布醋火。 她就那樣靜靜地躺著悠汽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芥驳。 梳的紋絲不亂的頭發(fā)上柿冲,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音兆旬,去河邊找鬼假抄。 笑死,一個胖子當著我的面吹牛丽猬,可吹牛的內(nèi)容都是我干的宿饱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼脚祟,長吁一口氣:“原來是場噩夢啊……” “哼刑棵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起愚铡,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胡陪,沒想到半個月后沥寥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡柠座,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年邑雅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妈经。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡淮野,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吹泡,到底是詐尸還是另有隱情骤星,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布爆哑,位于F島的核電站洞难,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏揭朝。R本人自食惡果不足惜队贱,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望潭袱。 院中可真熱鬧柱嫌,春花似錦、人聲如沸屯换。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瘪吏,卻和暖如春癣防,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背掌眠。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工蕾盯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蓝丙。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓级遭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親渺尘。 傳聞我的和親對象是個殘疾皇子挫鸽,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

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

  • 前言 學習AudioToolBox有一段時間了,期間有遇到不少坑(主要還是英文不夠好鸥跟,看官方文檔不甚明了)丢郊。隨著一...
    anyoptional閱讀 8,173評論 4 26
  • 音頻會話 在上面的代碼中還實現(xiàn)了拔出耳機暫停音樂播放的功能,這也是一個比較常見的功能医咨。在iOS7及以后的版本中可以...
    _淺墨_閱讀 1,225評論 3 4
  • 本篇為《iOS音頻播放》系列的第一篇枫匾,主要將對iOS下實現(xiàn)音頻播放的方法進行概述。 基礎 先來簡單了解一下一些基礎...
    評評分分閱讀 1,656評論 0 19
  • AudioFileStream介紹 AudioFileStreamer時提到它的作用是用來讀取采樣率拟淮、碼率干茉、時長等...
    VD2012閱讀 540評論 0 2
  • 8月份的工資交給了中秋節(jié)角虫,9月份的工資交給了國慶節(jié),10月份沒上幾天的工資交給暖氣費和物業(yè)費委造,11月份的工資交給雙...
    陳為專閱讀 224評論 0 1