关于AudioQueueService的使用总结

AudioQueueService是iOS上一套很古老的API了,用于录制、播放音频,由于年代久远,网上关于AudioQueueService的文章并不是很多,笔者在最近的项目中使用其播放来自摄像头的音频流,遇到了各种问题,在此记录下来以供后来人参考。

首先来了解一下几个常用的API

  1. AudioQueueNewOutput: 初始化音频输出队列
objectivec 复制代码
extern OSStatus             AudioQueueNewOutput(       
         const AudioStreamBasicDescription *inFormat, 
         AudioQueueOutputCallback        inCallbackProc,      
         void * __nullable               inUserData,      
        CFRunLoopRef __nullable         inCallbackRunLoop,  
        CFStringRef __nullable          inCallbackRunLoopMode,    
          UInt32                          inFlags,      
         AudioQueueRef __nullable * __nonnull outAQ)          API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

主要参数 :inFormat: 传入的格式,主要是类型ID、采样率、位深和通道数这些,

inCallbackProc: 播放的回调函数,当有音频缓冲被使用后,就会回调这个函数

inUserData: 任意对象,一般调用他的对象,即self

outAQ: 音频队列,注意这个队列需要我们先创建,再以引用形式传入。

其他的参数都可以填nil。

关于这个函数没有什么特别要注意的,即便多次调用也不会发生问题,内部应该是做了处理。

  1. AudioQueueAllocateBuffer : 给队列分配缓冲区

    extern OSStatusAudioQueueAllocateBuffer( AudioQueueRef inAQ, UInt32 inBufferByteSize, AudioQueueBufferRef __nullable * __nonnull outBuffer) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

主要参数: inAQ : AudioQueueNewOutput 创建的音频队列

inBufferByteSize : 缓冲区大小,以字节数为单位,不要太大也不要太小,一般设置为输入的音频Data大小即可,过大会浪费内存,过小会导致内存访问错误,有可能第一次分配的时候不会报错,但是再次分配就会报错

outBuffer: 缓冲对象

这个函数一般是用for循环调用N次,N为你需要的缓冲对象数量,一般情况下3个足矣。

  1. AudioQueueAddPropertyListener: 给队列添加属性监听,一般是监听队列的运行状态
scss 复制代码
extern OSStatusAudioQueueAddPropertyListener(
      AudioQueueRef                   inAQ,                                    
      AudioQueuePropertyID            inID,                                    
      AudioQueuePropertyListenerProc  inProc,                                    
      void * __nullable               inUserData)     API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

主要参数: inAQ : AudioQueueNewOutput 创建的音频队列

inID :监听的属性ID,比如运行状态是kAudioQueueProperty_IsRunning

inProc: 监听的回调函数,当属性的值发生变化的时候会回调

回调函数示例:

objectivec 复制代码
static void AudioQueueIsRunningCallback(
                                        void * __nullable       inUserData,
                                        AudioQueueRef           inAQ,
                                        AudioQueuePropertyID    inID) {
    UInt32 isRunning;
    UInt32 size;
    AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &isRunning, &size);
    NSLog(@"isRunning=%d",isRunning);
    if (!isRunning) {
        AudioQueueDispose(inAQ, true);
        NSLog(@"AudioQueueDisposed");
        inAQ =nil;
    }
    
}

这里需要注意一点,不同的属性的值的size是不一样的,像isRunning的size是4个字节,所以这里isRunning用UInt32类型,具体的大小要看苹果官方文档的说明,笔者在这里吃过亏,因为用错了类型导致得不到正确的值。

4.AudioQueueStart 开启音频队列

objectivec 复制代码
extern OSStatusAudioQueueStart(            
        AudioQueueRef                     inAQ,       
         const AudioTimeStamp * __nullable inStartTime) 

