写点什么

IOS 端音频的采集与播放

用户头像
floer rivor
关注
发布于: 2021 年 05 月 16 日

概述

音频在网络传输中大致会经过以下的步骤,采集->前处理->编码->网络->解码->后处理->播放。在移动端,主要任务是音频的采集播放,编解码,以及音频的处理。苹果公司设计了 Core Audio 解决方案来完成音频在 ios 端的任务。CoreAudio 设计了 3 个不同层次的 API,如下:

上图中 Low-Level Services 主要是关于音频的驱动和硬件,本文主要讲解 3 个常用的 API:

Audio Unit Services: ios 中音频底层 API,主要用来实现音频 pcm 数据的采集播放

Audio Converter Services:用于音频格式的转换,包含音频的编解码,pcm 文件格式的转换

Audio File Stream Services:用在流播放中,用于读取音频信息,分析音频帧。

通过这 3 个 api 我们就能在 ios 上打通音频链路。

在 ios 上,有着一个管理着 app 如何使用音频的单例,那就是 AVAudioSession,了解如何在 ios 端进行采集播放,首先要了解这个单例如何使用。

AVAudioSession

AVAudioSession 是一个只在 iOS 上,Mac OS X 没有的 API,用途是用来描述目前的 App 打算如何使用 audio,以及我们的 App 与其他 App 之间在 audio 这部分应该是怎样的关系。

如何使用 AVAudioSession 来管理我们 app 的 audio 呢?大致分为以下几个步骤:

1)ios 中的管理 APP 是通过一个全局单例得到的,通过sharedInstance获取系统中的 AVAudioSession

AVAudioSession* session= [AVAudioSession sharedInstance] ;

2) 设置AVAudioSession的 category、option、mode,确定当前 app 如何使用 audio,以及当前 app 使用 audio 时和其他 app 使用 Audio 的关联。

3)配置音频采样率,音频 buffer 大小等。这一步不是必须的。

4)添加 AVAudioSession 通知,例如音频中断以及音频路由改变(插拔耳机等事件)。

5)最后一步便是激活 AVAudioSession,在当前程序使用了

[session setActive:YES error:nil];

上述的第二步决定了当前 app 如何使用 audio 的核心,对应的 API 为:

/// Set session category and mode with options.- (BOOL)setCategory:(AVAudioSessionCategory)category			   mode:(AVAudioSessionMode)mode			options:(AVAudioSessionCategoryOptions)options			  error:(NSError **)outError;
复制代码

AVAudioSession 中 Category

AVAudioSession 中 Category 定义了音频使用的主场景,IOS 中现在定义了七种,如下:

/// A category defines a broad set of behaviors for a session.typedef NSString *AVAudioSessionCategory NS_STRING_ENUM;
/*! Use this category for background sounds such as rain, car engine noise, etc. Mixes with other music. */OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategoryAmbient API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Use this category for background sounds. Other music will stop playing. */OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategorySoloAmbient API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Use this category for music tracks.*///多运用于网易云等音乐app中OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategoryPlayback API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Use this category when recording audio. */OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategoryRecord API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Use this category when recording and playing back audio. *///多应用于音频通话类的app中的。OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategoryPlayAndRecord API_AVAILABLE(ios(3.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Use this category when using a hardware codec or signal processor while not playing or recording audio. */OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategoryAudioProcessing API_DEPRECATED("No longer supported", ios(3.0, 10.0)) API_UNAVAILABLE(watchos, tvos) API_UNAVAILABLE(macos);
/*! Use this category to customize the usage of available audio accessories and built-in audio hardware.*///支持音频播放和录制。允许多条音频流的同步输入和输出。(比如USB连接外部扬声器输出音频,蓝牙耳机同时播放另一路音频这种特殊需求)OS_EXPORT AVAudioSessionCategory const AVAudioSessionCategoryMultiRoute API_AVAILABLE(ios(6.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
复制代码


下面通过一些 app 常用场景来举例说明这些 category 的使用:

