天天看點

音頻播放2Audio File Stream Services

學習位址:iOS音頻播放三 以下純屬個人筆記

Audio File Stream Services

解析采樣率、碼率、時長等資訊,分離音頻幀 —— 音頻檔案解析器

一、初始化AudioFileStream

extern OSStatus 
AudioFileStreamOpen (
                            void * __nullable                       inClientData,
                            AudioFileStream_PropertyListenerProc    inPropertyListenerProc,
                            AudioFileStream_PacketsProc             inPacketsProc,
                            AudioFileTypeID                         inFileTypeHint,
                            AudioFileStreamID __nullable * __nonnull outAudioFileStream)           
  • inClientData :上下文資訊,生命周期長
  • inPropertyListenerProc :采樣率、碼率、時長采集回調函數(需要自己建立)
  • inPacketsProc :檔案包解析回調函數(需要自己建立)
  • inFileTypeHint :檔案格式提示
  • outAudioFileStream :傳回的是AudioFileStream執行個體對應的AudioFileStreamID,這個ID需存儲作為後續方法的參數
  • OSStatus :判斷是否初始化成功(OSStatus == noErr)

二、解析資料

extern OSStatus
AudioFileStreamParseBytes(  
                                AudioFileStreamID               inAudioFileStream,
                                UInt32                          inDataByteSize,
                                const void *                    inData,
                                AudioFileStreamParseFlags       inFlags)           
  • inAudioFileStream :AudioFileStreamID,即初始化時傳回的ID
  • inDataByteSize :本次解析的資料長度
  • inData :本次解析的資料
  • inFlags :這個參數說明本次解析和上一次解析是否是連續的關系,如果是不連續傳入

    kAudioFileStreamParseFlag_Discontinuity

    ,反之傳入0
  • OSStatus :解析傳回值,如果解析成功(OSStatus == noErr)

    網易音樂的部落客是如何解釋連續的:

因為在第一篇中提到過形如MP3的資料都是以幀的形式存在的,解析時也需要以幀為機關解析。但在解碼之前我們不可能知道每個幀的邊界在第幾個位元組,是以就會出現這樣的情況:我們傳給AudioFileStreamParseBytes的資料在解析完成之後會有一部分資料餘下來,這部分資料世界下去那一幀的前半部分,如果再次有資料輸入需要繼續解析時就必須要用前一次解析餘下來的資料才能保證幀資料完整,是以在正常播放的時候傳入0即可,目前知道需要傳入‘1’的情況有兩種:

  1. 在seek完畢之後顯然seek後的資料和之前的資料完全無關;
  2. 在work around 在回調得到

    kAudioFileStreamProperty_ReadyToProducePackets

    之後,在正常解析第一幀之前都傳入

    kAudioFileStreamParseFlag_Discontinuity

    比較好。

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

,文檔描述:

It is not possible to produce output packets because the streamed audio file's packet table or other defining information is not present or appears after the audio data.           

不能播放這個輸出包,因為這個流音頻檔案包表頭或者其他定義的資訊沒有顯示或展示在音頻資料之後。換句話說:檔案需要全部下載下傳完成才能進行播放,無法流播。

注意

AudioFileStreamParseBytes

方法每一次調用都應該注意傳回值,一旦出現錯誤就不必繼續Parse了。

三、解析檔案格式資訊

在調用

AudioFileStreamParseBytes

方法進行解析時會首先讀取格式資訊,并同步的進入

AudioFileStream_PropertyListenerProc

回調方法:

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                          inClientData,
                                            AudioFileStreamID               inAudioFileStream,
                                            AudioFileStreamPropertyID       inPropertyID,
                                            AudioFileStreamPropertyFlags *  ioFlags);           
  • inPropertyID :這個參數是此次回調解析的資訊ID。表示目前PropertyID對應的資訊已經解析完成資訊(例如資料格式、音頻資料的偏移量等等),使用者可以通過

    AudioFileStreamGetProperty

    接口擷取PropertyID對應的值或者資料結構:
extern OSStatus
AudioFileStreamGetPropertyInfo( 
                                AudioFileStreamID               inAudioFileStream,
                                AudioFileStreamPropertyID       inPropertyID,
                                UInt32 * __nullable             outPropertyDataSize,
                                Boolean * __nullable            outWritable)           
  • ioFlags :這個參數是一個傳回參數,表示這個Property是否需要被緩存,如果需要指派

    kAudioFileStreamPropertyFlag_PropertyIsCached

    ,反之不指派?

這個回調會進去多次,但并不是每次都需要進行處理,可以根據需求處理需要的PropertyID進行處理(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'
};           

