工作生活

AudioQueue实现音频流实时播放实战

2019-06-29  本文已影响0人  小东邪啊

需求

使用Audio Queue实现实时播放音频流数据.这里以一个装着pcm数据的caf文件为例进行播放.


实现原理

借助数据传输队列,将无论任务数据源的音频数据装入队列中,然后开启audio queue后从队列中循环取出音频数据以进行播放.


阅读前提


代码地址 : Audio Queue Player

掘金地址 : Audio Queue Player

简书地址 : Audio Queue Player

博客地址 : Audio Queue Player


总体架构

本例借助队列实现音频数据的中转, 这里用队列是因为audio queue是靠数据驱动以支持播放的,所以有数据回调函数才能持续调用,如果我们不借助队列,就只能在audio queue的类中从回调函数中取来自音频文件的数据,而且假设以后有别的数据源过来,使得音频播放模块代码耦合度越来越高,而这里借助队列的好处是外界不论是音频文件还是音频流仅仅需要放入队列中就好,开启音频模块后我们会从音频队列回调函数中取出队列中的数据,而无需关心数据的来源.

简易流程

文件结构

1.file_structure

快速使用

下面是本例中的格式,其他文件需要按文件格式自行配置

    // This is only for the testPCM.caf file.
    AudioStreamBasicDescription audioFormat = {
        .mSampleRate         = 44100,
        .mFormatID           = kAudioFormatLinearPCM,
        .mChannelsPerFrame   = 1,
        .mFormatFlags        = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked,
        .mBitsPerChannel     = 16,
        .mBytesPerPacket     = 2,
        .mBytesPerFrame      = 2,
        .mFramesPerPacket    = 1,
    };
    // Configure Audio Queue Player
    [[XDXAudioQueuePlayer getInstance] configureAudioPlayerWithAudioFormat:&audioFormat bufferSize:kXDXReadAudioPacketsNum * audioFormat.mBytesPerPacket];
    // Configure Audio File
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"testPCM" ofType:@"caf"];
    XDXAudioFileHandler *fileHandler = [XDXAudioFileHandler getInstance];
    [fileHandler configurePlayFilePath:filePath];

开始播放前先从文件中读取音频数据并放入队列,我们这里先让队列中缓存5帧音频数据,然后再启动audio queue player. 关于音频文件读取以及队列原理这里不做过多说明.如需帮助请参考上文阅读前提.

    // Put audio data from audio file into audio data queue
    [self putAudioDataIntoDataQueue];
    
    // First put 5 frame audio data to work queue then start audio queue to read it to play.
    [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer * _Nonnull timer) {
        dispatch_async(dispatch_get_main_queue(), ^{
            XDXCustomQueueProcess *audioBufferQueue = [XDXAudioQueuePlayer getInstance]->_audioBufferQueue;
            int size = audioBufferQueue->GetQueueSize(audioBufferQueue->m_work_queue);
            if (size > 5) {
                [[XDXAudioQueuePlayer getInstance] startAudioPlayer];
                [timer invalidate];
            }
        });
    }];

具体实现

1. 定义一个结构体存储音频相关数据

#define kXDXAudioPCMFramesPerPacket 1
#define kXDXAudioPCMBitsPerChannel  16

static const int kNumberBuffers = 3;

struct XDXAudioInfo {
    AudioStreamBasicDescription  mDataFormat;
    AudioQueueRef                mQueue;
    AudioQueueBufferRef          mBuffers[kNumberBuffers];
    int                          mbufferSize;
};
typedef struct XDXAudioInfo *XDXAudioInfoRef;

static XDXAudioInfoRef m_audioInfo;

+ (void)initialize {
    int size = sizeof(XDXAudioInfo);
    m_audioInfo = (XDXAudioInfoRef)malloc(size);
}

2. 初始化

在初始化方法中初始化音频队列,因为本例借助另一个类进行传输,所以这里作为实例对象,以便使用.

- (instancetype)init {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instace                  = [super init];
        self->_isInitFinish       = NO;
        self->_audioBufferQueue   = new XDXCustomQueueProcess();
    });
    return _instace;
}

3. 配置音频播放器