AVAudioSessionCategoryAmbient 和 AVAudioSessionCategorySoloAmbient 可应用于手机游戏等,缺失语音并不会影响这个 app 的核心功能,他们两者的区别是 AVAudioSessionCategoryAmbient 可以与其他 app 进行混音播放,可以边玩游戏边听音乐;AVAudioSessionCategoryPlayback 常应用于音乐播放器如网易云音乐中;AVAudioSessionCategoryRecord 常应用于各种录音软件中;AVAudioSessionCategoryPlayAndRecord 则运用于 voip 电话中;最后两种实在不常见,我也没有使用过。

AVAudioSession 中 Mode

Mode 可以对 Category 进行再设置,同样有七种 mode 来定制 Category,不同的 mode 兼容不同的 Category,兼容方式如下:

typedef NSString *AVAudioSessionMode NS_STRING_ENUM;
//默认模式 适用于所有CategoryOS_EXPORT AVAudioSessionMode const AVAudioSessionModeDefault API_AVAILABLE(ios(5.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
// 适用于VoIP 类型的应用。只能是 AVAudioSessionCategoryPlayAndRecord Category下// 在这个模式系统会自动配置AVAudioSessionCategoryOptionAllowBluetooth 这个选项。系统会自动选择最佳的内置麦克风组合支持语音聊天。OS_EXPORT AVAudioSessionMode const AVAudioSessionModeVoiceChat API_AVAILABLE(ios(5.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*适用于游戏类应用。使用 GKVoiceChat 对象的应用会自动设置这个模式和 AVAudioSessionCategoryPlayAndRecord Category。实际参数和AVAudioSessionModeVideoChat一致*/OS_EXPORT AVAudioSessionMode const AVAudioSessionModeGameChat API_AVAILABLE(ios(5.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*适用于使用摄像头采集视频的应用。只能是 AVAudioSessionCategoryPlayAndRecord 和 AVAudioSessionCategoryRecord 这两个Category下。这个模式搭配 AVCaptureSession API 结合来用可以更好地控制音视频的输入输出路径。(例如,设置 automaticallyConfiguresApplicationAudioSession 属性,系统会自动选择最佳输出路径。*/OS_EXPORT AVAudioSessionMode const AVAudioSessionModeVideoRecording API_AVAILABLE(ios(5.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
//最小化系统/*! Appropriate for applications that wish to minimize the effect of system-supplied signal processing for input and/or output audio signals. */OS_EXPORT AVAudioSessionMode const AVAudioSessionModeMeasurement API_AVAILABLE(ios(5.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! 适用于播放视频的应用。只用于 AVAudioSessionCategoryPlayback 这个Category。*/OS_EXPORT AVAudioSessionMode const AVAudioSessionModeMoviePlayback API_AVAILABLE(ios(6.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*!用于视频聊天类型应用,只能是 AVAudioSessionCategoryPlayAndRecord Category下。在这个模式系统会自动配置 AVAudioSessionCategoryOptionAllowBluetooth 和 AVAudioSessionCategoryOptionDefaultToSpeaker 选项。系统会自动选择最佳的内置麦克风组合支持视频聊天。*/OS_EXPORT AVAudioSessionMode const AVAudioSessionModeVideoChat API_AVAILABLE(ios(7.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Appropriate for applications which play spoken audio and wish to be paused (via audio session interruption) rather than ducked if another app (such as a navigation app) plays a spoken audio prompt. Examples of apps that would use this are podcast players and audio books. For more information, see the related category option AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers. */OS_EXPORT AVAudioSessionMode const AVAudioSessionModeSpokenAudio API_AVAILABLE(ios(9.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
/*! Appropriate for applications which play audio using text to speech. Setting this mode allows for different routing behaviors when connected to certain audio devices such as CarPlay. An example of an app that would use this mode is a turn by turn navigation app that plays short prompts to the user. Typically, these same types of applications would also configure their session to use AVAudioSessionCategoryOptionDuckOthers and AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers */OS_EXPORT AVAudioSessionMode const AVAudioSessionModeVoicePrompt API_AVAILABLE(ios(12.0), watchos(5.0), tvos(12.0)) API_UNAVAILABLE(macos);
复制代码