這裡列出幾個比較重要的PropertID

1.

kAudioFileStreamProperty_BitRate

表示音頻資料的碼率,擷取這個property是為了計算音頻的總時長duration(因為AudioFileStream沒有這樣的接口?)

四、分離音頻幀

讀取格式資訊之後繼續調用

AudioFileStreamParseBytes

方法可以對幀進行分離,并同步進入

AudioFileStream_PacketsProc

回調方法。

typedef void (*AudioFileStream_PacketsProc)(
                                            void *                          inClientData,
                                            UInt32                          inNumberBytes,
                                            UInt32                          inNumberPackets,
                                            const void *                    inInputData,
                                            AudioStreamPacketDescription    *inPacketDescriptions);           
  • inNumberBytes :本次處理的資料大小
  • inNumberPackets :本次總共處理了多少幀(即代碼裡的Packet)
  • inInputData :本次處理的所有的資料
  • inPacketDescriptions :一個數組,存儲了每一幀資料是從第幾個
// mVariableFramesInPacket是指實際的資料幀隻有VBR的資料才能用到(像MP3這樣壓縮資料一個幀裡面會有好幾個資料幀)
struct  AudioStreamPacketDescription
{
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket; 
    UInt32  mDataByteSize;
};           

下面是網易雲音樂工程師寫的毀掉方法片段 我重新整理了一下:

#pragma mark -
#pragma mark - Packet Call Back

static void MyAudioFileStreamPacketsCallBack(void * inClinetData,
                                             UInt32 inNumberBytes,
                                             UInt32 inNumberPackets,
                                             const void * inInputData,
                                             AudioStreamPacketDescription * inPacketDescriptions)
{
    LVPlayer * audioFileStream = (__bridge LVPlayer *)inClinetData;
    [audioFileStream handleAudioFileStreamPackets:inClinetData
                                    numberOfBytes:inNumberBytes
                                  numberOfPackets:inNumberPackets
                               packetDescriptions:inPacketDescriptions];
};

- (void)handleAudioFileStreamPackets:(const void *)packets
                       numberOfBytes:(UInt32)numberOfBytes
                     numberOfPackets:(UInt32)numberOfPackets
                  packetDescriptions:(AudioStreamPacketDescription *)packetDescription
{
    // 1.處理discontinue..
    if (_discontinuous) {
        _discontinuous = NO;
    }
    // 2.空傳回
    if (numberOfBytes == 0 || numberOfPackets == 0) {
        return;
    }
    // 3.
    BOOL deletePackDesc = NO;
    if (packetDescription == 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 = numberOfBytes - packetOffset;
            }
            else
            {
                descriptions[i].mDataByteSize = packetSize;
            }
        }
        packetDescription = descriptions;
    }
    
    NSMutableArray * parsedDataArray = [NSMutableArray array];
    for (int i = 0; i < numberOfPackets; ++ i)
    {
        SInt64 packetOffset = packetDescription[i].mStartOffset;
        SInt32 packetSize = packetDescription[i].mDataByteSize;
        
        // 解析出來的幀資料放進自己的buffer中
        // ...
    }
    
    if (deletePackDesc) {
        free(packetDescription);
    }
}
           

五、Seek

就音頻角度來說Seek功能描述為“我要拖到XX分XX秒”,而實際操作時我們需要操作的是檔案,是以我們需要知道的是“我要拖到XX分XX秒”這個操作對應檔案上是要從第幾個位元組開始讀取音頻資料。

對于原始的PCM資料來說每一個PCM幀都是固定長度的,對應的播放時長也是固定的,但一旦轉換成壓縮後的音頻資料就會因為編碼形式的不同而不同了。

對于CBR(固定碼率)而言每個幀所包含的PCM資料幀都是恒定的,是以每一幀對應的播放時長也是恒定的;

對于VBR(可變碼率)則是不同的,為了保證資料最優并且檔案最小,VBR的每一幀所包含的PCM資料幀是不固定的,這就導緻在流播放的情況下VBR的資料想要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對應的是第幾幀(Packet)

我們可以利用之前Parse得到的音頻格式資訊來計算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

進行解析。

如果是網絡流可以通過設定range頭來擷取位元組,本地檔案的話直接seek就好了。調用

AudioFileStreamParseByte

時注意剛seek完第一次Parse資料需要加參數

kAudioFileStreamParseFlag_Discontinuity

六、關閉AudioFileStream

AudioFileStream

使用完畢後需要調用

AudioFileStreamClose

進行關閉;

extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream);

轉載于:https://www.cnblogs.com/R0SS/p/5551440.html