From f5ad70d4807ae6db8c7792bf4b0603bd9bf151f3 Mon Sep 17 00:00:00 2001 From: zlh668 <86775608+zlh668@users.noreply.github.com> Date: Tue, 28 Mar 2023 19:51:02 +0800 Subject: [PATCH] =?UTF-8?q?iOS=20AVDemo=EF=BC=9A=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E5=B0=81=E8=A3=85=EF=BC=8C=E9=87=87=E9=9B=86=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E5=B9=B6=E5=B0=81=E8=A3=85=E4=B8=BA=20M4A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...¢‘å°è£…,采集编ç å¹¶å°è£…为 M4A.md | 789 ++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 iOS资料/iOS AVDemo:音频å°è£…,采集编ç å¹¶å°è£…为 M4A.md diff --git a/iOS资料/iOS AVDemo:音频å°è£…,采集编ç å¹¶å°è£…为 M4A.md b/iOS资料/iOS AVDemo:音频å°è£…,采集编ç å¹¶å°è£…为 M4A.md new file mode 100644 index 0000000..59285e6 --- /dev/null +++ b/iOS资料/iOS AVDemo:音频å°è£…,采集编ç å¹¶å°è£…为 M4A.md @@ -0,0 +1,789 @@ +# iOS AVDemo:音频å°è£…,采集编ç å¹¶å°è£…为 M4A + +iOS/Android 客户端开å‘åŒå­¦å¦‚果想è¦å¼€å§‹å­¦ä¹ éŸ³è§†é¢‘å¼€å‘,最ä¸æ»‘çš„æ–¹å¼æ˜¯å¯¹[音视频基础概念知识](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2140155659944787969#wechat_redirect)有一定了解åŽï¼Œå†å€ŸåŠ©æœ¬åœ°å¹³å°çš„音视频能力上手去实践音视频的`采集 → ç¼–ç  â†’ å°è£… → 解å°è£… → è§£ç  â†’ 渲染`过程,并借助[音视频工具](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2216997905264082945#wechat_redirect)æ¥åˆ†æžå’Œç†è§£å¯¹åº”的音视频数æ®ã€‚ + +在[音视频工程示例](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2273301900659851268#wechat_redirect)这个æ ç›®ï¼Œæˆ‘们将通过拆解`采集 → ç¼–ç  â†’ å°è£… → 解å°è£… → è§£ç  â†’ 渲染`æµç¨‹å¹¶å®žçŽ° Demo æ¥å‘大家介ç»å¦‚何在 iOS/Android å¹³å°ä¸Šæ‰‹éŸ³è§†é¢‘å¼€å‘。 + +这里是第三篇:**iOS 音频å°è£… Demo**。这个 Demo 里包å«ä»¥ä¸‹å†…容: + +- 1)实现一个音频采集模å—ï¼› +- 2)实现一个音频编ç æ¨¡å—ï¼› +- 3)实现一个音频å°è£…模å—ï¼› +- 4)串è”音频采集ã€ç¼–ç ã€å°è£…模å—,将采集到的音频数æ®è¾“入给 AAC ç¼–ç æ¨¡å—进行编ç ï¼Œå†å°†ç¼–ç åŽçš„æ•°æ®è¾“入给 M4A å°è£…模å—å°è£…和存储; +- 5)详尽的代ç æ³¨é‡Šï¼Œå¸®ä½ ç†è§£ä»£ç é€»è¾‘和原ç†ã€‚ + +## 1ã€éŸ³é¢‘é‡‡é›†æ¨¡å— + +在这个 Demo ä¸­ï¼ŒéŸ³é¢‘é‡‡é›†æ¨¡å— `KFAudioCapture` 的实现与 [《iOS 音频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484867&idx=1&sn=d857104930a86de8ab0bdf2358ca6283&scene=21#wechat_redirect) 中一样,这里就ä¸å†é‡å¤ä»‹ç»äº†ï¼Œå…¶æŽ¥å£å¦‚下: + +``` +KFAudioCapture.h +#import +#import +#import "KFAudioConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface KFAudioCapture : NSObject ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithConfig:(KFAudioConfig *)config; + +@property (nonatomic, strong, readonly) KFAudioConfig *config; +@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频采集数æ®å›žè°ƒã€‚ +@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频采集错误回调。 + +- (void)startRunning; // 开始采集音频数æ®ã€‚ +- (void)stopRunning; // åœæ­¢é‡‡é›†éŸ³é¢‘æ•°æ®ã€‚ +@end + +NS_ASSUME_NONNULL_END +``` + +## 2ã€éŸ³é¢‘ç¼–ç æ¨¡å— + +åŒæ ·çš„,音频编ç æ¨¡å— `KFAudioEncoder` 的实现与[《iOS éŸ³é¢‘ç¼–ç  Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484887&idx=1&sn=ac142cbeafddc27f3a8c2902524831c8&scene=21#wechat_redirect)中一样,这里就ä¸å†é‡å¤ä»‹ç»äº†ï¼Œå…¶æŽ¥å£å¦‚下: + +``` +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KFAudioEncoder : NSObject ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithAudioBitrate:(NSInteger)audioBitrate; + +@property (nonatomic, assign, readonly) NSInteger audioBitrate; // 音频编ç ç çŽ‡ã€‚ +@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频编ç æ•°æ®å›žè°ƒã€‚ +@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频编ç é”™è¯¯å›žè°ƒã€‚ + +- (void)encodeSampleBuffer:(CMSampleBufferRef)buffer; // ç¼–ç ã€‚ +@end + +NS_ASSUME_NONNULL_END +``` + +## 3ã€éŸ³é¢‘å°è£…æ¨¡å— + +接下æ¥ï¼Œæˆ‘们æ¥å®žçŽ°ä¸€ä¸ªéŸ³é¢‘å°è£…模å—,在这里输入编ç åŽçš„æ•°æ®ï¼Œè¾“出å°è£…åŽçš„文件。 + +这次我们è¦å°è£…çš„æ ¼å¼æ˜¯ M4A,属于 MPEG-4 标准,通常普通的 MPEG-4 文件扩展å是 `.mp4`,åªåŒ…å«éŸ³é¢‘çš„ MPEG-4 文件扩展å用 `.m4a`。所以,其实我们这里实现的是一个 MP4 å°è£…模å—,支æŒå°†éŸ³é¢‘ç¼–ç æ•°æ®å°è£…æˆ M4A,也支æŒå°†éŸ³è§†é¢‘æ•°æ®å°è£…æˆ MP4。关于 MP4 æ ¼å¼ï¼Œå¯ä»¥çœ‹ä¸€çœ‹[《MP4 æ ¼å¼ã€‹](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484677&idx=1&sn=a868911489519592930e18a60966d6a1&scene=21#wechat_redirect)这篇文章了解一下。 + +由于 MP4 å°è£…涉åŠåˆ°ä¸€äº›å‚数设置,所以我们先实现一个 `KFMuxerConfig` 类用于定义 MP4 å°è£…çš„å‚æ•°çš„é…置。这里包括了:å°è£…文件输出地å€ã€å°è£…文件类型ã€å›¾åƒå˜æ¢ä¿¡æ¯è¿™å‡ ä¸ªå‚数。 + +``` +KFMuxerConfig.h +#import +#import +#import "KFMediaBase.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface KFMuxerConfig : NSObject +@property (nonatomic, strong) NSURL *outputURL; // å°è£…文件输出地å€ã€‚ +@property (nonatomic, assign) KFMediaType muxerType; // å°è£…文件类型。 +@property (nonatomic, assign) CGAffineTransform preferredTransform; // 图åƒçš„å˜æ¢ä¿¡æ¯ã€‚比如:视频图åƒæ—‹è½¬ã€‚ +@end + +NS_ASSUME_NONNULL_END +KFMuxerConfig.m +#import "KFMuxerConfig.h" + +@implementation KFMuxerConfig + +- (instancetype)init { + self = [super init]; + if (self) { + _muxerType = KFMediaAV; + _preferredTransform = CGAffineTransformIdentity; + } + return self; +} + +@end +``` + +其中用到的 `KFMediaType` 是定义在 `KFMediaBase.h` 中的一个枚举: + +``` +KFMediaBase.h +#ifndef KFMediaBase_h +#define KFMediaBase_h + +#import + +typedef NS_ENUM(NSInteger, KFMediaType) { + KFMediaNone = 0, + KFMediaAudio = 1 << 0, // 仅音频。 + KFMediaVideo = 1 << 1, // 仅视频。 + KFMediaAV = KFMediaAudio | KFMediaVideo, // 音视频都有。 +}; + +#endif /* KFMediaBase_h */ +``` + +接下æ¥ï¼Œæˆ‘们æ¥å®žçŽ° `KFMP4Muxer` 模å—。 + +``` +KFMP4Muxer.h +#import +#import +#import "KFMuxerConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface KFMP4Muxer : NSObject ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithConfig:(KFMuxerConfig *)config; + +@property (nonatomic, strong, readonly) KFMuxerConfig *config; +@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // å°è£…错误回调。 + +- (void)startWriting; // 开始å°è£…写入数æ®ã€‚ +- (void)cancelWriting; // å–消å°è£…写入数æ®ã€‚ +- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 添加å°è£…æ•°æ®ã€‚ +- (void)stopWriting:(void (^)(BOOL success, NSError *error))completeHandler; // åœæ­¢å°è£…写入数æ®ã€‚ +@end + +NS_ASSUME_NONNULL_END +``` + +上é¢æ˜¯ `KFMP4Muxer` 的接å£è®¾è®¡ï¼Œé™¤äº†`åˆå§‹åŒ–方法`,主è¦æ˜¯æœ‰`获å–å°è£…é…ç½®`以åŠ`å°è£…错误回调`的接å£ï¼Œå¦å¤–就是`开始写入å°è£…æ•°æ®`ã€`å–消写入å°è£…æ•°æ®`ã€`添加å°è£…æ•°æ®`ã€`åœæ­¢å†™å…¥å°è£…æ•°æ®`的接å£ã€‚ + +在上é¢çš„`添加å°è£…æ•°æ®`接å£ä¸­ï¼Œæˆ‘们使用的是ä¾ç„¶ **CMSampleBufferRef**[1] 作为å‚数类型,å†æ¬¡ä½“现了它作为 `iOS éŸ³è§†é¢‘å¤„ç† pipeline 中的æµé€šè´§å¸`的通用性。关于这点,我们在[《iOS 音频采集 Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484867&idx=1&sn=d857104930a86de8ab0bdf2358ca6283&scene=21#wechat_redirect)å’Œ[《iOS éŸ³é¢‘ç¼–ç  Demo》](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484887&idx=1&sn=ac142cbeafddc27f3a8c2902524831c8&scene=21#wechat_redirect)两篇文章中都æ到过。 + +在这个 Demo 里我们通过 `CMSampleBufferRef` 打包的是编ç åŽçš„ AAC æ•°æ®ï¼Œå°†å…¶ä½œä¸ºè¾“å…¥é€ç»™å°è£…模å—。 + +``` +KFMP4Muxer.m +#import "KFMP4Muxer.h" +#import + +#define KFMP4MuxerAddOutputError 1000 +#define KFMP4MuxerMaxQueueCount 10000 + +// å°è£…器的状æ€æœºã€‚ +typedef NS_ENUM(NSInteger, KFMP4MuxerStatus) { + KFMP4MuxerStatusUnknown = 0, + KFMP4MuxerStatusRunning = 1, + KFMP4MuxerStatusFailed = 2, + KFMP4MuxerStatusCompleted = 3, + KFMP4MuxerStatusCancelled = 4, +}; + +@interface KFMP4Muxer () { + CMSimpleQueueRef _audioQueue; // 音频数æ®é˜Ÿåˆ—。 + CMSimpleQueueRef _videoQueue; // 视频数æ®é˜Ÿåˆ—。 +} +@property (nonatomic, strong, readwrite) KFMuxerConfig *config; +@property (nonatomic, strong) AVAssetWriter *muxWriter; // å°è£…器实例。 +@property (nonatomic, strong) AVAssetWriterInput *writerVideoInput; // Muxer 的视频输入。 +@property (nonatomic, strong) AVAssetWriterInput *writerAudioInput; // Muxer 的音频输入。 +@property (nonatomic, strong) dispatch_queue_t muxerQueue; +@property (nonatomic, strong) dispatch_semaphore_t semaphore; +@property (nonatomic, assign) KFMP4MuxerStatus muxerStatus; +@end + +@implementation KFMP4Muxer +#pragma mark - LifeCycle +- (instancetype)initWithConfig:(KFMuxerConfig *)config { + self = [super init]; + if (self) { + _config = config; + _muxerQueue = dispatch_queue_create("com.KeyFrameKit.muxerQueue", DISPATCH_QUEUE_SERIAL); // å°è£…任务队列。 + _semaphore = dispatch_semaphore_create(1); + CMSimpleQueueCreate(kCFAllocatorDefault, KFMP4MuxerMaxQueueCount, &_audioQueue); + CMSimpleQueueCreate(kCFAllocatorDefault, KFMP4MuxerMaxQueueCount, &_videoQueue); + } + + return self; +} + +- (void)dealloc { + dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); + [self _reset]; // 清ç†ã€‚ + dispatch_semaphore_signal(_semaphore); +} + +#pragma mark - Public Method +- (void)startWriting { + // 开始写入。 + __weak typeof(self) weakSelf = self; + dispatch_async(self.muxerQueue, ^{ + dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); + [weakSelf _reset]; // 清ç†ã€‚ + weakSelf.muxerStatus = KFMP4MuxerStatusRunning; // 标记状æ€ã€‚ + dispatch_semaphore_signal(weakSelf.semaphore); + }); +} + +- (void)cancelWriting { + // å–消写入。 + __weak typeof(self) weakSelf = self; + dispatch_async(self.muxerQueue, ^{ + dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); + if (weakSelf.muxWriter && weakSelf.muxWriter.status == AVAssetWriterStatusWriting) { + [weakSelf.muxWriter cancelWriting]; + } + weakSelf.muxerStatus = KFMP4MuxerStatusCancelled; // 标记状æ€ã€‚ + dispatch_semaphore_signal(weakSelf.semaphore); + }); +} + +- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer { + if (!sampleBuffer || !CMSampleBufferGetDataBuffer(sampleBuffer) || self.muxerStatus != KFMP4MuxerStatusRunning) { + return; + } + + // 异步添加数æ®ã€‚ + + __weak typeof(self) weakSelf = self; + CFRetain(sampleBuffer); + dispatch_async(self.muxerQueue, ^{ + dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); + + // 1ã€æ·»åŠ æ•°æ®åˆ°é˜Ÿåˆ—。 + [weakSelf _enqueueSampleBuffer:sampleBuffer]; + + // 2ã€ç¬¬ä¸€æ¬¡æ·»åŠ æ•°æ®æ—¶ï¼Œåˆ›å»º Muxer 实例并触å‘写数æ®æ“作。 + if (!weakSelf.muxWriter) { + // 检查数æ®æ˜¯å¦æ­£å¸¸ã€‚队列里é¢æœ‰æ•°æ®åˆ™è¡¨ç¤ºå¯¹åº”çš„æ•°æ®æºå¸¦çš„音视频格å¼ä¿¡æ¯æ˜¯æ­£å¸¸çš„,这个在åˆå§‹åŒ– Muxer 的输入æºæ—¶éœ€è¦ç”¨åˆ°ã€‚ + if (![weakSelf _checkFormatDescriptionLoadSuccess]) { + CFRelease(sampleBuffer); + dispatch_semaphore_signal(weakSelf.semaphore); + return; + } + + // 创建 Muxer 实例。 + NSError *error = nil; + BOOL success = [weakSelf _setupMuxWriter:&error]; + if (!success) { + weakSelf.muxerStatus = KFMP4MuxerStatusFailed; + CFRelease(sampleBuffer); + dispatch_semaphore_signal(weakSelf.semaphore); + [weakSelf _callBackError:error]; + return; + } + + // 开始å°è£…写入。 + success = [weakSelf.muxWriter startWriting]; + if (success) { + // å¯åŠ¨å°è£…会è¯ï¼Œä¼ å…¥æ•°æ®èµ·å§‹æ—¶é—´ã€‚这个起始时间是音视频 pts 的最å°å€¼ã€‚ + [weakSelf.muxWriter startSessionAtSourceTime:[weakSelf _sessionSourceTime]]; + } + } + + // 3ã€æ£€æŸ¥ Muxer 状æ€ã€‚ + if (!weakSelf.muxWriter || weakSelf.muxWriter.status != AVAssetWriterStatusWriting) { + weakSelf.muxerStatus = KFMP4MuxerStatusFailed; + CFRelease(sampleBuffer); + dispatch_semaphore_signal(weakSelf.semaphore); + [weakSelf _callBackError:weakSelf.muxWriter.error]; + return; + } + + // 4ã€åšéŸ³è§†é¢‘æ•°æ®äº¤ç»‡ã€‚ + [weakSelf _avInterLeavedSample]; + + CFRelease(sampleBuffer); + dispatch_semaphore_signal(weakSelf.semaphore); + }); +} + +- (void)stopWriting:(void (^)(BOOL success, NSError *error))completeHandler { + // åœæ­¢å†™å…¥ã€‚ + __weak typeof(self) weakSelf = self; + dispatch_async(self.muxerQueue, ^{ + dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER); + [weakSelf _stopWriting:^(BOOL success, NSError *error) { + weakSelf.muxerStatus = success ? KFMP4MuxerStatusCompleted : KFMP4MuxerStatusFailed; + dispatch_semaphore_signal(weakSelf.semaphore); + if (completeHandler) { + completeHandler(success, error); + } + }]; + }); +} + +#pragma mark - Private Method +- (BOOL)_setupMuxWriter:(NSError **)error { + if (!self.config.outputURL) { + *error = [NSError errorWithDomain:NSStringFromClass([KFMP4Muxer class]) code:40003 userInfo:nil]; + return NO; + } + + // 1ã€æ¸…ç†å†™å…¥è·¯å¾„的文件。 + if ([[NSFileManager defaultManager] fileExistsAtPath:self.config.outputURL.path]) { + [[NSFileManager defaultManager] removeItemAtPath:self.config.outputURL.path error:nil]; + } + + + // 2ã€åˆ›å»ºå°è£…器实例。 + if (_muxWriter) { + return YES; + } + // 使用 AVAssetWriter 作为å°è£…器,类型使用 AVFileTypeMPEG4。M4A æ ¼å¼æ˜¯éµå¾ª MPEG4 规范的一ç§éŸ³é¢‘æ ¼å¼ã€‚ + _muxWriter = [[AVAssetWriter alloc] initWithURL:self.config.outputURL fileType:AVFileTypeMPEG4 error:error]; + if (*error) { + return NO; + } + _muxWriter.movieTimeScale = 1000000000; + _muxWriter.shouldOptimizeForNetworkUse = YES; // 这个选项会将 MP4 çš„ moov box å‰ç½®ã€‚ + + // 3ã€å½“å°è£…内容包å«è§†é¢‘时,创建 Muxer 的视频输入。 + if ((self.config.muxerType & KFMediaVideo) && !_writerVideoInput) { + // 从队列中的视频数æ®é‡ŒèŽ·å–视频格å¼ä¿¡æ¯ï¼Œç”¨äºŽåˆå§‹åŒ–视频输入æºã€‚ + CMVideoFormatDescriptionRef videoDecscription = CMSampleBufferGetFormatDescription((CMSampleBufferRef)CMSimpleQueueGetHead(_videoQueue)); + _writerVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:nil sourceFormatHint:videoDecscription]; + _writerVideoInput.expectsMediaDataInRealTime = YES; // 输入是å¦ä¸ºå®žæ—¶æ•°æ®æºï¼Œæ¯”如相机采集。 + _writerVideoInput.transform = self.config.preferredTransform; // ç”»é¢æ˜¯å¦åšå˜æ¢ã€‚ + if ([self.muxWriter canAddInput:self.writerVideoInput]) { + [self.muxWriter addInput:self.writerVideoInput]; + } else { + *error = self.muxWriter.error ? self.muxWriter.error : [NSError errorWithDomain:NSStringFromClass([KFMP4Muxer class]) code:KFMP4MuxerAddOutputError userInfo:nil]; + return NO; + } + } + + // 4ã€å½“å°è£…内容包å«éŸ³é¢‘时,创建 Muxer 的音频输入。 + if ((self.config.muxerType & KFMediaAudio) && !_writerAudioInput) { + // 从队列中的音频数æ®é‡ŒèŽ·å–音频格å¼ä¿¡æ¯ï¼Œç”¨äºŽåˆå§‹åŒ–音频输入æºã€‚ + CMAudioFormatDescriptionRef audioDecscription = CMSampleBufferGetFormatDescription((CMSampleBufferRef)CMSimpleQueueGetHead(_audioQueue)); + _writerAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:nil sourceFormatHint:audioDecscription]; + _writerAudioInput.expectsMediaDataInRealTime = YES; // 输入是å¦ä¸ºå®žæ—¶æ•°æ®æºï¼Œæ¯”如麦克风采集。 + if ([self.muxWriter canAddInput:self.writerAudioInput]) { + [self.muxWriter addInput:self.writerAudioInput]; + } else { + *error = self.muxWriter.error ? self.muxWriter.error : [NSError errorWithDomain:NSStringFromClass([KFMP4Muxer class]) code:KFMP4MuxerAddOutputError userInfo:nil]; + return NO; + } + } + + return YES; +} + +- (void)_enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer { + CFRetain(sampleBuffer); + // 音频ã€è§†é¢‘çš„æ ¼å¼ä¿¡æ¯æ­£ç¡®æ‰èƒ½å…¥é˜Ÿã€‚因为åŽé¢åˆ›å»º Muxer 实例的输入æºæ—¶ä¹Ÿéœ€è¦ä»Žé˜Ÿåˆ—中的音视频数æ®ä¸­èŽ·å–相关格å¼ä¿¡æ¯ã€‚ + if (CMFormatDescriptionGetMediaType(CMSampleBufferGetFormatDescription(sampleBuffer)) == kCMMediaType_Audio) { + CMSimpleQueueEnqueue(_audioQueue, sampleBuffer); // 音频数æ®å…¥é˜Ÿåˆ—。 + } else if (CMFormatDescriptionGetMediaType(CMSampleBufferGetFormatDescription(sampleBuffer)) == kCMMediaType_Video) { + CMSimpleQueueEnqueue(_videoQueue, sampleBuffer); // 视频数æ®å…¥é˜Ÿåˆ—。 + } +} + +- (void)_flushMuxer { + // 将队列数æ®æ¶ˆè´¹æŽ‰ã€‚ + [self _appendAudioSample]; + [self _appendVideoSample]; +} + +- (void)_appendAudioSample { + // 音频写入å°è£…。 + while (self.writerAudioInput && self.writerAudioInput.readyForMoreMediaData && CMSimpleQueueGetCount(_audioQueue) > 0) { + CMSampleBufferRef audioSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_audioQueue); + [self.writerAudioInput appendSampleBuffer:audioSample]; + CFRelease(audioSample); + } +} + +- (void)_appendVideoSample { + // 视频写入å°è£…。 + while (self.writerVideoInput && self.writerVideoInput.readyForMoreMediaData && CMSimpleQueueGetCount(_videoQueue) > 0) { + CMSampleBufferRef videoSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_videoQueue); + [self.writerVideoInput appendSampleBuffer:videoSample]; + CFRelease(videoSample); + } +} + +- (void)_avInterLeavedSample { + // 当åŒæ—¶å°è£…音频和视频时,需è¦åšå¥½äº¤ç»‡ï¼Œè¿™æ ·å¯ä»¥æå‡éŸ³è§†é¢‘播放时的体验。 + if ((self.config.muxerType & KFMediaAudio) && (self.config.muxerType & KFMediaVideo)) { // åŒæ—¶å°è£…音频和视频。 + while (CMSimpleQueueGetCount(_audioQueue) > 0 && CMSimpleQueueGetCount(_videoQueue) > 0) { + if (self.writerAudioInput.readyForMoreMediaData && self.writerVideoInput.readyForMoreMediaData) { + // 音频ã€è§†é¢‘队列数æ®å„出队 1 个。 + CMSampleBufferRef audioHeader = (CMSampleBufferRef)CMSimpleQueueGetHead(_audioQueue); + CMTime audioDtsTime = CMSampleBufferGetPresentationTimeStamp(audioHeader); + CMSampleBufferRef videoHeader = (CMSampleBufferRef)CMSimpleQueueGetHead(_videoQueue); + CMTime videoDtsTime = CMSampleBufferGetDecodeTimeStamp(videoHeader).value > 0 ? CMSampleBufferGetDecodeTimeStamp(videoHeader) : CMSampleBufferGetPresentationTimeStamp(videoHeader); + // 比较 dts 较å°è€…写入å°è£…。 + if (CMTimeGetSeconds(audioDtsTime) >= CMTimeGetSeconds(videoDtsTime)) { + CMSampleBufferRef videoSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_videoQueue); + [self.writerVideoInput appendSampleBuffer:videoSample]; + CFRelease(videoSample); + } else { + CMSampleBufferRef audioSample = (CMSampleBufferRef)CMSimpleQueueDequeue(_audioQueue); + [self.writerAudioInput appendSampleBuffer:audioSample]; + CFRelease(audioSample); + } + } else { + break; + } + } + } else if (self.config.muxerType & KFMediaAudio) { // åªå°è£…音频。 + [self _appendAudioSample]; + } else if (self.config.muxerType & KFMediaVideo) { // åªå°è£…视频。 + [self _appendVideoSample]; + } +} + +- (BOOL)_checkFormatDescriptionLoadSuccess { + // 检查数æ®æ˜¯å¦æ­£å¸¸ã€‚ + if (!_muxWriter) { + if ((self.config.muxerType & KFMediaAudio) && (self.config.muxerType & KFMediaVideo)) { + return CMSimpleQueueGetCount(_videoQueue) > 0 && CMSimpleQueueGetCount(_audioQueue) > 0; + } else if (self.config.muxerType & KFMediaAudio) { + return CMSimpleQueueGetCount(_audioQueue) > 0; + } else if (self.config.muxerType & KFMediaVideo) { + return CMSimpleQueueGetCount(_videoQueue) > 0; + } + } + + return NO; +} + +- (CMTime)_sessionSourceTime { + // æ•°æ®èµ·å§‹æ—¶é—´ï¼šéŸ³è§†é¢‘ pts 的最å°å€¼ã€‚ + CMSampleBufferRef audioFirstBuffer = (CMSampleBufferRef)CMSimpleQueueGetHead(_audioQueue); + CMSampleBufferRef videoFirstBuffer = (CMSampleBufferRef)CMSimpleQueueGetHead(_videoQueue); + if (audioFirstBuffer && videoFirstBuffer) { + Float64 audioPtsTime = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(audioFirstBuffer)); + Float64 videoPtsTime = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(videoFirstBuffer)); + return audioPtsTime >= videoPtsTime ? CMSampleBufferGetPresentationTimeStamp(videoFirstBuffer) : CMSampleBufferGetPresentationTimeStamp(audioFirstBuffer); + } else if (audioFirstBuffer) { + return CMSampleBufferGetPresentationTimeStamp(audioFirstBuffer); + } else if (videoFirstBuffer) { + return CMSampleBufferGetPresentationTimeStamp(videoFirstBuffer); + } + + return kCMTimeInvalid; +} + +- (void)_stopWriting:(void (^)(BOOL success, NSError *error))completeHandler { + // 1ã€çŠ¶æ€ä¸å¯¹ï¼Œå›žè°ƒé”™è¯¯ã€‚ + if (!self.muxWriter || self.muxWriter.status == AVAssetWriterStatusCompleted || self.muxWriter.status == AVAssetWriterStatusCancelled || self.muxWriter.status == AVAssetWriterStatusUnknown) { + if (completeHandler) { + completeHandler(NO, self.muxWriter.error ? self.muxWriter.error : [NSError errorWithDomain:NSStringFromClass(self.class) code:self.muxWriter.status userInfo:nil]); + } + return; + } + + // 2ã€æ¶ˆè´¹æŽ‰é˜Ÿåˆ—中剩余的数æ®ã€‚ + // å…ˆåšå‰©ä½™æ•°æ®çš„音视频交织。 + [self _avInterLeavedSample]; + // 消费剩余数æ®ã€‚ + [self _flushMuxer]; + + // 3ã€æ ‡è®°è§†é¢‘输入和音频输入为结æŸçŠ¶æ€ã€‚ + [self _markVideoAsFinished]; + [self _markAudioAsFinished]; + + // 4ã€ç»“æŸå†™å…¥ã€‚ + __weak typeof(self) weakSelf = self; + [self.muxWriter finishWritingWithCompletionHandler:^{ + BOOL complete = weakSelf.muxWriter.status == AVAssetWriterStatusCompleted; + if (completeHandler) { + completeHandler(complete, complete ? nil : weakSelf.muxWriter.error); + } + }]; +} + +- (void)_markVideoAsFinished { + // 标记视频输入æºä¸ºç»“æŸçŠ¶æ€ã€‚ + if (self.muxWriter.status == AVAssetWriterStatusWriting && self.writerVideoInput) { + [self.writerVideoInput markAsFinished]; + } +} + +- (void)_markAudioAsFinished { + // 标记音频输入æºä¸ºç»“æŸçŠ¶æ€ã€‚ + if (self.muxWriter.status == AVAssetWriterStatusWriting && self.writerAudioInput) { + [self.writerAudioInput markAsFinished]; + } +} + +- (void)_reset { + // å–消写入æ“作。 + if (_muxWriter && _muxWriter.status == AVAssetWriterStatusWriting) { + [_muxWriter cancelWriting]; + } + + // 清ç†å®žä¾‹ã€‚ + _muxWriter = nil; + _writerVideoInput = nil; + _writerVideoInput = nil; + + // 清ç†éŸ³é¢‘和视频数æ®é˜Ÿåˆ—。 + while (CMSimpleQueueGetCount(_audioQueue) > 0) { + CMSampleBufferRef sampleBuffer = (CMSampleBufferRef) CMSimpleQueueDequeue(_audioQueue); + CFRelease(sampleBuffer); + } + while (CMSimpleQueueGetCount(_videoQueue) > 0) { + CMSampleBufferRef sampleBuffer = (CMSampleBufferRef) CMSimpleQueueDequeue(_videoQueue); + CFRelease(sampleBuffer); + } +} + +- (void)_callBackError:(NSError *)error { + if (error && self.errorCallBack) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.errorCallBack(error); + }); + } +} + +@end +``` + +上é¢æ˜¯ `KFMP4Muxer` 的实现,从代ç ä¸Šå¯ä»¥çœ‹åˆ°ä¸»è¦æœ‰è¿™å‡ ä¸ªéƒ¨åˆ†ï¼š + +- 1)创建å°è£…器实例åŠå¯¹åº”的音频和视频数æ®è¾“å…¥æºã€‚第一次调用 `-appendSampleBuffer:` 添加待å°è£…æ•°æ®æ—¶æ‰ä¼šåˆ›å»ºå°è£…器实例。 + +- - 在 `-_setupMuxWriter:` 方法中实现。音频和视频的输入æºåˆ†åˆ«æ˜¯ `writerAudioInput` å’Œ `writerVideoInput`。 + +- 2)用两个队列作为缓冲区,分别管ç†éŸ³é¢‘和视频待å°è£…æ•°æ®ã€‚ + +- - 这两个队列分别是 `_audioQueue` å’Œ `_videoQueue`。 + - æ¯æ¬¡å½“外部调用 `-appendSampleBuffer:` 方法é€å…¥å¾…å°è£…æ•°æ®æ—¶ï¼Œå…¶å®žéƒ½æ˜¯å…ˆè°ƒç”¨ `-_enqueueSampleBuffer:` 把数æ®æ”¾å…¥ä¸¤ä¸ªé˜Ÿåˆ—中的一个,以便根æ®æƒ…况进行åŽç»­çš„音视频数æ®äº¤ç»‡ã€‚ + +- 3)åŒæ—¶å°è£…音频和视频数æ®æ—¶ï¼Œè¿›è¡ŒéŸ³è§†é¢‘æ•°æ®äº¤ç»‡ã€‚ + +- - 在 `-_avInterLeavedSample` 方法中实现音视频数æ®äº¤ç»‡ã€‚当带å°è£…çš„æ•°æ®æ—¢æœ‰éŸ³é¢‘åˆæœ‰è§†é¢‘,就需è¦æ ¹æ®ä»–们的时间戳信æ¯è¿›è¡Œäº¤ç»‡ï¼Œè¿™æ ·ä¾¿äºŽåœ¨æ’­æ”¾è¯¥éŸ³è§†é¢‘æ—¶æå‡ä½“验。 + +- 4)音视频数æ®å†™å…¥å°è£…。 + +- - åŒæ—¶å°è£…音频和视频数æ®æ—¶ï¼Œåœ¨åšå®ŒéŸ³è§†é¢‘交织åŽï¼Œå³åˆ†åˆ«å°†äº¤ç»‡åŽçš„音视频数æ®å†™å…¥å¯¹åº”çš„ `writerAudioInput` å’Œ `writerVideoInput`。在 `-_avInterLeavedSample` 中实现。 + - å•ç‹¬å°è£…音频或视频数æ®æ—¶ï¼Œåˆ™ç›´æŽ¥å°†æ•°æ®å†™å…¥å¯¹åº”çš„ `writerAudioInput` å’Œ `writerVideoInput`。分别在 `-_appendAudioSample` å’Œ `-_appendVideoSample` 方法中实现。 + +- 5)åœæ­¢å†™å…¥ã€‚ + +- - 在 `-stopWriting:` → `-_stopWriting:` 方法中实现。 + - 在åœæ­¢å‰ï¼Œè¿˜éœ€è¦æ¶ˆè´¹æŽ‰ `_audioQueue` å’Œ `_videoQueue` 的剩余数æ®ï¼Œè¦è°ƒç”¨ `-_avInterLeavedSample` → `-_flushMuxer`。 + - 并将视频输入æºå’ŒéŸ³é¢‘输入æºæ ‡è®°ä½ç»“æŸï¼Œåˆ†åˆ«åœ¨ `-_markVideoAsFinished` å’Œ `-_markAudioAsFinished` 方法中实现。 + +- 6)贯穿整个å°è£…过程的状æ€æœºç®¡ç†ã€‚ + +- - 在枚举 `KFMP4MuxerStatus` 中定义了å°è£…器的å„ç§çŠ¶æ€ï¼Œå¯¹äºŽå°è£…器的状æ€æœºç®¡ç†è´¯ç©¿åœ¨å°è£…的整个过程中。 + +- 7)错误回调。 + +- - 在 `-callBackError:` 方法å‘外回调错误。 + +- 8)清ç†å°è£…器实例åŠæ•°æ®ç¼“冲区。 + +- - 在 `-dealloc` 方法中实现。需è¦è°ƒç”¨ `-_reset` 方法清ç†å°è£…器实例ã€éŸ³é¢‘和视频输入æºã€éŸ³é¢‘和视频缓冲区。 + +更具体细节è§ä¸Šè¿°ä»£ç åŠå…¶æ³¨é‡Šã€‚ + +## 4ã€é‡‡é›†éŸ³é¢‘æ•°æ®è¿›è¡Œ AAC ç¼–ç ä»¥åŠ M4A å°è£…和存储 + +我们还是在一个 ViewController 中æ¥å®žçŽ°é‡‡é›†éŸ³é¢‘æ•°æ®è¿›è¡Œ AAC ç¼–ç ã€M4A å°è£…和存储的逻辑。 + +``` +KFAudioCaptureViewController.m +#import "KFAudioMuxerViewController.h" +#import +#import "KFAudioCapture.h" +#import "KFAudioEncoder.h" +#import "KFMP4Muxer.h" + +@interface KFAudioMuxerViewController () +@property (nonatomic, strong) KFAudioConfig *audioConfig; +@property (nonatomic, strong) KFAudioCapture *audioCapture; +@property (nonatomic, strong) KFAudioEncoder *audioEncoder; +@property (nonatomic, strong) KFMuxerConfig *muxerConfig; +@property (nonatomic, strong) KFMP4Muxer *muxer; +@end + +@implementation KFAudioMuxerViewController +#pragma mark - Property +- (KFAudioConfig *)audioConfig { + if (!_audioConfig) { + _audioConfig = [KFAudioConfig defaultConfig]; + } + + return _audioConfig; +} + +- (KFAudioCapture *)audioCapture { + if (!_audioCapture) { + __weak typeof(self) weakSelf = self; + _audioCapture = [[KFAudioCapture alloc] initWithConfig:self.audioConfig]; + _audioCapture.errorCallBack = ^(NSError* error) { + NSLog(@"KFAudioCapture error:%zi %@", error.code, error.localizedDescription); + }; + // 音频采集数æ®å›žè°ƒã€‚在这里采集的 PCM æ•°æ®é€ç»™ç¼–ç å™¨ã€‚ + _audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { + [weakSelf.audioEncoder encodeSampleBuffer:sampleBuffer]; + }; + } + + return _audioCapture; +} + +- (KFAudioEncoder *)audioEncoder { + if (!_audioEncoder) { + __weak typeof(self) weakSelf = self; + _audioEncoder = [[KFAudioEncoder alloc] initWithAudioBitrate:96000]; + _audioEncoder.errorCallBack = ^(NSError* error) { + NSLog(@"KFAudioEncoder error:%zi %@", error.code, error.localizedDescription); + }; + // 音频编ç æ•°æ®å›žè°ƒã€‚这里编ç çš„ AAC æ•°æ®é€ç»™å°è£…器。 + // 与之å‰å°†ç¼–ç åŽçš„ AAC æ•°æ®å­˜å‚¨ä¸º AAC 文件ä¸åŒçš„是,这里编ç åŽé€ç»™å°è£…器的 AAC æ•°æ®æ˜¯æ²¡æœ‰æ·»åŠ  ADTS 头的,因为我们这里å°è£…的是 M4A æ ¼å¼ï¼Œä¸éœ€è¦ ADTS 头。 + _audioEncoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) { + [weakSelf.muxer appendSampleBuffer:sampleBuffer]; + }; + } + + return _audioEncoder; +} + +- (KFMuxerConfig *)muxerConfig { + if (!_muxerConfig) { + _muxerConfig = [[KFMuxerConfig alloc] init]; + NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.m4a"]; + NSLog(@"M4A file path: %@", audioPath); + [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil]; + _muxerConfig.outputURL = [NSURL fileURLWithPath:audioPath]; + _muxerConfig.muxerType = KFMediaAudio; + } + + return _muxerConfig; +} + +- (KFMP4Muxer *)muxer { + if (!_muxer) { + _muxer = [[KFMP4Muxer alloc] initWithConfig:self.muxerConfig]; + _muxer.errorCallBack = ^(NSError* error) { + NSLog(@"KFMP4Muxer error:%zi %@", error.code, error.localizedDescription); + }; + } + + return _muxer; +} + +#pragma mark - Lifecycle +- (void)viewDidLoad { + [super viewDidLoad]; + + [self setupAudioSession]; + [self setupUI]; + + // 完æˆéŸ³é¢‘ç¼–ç åŽï¼Œå¯ä»¥å°† App Document 文件夹下é¢çš„ test.m4a 文件拷è´åˆ°ç”µè„‘上,使用 ffplay 播放: + // ffplay -i test.m4a +} + +#pragma mark - Setup +- (void)setupUI { + self.edgesForExtendedLayout = UIRectEdgeAll; + self.extendedLayoutIncludesOpaqueBars = YES; + self.title = @"Audio Muxer"; + self.view.backgroundColor = [UIColor whiteColor]; + + + // Navigation item. + UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)]; + UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)]; + self.navigationItem.rightBarButtonItems = @[startBarButton, stopBarButton]; + +} + +- (void)setupAudioSession { + NSError *error = nil; + + // 1ã€èŽ·å–音频会è¯å®žä¾‹ã€‚ + AVAudioSession *session = [AVAudioSession sharedInstance]; + + // 2ã€è®¾ç½®åˆ†ç±»å’Œé€‰é¡¹ã€‚ + [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker error:&error]; + if (error) { + NSLog(@"AVAudioSession setCategory error."); + error = nil; + return; + } + + // 3ã€è®¾ç½®æ¨¡å¼ã€‚ + [session setMode:AVAudioSessionModeVideoRecording error:&error]; + if (error) { + NSLog(@"AVAudioSession setMode error."); + error = nil; + return; + } + + // 4ã€æ¿€æ´»ä¼šè¯ã€‚ + [session setActive:YES error:&error]; + if (error) { + NSLog(@"AVAudioSession setActive error."); + error = nil; + return; + } +} + +#pragma mark - Action +- (void)start { + // å¯åŠ¨é‡‡é›†å™¨ã€‚ + [self.audioCapture startRunning]; + // å¯åŠ¨å°è£…器。 + [self.muxer startWriting]; +} + +- (void)stop { + // åœæ­¢é‡‡é›†å™¨ã€‚ + [self.audioCapture stopRunning]; + // åœæ­¢å°è£…器。 + [self.muxer stopWriting:^(BOOL success, NSError * _Nonnull error) { + NSLog(@"KFMP4Muxer %@", success ? @"success" : [NSString stringWithFormat:@"error %zi %@", error.code, error.localizedDescription]); + }]; +} + +@end +``` + +上é¢æ˜¯ `KFAudioMuxerViewController` 的实现,其中主è¦åŒ…å«è¿™å‡ ä¸ªéƒ¨åˆ†ï¼š + +- 1)在采集音频å‰éœ€è¦è®¾ç½® **AVAudioSession**[2] 为正确的采集模å¼ã€‚ + +- - 在 `-setupAudioSession` 中实现。 + +- 2)通过å¯åŠ¨å’Œå…³é—­éŸ³é¢‘采集和å°è£…æ¥é©±åŠ¨æ•´ä¸ªé‡‡é›†ã€ç¼–ç ã€å°è£…æµç¨‹ã€‚ + +- - 分别在 `-start` å’Œ `-stop` 中实现开始和åœæ­¢åŠ¨ä½œã€‚ + +- 3ï¼‰åœ¨é‡‡é›†æ¨¡å— `KFAudioCapture` çš„æ•°æ®å›žè°ƒä¸­å°†æ•°æ®äº¤ç»™ç¼–ç æ¨¡å— `KFAudioEncoder` 进行编ç ã€‚ + +- - 在 `KFAudioCapture` çš„ `sampleBufferOutputCallBack` 回调中实现。 + +- 4)在编ç æ¨¡å— `KFAudioEncoder` çš„æ•°æ®å›žè°ƒä¸­èŽ·å–ç¼–ç åŽçš„ AAC 裸æµæ•°æ®ï¼Œå¹¶å°†æ•°æ®äº¤ç»™å°è£…器 `KFMP4Muxer` 进行å°è£…。 + +- - 在 `KFAudioEncoder` çš„ `sampleBufferOutputCallBack` 回调中实现。 + +- 5)在调用 `-stop` åœæ­¢æ•´ä¸ªæµç¨‹åŽï¼Œå¦‚果没有出现错误,å°è£…çš„ M4A 文件会被存储到 `muxerConfig` 设置的路径。 + +## 5ã€ç”¨å·¥å…·æ’­æ”¾ M4A 文件 + +完æˆéŸ³é¢‘采集和编ç åŽï¼Œå¯ä»¥å°† App Document 文件夹下é¢çš„ `test.m4a` 文件拷è´åˆ°ç”µè„‘上,使用 `ffplay` 播放æ¥éªŒè¯ä¸€ä¸‹éŸ³é¢‘采集是效果是å¦ç¬¦åˆé¢„期: + +``` +$ ffplay -i test.m4a +``` + +关于播放 M4A 文件的工具,å¯ä»¥å‚考[《FFmpeg 工具》第 2 节 ffplay 命令行工具](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484831&idx=1&sn=6bab905a5040c46b971bab05f787788b&scene=21#wechat_redirect)å’Œ[《å¯è§†åŒ–音视频分æžå·¥å…·ã€‹ç¬¬ 1.1 节 Adobe Audition](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect)。 + +上é¢æˆ‘们讲过 M4A æ ¼å¼æ˜¯å±žäºŽ MPEG-4 标准,所以我们这里还å¯ä»¥ç”¨[《å¯è§†åŒ–音视频分æžå·¥å…·ã€‹ç¬¬ 3.1 节 MP4Box.js](https://mp.weixin.qq.com/s?__biz=MjM5MTkxOTQyMQ==&mid=2257484834&idx=1&sn=5dd9768bfc0d01ca1b036be8dd2f5fa1&scene=21#wechat_redirect) 等工具æ¥æŸ¥çœ‹å®ƒçš„æ ¼å¼ï¼š + +![图片](https://mmbiz.qpic.cn/mmbiz_png/gUnqKPeSuejDJHRicNdoGX06V5TeO2y8kKRhgQmZzal2dlyNdiaVRalLv4KHU1BlpTFPX4aS7oKqCM0jG7hVjW1w/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1)Demo 生æˆçš„ M4A 文件结构 + +## 6ã€å‚考资料 + +[1]CMSampleBufferRef: *https://developer.apple.com/documentation/coremedia/cmsamplebufferref/* + +[2]AVAudioSession: *https://developer.apple.com/documentation/avfaudio/avaudiosession/* + + + + + +原文链接:https://mp.weixin.qq.com/s/R86qnQAi2njr6k7tFvTF-w \ No newline at end of file