AudioSessionMode 大多都与录音相关,不同的 mode 代表了语音信号从硬件录制后会经过不同处理回调到录制 API。常用的录音场景有 voiceChat,gameChat,videoChat 等。

AVAudioSession 中 Options

IOS 设置了 4 个 Option 对音频选项进行更细化的管理。分别为:

typedef NS_OPTIONS(NSUInteger, AVAudioSessionCategoryOptions) {    AVAudioSessionCategoryOptionMixWithOthers            = 0x1,    AVAudioSessionCategoryOptionDuckOthers               = 0x2,    AVAudioSessionCategoryOptionAllowBluetooth API_UNAVAILABLE(tvos, watchos, macos) = 0x4,    AVAudioSessionCategoryOptionDefaultToSpeaker API_UNAVAILABLE(tvos, watchos, macos) = 0x8,    AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers API_AVAILABLE(ios(9.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos) = 0x11,    AVAudioSessionCategoryOptionAllowBluetoothA2DP API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0)) API_UNAVAILABLE(macos) = 0x20,    AVAudioSessionCategoryOptionAllowAirPlay API_AVAILABLE(ios(10.0), tvos(10.0)) API_UNAVAILABLE(watchos, macos) = 0x40,};
复制代码

最常用的是以下 3 个:

1、AVAudioSessionCategoryOptionMixWithOthers:支持是否和其他音频 APP 进行混音,默认为 false。在 AVAudioSessionCategoryPlayAndRecord 和 AVAudioSessionCategoryMultiRoute 下,即使当前的 APP 在录音或播放时,能够允许其他 app 在后台播放。在 AVAudioSessionCategoryPlayback 下,即使关闭了音量键,也允许当前 app 进行播放。

2、AVAudioSessionCategoryOptionDuckOthers:系统智能降低其他 APP 声音,如高德地图播放导航时会降低音乐播放的声音。其他的 APP 音量会一直降低直到当前的 app 的 audiosessiong 进行 deactive。打开这个 option 会设置 AVAudioSessionCategoryOptionMixWithOthers。

3、AVAudioSessionCategoryOptionDefaultToSpeaker:设置默认输出到扬声器,兼容 AVAudioSessionCategoryPlayAndRecord。

设置好 AVAudioSession 的 category,mode,options。在 app 的运行期间,如果 audio 状态出现了变化,我们要能够实时监听,这个时候就要使用添加 AVAudioSession 通知。

AVAudioSession 的 Notification

常用的监听为音频中断和音频路由改变。音频中断和音频路由改变都会发出系统通知,通过监听系统通知则可以完成音频状态改变的处理。

当音频中断发生时:当前 app 在播放音频,此时打开另外一个 app,或者系统铃声响起,会我们的 app 被打断的现象,此时我们需要暂停我们的播放界面,以及其它一系列的动作,那我们需要获取到当前打断的这个事件系统提供了一个打断通知 供我们进行打断处理 AVAudioSessionInterruptionNotification。事件 AVAudioSessionInterruptionNotification 中包含了 3 个 key,分别为:

1 )AVAudioSessionInterruptionTypeKey

可以取值为:AVAudioSessionInterruptionTypeBegan 和 AVAudioSessionInterruptionTypeEnded

2 )AVAudioSessionInterruptionOptionKey

可以取值为:AVAudioSessionInterruptionOptionShouldResume = 1

3 )AVAudioSessionInterruptionWasSuspendedKey

可以取值为:AVAudioSessionInterruptionWasSuspendedKey 为 true 表示当前 app 暂停,false 表示被其他 app 中断

当音频路由改变时:当用户插拔耳机或链接蓝牙耳机,则 IOS 的音频线路便发生了切换,此时 IOS 系统会发生一个 AVAudioSessionRouteChangeNotification,通过监听该通知实现音频线路切换的回调。IOS 定义了八种路由改变的原因 如下:

typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason) {    /// The reason is unknown.    AVAudioSessionRouteChangeReasonUnknown = 0,
//发现有新的设备可用 比如蓝牙耳机或有限耳机的插入 AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
//旧设备不可用 比如耳机的拔出 AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
// AVAudioSession的category改变 AVAudioSessionRouteChangeReasonCategoryChange = 3,
// APP修改输出设备 AVAudioSessionRouteChangeReasonOverride = 4,
// 设备唤醒 AVAudioSessionRouteChangeReasonWakeFromSleep = 6, // 当前的路由不适配AVAudioSession的category AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
// 路由的设置改变了 AVAudioSessionRouteChangeReasonRouteConfigurationChange = 8};
复制代码


在监听事件的回调中处理对于各种路由改变的原因进行处理:常见的处理逻辑为当 AVAudioSessionRouteChangeReasonOldDeviceUnavailable 发生时,检测是否耳机被拔出,若耳机被拔出,则停止播放,再次使用外放播放时,系统的音量不会出现巨大的改变。耳机被拔出时,正在使用耳机的麦克风录音的情况下应该停止录音。

当 AVAudioSessionRouteChangeReasonNewDeviceAvailable 发生时,检测是否耳机被插入,若耳机被插入,则配置 sdk 是否进行回声消除等模块。

配置好 AVAudioSession 后,我们就该使用 AudioUnit 进行音频的采集播放


AudioUnit

IOS 中提供了七种 AuidoUnit 来满足四种不同场景的需求。


本文中使用 audiounit 来进行采集播放,只使用到了其中的 I/O 功能。

Audio Units 由 Scopes,Elements 组成。在 I/O 功能下,Scope 和 Elements 的使用如下:


  当使用 I/O Unit 情况下时,element 是固定的。element 直接与硬件挂钩,element1 的 inputscope 为 mic,element0 的 outputscope 为 speaker,我们需要处理的便是上图中黄色的部分。上图的理解为:系统通过 mic 采集音频到 element1 的 inputscope,我们的 APP 通过 element1 的 outputscope 拿到音频输入数据。经过处理后,我们的 APP 输出到 element0 的 inputscope,element0 的 outputscope 从 inputscope 拿到数据后从扬声器完成音频的播放。

使用 AudioUnit 经过以下步骤

1)描述要使用的 audiounit。描述格式为 AudioComponentDescription

AudioComponentDescription ioUnitDescription;ioUnitDescription.componentType          = kAudioUnitType_Output; //设置ioUnitDescription.componentSubType       = kAudioUnitSubType_RemoteIO;ioUnitDescription.componentManufacturer  = kAudioUnitManufacturer_Apple;ioUnitDescription.componentFlags         = 0;ioUnitDescription.componentFlagsMask     = 0;
复制代码

2)通过 api 得到 audiounit 实例。一般是通过 AudioComponentFindNext 和 AudioComponentInstanceNew 两个 API 来完成。

//根据音频属性查找音频单元AudioComponent foundIoUnitReference = AudioComponentFindNext (NULL,&ioUnitDescription);
AudioUnit audioUnit;//得到实例AudioComponentInstanceNew (foundIoUnitReference, &audioUnit);
复制代码

3)通过 AudioUnitSetProperty 设置 audiounit 属性

//设置mic enable // 在io模式下:便要设置element1的input scope为 1int inputEnable = 1;//kAudioOutputUnitProperty_EnableIO 启用或禁止 I/O,默认输出开启,输入禁止。status = AudioUnitSetProperty(audioUnit,                              kAudioOutputUnitProperty_EnableIO,                              kAudioUnitScope_Input, // element 1                              kInputBus,   //input scope                              &inputEnable,                              sizeof(inputEnable));CheckError(status, "setProperty EnableIO error");
//设置采集数据的格式// 设置element1的output scope为 要采集的格式AudioStreamBasicDescription inputStreamDesc OSStatus status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &inputStreamDesc, sizeof(inputStreamDesc));CheckError(status, "setProperty StreamFormat error");

//设置speaker enbaleint outputEnable = 1;result = AudioUnitSetProperty(_auVoiceProcessing, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, // output bus &outputEnable, sizeof(outputEnable));//设置播放的格式AudioStreamBasicDescription streamDesc;OSStatus status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &streamDesc, sizeof(streamDesc));CheckError(status, "SetProperty StreamFormat failure");
复制代码

