基于AudioQueue实现音频的录制和播放
@
目录- 基于AudioQueue实现音频的录制和播放
- 背景
- 总览
- Audio Queue 架构
- 音频录制
- 音频播放
- Audio Queue 的控制和状态
- Audio Queue 运行状态的监控
- demo地址
- 参考文献
背景
在iOS中常使用AVPlayer
、AVAudioPlayer
来播放在线音乐或者本地音乐,但是支持的格式都是封装好的,比如Mp3,Wav 格式的音频,但是如果需要播放流式的PCM音频数据该怎么办呢? 答案是使用Audio Queue,它也是苹果官方封装的音频处理框架,可以用来播放或录制音频,并且支持平台级音频格式的编码和解码。
AudioQueue 有以下作用
- 连接设备的音频硬件
- 管理音频播放的内存数据
- 协作codec 进行音频的的编解码
- 实现音频的录制和播放
本篇文章主要以PCM 数据为例子进行讲解,讲解音频的录制和实现,文末会附带基于AudioQueue的录音器和播放起的源代码文件;
总览
本篇主要介绍来音频的录制和播放过程,共包含三个部分,Audio Queue 架构、音频的录制、和音频的播放,其中Audio Queue 架构是实现录制和播放的核心,理解了AudioQueue的实现原理,再来看录制和播放将会更加高效率;
Audio Queue 架构
Audio Queue 架包含三个部分: audio queue buffers
, Buffer queue
和 audio queue callback
; audio queue buffers
在结构上是一个数组结构,存储的AudioBuffer数据,下面会专门分析AudioQueueBuffer
的数据结构; Buffer queue
可以理解为管理类,用来管理和组织这些audio queue buffers
按照一定的顺序进行排列和运行, 并且协调audio queue callback
的调用;
AudioQueueBuffer数据结构
下面重点来说明AudioQueueBuffer的数据结构的数据结构如下,主要包含四个部分,其中最核心的是aAduioData
部分。
typedef struct AudioQueueBuffer {
const UInt32 mAudioDataBytesCapacity;
void *const mAudioData;
UInt32 mAudioDataByteSize;
void *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;
mAudioData
, 它代表要录制和播放的音频数据;mAudioDataByteSize
: 用来表示audioData
的length
;mAudioDataBytesCapacity
: 表示一个mAudioData 需要分配的空间,单位是字节(Byte
),它的值必须大于mAudioDataByteSize,否则音频的数据放不下会出现丢失;mUserData
: 一个万能指针,用来传递调用者的值,一般结合bridge(void *)
传递self
对象,实现C函数和OC 语言的交互
创建AudioQueueBuffer
调用函数AudioQueueAllocateBuffer
来分配,以下为示例代码;
int result = AudioQueueAllocateBuffer(_audioQueue, kAudioBufferSize, &_audioQueueBuffers[i]);
NSLog(@"Mic AudioQueueAllocateBuffer i = %d,result = %d", i, result);
AudioQueueEnqueueBuffer(_audioQueue, _audioQueueBuffers[i], 0, NULL);
释放 AudioQueueBuffer
销毁则是通过AudioQueueDispose
来实现;
- (void)freeAudioBuffers {
for(int i=0; i<kAudioQueueBufferCount; i++) {
int result = AudioQueueFreeBuffer(_audioQueue, _audioQueueBuffers[i]);
NSLog(@"AudioQueueFreeBuffer i = %d,result = %d", i, result);
}
AudioQueueDispose(_audioQueue, YES);
}
Buffer Queue 和Enqueuing
Buffer queue 的数据结构是一个队列,队列里可以存放许多Auido Queue Buffer
, 存放的Buffer数量不受限制,推荐使用三个,例如录制的过程,一个buffer 负责收集麦克风的数据,一个buffer负责将数据传递给磁盘,还有一个Buffe用来做备用,防止磁盘I/O 时间过长出现卡顿。Enqueuing的含义是加入队列, 可以通过下图来了解Enqueue的过程。
音频录制Buffer Queuing的Enqueuing说明:
- 读取麦克风的数据到内存中,然后将数据写入到buffer中;
- 当第一个buffer数据写满后,会将数据存放到磁盘中,并且触发回调函数,这个时候回调函数需要处理数据,将新的音频数据覆写到该buffer中;
- 在回调函数中将采集的音频数据写入到磁盘中;
- 当三个buffer 都填满后之后会继续复用第一个buffer;
- 重复第2部进行填充数据和进行回调;
- 重复第3步将数据写入磁盘中
Audio Queue Callback
Audio Queue 的Callback 是开发的重要内容,它是通过AudioQueueEnqueueBuffer
函数来驱动数据,它是在音频录制或播放中进行重复调用的,调用的间隔取决于buffer的大小,设置的buffer数据容量越大,回调触发的间隔也越大,一般为0.5秒到几秒不等; AudioQueueCallback
分为两个部分,录制AuidoQueueInputCallback
和播放 AudioQueuOutputCallBack
音频录制
音频录制的本质是调用手机上的录音设置(如麦克风,耳机)来采集声音,采集声音的声音进行数字编码调制,形成音频数据,然后读取到内存中,最后写入到手机设备的硬盘中进行保存。iOS 的录音实现是通过Audio Queue 进行实现,下面主要分析AudioQueu的的结构和使用原理。
录制 AuidoQueueInputCallback 回调函数
AudioQueueInputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription *inPacketDescs
);
inUserData
用户数据指针,本身是一个无类型的指针,常被用来指向调用实例;inAQ
调用该 CallBack的AudioQueue;inBuffer
初始化AudioBuffer的时候我们封装好的音频数据;inStartTime
每个buffer 对应的时间,这里我们用不上;inNumberPacketDescriptions
结合inPacketDescs
的参数使用,一般涉及到编码的地方会用到;inPacketDescs
对应buffer的音频包描述,一般涉及到编码的地方会用到;
音频录制的调用实例
// 音频录制的回调函数
void AudioAQInputCallback(void * __nullable inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumberPacket,
const AudioStreamPacketDescription * __nullable inPacketDescs) {
DBAudioMicrophone * SELF = (__bridge DBAudioMicrophone *)inUserData;
NSLog(@"Mic Audio Callback");
if (inNumberPacket > 0)
{
[SELF processAudioBuffer:inBuffer withQueue:inAQ];
}
}
// 读取录制到的音频数据
- (void)processAudioBuffer:(AudioQueueBufferRef)inBuffer withQueue:(AudioQueueRef)inAudioQueue {
NSData *data = [NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];
[self.sendData appendData:data];
if (_isOn) {
AudioQueueEnqueueBuffer(inAudioQueue, inBuffer, 0, NULL);
}
}
创建一个录音 AudioQueue 的示例
- (void)audioNewInput {
/// 创建一个新的从audioqueue到硬件层的通道
AudioQueueNewInput(&_audioDescription, AudioAQInputCallback, (__bridge void * _Nullable)(self), NULL, kCFRunLoopCommonModes, 0, &_audioQueue);}
说明:
1._audioDescription 是 AudioStreamBasicDescription 类型的数据结构,主要用来描述数据的特点,包含采样率,位深,声道数量,音频格式,音频包,音频帧等数据;
2.AudioAQInputCallback 是音频的回调函数;
3.kCFRunLoopCommonModes 当前AudioQueue所运行的Runloop;
4._audioQueue 是AudioQueuueRef 类型的指针,此处用它的指针来进行实例化;
下面看一下AudioStreamBasicDescription的初始化赋值;
+ (AudioStreamBasicDescription)defaultAudioDescriptionWithSampleRate:(Float64)sampleRate numOfChannel:(NSInteger)numOfChannel {
AudioStreamBasicDescription asbd;
memset(&asbd, 0, sizeof(asbd));
asbd.mSampleRate = sampleRate;
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
asbd.mChannelsPerFrame = (UInt32)numOfChannel;
asbd.mFramesPerPacket = 1;//每一个packet一侦数据
asbd.mBitsPerChannel = 16;//每个采样点16bit量化
asbd.mBytesPerFrame = (asbd.mBitsPerChannel/8) * asbd.mChannelsPerFrame;
asbd.mBytesPerPacket = asbd.mBytesPerFrame * asbd.mFramesPerPacket;
return asbd;
}
说明:传入的参数是采样率和声道的数据,这里位深一般用的是16位;
音频播放
播放流程说明
音频的播放也包含三个部分,1.磁盘的的音频输入流;2.音频队列; 3. 扬声器;
说明:
首先读取磁盘中的音频数据,第二,填充音频数据到Audio Queue 中; 第三, 驱动数据,使用扬声器播放数据;
通过AudioQueue来控制音频的播放
1.声明AudioStreamBasicDescription 来描述音频特征,如采样率,位深,声道数量等;
audioDescription.mSampleRate =16000;//采样率
audioDescription.mFormatID =kAudioFormatLinearPCM;
audioDescription.mFormatFlags =kLinearPCMFormatFlagIsSignedInteger |kAudioFormatFlagIsPacked;
audioDescription.mChannelsPerFrame =1;///单声道
audioDescription.mFramesPerPacket =1;//每一个packet一侦数据
audioDescription.mBitsPerChannel =16;//每个采样点16bit量化
audioDescription.mBytesPerFrame = (audioDescription.mBitsPerChannel / 8) * audioDescription.mChannelsPerFrame;
2.AudioQueueOutputCallback 设置回调函数
AudioQueueOutputCallback (
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
);
说明:
inUserData: 用户指针,用来处理用户数据;
inAQ: auidoQueue的引用对象
inBuffer: AudioQueueBufferRef 对象,用来描述音频数据;
3.处理音频回调函数收到的数据
static void AudioPlayerAQInputCallbackV2(void* inUserData,AudioQueueRef outQ, AudioQueueBufferRef outQB){
DSAQPool* pool = (__bridge DSAQPool*)inUserData;
[pool playCallBack:outQB];
}
-(BOOL)enqueueBuffer:(AudioQueueBufferRefWrapper*)buf{
if(AudioQueueEnqueueBuffer(audioQueue, buf.ref,0,NULL) == noErr){
buf.inUse = YES;
@synchronized (_buffers) {
[_buffers addObject:buf];
}
return YES;
}else{
//DDLogError(@"AudioQueueEnqueueBuffer error.");
return NO;
}
}
说明:
1.AudioPlayerAQInputCallbackV2是回调函数,数据在播放的过程中会不停的触发;
2.-(BOOL)enqueueBuffer:(AudioQueueBufferRefWrapper*)buf;
在回调的过程中需要不停的对buffer进行赋值,驱动扬声器进行播放;
Audio Queue 的控制和状态
常用的控制有开始,暂停和停止;
开始-AudioQueueStart
控制开始录制或者开始播放;
暂停AudioQueuePause
控制暂停,可以调用开始的方法继续播放;
停止 AudioQueueStop
结束,调用这个方法后不能再调用start方法进行播放了,表示音频已经播放完成了。
Audio Queue 运行状态的监控
1.检测声音的分贝值
-(float)getCurrentPower {
UInt32 dataSize = sizeof(AudioQueueLevelMeterState) * _audioDescription.mChannelsPerFrame;
AudioQueueLevelMeterState *levels = (AudioQueueLevelMeterState*)malloc(dataSize);
OSStatus rc = AudioQueueGetProperty(_audioQueue, kAudioQueueProperty_CurrentLevelMeterDB, levels, &dataSize);
if (rc) {
NSLog(@"NoiseLeveMeter>>takeSample - AudioQueueGetProperty(CurrentLevelMeter) returned %d", rc);
}
float channelAvg = 0;
for (int i = 0; i < _audioDescription.mChannelsPerFrame; i++) {
channelAvg += levels[i].mAveragePower;
}
free(levels);
return channelAvg ;
}
说明: 这个方法主要用来监控声音的分贝数,一般在录音的过程中需要对音量的大小进行反馈会用到;也可以利用AudioQueueGetProperty
来监控音频的运行状态,具体可以参照: AudioQueuePropertyID 中的说明;
demo地址
音频录制: https://github.com/data-baker/BakerIosSdks/tree/main/DBAudioSDK/Classes/DBToolKit/DBMicrophone
音频播放:https://github.com/data-baker/BakerIosSdks/tree/main/DBAudioSDK/Classes/DBToolKit/DBPlayer