- (void)configureAudioPlayerWithAudioFormat:(AudioStreamBasicDescription *)audioFormat bufferSize:(int)bufferSize {
    memcpy(&m_audioInfo->mDataFormat, audioFormat, sizeof(XDXAudioInfo));
    m_audioInfo->mbufferSize = bufferSize;    
    BOOL isSuccess = [self configureAudioPlayerWithAudioInfo:m_audioInfo
                                                playCallback:PlayAudioDataCallback
                                            listenerCallback:AudioQueuePlayerPropertyListenerProc];
    
    self.isInitFinish = isSuccess;
}

通过传入的视频数据格式ASBD, 及回调函数名称即可创建一个对应的audio queue对象.这里将本类作为实例传入,以便回调函数与本类交流.

注意: 因为回调函数是C语言函数的形式,所以无法直接调用类的实例方法.

    // Create audio queue
    OSStatus status = AudioQueueNewOutput(&audioInfo->mDataFormat,
                                         playCallback,
                                         (__bridge void *)(self),
                                         CFRunLoopGetCurrent(),
                                         kCFRunLoopCommonModes,
                                         0,
                                         &audioInfo->mQueue);
    
    if (status != noErr) {
        NSLog(@"Audio Player: audio queue new output failed status:%d \n",(int)status);
        return NO;
    }
    // Listen the queue is whether working
    AudioQueueAddPropertyListener (audioInfo->mQueue,
                                   kAudioQueueProperty_IsRunning,
                                   listenerCallback,
                                   (__bridge void *)(self));
                                   
......

static void AudioQueuePlayerPropertyListenerProc  (void *              inUserData,
                                                   AudioQueueRef           inAQ,
                                                   AudioQueuePropertyID    inID) {
    XDXAudioQueuePlayer * instance = (__bridge XDXAudioQueuePlayer *)inUserData;
    UInt32 isRunning = 0;
    UInt32 size = sizeof(isRunning);
    
    if(instance == NULL)
        return ;
    
    OSStatus err = AudioQueueGetProperty (inAQ, kAudioQueueProperty_IsRunning, &isRunning, &size);
    if (err) {
        instance->_isRunning = NO;
    }else {
        instance->_isRunning = isRunning;
    }
    
    NSLog(@"The audio queue work state: %d",instance->_isRunning);
}
    // Get audio ASBD
    UInt32 size = sizeof(audioInfo->mDataFormat);
    status = AudioQueueGetProperty(audioInfo->mQueue,
                                   kAudioQueueProperty_StreamDescription,
                                   &audioInfo->mDataFormat,
                                   &size);
    if (status != noErr) {
        NSLog(@"Audio Player: get ASBD status:%d",(int)status);
        return NO;
    }
    
    // Set volume
    status = AudioQueueSetParameter(audioInfo->mQueue, kAudioQueueParam_Volume, 1.0);
    if (status != noErr) {
        NSLog(@"Audio Player: set volume failed:%d",(int)status);
        return NO;
    }
    // Allocate buffer for audio queue buffer
    for (int i = 0; i != kNumberBuffers; i++) {
        status = AudioQueueAllocateBuffer(audioInfo->mQueue,
                                          audioInfo->mbufferSize,
                                          &audioInfo->mBuffers[i]);
        if (status != noErr) {
            NSLog(@"Audio Player: Allocate buffer status:%d",(int)status);
        }
    }
    

4. 启动audio queue

因为audio queue是驱动播放的模式,所以只有数据先入队之后才会继续从回调函数中轮循播放,也就是我们需要将前面分配好内存的buffer入队来完成播放.

播放采用从原始音频数据队列中读取音频数据,如下,先出队,然后将音频数据拷贝到AudioQueueBufferRef实例,取出需要的信息(此队列仍可继续扩展).

    for (int i = 0; i != kNumberBuffers; i++) {
        [self receiveAudioDataWithAudioQueueBuffer:audioInfo->mBuffers[i]
                                         audioInfo:audioInfo
                                  audioBufferQueue:_audioBufferQueue];
    }
    
    ......
    
