1 基礎知識
AudioFileStream
將音頻文件流解析為音頻數據包的 API。
1.1 文件流錯誤碼類型
音頻文件流可能出現的錯誤類型,部分特殊場景,需要針對特定錯誤碼做處理,完整錯誤碼定義如下:
CF_ENUM(OSStatus)
{
kAudioFileStreamError_UnsupportedFileType = 'typ?', // 不支持指定的文件類型
kAudioFileStreamError_UnsupportedDataFormat = 'fmt?', // 指定的文件類型不支持數據格式
kAudioFileStreamError_UnsupportedProperty = 'pty?', // 不支持該屬性
kAudioFileStreamError_BadPropertySize = '!siz', // 屬性數據提供的緩沖區大小不正確
kAudioFileStreamError_NotOptimized = 'optm', // 無法產生輸出數據包,因為流式音頻文件的數據包表或其他定義信息不存在或出現在音頻數據之后
kAudioFileStreamError_InvalidPacketOffset = 'pck?', // 數據包偏移量小于0或超過文件末尾,或者在構建數據包表時讀取了損壞的數據包大小
kAudioFileStreamError_InvalidFile = 'dta?', // 文件格式錯誤,不是其類型的音頻文件的有效實例,或未被識別為音頻文件
kAudioFileStreamError_ValueUnknown = 'unk?', // 在音頻數據之前,此文件中不存在屬性值
kAudioFileStreamError_DataUnavailable = 'more', // 提供給解析器的數據量不足以產生任何結果
kAudioFileStreamError_IllegalOperation = 'nope', // 試圖進行非法操作
kAudioFileStreamError_UnspecifiedError = 'wht?', // 發生未指明的錯誤
kAudioFileStreamError_DiscontinuityCantRecover = 'dsc!' // 音頻數據出現中斷,音頻文件流服務無法恢復
};
1.2 AudioFileStream Properties
在 AudioFileStream
中,支持從文件流中獲取以下 Property
,但不支持給文件設置 Property
。完整的 Property
定義如下:
CF_ENUM(AudioFileStreamPropertyID)
{
// UInt32值,在解析器解析到音頻數據的開頭為止一直為0,當到達音頻數據即設置為1,為1時,所有可以知道的音頻文件流屬性都是已知的。
kAudioFileStreamProperty_ReadyToProducePackets = 'redy',
// 音頻文件的格式
kAudioFileStreamProperty_FileFormat = 'ffmt',
// 音頻文件數據格式的結構
kAudioFileStreamProperty_DataFormat = 'dfmt',
// 為了支持帶有SBR的AAC等格式,已編碼的數據流可以被解碼為多種目標格式,此屬性返回一個AudioFormatListItem結構數組,每個目標格式對應一個。
kAudioFileStreamProperty_FormatList = 'flst',
// 一個指向 magic cookie 的空指針
kAudioFileStreamProperty_MagicCookieData = 'mgic',
// UInt64值,表示流文件中音頻數據的字節數。僅當從標頭中解析的數據知道整個流的字節數時,此屬性才有效。對于某些類型的流,此屬性可能沒有價值。
kAudioFileStreamProperty_AudioDataByteCount = 'bcnt',
// UInt64值,流文件中的音頻數據的數據包的數量的值。
kAudioFileStreamProperty_AudioDataPacketCount = 'pcnt',
// UInt32值,表示所述數據的最大數據包大小值。
kAudioFileStreamProperty_MaximumPacketSize = 'psze',
// SInt64值,表示音頻數據開始的流文件中的字節偏移量。
kAudioFileStreamProperty_DataOffset = 'doff',
// 一個 AudioChannelLayout 數據結構
kAudioFileStreamProperty_ChannelLayout = 'cmap',
kAudioFileStreamProperty_PacketToFrame = 'pkfr',
kAudioFileStreamProperty_FrameToPacket = 'frpk',
kAudioFileStreamProperty_RestrictsRandomAccess = 'rrap',
kAudioFileStreamProperty_PacketToRollDistance = 'pkrl',
kAudioFileStreamProperty_PreviousIndependentPacket = 'pind',
kAudioFileStreamProperty_NextIndependentPacket = 'nind',
kAudioFileStreamProperty_PacketToDependencyInfo = 'pkdp',
kAudioFileStreamProperty_PacketToByte = 'pkby',
kAudioFileStreamProperty_ByteToPacket = 'bypk',
kAudioFileStreamProperty_PacketTableInfo = 'pnfo',
// UInt32值,表示指示在流文件中的理論上的最大數據包大小值。例如,此值可用于確定最小緩沖區大小。
kAudioFileStreamProperty_PacketSizeUpperBound = 'pkub',
// Float64值,指示每個數據包的平均字節數。對于 CBR 和帶有數據包表的文件,這個數字是準確的。否則,它是解析的數據包的運行平均值。
kAudioFileStreamProperty_AverageBytesPerPacket = 'abpp',
// UInt32值,表示每秒比特數表示流的比特率。
kAudioFileStreamProperty_BitRate = 'brat',
kAudioFileStreamProperty_InfoDictionary = 'info'
};
1.3 AudioFileStream Types
1.3.1 流屬性回調類型
解析器在音頻文件流中找到屬性值時調用。
typedef UInt32 AudioFileStreamPropertyID;
typedef struct OpaqueAudioFileStreamID *AudioFileStreamID;
typedef void (*AudioFileStream_PropertyListenerProc)(
void * inClientData,
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
AudioFileStreamPropertyFlags * ioFlags);
inClientData:調用函數時在參數中提供的值;
inAudioFileStream:音頻文件流解析器的 ID;
inPropertyID:解析器在音頻文件數據流中找到的屬性 ID;
ioFlags:在輸入時,如果設置了kAudioFileStreamPropertyFlag_PropertyIsCached值,解析器將緩存該屬性值。如果不是,可以在輸出上設置kAudioFileStreamPropertyFlag_CacheProperty標志,以使解析器緩存該值。參見音頻文件流標志。
1.3.2 流數據包回調類型
當音頻文件流解析器在音頻文件流中找到音頻數據時調用。對于恒定比特率 (CBR) 音頻數據,通常會使用與傳遞給函數的數據一樣多的數據調用回調。然而,有時由于輸入數據的邊界,可能只傳遞一個數據包。對于可變比特率 (VBR) 音頻數據,每次調用該函數時可能會多次調用回調。
typedef void (*AudioFileStream_PacketsProc)(
void * inClientData,
UInt32 inNumberBytes,
UInt32 inNumberPackets,
const void * inInputData,
AudioStreamPacketDescription * __nullable inPacketDescriptions);
inClientData:調用函數時在參數中提供的值;
inNumberBytes:緩沖區中數據的字節數;
inNumberPackets:緩沖區中音頻數據的包數;
inInputData:音頻數據;
inPacketDescriptions:音頻文件流數據包描述結構數組。
1.4 AudioFileStream Flags
音頻文件流中標識類型集合:
typedef CF_OPTIONS(UInt32, AudioFileStreamPropertyFlags) {
// 這個標志是在調用回調AudioFileStream_PropertyListenerProc時設置的,在這種情況下,該屬性的值已經被緩存并且可以在以后獲得。
kAudioFileStreamPropertyFlag_PropertyIsCached = 1,
// 屬性偵聽器設置此標志以指示解析器緩存屬性值,以便在回調返回后它仍然可用。
kAudioFileStreamPropertyFlag_CacheProperty = 2
};
typedef CF_OPTIONS(UInt32, AudioFileStreamParseFlags) {
// AudioFileStreamParseBytes方法中,將此標志傳遞給函數以表示音頻數據的不連續性。
kAudioFileStreamParseFlag_Discontinuity = 1
};
typedef CF_OPTIONS(UInt32, AudioFileStreamSeekFlags) {
// AudioFileStreamSeek 方法,如果字節偏移量只是一個估計值,則此標志由函數返回。
kAudioFileStreamSeekFlag_OffsetIsEstimated = 1
};
1.5 AudioFileStream Functions
1.5.1 初始化與釋放文件流服務
- 創建并打開一個新的音頻文件流解析器。
extern OSStatus
AudioFileStreamOpen (
void * __nullable inClientData,
AudioFileStream_PropertyListenerProc inPropertyListenerProc,
AudioFileStream_PacketsProc inPacketsProc,
AudioFileTypeID inFileTypeHint,
AudioFileStreamID __nullable * __nonnull outAudioFileStream);
inClientData:傳遞給回調函數的值或結構的指針;
inPropertyListenerProc:屬性監聽器回調,當解析器在數據流中找到Property
的值時回調;
inPacketsProc:音頻數據回調,當解析器在數據流中找到音頻數據包時回調;
inFileTypeHint:音頻文件類型,如果不知道音頻文件類型,則設置為 0;
outAudioFileStream:音頻文件流解析器的 ID,需要將其保存,供其它音頻文件流 API 使用。
- 關閉并釋放指定的音頻文件流解析器。
extern OSStatus
AudioFileStreamClose(AudioFileStreamID inAudioFileStream);
inAudioFileStream:指定的音頻文件流解析器的 ID。
1.5.2 解析數據
將音頻文件流數據傳遞給解析器。當向解析器提供數據時,解析器將查找屬性數據和音頻數據包,當數據準備好時,將調用AudioFileStream_PropertyListenerProc和AudioFileStream_PacketsProc回調函數來處理數據。實際提供的數據量至少多于一個包的音頻文件數據,但最好一次提供幾個包到幾秒鐘的數據。
extern OSStatus
AudioFileStreamParseBytes(
AudioFileStreamID inAudioFileStream,
UInt32 inDataByteSize,
const void * __nullable inData,
AudioFileStreamParseFlags inFlags);
inAudioFileStream:音頻文件流解析器的 ID;
inDataByteSize:要解析的數據的字節數;
inData:要解析的數據;
inFlags:音頻文件流標志。如果傳遞給解析器的最后一個數據存在不連續性,請設置該標志為:kAudioFileStreamParseFlag_Discontinuity
。
1.5.3 Seek
為數據流中的指定數據包提供字節偏移量。
extern OSStatus
AudioFileStreamSeek(
AudioFileStreamID inAudioFileStream,
SInt64 inPacketOffset,
SInt64 * outDataByteOffset,
AudioFileStreamSeekFlags * ioFlags);
inAudioFileStream:音頻文件流解析器的 ID;
inAbsolutePacketOffset:希望返回其字節偏移量的數據包文件開頭的數據包數;
outAbsoluteByteOffset:在輸出時,參數中指定其偏移量的數據包的絕對字節偏移量。對于不包含數據包表的音頻文件格式,返回的偏移量可能是一個估計值;
ioFlags:在輸出中,如果outAbsoluteByteOffset
參數返回一個估計值,則該參數返回常量kAudioFileStreamSeekFlag_OffsetIsEstimated
。
1.5.4 獲取屬性
獲取有關屬性值的信息。
extern OSStatus
AudioFileStreamGetPropertyInfo(
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 * __nullable outPropertyDataSize,
Boolean * __nullable outWritable);
inAudioFileStream:音頻文件流解析器的 ID;
inPropertyID:需要其信息的音頻文件流PropertyID
;
outPropertyDataSize:在輸出時,指定屬性的當前值的大小(以字節為單位)。
outWritable:在輸出時,true
如果可以寫入屬性,但目前沒有可寫的音頻文件流屬性。
1.5.5 獲取屬性值
檢索指定屬性的值。
extern OSStatus
AudioFileStreamGetProperty(
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 * ioPropertyDataSize,
void * outPropertyData);
inAudioFileStream:音頻文件流解析器的 ID;
inPropertyID:讀取其值的音頻文件流屬性;
ioPropertyDataSize:參數中緩沖區的大小。可能通過調用AudioFileStreamGetPropertyInfo
獲取屬性值的大小;
outPropertyData:輸出指定屬性的值。
1.5.6 設置屬性
設置指定屬性的值。目前音頻文件流中,沒有可以設置的屬性。
extern OSStatus
AudioFileStreamSetProperty(
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 inPropertyDataSize,
const void * inPropertyData);
inAudioFileStream:音頻文件流解析器的 ID;
inPropertyID:要設置其值的音頻文件流的PropertyID;
inPropertyDataSize:屬性數據的大小(以字節為單位);
inPropertyData:屬性數據。
2 實踐與應用
為了驗證AudioFileStream
能力,這里僅通過 API,實現一個簡化版本的 AudioFileParser,目標實現創建、解碼、Seek、關閉能力。
2.1 主體框架
主體框架僅包含必要的定義,未實現任何功能,在下文,會針對每個功能補充必要的能力,完善 AudioFileParser。
@interface AudioFileParser () {
AudioFileStreamID _audioFileStreamID;
}
/// 是否不連續
@property (nonatomic, assign) BOOL discontinuous;
/// 解析出來的packets
@property (nonatomic, strong) NSMutableArray *packets;
/// 音頻數據在文件中的偏移
@property (nonatomic, assign) SInt64 dataOffset;
/// 已讀數據在數據源文件中的偏移
@property (nonatomic, assign) SInt64 fileReadOffset;
/// 文件頭解析完畢
@property (nonatomic, assign) BOOL readyToProducePackets;
@end
static void KSKitAudioFileStreamPropertyListener(void *inClientData,AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *inFlags) {
AudioFileParser *parser = (__bridge AudioFileParser *)inClientData;
[parser handleAudioFileStreamProperty:inPropertyID];
}
static void KSKitAudioFileStreamPacketCallBack(void *inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void *inInputData, AudioStreamPacketDescription *inPacketDescrrptions) {
AudioFileParser *parser = (__bridge AudioFileParser *)inClientData;
[parser handleAudioFileStreamPackets:inInputData
numberOfBytes:inNumberBytes
numberOfPackets:inNumberPackets
packetDescription:inPacketDescrrptions];
}
@implementation AudioFileParser
/// 初始化
- (instancetype)init {
if (self = [super init]) {
}
return self;
}
/// 解析數據
- (BOOL)parse:(NSData *)data error:(NSError **)error {
}
/// 音頻文件解析器Seek
- (BOOL)seek:(UInt32)packetCount error:(NSError **)error {
}
/// 關閉解析器
- (void)close {
}
/// 處理音頻文件流的Property
/// @param propertyID Property 對應的 ID
- (void)handleAudioFileStreamProperty:(AudioFileStreamPropertyID)propertyID {
}
/// 處理音頻文件流的 packets
/// @param packets 音頻包數據
/// @param numberOfBytes 緩沖區中數據的字節數
/// @param numberOfPackets 緩沖區中音頻數據的包數
/// @param packetDescriptions 描述數據的音頻文件流數據包描述結構數組
- (void)handleAudioFileStreamPackets:(const void *)packets
numberOfBytes:(UInt32)numberOfBytes
numberOfPackets:(UInt32)numberOfPackets
packetDescription:(AudioStreamPacketDescription *)packetDescriptions {
}
@end
2.2 核心能力
2.2.1 初始化與關閉
- 在初始化AudioFileParser時,通過
AudioFileStreamOpen
創建音頻文件流服務。readyToProducePackets 用來標識是否已經解析出音頻文件頭信息,discontinuous 用來標識是否連續,會在 Seek 實現中詳細講解。這里需要重點關注的是 KSKitAudioFileStreamPropertyListener 與 KSKitAudioFileStreamPacketCallBack,負責了音頻數據回調與屬性監聽器回調。
- (instancetype)init {
if (self = [super init]) {
_readyToProducePackets = NO;
_discontinuous = NO;
_packets = [[NSMutableArray alloc] init];
// inFileTypeHint 可以根據實際的傳或者不指定
OSStatus status = AudioFileStreamOpen((__bridge void *)self, KSKitAudioFileStreamPropertyListener, KSKitAudioFileStreamPacketCallBack, kAudioFileM4AType, &_audioFileStreamID);
if (status != noErr) {
return nil;
}
}
return self;
}
- 音頻文件流服務,需要手機關閉,通過
AudioFileStreamClose
關閉指定的解析器。
- (void)close {
if (_audioFileStreamID) {
AudioFileStreamClose(_audioFileStreamID);
_audioFileStreamID = NULL;
}
}
2.2.3 解析數據
初始化文件流解析器后,通過AudioFileStreamParseBytes
對數據進行解碼,數據由外部傳遞進來。我們通過 fileReadOffset 來標識,當前我們訪問的數據在原始文件中的偏移。需要注意,在未解析到音頻數據包前或者 Seek 之后,AudioFileStreamParseFlags
需要設置為 kAudioFileStreamParseFlag_Discontinuity
。
- (BOOL)parse:(NSData *)data error:(NSError **)error {
BOOL bResult = YES;
do {
if (!data || !data.length) {
bResult = NO;
break;
}
// 已讀偏移加上實際讀取到的數據量,有可能讀取到的數據要比要讀的size少
_fileReadOffset += data.length;
OSStatus status;
if (_discontinuous) {
status = AudioFileStreamParseBytes(_audioFileStreamID, (UInt32)data.length, data.bytes, kAudioFileStreamParseFlag_Discontinuity);
} else {
status = AudioFileStreamParseBytes(_audioFileStreamID, (UInt32)data.length, data.bytes, 0);
}
if (status != noErr) {
// handle error
bResult = NO;
}
} while (NO);
return bResult;
}
Note:AudioFileStream 本質上是對數據流的處理,并不特指是流媒體的資源,即使數據是本地文件,也是可以正常工作的,估這里命名為 AudioFileParser 而不是 AudioFileStreamParser。
2.2.3 獲取音頻文件信息
通過解析音頻數據,解析器會解析并獲取音頻文件的頭文件,會通過AudioFileStream_PropertyListenerProc
回調(多次回調),這里重點關注關注:
- kAudioFileStreamProperty_ReadyToProducePackets 成功獲取頭信息會回調,回調后,discontinuous 與 readyToProducePackets 可以標識為 YES;
- kAudioFileStreamProperty_DataOffset 獲取音頻真實數據在音頻文件的偏移值,Seek 時使用,這里注意上文說到的 fileReadOffset 原始數據偏移的區別
/// 處理音頻文件流的Property
/// @param propertyID Property 對應的 ID
- (void)handleAudioFileStreamProperty:(AudioFileStreamPropertyID)propertyID {
if (propertyID == kAudioFileStreamProperty_ReadyToProducePackets) {
// 成功獲取頭部信息
_readyToProducePackets = YES;
_discontinuous = YES;
} else if (propertyID == kAudioFileStreamProperty_DataOffset) {
UInt32 offsetSize = sizeof(_dataOffset);
// 獲取音頻真實數據在音頻文件的偏移值
OSStatus status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_DataOffset, &offsetSize, &_dataOffset);
if(status != noErr) {
NSLog(@"Parser get dataOffset error: %d", (int)status);
}
}
}
2.2.3 處理音頻數據包
在解析到音頻文件信息之后,當解析器接收到足夠的數據,會將解析到的音頻數據包,通過AudioFileStream_PacketsProc
回調出來,我們需要在該回調中,保存音頻數據包的格式數據及音頻包數據,提供給后繼的轉碼器或者處理器使用。
- (void)handleAudioFileStreamPackets:(const void *)packets
numberOfBytes:(UInt32)numberOfBytes
numberOfPackets:(UInt32)numberOfPackets
packetDescription:(AudioStreamPacketDescription *)packetDescriptions {
_discontinuous = NO;
if (numberOfBytes == 0 || numberOfPackets == 0) {
return;
}
BOOL deletePackDesc = NO;
if (packetDescriptions == NULL) {
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;
}
for (int i = 0; i < numberOfPackets; i++) {
SInt64 packetOffset = packetDescriptions[i].mStartOffset;
AudioStreamPacketDescription aspd = packetDescriptions[i];
UInt32 packetSize = aspd.mDataByteSize;
// data該初始化方法底層默認copy一份數據
NSData *data = [[NSData alloc] initWithBytes:packets+packetOffset length:packetSize];
[_packets addObject:data];
}
if (deletePackDesc) {
free(packetDescriptions);
packetDescriptions = NULL;
}
}
2.2.4 Seek 實現
AudioFileStream 中,Seek 本身只是獲取音頻文件在文件中偏移值,然后通過計算出在原始音頻文件中偏移,通過讀取新的數據包,實現 Seek 能力,需要注意的是在 Seek 之后,需要將 discontinuous 設置 YES,否則可能會遇到數據解碼異常,同時需要把已經緩存的音頻數據包清空,避免出現串數據而出現雜音。
- (BOOL)seek:(UInt32)packetCount error:(NSError **)error; {
SInt64 outDataByteOffset;
UInt32 ioFlags;
OSStatus status = AudioFileStreamSeek(_audioFileStreamID, packetCount, &outDataByteOffset, &ioFlags);
if ((status == noErr) && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)) {
_fileReadOffset = _dataOffset + outDataByteOffset;
} else {
// handle error
return NO;
}
_discontinuous = YES;
// seek 后需要移除已經解析出來的包
[_packets removeAllObjects];
return YES;
}
Note:如果使用了轉碼器,Seek 之后,需要刷新其緩沖區。
2.3 小結
AudioFileParser 中僅實現簡化版本的文件流解碼器,比如音頻文件格式、時長、總幀數。最大包大小等數據,需要讀者去擴展其能力。這里僅介紹 AudioFileStream,實際應用中,AudioFileStream 很少單獨應該,一般會結合 AudioConverter 、Audio Unit 或者更高級的音頻 API 一起實現,實現解碼器、轉碼器、處理器、播放器之間的聯動。