AudioQueue實現音頻流實時播放實戰

需求

使用Audio Queue實現實時播放音頻流數據.這裏以一個裝着pcm數據的caf文件爲例進行播放.node


實現原理

藉助數據傳輸隊列,將不管任務數據源的音頻數據裝入隊列中,而後開啓audio queue後從隊列中循環取出音頻數據以進行播放.ios


閱讀前提


代碼地址 : Audio Queue Player

掘金地址 : Audio Queue Player

簡書地址 : Audio Queue Player

博客地址 : Audio Queue Player


整體架構

本例藉助隊列實現音頻數據的中轉, 這裏用隊列是由於audio queue是靠數據驅動以支持播放的,因此有數據回調函數才能持續調用,若是咱們不借助隊列,就只能在audio queue的類中從回調函數中取來自音頻文件的數據,並且假設之後有別的數據源過來,使得音頻播放模塊代碼耦合度愈來愈高,而這裏藉助隊列的好處是外界不管是音頻文件仍是音頻流僅僅須要放入隊列中就好,開啓音頻模塊後咱們會從音頻隊列回調函數中取出隊列中的數據,而無需關心數據的來源.git

簡易流程

  • AudioStreamBasicDescription: 配置傳入的音頻數據格式
  • AudioQueueNewOutput : 新建audio queue
  • AudioQueueAddPropertyListener : 監聽audio queue是否正在工做
  • AudioQueueSetParameter : 設置音量
  • AudioQueueAllocateBuffer : 爲audio queue buffer 分配內存
  • 從隊列中取出數據裝入audio queue buffer,併入隊AudioQueueEnqueueBuffer
  • AudioQueueStart : 開啓audio queue
  • 進入播放回調函數, 在回調函數中取出隊列中存儲的音頻數據
  • 再次入隊, 以播放音頻數據,播放完後會自動觸發回調函數,依次循環

文件結構

1.file_structure

快速使用

  • 配置音頻數據來源的ASBD

下面是本例中的格式,其餘文件須要按文件格式自行配置github

// 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,
    };
複製代碼
  • 配置audio queue player
// 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. 初始化

在初始化方法中初始化音頻隊列,由於本例藉助另外一個類進行傳輸,因此這裏做爲實例對象,以便使用.bash

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

3. 配置音頻播放器

  • 將傳入的音頻格式,音頻Buffer大小拷貝到實例中
- (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;
}

複製代碼
  • 建立audio queue對象實例

經過傳入的視頻數據格式ASBD, 及回調函數名稱便可建立一個對應的audio queue對象.這裏將本類做爲實例傳入,以便回調函數與本類交流.架構

注意: 由於回調函數是C語言函數的形式,因此沒法直接調用類的實例方法.async

// 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;
    }
複製代碼
  • 監聽audio queue工做狀態 在回調函數中能夠監聽audio queue實例工做工做的變化,如正在播放或中止播放.
// 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);
}
複製代碼
  • 驗證設置的ASBD音頻格式及設置音量
// 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;
    }
複製代碼
  • 爲audio queue buffer分配內存
// 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

  • 預入隊幾個buffer以驅動播放

由於audio queue是驅動播放的模式,因此只有數據先入隊以後纔會繼續從回調函數中輪循播放,也就是咱們須要將前面分配好內存的buffer入隊來完成播放.函數

播放採用從原始音頻數據隊列中讀取音頻數據,以下,先出隊,而後將音頻數據拷貝到AudioQueueBufferRef實例,取出須要的信息(此隊列仍可繼續擴展).oop

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. 從音頻文件中讀取音頻數據

  • 經過本地文件路徑實例化爲CFURLRef對象
- (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;
    }
複製代碼
相關文章
相關標籤/搜索