- (void)receiveAudioDataWithAudioQueueBuffer:(AudioQueueBufferRef)inBuffer audioInfo:(XDXAudioInfoRef)audioInfo audioBufferQueue:(XDXCustomQueueProcess *)audioBufferQueue {
    XDXCustomQueueNode *node = audioBufferQueue->DeQueue(audioBufferQueue->m_work_queue);
    
    if (node != NULL) {
        if (node->size > 0) {
            UInt32 size = (UInt32)node->size;
            inBuffer->mAudioDataByteSize = size;
            memcpy(inBuffer->mAudioData, node->data, size);
            AudioStreamPacketDescription *packetDesc = (AudioStreamPacketDescription *)node->userData;
            AudioQueueEnqueueBuffer (
                                     audioInfo->mQueue,
                                     inBuffer,
                                     (packetDesc ? size : 0),
                                     packetDesc);

        }

        free(node->data);
        node->data = NULL;
        audioBufferQueue->EnQueue(audioBufferQueue->m_free_queue, node);
    }else {
        AudioQueueStop (
                        audioInfo->mQueue,
                        false
                        );
    }
}
    OSStatus status;
    status = AudioQueueStart(m_audioInfo->mQueue, NULL);
    if (status != noErr) {
        NSLog(@"Audio Player: Audio Queue Start failed status:%d \n",(int)status);
        return NO;
    }else {
        NSLog(@"Audio Player: Audio Queue Start successful");
        return YES;
    }

5. 触发回调函数轮循播放

正如前面所说, audio queue的播放模式是数据驱动式,也就是我们已经预先入队了几个音频队列数据,然后开启audio queue后我们它会自动播放前面已经入队的数据,每当播放完会自动触发回调函数读取数据以完成下一次播放.

static void PlayAudioDataCallback(void * aqData,AudioQueueRef inAQ , AudioQueueBufferRef inBuffer) {
    XDXAudioQueuePlayer *instance = (__bridge XDXAudioQueuePlayer *)aqData;
    if(instance == NULL){
        return;
    }
    
    /* Debug
    static Float64 lastTime = 0;
    NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]*1000;
    NSLog(@"Test duration - %f",currentTime - lastTime);
    lastTime = currentTime;
    */
    
    [instance receiveAudioDataWithAudioQueueBuffer:inBuffer
                                         audioInfo:m_audioInfo
                                  audioBufferQueue:instance->_audioBufferQueue];
}

6. 其他

Demo中还有音频队列的暂停, 恢复, 停止, 销毁等功能,较为简单,这里不再说明.

7. 从音频文件中读取音频数据

- (void)configurePlayFilePath:(NSString *)filePath {
    char path[256];
    [filePath getCString:path maxLength:sizeof(path) encoding:NSUTF8StringEncoding];
    self->m_playFileURL = CFURLCreateFromFileSystemRepresentation (
                                                                   NULL,
                                                                   (const UInt8 *)path,
                                                                   strlen (path),
                                                                   false
                                                                   );
}

函数中可配置文件权限及类型,本例中文件类型为caf文件.

        OSStatus status;
        status = AudioFileOpenURL(self->m_playFileURL,
                                  kAudioFileReadPermission,
                                  kAudioFileCAFType,
                                  &self->m_playFile);
        if (status != noErr) {
            NSLog(@"open file failed: %d", (int)status);
        }

首先指定每次读取多少个音频数据包, 该函数会返回最终读取的字节数. 这里通过m_playCurrentPacket记录当前读取的音频包数以便下次继续读取.读取完成后关闭文件.

    UInt32 bytesRead = 0;
    UInt32 numPackets = readPacketsNum;
    OSStatus status = AudioFileReadPackets(m_playFile,
                                  false,
                                  &bytesRead,
                                  packetDesc,
                                  m_playCurrentPacket,
                                  &numPackets,
                                  audioDataRef);
    
    if (status != noErr) {
        NSLog(@"read packet failed: %d", (int)status);
    }
    
    if (bytesRead > 0) {
        m_playCurrentPacket += numPackets;
    }else {
        status = AudioFileClose(m_playFile);
        if (status != noErr) {
            NSLog(@"close file failed: %d", (int)status);
        }
        self.isPlayFileWorking = NO;
        m_playCurrentPacket = 0;
    }
上一篇下一篇

猜你喜欢

热点阅读