基于AudioQueue实现音频的录制和播放

基于AudioQueue实现音频的录制和播放

@

背景

在iOS中常使用 AVPlayerAVAudioPlayer来播放在线音乐或者本地音乐,但是支持的格式都是封装好的,比如Mp3,Wav 格式的音频,但是如果需要播放流式的PCM音频数据该怎么办呢? 答案是使用Audio Queue,它也是苹果官方封装的音频处理框架,可以用来播放或录制音频,并且支持平台级音频格式的编码和解码。

AudioQueue 有以下作用

  1. 连接设备的音频硬件
  2. 管理音频播放的内存数据
  3. 协作codec 进行音频的的编解码
  4. 实现音频的录制和播放

本篇文章主要以PCM 数据为例子进行讲解,讲解音频的录制和实现,文末会附带基于AudioQueue的录音器和播放起的源代码文件;

总览

本篇主要介绍来音频的录制和播放过程,共包含三个部分,Audio Queue 架构、音频的录制、和音频的播放,其中Audio Queue 架构是实现录制和播放的核心,理解了AudioQueue的实现原理,再来看录制和播放将会更加高效率;

Audio Queue 架构

Audio Queue 架包含三个部分: audio queue buffers, Buffer queueaudio queue callback; audio queue buffers 在结构上是一个数组结构,存储的AudioBuffer数据,下面会专门分析 AudioQueueBuffer 的数据结构; Buffer queue 可以理解为管理类,用来管理和组织这些 audio queue buffers 按照一定的顺序进行排列和运行, 并且协调 audio queue callback 的调用;

基于AudioQueue实现音频的录制和播放

AudioQueueBuffer数据结构

下面重点来说明AudioQueueBuffer的数据结构的数据结构如下,主要包含四个部分,其中最核心的是 aAduioData 部分。

typedef struct AudioQueueBuffer {
    const UInt32   mAudioDataBytesCapacity;
    void *const    mAudioData;
    UInt32         mAudioDataByteSize;
    void           *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;

mAudioData, 它代表要录制和播放的音频数据;
mAudioDataByteSize: 用来表示 audioDatalength;
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); < code></kaudioqueuebuffercount;>

Buffer Queue 和Enqueuing

Buffer queue 的数据结构是一个队列,队列里可以存放许多 Auido Queue Buffer, 存放的Buffer数量不受限制,推荐使用三个,例如录制的过程,一个buffer 负责收集麦克风的数据,一个buffer负责将数据传递给磁盘,还有一个Buffe用来做备用,防止磁盘I/O 时间过长出现卡顿。Enqueuing的含义是加入队列, 可以通过下图来了解Enqueue的过程。

基于AudioQueue实现音频的录制和播放

音频录制Buffer Queuing的Enqueuing说明:

  1. 读取麦克风的数据到内存中,然后将数据写入到buffer中;
  2. 当第一个buffer数据写满后,会将数据存放到磁盘中,并且触发回调函数,这个时候回调函数需要处理数据,将新的音频数据覆写到该buffer中;
  3. 在回调函数中将采集的音频数据写入到磁盘中;
  4. 当三个buffer 都填满后之后会继续复用第一个buffer;
  5. 重复第2部进行填充数据和进行回调;
  6. 重复第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的音频包描述,一般涉及到编码的地方会用到;

音频录制的调用实例

// &#x97F3;&#x9891;&#x5F55;&#x5236;&#x7684;&#x56DE;&#x8C03;&#x51FD;&#x6570;
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];
    }
}

// &#x8BFB;&#x53D6;&#x5F55;&#x5236;&#x5230;&#x7684;&#x97F3;&#x9891;&#x6570;&#x636E;

- (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 {
    /// &#x521B;&#x5EFA;&#x4E00;&#x4E2A;&#x65B0;&#x7684;&#x4ECE;audioqueue&#x5230;&#x786C;&#x4EF6;&#x5C42;&#x7684;&#x901A;&#x9053;
    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;//&#x6BCF;&#x4E00;&#x4E2A;packet&#x4E00;&#x4FA6;&#x6570;&#x636E;
    asbd.mBitsPerChannel = 16;//&#x6BCF;&#x4E2A;&#x91C7;&#x6837;&#x70B9;16bit&#x91CF;&#x5316;
    asbd.mBytesPerFrame = (asbd.mBitsPerChannel/8) * asbd.mChannelsPerFrame;
    asbd.mBytesPerPacket = asbd.mBytesPerFrame * asbd.mFramesPerPacket;
    return asbd;
}

说明:传入的参数是采样率和声道的数据,这里位深一般用的是16位;

音频播放

播放流程说明

音频的播放也包含三个部分,1.磁盘的的音频输入流;2.音频队列; 3. 扬声器;

基于AudioQueue实现音频的录制和播放

说明:
首先读取磁盘中的音频数据,第二,填充音频数据到Audio Queue 中; 第三, 驱动数据,使用扬声器播放数据;

通过AudioQueue来控制音频的播放

1.声明AudioStreamBasicDescription 来描述音频特征,如采样率,位深,声道数量等;

   audioDescription.mSampleRate =16000;//&#x91C7;&#x6837;&#x7387;
        audioDescription.mFormatID =kAudioFormatLinearPCM;
        audioDescription.mFormatFlags =kLinearPCMFormatFlagIsSignedInteger |kAudioFormatFlagIsPacked;
        audioDescription.mChannelsPerFrame =1;///&#x5355;&#x58F0;&#x9053;
        audioDescription.mFramesPerPacket =1;//&#x6BCF;&#x4E00;&#x4E2A;packet&#x4E00;&#x4FA6;&#x6570;&#x636E;
        audioDescription.mBitsPerChannel =16;//&#x6BCF;&#x4E2A;&#x91C7;&#x6837;&#x70B9;16bit&#x91CF;&#x5316;
        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&#xFF1B; 在回调的过程中需要不停的对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

参考文献

苹果开发者文档:https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/AQPlayback/PlayingAudio.html

Original: https://www.cnblogs.com/DataBaker/p/15775000.html
Author: DataBaker
Title: 基于AudioQueue实现音频的录制和播放

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/576842/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球