4)注册采集播放回调

/设置采集回调 input bus下的 output scope    AURenderCallbackStruct inputCallBackStruce;    inputCallBackStruce.inputProc = inputCallBackFun;    inputCallBackStruce.inputProcRefCon = (__bridge void * _Nullable)(self);        status = AudioUnitSetProperty(audioUnit,                                  kAudioOutputUnitProperty_SetInputCallback,                                  kAudioUnitScope_Output,                                  kInputBus,                                  &inputCallBackStruce,                                  sizeof(inputCallBackStruce));    CheckError(status, "setProperty InputCallback error");
// 回调的静态函数 static OSStatus inputCallBackFun( void * inRefCon, AudioUnitRenderActionFlags * ioActionFlags, //描述上下文信息 const AudioTimeStamp * inTimeStamp, //采样时间戳 UInt32 inBusNumber, //采样的总线数量 UInt32 inNumberFrames, //多少帧的数据 AudioBufferList * __nullable ioData) { AudioRecord *recorder = (__bridge AudioRecord *)(inRefCon); //获取上下文 return [recorder RecordProcessImpl:ioActionFlags stamp:inTimeStamp bus:inBusNumber numFrames:inNumberFrames]; //处理得到的数据 }
(OSStatus)RecordProcessImpl: (AudioUnitRenderActionFlags *)ioActionFlags stamp: (const AudioTimeStamp *)inTimeStamp bus: (uint32_t) inBusNumber numFrames: (uint32_t) inNumberFrames { uint32_t recordSamples = inNumberFrames *m_channels; // 采集了多少数据 int16 if (m_recordTmpData != NULL) { delete [] m_recordTmpData; m_recordTmpData = NULL; } m_recordTmpData = new int8_t[2 * recordSamples]; memset(m_recordTmpData, 0, 2 * recordSamples);
AudioBufferList bufferList; bufferList.mNumberBuffers = 1; bufferList.mBuffers[0].mData = m_recordTmpData; bufferList.mBuffers[0].mDataByteSize = 2*recordSamples; AudioUnitRender(audioUnit, ioActionFlags, inTimeStamp, kInputBus, inNumberFrames, &bufferList); AudioBuffer buffer = bufferList.mBuffers[0]; // 回调得到的数据 int recordBytes = buffer.mDataByteSize;
[dataWriter writeBytes:(Byte *)buffer.mData len:recordBytes]; //数据处理 return noErr; }

//设置播放回调 outputbus下的input scope AURenderCallbackStruct outputCallBackStruct; outputCallBackStruct.inputProc = outputCallBackFun; outputCallBackStruct.inputProcRefCon = (__bridge void * _Nullable)(self); status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, kOutputBus, &outputCallBackStruct, sizeof(outputCallBackStruct)); CheckError(status, "SetProperty EnableIO failure");
//回调函数 static OSStatus outputCallBackFun( void * inRefCon, AudioUnitRenderActionFlags * ioActionFlags, const AudioTimeStamp * inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList * __nullable ioData) { memset(ioData->mBuffers[0].mData, 0, ioData->mBuffers[0].mDataByteSize); // memset(ioData->mBuffers[1].mData, 0, ioData->mBuffers[1].mDataByteSize); AudioPlay *player = (__bridge AudioPlay *)(inRefCon); return [player PlayProcessImpl:ioActionFlags stamp:inTimeStamp bus:inBusNumber numFrames:inNumberFrames ioData:ioData]; }
//获取得到的数据 - (OSStatus)PlayProcessImpl: (AudioUnitRenderActionFlags *)ioActionFlags stamp: (const AudioTimeStamp *)inTimeStamp bus: (uint32_t) inBusNumber numFrames: (uint32_t) inNumberFrames ioData:(AudioBufferList *)ioData { AudioBuffer buffer = ioData->mBuffers[0]; int len = buffer.mDataByteSize; //需要的数据长度 int readLen = [dataReader readData:len forData:(Byte*)buffer.mData];//读取录音文件 buffer.mDataByteSize = readLen; if (readLen == 0){ [_delegate playToEnd]; [self stop]; } return noErr; }
复制代码

5)启动或停止 audiounit

