iOS 音频流式解码器 - AudioFileStream

1 基础知识

AudioFileStream将音频文件流解析为音频数据包的 API。

1.1 文件流错误码类型

音频文件流可能出现的错误类型,部分特殊场景,需要针对特定错误码做处理,完整错误码定义如下:

objc 复制代码
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定义如下:

objc 复制代码
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 流属性回调类型

解析器在音频文件流中找到属性值时调用。

objc 复制代码
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) 音频数据,每次调用该函数时可能会多次调用回调。

objc 复制代码
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

音频文件流中标识类型集合:

objc 复制代码
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 初始化与释放文件流服务

  1. 创建并打开一个新的音频文件流解析器。
objc 复制代码
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 使用。

  1. 关闭并释放指定的音频文件流解析器。
objc 复制代码
extern OSStatus 
AudioFileStreamClose(AudioFileStreamID inAudioFileStream);

inAudioFileStream:指定的音频文件流解析器的 ID。

1.5.2 解析数据

将音频文件流数据传递给解析器。当向解析器提供数据时,解析器将查找属性数据和音频数据包,当数据准备好时,将调用AudioFileStream_PropertyListenerProc和AudioFileStream_PacketsProc回调函数来处理数据。实际提供的数据量至少多于一个包的音频文件数据,但最好一次提供几个包到几秒钟的数据。

objc 复制代码
extern OSStatus
AudioFileStreamParseBytes(	
								AudioFileStreamID				    inAudioFileStream,
								UInt32							        inDataByteSize,
								const void * __nullable			inData,
								AudioFileStreamParseFlags		inFlags);

inAudioFileStream:音频文件流解析器的 ID;

inDataByteSize:要解析的数据的字节数;

inData:要解析的数据;

inFlags :音频文件流标志。如果传递给解析器的最后一个数据存在不连续性,请设置该标志为:kAudioFileStreamParseFlag_Discontinuity

1.5.3 Seek

为数据流中的指定数据包提供字节偏移量。

objc 复制代码
extern OSStatus
AudioFileStreamSeek(	
								AudioFileStreamID				   inAudioFileStream,
								SInt64							       inPacketOffset,
								SInt64 *						       outDataByteOffset,
								AudioFileStreamSeekFlags * ioFlags);

inAudioFileStream:音频文件流解析器的 ID;

inAbsolutePacketOffset:希望返回其字节偏移量的数据包文件开头的数据包数;

outAbsoluteByteOffset:在输出时,参数中指定其偏移量的数据包的绝对字节偏移量。对于不包含数据包表的音频文件格式,返回的偏移量可能是一个估计值;

ioFlags :在输出中,如果outAbsoluteByteOffset参数返回一个估计值,则该参数返回常量kAudioFileStreamSeekFlag_OffsetIsEstimated

1.5.4 获取属性

获取有关属性值的信息。

objc 复制代码
extern OSStatus
AudioFileStreamGetPropertyInfo(	
								AudioFileStreamID				    inAudioFileStream,
								AudioFileStreamPropertyID		inPropertyID,
								UInt32 * __nullable				  outPropertyDataSize,
								Boolean * __nullable			  outWritable);

inAudioFileStream:音频文件流解析器的 ID;

inPropertyID :需要其信息的音频文件流PropertyID

outPropertyDataSize:在输出时,指定属性的当前值的大小(以字节为单位)。

outWritable :在输出时,true如果可以写入属性,但目前没有可写的音频文件流属性。

1.5.5 获取属性值

检索指定属性的值。

objc 复制代码
extern OSStatus
AudioFileStreamGetProperty(	
							AudioFileStreamID					  inAudioFileStream,
							AudioFileStreamPropertyID	  inPropertyID,
							UInt32 *							      ioPropertyDataSize,
							void *								      outPropertyData);

inAudioFileStream:音频文件流解析器的 ID;

inPropertyID:读取其值的音频文件流属性;

ioPropertyDataSize :参数中缓冲区的大小。可能通过调用AudioFileStreamGetPropertyInfo获取属性值的大小;

outPropertyData:输出指定属性的值。

1.5.6 设置属性

设置指定属性的值。目前音频文件流中,没有可以设置的属性。

objc 复制代码
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。

objc 复制代码
@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 初始化与关闭

  1. 在初始化AudioFileParser时,通过AudioFileStreamOpen创建音频文件流服务。readyToProducePackets 用来标识是否已经解析出音频文件头信息,discontinuous 用来标识是否连续,会在 Seek 实现中详细讲解。这里需要重点关注的是 KSKitAudioFileStreamPropertyListener 与 KSKitAudioFileStreamPacketCallBack,负责了音频数据回调与属性监听器回调。
objc 复制代码
- (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;
}
  1. 音频文件流服务,需要手机关闭,通过AudioFileStreamClose关闭指定的解析器。
objc 复制代码
- (void)close {
    if (_audioFileStreamID) {
        AudioFileStreamClose(_audioFileStreamID);
        _audioFileStreamID = NULL;
    }
}

2.2.3 解析数据

初始化文件流解析器后,通过AudioFileStreamParseBytes对数据进行解码,数据由外部传递进来。我们通过 fileReadOffset 来标识,当前我们访问的数据在原始文件中的偏移。需要注意,在未解析到音频数据包前或者 Seek 之后,AudioFileStreamParseFlags 需要设置为 kAudioFileStreamParseFlag_Discontinuity

objc 复制代码
- (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回调(多次回调),这里重点关注关注:

  1. kAudioFileStreamProperty_ReadyToProducePackets 成功获取头信息会回调,回调后,discontinuous 与 readyToProducePackets 可以标识为 YES;
  2. kAudioFileStreamProperty_DataOffset 获取音频真实数据在音频文件的偏移值,Seek 时使用,这里注意上文说到的 fileReadOffset 原始数据偏移的区别
objc 复制代码
/// 处理音频文件流的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回调出来,我们需要在该回调中,保存音频数据包的格式数据及音频包数据,提供给后继的转码器或者处理器使用。

objc 复制代码
- (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,否则可能会遇到数据解码异常,同时需要把已经缓存的音频数据包清空,避免出现串数据而出现杂音。

objc 复制代码
- (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 一起实现,实现解码器、转码器、处理器、播放器之间的联动。

相关推荐
逻辑克3 小时前
使用 MultipeerConnectivity 在 iOS 中实现近场无线数据传输
ios
dnekmihfbnmv7 小时前
好用的电容笔有哪些推荐一下?年度最值得推荐五款电容笔分享!
ios·电脑·ipad·平板
Magnetic_h1 天前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...1 天前
「iOS」——单例模式
ios·单例模式·cocoa
yanling20231 天前
黑神话悟空mac可以玩吗
macos·ios·crossove·crossove24
归辞...1 天前
「iOS」viewController的生命周期
ios·cocoa·xcode
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
2401_852403551 天前
Mac导入iPhone的照片怎么删除?快速方法讲解
macos·ios·iphone
SchneeDuan1 天前
iOS六大设计原则&&设计模式
ios·设计模式·cocoa·设计原则
JohnsonXin2 天前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性