iOS播放PCM,NSData流程式碼(Audio Queue Services)

喵了個咪發表於2017-07-13

最近有需求從藍芽接收音訊資料進行播放,之前沒做過,就各種百度啊,谷歌,看官方文件,然後順帶說一下,這裡是用的是Audio Queue Services,只能用於PCM資料,其他壓縮的音訊檔案要配合AudioFileStream或者AudioFile解析後播放。

在我的這篇文章中有一些音訊的介紹(主要是使用Speex這個庫),適合萌新觀看。

  1. Audio Queue Service的播放模式是先配置好播放的引數,然後開幾個播放佇列,相當於緩衝佇列,初始化佇列會要求填寫緩衝佇列的大小,後面填充的時候不要超過這個大小,不然會導致記憶體溢位。
  2. 此時往緩衝佇列插入音訊(PCM)資料,再把佇列填充進Audio Queue Service中,然後啟動Audio Queue Service,此時Audio Queue Service會播放(消費)這個資料。
  3. 當播放完這段資料後,Audio Queue Service呼叫一個方法,這個方法在你初始化(AudioQueueNewOutput)時,會要求填寫這個方法,這個方法包含了你當初設定的資料,預設是當前物件,還有一個Audio Queue Service物件和一個消費完的緩衝物件,此時你可以對比你的緩衝物件物件列表,把這個物件設定為未使用狀態,重新填充資料,然後在反覆操作,直到音訊播放完成。

注意點:

  • 當發現音訊播放吵雜,播放過快(慢)時,可以檢查你的取樣率(khz)和取樣位數設定的可能和音訊的取樣率和取樣位數不同。
  • 當發現播放一會之後沒有聲音,檢查是不是你的生產者(音訊來源)提供的資料不足,導致消費者(播放)在中斷後即使有資料也不播放(Audio Queue Service 尿性)。

上述的解決辦法是往播放佇列中插入空資料(感覺效果不好),或者是先暫停後,等資料來了再播放。

具體可以看碼農人生這個部落格,講的非常詳細。

AudioQueuePlay.h (OC)

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

@interface AudioQueuePlay : NSObject

// 播放並順帶附上資料
- (void)playWithData: (NSData *)data;

// reset
- (void)resetPlay;

@end

AudioQueuePlay.m

#import "AudioQueuePlay.h"

#define MIN_SIZE_PER_FRAME 2000
#define QUEUE_BUFFER_SIZE 3      //佇列緩衝個數

@interface AudioQueuePlay() {
    
    AudioQueueRef audioQueue;                                 //音訊播放佇列
    AudioStreamBasicDescription _audioDescription;
    AudioQueueBufferRef audioQueueBuffers[QUEUE_BUFFER_SIZE]; //音訊快取
    BOOL audioQueueBufferUsed[QUEUE_BUFFER_SIZE];             //判斷音訊快取是否在使用
    NSLock *sysnLock;
    NSMutableData *tempData;
    OSStatus osState;
}

@end

@implementation AudioQueuePlay

- (instancetype)init
{
    self = [super init];
    if (self) {
        sysnLock = [[NSLock alloc]init];
        
        // 播放PCM使用
        if (_audioDescription.mSampleRate <= 0) {
            //設定音訊引數
            _audioDescription.mSampleRate = 8000.0;//取樣率
            _audioDescription.mFormatID = kAudioFormatLinearPCM;
            // 下面這個是儲存音訊資料的方式的說明,如可以根據大端位元組序或小端位元組序,浮點數或整數以及不同體位去儲存資料
            _audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
            //1單聲道 2雙聲道
            _audioDescription.mChannelsPerFrame = 1;
            //每一個packet一偵資料,每個資料包下的楨數,即每個資料包裡面有多少楨
            _audioDescription.mFramesPerPacket = 1;
            //每個取樣點16bit量化 語音每取樣點佔用位數
            _audioDescription.mBitsPerChannel = 16;
            _audioDescription.mBytesPerFrame = (_audioDescription.mBitsPerChannel / 8) * _audioDescription.mChannelsPerFrame;
            //每個資料包的bytes總數,每楨的bytes數*每個資料包的楨數
            _audioDescription.mBytesPerPacket = _audioDescription.mBytesPerFrame * _audioDescription.mFramesPerPacket;
        }
        
        // 使用player的內部執行緒播放 新建輸出
        AudioQueueNewOutput(&_audioDescription, AudioPlayerAQInputCallback, (__bridge void * _Nullable)(self), nil, 0, 0, &audioQueue);
        
        // 設定音量
        AudioQueueSetParameter(audioQueue, kAudioQueueParam_Volume, 1.0);
        
        // 初始化需要的緩衝區
        for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
            audioQueueBufferUsed[i] = false;
            
            osState = AudioQueueAllocateBuffer(audioQueue, MIN_SIZE_PER_FRAME, &audioQueueBuffers[i]);
            
            printf("第 %d 個AudioQueueAllocateBuffer 初始化結果 %d (0表示成功)", i + 1, osState);
        }
        
        osState = AudioQueueStart(audioQueue, NULL);
        if (osState != noErr) {
            printf("AudioQueueStart Error");
        }
    }
    return self;
}