这个函数比较简单,只有两个参数,inAQ 即音频队列,inStartTime是这个队列多久开始启动,一般传NULL即可,表示立即启动。 函数虽然简单,但是使用上确是有不少的坑。 首先,需要说明一下音频队列的运行状态,并不只有运行和停止两种,还包括闲置和初始化状态。一个队列创建之后,调用AudioQueueNewOutput就进入了初始化状态。 这时候还不能直接向里面发送数据,必须要调用AudioQueueStart 让其进入运行状态。 当运行到一半,队列中已有的buffer都用完了(回调函数执行了N次),还没有新的buffer入队列的话,就会进行闲置状态。这时候如果只是调用AudioQueueStart的话并不能让其重新进入运行状态,还需要再其之前调用另外一个函数****AudioQueueReset才行。 当我们不再需要播放声音的时候,我们就调用AudioQueueStop让其停下来,进入停止状态。这时可以调用AudioQueueStart使其再次进入运行状态。

  1. AudioQueueReset 重置音频队列
scss 复制代码
extern OSStatusAudioQueueReset(   
                 AudioQueueRef           inAQ) 

这个函数更加简单,只有一个参数,即音频队列,但是注释还是挺长的,大意就是说这个函数会立即重置队列,清空所有缓冲区,移除已经使用的buffer,并且重置解码器之类的硬件,并且还有可能多次调用回调函数。调用这个函数时音频会产生间断的感觉,因此不要随意调用。目前发现必须要调用的地方就是队列缓冲区使用完了,但是又没有新的缓冲区填入的时候。

arduino 复制代码
	This function immediately resets an audio queue, flushes any queued buffer, removes all
	buffers from previously scheduled use, and resets any decoder and digital signal
	processing (DSP) state information. It also invokes callbacks for any flushed buffers.
	If you queue any buffers after calling this function, processing does not occur until
	the decoder and DSP state information is reset. Hence, a discontinuity (that is, a
	"glitch") might occur.

	Note that when resetting, all pending buffer callbacks are normally invoked
	during the process of resetting. But if the calling thread is responding to a buffer
	callback, then it is possible for additional buffer callbacks to occur after
	AudioQueueReset returns.
  1. AudioQueueEnqueueBuffer 往队列里面塞buffer
objectivec 复制代码
extern OSStatusAudioQueueEnqueueBuffer(  
          AudioQueueRef                       inAQ,                                    AudioQueueBufferRef                 inBuffer,                                    UInt32                              inNumPacketDescs,                                    const AudioStreamPacketDescription * __nullable inPacketDescs) z

其中重点说一下AudioQueueBufferRef 这个结构体,构造方法我省略了

objectivec 复制代码
typedef struct AudioQueueBuffer {    const UInt32     
               mAudioDataBytesCapacity;  
               void * const                    mAudioData;   
               UInt32                          mAudioDataByteSize;    void * __nullable               mUserData;    const UInt32                    mPacketDescriptionCapacity;    AudioStreamPacketDescription * const __nullable mPacketDescriptions;    UInt32                          mPacketDescriptionCount;} AudioQueueBuffer;

重点关注mAudioData 和mAudioDataByteSize这两个属性,我们需要使用memcpy把我们的音频数据拷贝到mAudioData中,并把数据的长度赋值给mAudioDataByteSize,注意AudioQueueBuffer是复用之前使用过的缓冲区,因此如果输入的数据长度不是恒定的,那最好是通过memset清空之前的数据再拷贝。前面说过AudioQueueAllocateBuffer分配的大小不能小于实际的数据大小,如果过小,则这里会发生EXC_BAD_ACCESS错误。

由于buffer进入队列后,播放的时间长度不固定,因此为了声音听起来连续,我们可以在调用AudioQueueEnqueueBuffer前,通过一个while循环不停的轮询,看有没有空余的buffer,如果有,则使用。否则你拿不到空余buffer, 调用AudioQueueEnqueueBuffer也没有意义,只能丢弃,这样声音就会听起来时断时续。参考代码如下:

ini 复制代码
  AudioQueueBufferRef audioQueueBuffer =NULL;

    while (true) {
        audioQueueBuffer = [self getNotUsedBuffer];
        if (audioQueueBuffer !=NULL) {
            break;
        }
        usleep(1000);
    }



- (AudioQueueBufferRef)getNotUsedBuffer
{
    AudioQueueBufferRef buffer = NULL;
    for (int i =0; i <QUEUE_BUFFER_SIZE; i++) {
        if (NO ==audioQueueUsed[i]) {
            audioQueueUsed[i] = YES;
            //            NSLog(@"PCMAudioPlayer play buffer index:%d", i);
            buffer = audioQueueBuffers[i];
            break;
        }
    }
   

    return buffer;
}

