iOS音頻播放(七)AudioFileStream介紹與實戰

AudioFileStream介紹

AudioFileStreamer的做用是用來讀取採樣率、碼率、時長等基本信息以及分離音頻幀。git

數據的相關內容都和它相關,因此仍是很重要的,其實AudioQueue使用起來比較簡單,複雜的部分都在這個數據的處理上了。。。github

根據Apple的描述AudioFileStreamer用在流播放中,固然不只限於網絡流,本地文件一樣能夠用它來讀取信息和分離音頻幀。AudioFileStreamer的主要數據是文件數據而不是文件路徑,因此數據的讀取須要使用者自行實現。數組

AudioFileStreamer的主要數據是文件數據,支持的文件格式有:緩存

  • MPEG-1 Audio Layer 3, used for .mp3 files
  • MPEG-2 ADTS, used for the .aac audio data format
  • AIFC
  • AIFF
  • CAF
  • MPEG-4, used for .m4a, .mp4, and .3gp files
  • NeXT
  • WAVE

初始化AudioFileStream

初始化AudioFileStream,建立一個音頻流解析器,生成一個AudioFileStream示例。bash

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);
複製代碼
  • inClientData:用戶指定的數據,用於傳遞給回調函數,這裏咱們指定(__bridge LocalAudioPlayer*)self
  • inPropertyListenerProc:是歌曲信息解析的回調,每解析出一個歌曲信息都會進行一次回調
  • inPacketsProc:是分離幀的回調,當解析到一個音頻幀時,將回調該方法
  • inFileTypeHint:指明音頻數據的格式,若是你不知道音頻數據的格式,能夠傳0
  • outAudioFileStream:AudioFileStreamID實例,需保存供後續使用
//AudioFileTypeID枚舉
enum {
        kAudioFileAIFFType             = 'AIFF',
        kAudioFileAIFCType             = 'AIFC',
        kAudioFileWAVEType             = 'WAVE',
        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'        
};
複製代碼

這個函數會建立一個AudioFileStreamID,以後全部的操做都是基於這個ID來的,而後仍是建立2個回調 inPropertyListenerProc 和 inPacketsProc,這2個回調函數比較重要,下面詳說。markdown

OSStatus返回值用來判斷是否成功初始化(OSStatus == noErr)。網絡

解析數據

在初始化完成以後,調用該方法解析文件數據。解析時調用方法:app

extern OSStatus AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream,
                                          UInt32 inDataByteSize,
                                          const void* inData,
                                          UInt32 inFlags);
複製代碼

參數的說明以下:函數

  • inAudioFileStream:AudioFileStreamID實例,由AudioFileStreamOpen打開
  • inDataByteSize:這次解析的數據字節大小
  • inData:這次解析的數據大小
  • inFlags:數據解析標誌,其中只有一個值kAudioFileStreamParseFlag_Discontinuity = 1,表示解析的數據是不是不連續的,目前咱們能夠傳0。

OSStatus的值不是noErr則表示解析不成功,對應的錯誤碼:oop

enum
{
  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!'
};
複製代碼

每次調用成功後應該注意返回值,一旦出現錯誤就沒必要要進行後續的解析。

回調介紹

解析文件格式信息

AudioFileStream_PropertyListenerProc,解析文件格式信息的回調,在調用AudioFileStreamParseBytes方法進行解析時會首先讀取格式信息,並同步的進入AudioFileStream_PropertyListenerProc回調方法。

在這個回調中,你能夠拿到你想要的音頻相關信息,好比音頻結構(AudioStreamBasicDescription),碼率(BitRate),MagicCookie等等,經過這些信息,你還能夠計算其餘數據,好比音頻總時長。

進入這個方法看一下:

typedef void (*AudioFileStream_PropertyListenerProc)(
            void *                          inClientData,
            AudioFileStreamID           inAudioFileStream,
            AudioFileStreamPropertyID       inPropertyID,
            AudioFileStreamPropertyFlags *  ioFlags);
複製代碼

第一個參數是咱們初始化實例的上下文對象

第二個參數是實例的ID