- (void)resetPlay {
    if (audioQueue != nil) {
        AudioQueueReset(audioQueue);
    }
}

// 播放相關
-(void)playWithData:(NSData *)data {
    
    [sysnLock lock];
    
    tempData = [NSMutableData new];
    [tempData appendData: data];
    // 得到資料
    NSUInteger len = tempData.length;
    Byte *bytes = (Byte*)malloc(len);
    [tempData getBytes:bytes length: len];
    
    int i = 0;
    while (true) {
        if (!audioQueueBufferUsed[i]) {
            audioQueueBufferUsed[i] = true;
            break;
        }else {
            i++;
            if (i >= QUEUE_BUFFER_SIZE) {
                i = 0;
            }
        }
    }
    
    audioQueueBuffers[i] -> mAudioDataByteSize =  (unsigned int)len;
    // 把bytes的頭地址開始的len位元組給mAudioData
    memcpy(audioQueueBuffers[i] -> mAudioData, bytes, len);
    
    //
    free(bytes);
    AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffers[i], 0, NULL);
    
    printf("本次播放資料大小: %lu", len);
    [sysnLock unlock];
}

// ************************** 回撥 **********************************

// 回撥回來把buffer狀態設為未使用
static void AudioPlayerAQInputCallback(void* inUserData,AudioQueueRef audioQueueRef, AudioQueueBufferRef audioQueueBufferRef) {
    
    AudioQueuePlay* player = (__bridge AudioQueuePlay*)inUserData;
    
    [player resetBufferState:audioQueueRef and:audioQueueBufferRef];
}

- (void)resetBufferState:(AudioQueueRef)audioQueueRef and:(AudioQueueBufferRef)audioQueueBufferRef {
    
    for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
        // 將這個buffer設為未使用
        if (audioQueueBufferRef == audioQueueBuffers[i]) {
            audioQueueBufferUsed[i] = false;
        }
    }
}

// ************************** 記憶體回收 **********************************

- (void)dealloc {
    
    if (audioQueue != nil) {
        AudioQueueStop(audioQueue,true);
    }
    
    audioQueue = nil;
    sysnLock = nil;
}

@end

~Swift 3 版~ (棄)

import UIKit
import AudioToolbox

class PCMPlayerConstant: NSObject {
    
    // 緩衝個數
    static let BUFF_NUM = 3
    
    // 一次播放的大小
    static let ONCE_PLAY_SIZE: UInt32 = 2000
}

class PCMPlayer: NSObject {

    fileprivate var audioQueueRef: AudioQueueRef?
    fileprivate var audioQueueBuffer: [AudioQueueBufferRef?]!
    fileprivate var audioDescription: AudioStreamBasicDescription!
    fileprivate var audioQueueBufferUsed: [Bool]!
    fileprivate var syncLock: NSLock!
    fileprivate var playData: NSMutableData!
    fileprivate var oSStatus: OSStatus!
    