//首先得启动AVAduiosessionNSError *error = nil;[AVAudioSession sharedInstance]     	setCategory:AVAudioSessionCategoryPlayAndRecord         withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker         error:&error];[[AVAudioSession sharedInstance] setActive:YES error:&error];
AudioOutputUnitStart(audioUnit); //开始AudioOutputUnitStop(audioUnit); //停止
//释放audiounit步骤OSStatus result = AudioOutputUnitStop(audioUnit);result = AudioUnitUninitialize(audioUnit); //最好将初始状态还原 AudioComponentInstanceDispose(audioUnit); // 卸载当前的audiounit
复制代码


使用 AudioUnit 的需注意的一些问题:

1)ios 每次回调的音频数据是不能确定的,因此需要设置音频采样时间间隔来达到每次最大可读取数据,可通过 setPreferredIOBufferDuration 设置缓存 BUFFER 的大小,比如: 采样率是 44.1kHz, 采样位数是 16, 声道数是 1, 采样时间为 0.01 秒,则最大的采样数据为 882. 所以即使我们设置超过此数值,系统最大也只能采集 882 个字节的音频数据.

2)IOS 中每次采集播放回调的时间大约在 10ms 左右,但不精确保证为 10ms。、

3)在播放双声道语音时:注意 mFormatFlags 的 kAudioFormatFlagIsNonInterleaved 的使用

4)由于 IOS 每次回调的数据不是精确值,但我们 app 和 audiounit 交互都是 10ms 数据,因此,对 audiounit 的音频播放采集都需要设置音频缓存。


AudioConvert

  AudioConverter 用于 IOS 中音频格式的转换。包含 pcm 文件不同采样深度、采样率、采样精度、声道数之间的转换,以及 pcm 与各种压缩格式之间的相互转换。

关于 AudioConverter 的函数和回调有:


AudioConcert 的使用步骤如下:

1)确定输入输出格式,创建 audiconverter 对象

OSStatus AudioConverterNew(const AudioStreamBasicDescription *inSourceFormat,                            const AudioStreamBasicDescription *inDestinationFormat,                           AudioConverterRef  _Nullable *outAudioConverter);
OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription *inSourceFormat, const AudioStreamBasicDescription *inDestinationFormat, UInt32 inNumberClassDescriptions, const AudioClassDescription *inClassDescriptions, AudioConverterRef _Nullable *outAudioConverter);
复制代码


2)以回调的形式解码数据,支持 packet 和 non-interleaved。在回调 AudioConverterComplexInputDataProc 在其中输入数据

OSStatus AudioConverterFillComplexBuffer(AudioConverterRef inAudioConverter,
AudioConverterComplexInputDataProc inInputDataProc,
void *inInputDataProcUserData,
UInt32 *ioOutputDataPacketSize,
AudioBufferList *outOutputData,
AudioStreamPacketDescription *outPacketDescription);
typedef OSStatus (*AudioConverterComplexInputDataProc)(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, //输入数据的包个数 AudioBufferList *ioData, //输入数据 AudioStreamPacketDescription * _Nullable *outDataPacketDescription, //输出数据的格式 void *inUserData);
复制代码

3) 释放 audioconverter

OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter);

AudioFileStream

AudioFileStream 用在流播放中,用来读取音频信息,如采样率、声道数、码率、时长、分析音频帧,AudioFileStream 不仅用于文件播放,同样可用于网络在线音频流播放。AudioFileStream 只处理数据,不对文件源进行处理,因此数据的读取需要自己实现。因此,进行在线播放时,需要从网络流获取数据,在本地播放时,从文件流中获取数据。  

    关于 AudioFileStream 的函数和回调有:


AudioFileStream 支持的格式有:

AIFF

AIFC

WAVE

CAF

NeXT

ADTS

MPEG Audio Layer 3

AAC

使用 AudioFileStream 的步骤如下

1)AudioFileStreamOpen 函数注册属性监听回调和解析帧回调