第三個參數是這次回調解析的信息ID,表示當前PropertyID對應的信息已經解析完成(例如數據格式,音頻信息的偏移量),能夠經過AudioFileStreamGetProperty來獲取這個propertyID裏面對應的值

extern OSStatus
AudioFileStreamGetPropertyInfo( 
     AudioFileStreamID               inAudioFileStream,
    AudioFileStreamPropertyID       inPropertyID,
    UInt32 * __nullable             outPropertyDataSize,
    Boolean * __nullable            outWritable)
    __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
複製代碼

第四個參數ioFlags是一個返回的參數,表示這個property是否須要緩存,若是須要的話就能夠賦值kAudioFileStreamPropertyFlag_PropertyIsCached

這個回調會進行屢次,但不是每一次都須要進行處理,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 表示解析完成,能夠對音頻數據開始進行幀的分離

2.kAudioFileStreamProperty_BitRate 表示音頻數據的碼率,獲取這個property是爲了計算音頻的總時長duration,並且在數據量比較小時出現ReadyToProducePackets仍是沒有獲取到bitRate,這時須要分離一些幀,而後計算平均bitRate UInt32 averageBitRate = totalPackectByteCount / totalPacketCout;

3.kAudioFileStreamProperty_DataOffset 表示音頻數據在整個音頻文件的offset,由於大多數音頻文件都會有一個文件頭。個值在seek時會發揮比較大的做用,音頻的seek並非直接seek文件位置而seek時間(好比seek到2分10秒的位置),seek時會根據時間計算出音頻數據的字節offset而後須要再加上音頻數據的offset才能獲得在文件中的真正offset。

4.kAudioFileStreamProperty_DataFormat 表示音頻文件結構信息,是一個AudioStreamBasicDescription

struct AudioStreamBasicDescription
{
    Float64             mSampleRate;
    AudioFormatID       mFormatID;
    AudioFormatFlags    mFormatFlags;
    UInt32              mBytesPerPacket;
    UInt32              mFramesPerPacket;
    UInt32              mBytesPerFrame;
    UInt32              mChannelsPerFrame;
    UInt32              mBitsPerChannel;
    UInt32              mReserved;
};
複製代碼

5.kAudioFileStreamProperty_FormatList 做用和kAudioFileStreamProperty_DataFormat同樣,不過這個獲取到的是一個AudioStreamBasicDescription的數組,這個參數用來支持AAC SBR這樣包含多個文件類型的音頻格式。可是咱們不知道有多少個format,因此要先獲取總數據大小

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);
複製代碼

6.kAudioFileStreamProperty_AudioDataByteCount 表示音頻文件音頻數據的總量。這個是用來計算音頻的總時長而且能夠在seek的時候計算時間對應的字節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同樣,在數據量比較小的時候可能獲取不到audioDataByteCount,這時就須要近似計算

UInt32 dataOffset = ...; //kAudioFileStreamProperty_DataOffset
UInt32 fileLength = ...; //音頻文件大小
UInt32 audioDataByteCount = fileLength - dataOffset;
複製代碼

這裏分享下音頻時長的2種計算方式:

  • 總時長 = 總幀數*單幀的時長

    單幀的時長 = 單幀的採樣個數*每幀的時長

    每幀的時長 = 1/採樣率

    採樣率:單位時間內的採樣個數

  • 總時長 = 文件總的字節數/碼率

    碼率:單位時間內的文件字節數

解析完音頻幀以後,咱們來分離音頻幀。

分離音頻幀

讀取完格式信息完成後,咱們來繼續調用AudioFileStreamParseBytes方法對幀進行分離,並進入AudioFileStream_PacketsProc回調方法。

typedef void (*AudioFileStream_PacketsProc)(
            void *                          inClientData,
            UInt32                          inNumberBytes,
            UInt32                          inNumberPackets,
            const void *                    inInputData,
            AudioStreamPacketDescription    *inPacketDescriptions);
複製代碼

第一個參數一樣是上下文對象

第二個參數,本次處理的數據大小

第三個參數,本次共處理了多少幀,

第四個參數,處理的全部數據

