學習位址: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 :這個參數說明本次解析和上一次解析是否是連續的關系,如果是不連續傳入
,反之傳入0kAudioFileStreamParseFlag_Discontinuity
-
OSStatus :解析傳回值,如果解析成功(OSStatus == noErr)
網易音樂的部落客是如何解釋連續的:
因為在第一篇中提到過形如MP3的資料都是以幀的形式存在的,解析時也需要以幀為機關解析。但在解碼之前我們不可能知道每個幀的邊界在第幾個位元組,是以就會出現這樣的情況:我們傳給AudioFileStreamParseBytes的資料在解析完成之後會有一部分資料餘下來,這部分資料世界下去那一幀的前半部分,如果再次有資料輸入需要繼續解析時就必須要用前一次解析餘下來的資料才能保證幀資料完整,是以在正常播放的時候傳入0即可,目前知道需要傳入‘1’的情況有兩種:
- 在seek完畢之後顯然seek後的資料和之前的資料完全無關;
- 在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
三、解析檔案格式資訊
在調用
AudioFileStreamParseBytes
方法進行解析時會首先讀取格式資訊,并同步的進入
AudioFileStream_PropertyListenerProc
回調方法:
typedef void (*AudioFileStream_PropertyListenerProc)(
void * inClientData,
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
AudioFileStreamPropertyFlags * ioFlags);
- inPropertyID :這個參數是此次回調解析的資訊ID。表示目前PropertyID對應的資訊已經解析完成資訊(例如資料格式、音頻資料的偏移量等等),使用者可以通過
接口擷取PropertyID對應的值或者資料結構:AudioFileStreamGetProperty
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