/*1 上下文对象2 属性监听回调。每当解析器在数据流中找到属性的值时触发回调。3 解析帧回调。每当解析器解析出一帧后触发回调。4 文件类型的提示。当文件信息不完整的时候可以给AudioFileStream一定的提示。帮助其绕过文件中的错误或者缺失从而成功解析文件。所以在确定文件类型的情况下建议各位还是填上这个参数,如果无法确定可以传入0。AudioFileTypeID的类型为:5 返回的AudioFileStream实例对应的AudioFileStreamID,这个ID需要保存起来作为后续一些方法的参数使用;*/OSStatus AudioFileStreamOpen(void *inClientData,                              AudioFileStream_PropertyListenerProc inPropertyListenerProc,                              AudioFileStream_PacketsProc inPacketsProc,                              AudioFileTypeID inFileTypeHint,                              AudioFileStreamID  _Nullable *outAudioFileStream);
复制代码

2)AudioFileStreamParseBytes 对传入的数据进行解析。并触发属性和解析帧这两个回调。

`

/*1.AudioFileStreamID2.解析数据的长度3.数据4.本次的解析和上一次解析是否是连续的关系,如果是连续的传入0,否则传入kAudioFileStreamParseFlag_Discontinuity。使用kAudioFileStreamParseFlag_Discontinuity的典型场景为:    1)seek完毕后,数据不连续    2)正常解析第一帧前都建议传入kAudioFileStreamParseFlag_Discontinuity5 返回值的错误类型*/extern OSStatus AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream,                                          UInt32 inDataByteSize,                                          const void* inData,                                          UInt32 inFlags);
复制代码

3)解析音频属性

这个回调会回调很多次,但是我们根据需要的音频格式信息进行处理。

/*1、回调的第一个参数是Open方法中的上下文对象;
2、第二个参数inAudioFileStream是和Open方法中第四个返回参数AudioFileStreamID一样,表示当前FileStream的ID;
3、第三个参数是此次回调解析的信息ID。表示当前PropertyID对应的信息已经解析完成信息(例如数据格式、音频数据的偏移量等等),使用者可以通过AudioFileStreamGetProperty接口获取PropertyID对应的值或者数据结构;
4、第四个参数ioFlags是一个返回参数,表示这个property是否需要被缓存,如果需要赋值kAudioFileStreamPropertyFlag_PropertyIsCached否则不赋值*/typedef void (*AudioFileStream_PropertyListenerProc)(void * inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 * ioFlags);
//示例程序如下:static void ASPropertyListenerProc(void * inClientData, AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 * ioFlags){ // this is called by audio file stream when it finds property values AudioStreamer* streamer = (AudioStreamer *)inClientData; [streamer handlePropertyChangeForFileStream:inAudioFileStream fileStreamPropertyID:inPropertyID ioFlags:ioFlags];}
(void)handlePropertyChangeForFileStream:(AudioFileStreamID)inAudioFileStream fileStreamPropertyID:(AudioFileStreamPropertyID)inPropertyID ioFlags:(UInt32 *)ioFlags{ @synchronized(self) { if (inPropertyID == kAudioFileStreamProperty_ReadyToProducePackets) { discontinuous = true; //准备好处理数据 } else if (inPropertyID == kAudioFileStreamProperty_DataOffset) { SInt64 offset; UInt32 offsetSize = sizeof(offset); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &offset); //得到偏移 } else if (inPropertyID == kAudioFileStreamProperty_AudioDataByteCount) { UInt32 byteCountSize = sizeof(UInt64); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount); fileLength = dataOffset + audioDataByteCount; //获取文件长度 } else if (inPropertyID == kAudioFileStreamProperty_DataFormat) { if (asbd.mSampleRate == 0){ UInt32 asbdSize = sizeof(asbd); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &asbdSize, &asbd);//获取流的格式 } } else if (inPropertyID == kAudioFileStreamProperty_FormatList) { Boolean outWriteable; UInt32 formatListSize; err = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable);//得到属性信息 AudioFormatListItem *formatList = malloc(formatListSize); err = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);//获取formatList
for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem)) { AudioStreamBasicDescription pasbd = formatList[i].mASBD; if (pasbd.mFormatID == kAudioFormatMPEG4AAC_HE || pasbd.mFormatID == kAudioFormatMPEG4AAC_HE_V2){ asbd = pasbd; break; } } free(formatList); } }}
复制代码

4)解析音频帧

/*1、上下文对象;
2、本次处理的数据大小;
3、本次总共处理了多少帧(即代码里的Packet);
4、本次处理的所有数据;
5、AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节。*/typedef void (*AudioFileStream_PacketsProc)(void * inClientData, UInt32 numberOfBytes, UInt32 numberOfPackets, const void * inInputData, AudioStreamPacketDescription * inPacketDescriptions);
//示例程序如下:static void ASPacketsProc( void * inClientData, UInt32 inNumberBytes, UInt32 inNumberPackets, const void * inInputData, AudioStreamPacketDescription *inPacketDescriptions){ // this is called by audio file stream when it finds packets of audio AudioStreamer* streamer = (AudioStreamer *)inClientData; [streamer handleAudioPackets:inInputData numberBytes:inNumberBytes numberPackets:inNumberPackets packetDescriptions:inPacketDescriptions];}
(void)handleAudioPackets:(const void *)inInputData numberBytes:(UInt32)inNumberBytes numberPackets:(UInt32)inNumberPackets packetDescriptions:(AudioStreamPacketDescription *)inPacketDescriptions;{ @synchronized(self) { // we have successfully read the first packests from the audio stream, so clear the "discontinuous" flag if (discontinuous){ //解析音频格式完成 discontinuous = false; } if (!audioQueue){ [self createQueue]; //创建AudioQueue 用于播放 } }
// inPacketDescriptions便是处理VBR数据 the following code assumes we're streaming VBR data. for CBR data, the second branch is used. if (inPacketDescriptions) { for (int i = 0; i < inNumberPackets; ++i) { SInt64 packetOffset = inPacketDescriptions[i].mStartOffset; SInt64 packetSize = inPacketDescriptions[i].mDataByteSize; // AudioQueue缓冲区分析 // ... AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)inInputData + packetOffset, packetSize); } } else { size_t offset = 0; while (inNumberBytes) { // AudioQueue缓冲区分析 // ... AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex]; memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)(inInputData + offset), copySize); inNumberBytes -= copySize; } }}
复制代码

5)若有必要,进行 AudioFileStreamSeek,AudioFileStreamSeek 是用来寻找精确的某一个帧的字节偏移。

6)AudioFileStreamClose 关闭 AudioFileStream


小结

本文介绍了 IOS 中的音频采集播放 API,其中 AVAudioSession 负责管理 APP 如何操作音频,AudioUnit 负责采集播放 PCM 形式的音频数据,AudioConvert 负责音频的编解码,AudioFileStream 负责流播放,解析音频帧,播放网络流中的音频有很大的用处。以播放本地 mp3 文件为例,首先 AudioFileStream 解析 mp3 音频帧,解析出帧后送到 AudioConvert 解码成 PCM 数据,再送到 AudioUnit 进行音频播放。采集则是完全相反的步骤。

IOS 的音频播放其实最好的是参照一些开源的 IOS 音频播放器,这些都可以在 github 上找到,比如豆瓣开源的播放器https://github.com/douban/DOUAudioStreamer。最后,文章中有什么错误的地方,希望和大家一起讨论。


参考

IOS音频播放

https://github.com/mattgallagher/AudioStreamer

https://www.jianshu.com/p/25188072a11a

webrtc系列之音频会话管理https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html

https://www.jianshu.com/p/fb0e5fb71b3c

在线教室 iOS 端声音问题综合解决方案

https://stackoverflow.com/questions/16841831/specifying-number-of-frames-to-process-in-audiounit-render-callback-on-ios


发布于: 2021 年 05 月 16 日阅读数: 545
用户头像

floer rivor

关注

还未添加个人签名 2021.04.24 加入

还未添加个人简介

评论

发布
暂无评论
IOS端音频的采集与播放