第五個參數,AudioStreamPacketDescription數組,存儲了每一幀數據是從第幾個字節開始的,這一幀總共多少字節

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處理,平均每一幀數據的數據後生成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;
    //把解析出來的幀數據放進本身的buffer中
    NParseAudioData *parsedData = [NParseAudioData 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的數據處理。但其實在解析CBR數據時inPacketDescriptions通常也有返回,由於即便是CBR數據幀的大小也不是恆定不變的,例如CBR的MP3會在每一幀的數據後放1byte的填充位,這個填充位也不必定一直存在,因此幀會有1byte的浮動

Seek

這個其實就是咱們拖動進度條,須要到幾分幾秒,而咱們實際上操做的是文件,即尋址到第幾個字節開始播放音頻數據

對於原始的PCM數據來講每個PCM幀都是固定長度的,對應的播放時長也是固定的,但一旦轉換成壓縮後的音頻數據就會由於編碼形式的不一樣而不一樣了。對於CBR而言每一個幀中所包含的PCM數據幀是恆定的,因此每一幀對應的播放時長也是恆定的;而VBR則不一樣,爲了保證數據最優而且文件大小最小,VBR的每一幀中所包含的PCM數據幀是不固定的,這就致使在流播放的狀況下VBR的數據想要作seek並不容易。這裏咱們也只討論CBR下的seek。

咱們通常是這樣實現CBR的seek

1.近似地計算seek到哪一個字節

double seekToTime = ...; //須要seek到哪一個時間,秒爲單位
UInt64 audioDataByteCount = ...; //經過kAudioFileStreamProperty_AudioDataByteCount獲取的值
SInt64 dataOffset = ...; //經過kAudioFileStreamProperty_DataOffset獲取的值
double durtion = ...; //經過公式(AudioDataByteCount * 8) / BitRate計算獲得的時長

//近似seekOffset = 數據偏移 + seekToTime對應的近似字節數
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計算精確的字節偏移時間 AudioFileStreamSeek能夠用來尋找某一個幀(Packet)對應的字節偏移(byte offset):

  • 若是ioFlags裏有kAudioFileStreamSeekFlag_OffsetIsEstimated說明給出的outDataByteOffset是估算的,並不許確,那麼仍是應該用第1步計算出來的approximateSeekOffset來作seek;

  • 若是ioFlags裏沒有kAudioFileStreamSeekFlag_OffsetIsEstimated說明給出了準確的outDataByteOffset,就是輸入的seekToPacket對應的字節偏移量,咱們能夠根據outDataByteOffset來計算出精確的seekOffset和seekToTime;

4.按照seekByteOffset讀取對應的數據繼續使用AudioFileStreamParseByte進行解析

計算duration

獲取時長的最佳方法是從ID3信息中去讀取,那樣是最準確的。若是ID3信息中沒有存,那就依賴於文件頭中的信息去計算了。

計算duration的公式以下:

double duration = (audioDataByteCount * 8) / bitRate
複製代碼

音頻數據的字節總量audioDataByteCount能夠經過kAudioFileStreamProperty_AudioDataByteCount獲取,碼率bitRate能夠經過kAudioFileStreamProperty_BitRate獲取也能夠經過Parse一部分數據後計算平均碼率來獲得。

對於CBR數據來講用這樣的計算方法的duration會比較準確,對於VBR數據就很差說了。因此對於VBR數據來講,最好是可以從ID3信息中獲取到duration,獲取不到再想辦法經過計算平均碼率的途徑來計算duration。

最後須要關閉AudioFileStream

extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream); 
複製代碼

小結

  • 使用AudioFileStream須要先調用AudioFileStreamOpen,最好提供文件類型幫助解析
  • 當有數據時調用AudioFileStreamParseBytes進行解析,當出現noErr之外的值則表明解析出錯,kAudioFileStreamError_NotOptimized則表明文件缺乏頭信息或者在文件尾部不適合流播放
  • 在調用AudioFileStreamParseBytes以後會先進入AudioFileStream_PropertyListenerProc,當回調獲得kAudioFileStreamProperty_ReadyToProducePackets則再進入MyAudioFileStreamPacketsCallBack分離幀信息。
  • 使用後需關閉AudioFileStream

這裏(github.com/Nicholas86/…)是代碼

參考資料

相關文章
相關標籤/搜索