    override init() {
        super.init()
        
        self.playData = NSMutableData()
        self.syncLock = NSLock()
        oSStatus = OSStatus()
        audioQueueBufferUsed = []
        self.audioQueueBuffer = []
        
        audioDescription = AudioStreamBasicDescription()
        // 設定音訊引數
        audioDescription.mSampleRate = 8000.0 //取樣率
        audioDescription.mFormatID = kAudioFormatLinearPCM
        // 下面這個是儲存音訊資料的方式的說明,如可以根據大端位元組序或小端位元組序,浮點數或整數以及不同體位去儲存資料
        audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked
        //1單聲道 2雙聲道
        audioDescription.mChannelsPerFrame = 1
        //每一個packet一偵資料,每個資料包下的楨數,即每個資料包裡面有多少楨
        audioDescription.mFramesPerPacket = 1
        //每個取樣點16bit量化 語音每取樣點佔用位數
        audioDescription.mBitsPerChannel = 16
        audioDescription.mBytesPerFrame = (audioDescription.mBitsPerChannel / 8) * audioDescription.mChannelsPerFrame
        //每個資料包的bytes總數,每楨的bytes數*每個資料包的楨數
        audioDescription.mBytesPerPacket = audioDescription.mBytesPerFrame * audioDescription.mFramesPerPacket
        
        self.initPlay()
    }
    
    fileprivate func initPlay() -> Void {
        let selfPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        // 使用audioDescripton新建audioQueue
        oSStatus = AudioQueueNewOutput(&self.audioDescription!, MyAudioQueueOutputCallback, selfPointer, CFRunLoopGetCurrent(), nil, 0, &self.audioQueueRef)
        
        if oSStatus != noErr {
            print("AudioQueueNewOutput Error")
            return
        }
        
        // 設定音量
        AudioQueueSetParameter(self.audioQueueRef!, kAudioQueueParam_Volume, 1.0)
        
        for index in 0..<PCMPlayerConstant.BUFF_NUM {
            var audioBuffer: AudioQueueBufferRef? = nil
            //
            oSStatus = AudioQueueAllocateBuffer(self.audioQueueRef!, PCMPlayerConstant.ONCE_PLAY_SIZE, &audioBuffer)
            if oSStatus != noErr {
                print("AudioQueueAllocateBuffer Error \\\\(index)")
                return
            }else{
                self.audioQueueBuffer.append(audioBuffer)
                // 表示未使用
                self.audioQueueBufferUsed.append(false)
                print("第 \\\\(index + 1) 個AudioQueueAllocateBuffer 初始化結果 \\\\(oSStatus) (0表示成功)")
            }
        }
        
        AudioQueueStart(self.audioQueueRef!, nil)
        
    }
    
    func playWithData(data: Data) -> Void {
        syncLock.lock()
        
        playData.append(data)
        // 數值大於980 再播放 這裡可以按需求改
        if playData.length > 980 {
            let playDataLength = playData.length
            
            var i = 0
            // 迴圈找出可用buffer
            while true {
                if !self.audioQueueBufferUsed[i] {
                    // 表示已使用
                    self.audioQueueBufferUsed[i] = true
                    break
                }else {
                    i += 1
                    // 當迴圈到頭了就重新迴圈
                    if i >= PCMPlayerConstant.BUFF_NUM {
                        i = 0
                    }
                }
            }
            
            let p = self.audioQueueBuffer[i]
            let selfPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
            p?.pointee.mUserData = selfPointer
            p?.pointee.mAudioDataByteSize = UInt32(playDataLength)
            p?.pointee.mAudioData.advanced(by: 0).copyBytes(from: playData.bytes, count: playDataLength)
            
            // 丟入audioQueue中
            AudioQueueEnqueueBuffer(self.audioQueueRef!, self.audioQueueBuffer[i]!, 0, nil)
            
            playData = NSMutableData()
            
            print("play length \\\\(playDataLength)")
        }
        
        syncLock.unlock()
    }
    
}

// 播放完的回撥
func MyAudioQueueOutputCallback(clientData: UnsafeMutableRawPointer?, AQ: AudioQueueRef, buffer: AudioQueueBufferRef) {
    let my = Unmanaged<PCMPlayer>.fromOpaque(UnsafeRawPointer(clientData)!).takeUnretainedValue()
    // AudioQueueFreeBuffer(AQ, buffer)
    for index in 0..<PCMPlayerConstant.BUFF_NUM {
        if my.audioQueueBuffer[index] == buffer {
            // 把當前放完的buffer設為未使用
            my.audioQueueBufferUsed[index] = false
            //print("|-> \\\\(index) buffer is \\\\(self.audioQueueBufferUsed[index]) <-|")
        }
    }
}

相關文章