- (void)playerCallback:(AudioQueueBufferRef)outQB
{
    for (int i =0; i <QUEUE_BUFFER_SIZE; i++) {
        if (outQB == self->audioQueueBuffers[i]) {
            self->audioQueueUsed[i] = NO;
        }
    }
    
//    NSLog(@"PCMAudioPlayer播放中...");
    if(!self.isStarted) {
        if(self.delegate && [self.delegate respondsToSelector:@selector(didStartPlay)]){
            [self.delegate didStartPlay];
        }
        self.isStarted = YES;
    }
}




static void AudioPlayerOutputCallback(void* inUserData,AudioQueueRef outQ, AudioQueueBufferRef outQB)
{
    PCMAudioPlayer* player = (__bridge PCMAudioPlayer*)inUserData;
    [player playerCallback:outQB];
}
  1. AudioQueueStop 停止队列
scss 复制代码
extern OSStatus
AudioQueueStop(                     AudioQueueRef           inAQ,
                                    Boolean                 inImmediate) 

一般来讲都是inImmediate 为 true立即停止,有些人会在AudioQueueStop之后再调用AudioQueueReset,在我看来这是没有意义的,因为AudioQueueStart之后可以重新播放。

那用AudioQueueStop 代替AudioQueueReset 行不行呢?也是可以的,只是如果你像我上面那样在 stop之后dispose了的话,就不行了,这时候会引起崩溃或者是没有声音。

  1. AudioQueueDispose 销毁队列
scss 复制代码
extern OSStatus
AudioQueueDispose(                  AudioQueueRef           inAQ, 
                                    Boolean                 inImmediate) 

这个方法调用之后队列就不能再使用了,这时候再往里面塞buffer就会引起崩溃,或者是没有声音。一般情况下也很少人会去用这个方法,如果AudioQueuStop的inImmediate是false的话,建议在isRunning的监控回调函数中使用,不要直接在AudioQueueStop之后调用。

好了,关于API的使用就介绍到这,还有一个问题需要注意下,就是如果你像我上面的示例代码用一个while循环来等待空闲的buffer,要注意这是会阻塞线程的,所以最好有一个sleep来减少线程的忙等,再加上一个bool变量来强制退出循环,否则出现没有声音时很可能就是这里出问题了。

另外,有个问题还想跟大家探讨下,就是AudioQueueService内部的设计是一个单例吗?从我之前排查问题的经验来看,感觉是的,因为有时候新创建的一个队列给buffer分配的内存大小有问题,当时并没有反映出来,但是重新创建队列再次分配buffer时,就报错了。但是从API的设计来看,又不像是,而且多个队列同时播放的话,是能感觉到声音重叠的。也有可能是到了底层某些变量或属性是共享的,导致重新分配内存时会访问到之前的变量。

相关推荐
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
音视频牛哥1 个月前
Android摄像头采集选Camera1还是Camera2?
音视频开发·视频编码·直播
九酒2 个月前
【harmonyOS NEXT 下的前端开发者】WAV音频编码实现
前端·harmonyos·音视频开发
音视频牛哥2 个月前
结合GB/T28181规范探讨Android平台设备接入模块心跳实现
音视频开发·视频编码·直播
哔哩哔哩技术2 个月前
自研点直播转码核心
音视频开发
音视频牛哥2 个月前
Android平台轻量级RTSP服务模块二次封装版调用说明
音视频开发·视频编码·直播
音视频牛哥2 个月前
Android平台RTSP|RTMP直播播放器技术接入说明
音视频开发·视频编码·直播
山雨楼2 个月前
ExoPlayer架构详解与源码分析(15)——Renderer
android·架构·音视频开发
音视频牛哥2 个月前
Windows平台如何实现多路RTSP|RTMP流合成后录像或转发RTMP服务
音视频开发·视频编码·直播
音视频牛哥2 个月前
GB28181设备接入模块和轻量级RTSP服务有什么区别?
音视频开发·视频编码·直播