1. 程式人生 > >iOS 實時錄音和播放

iOS 實時錄音和播放

需求:最近公司需要做一個樓宇對講的功能:門口機(連線WIFI)撥號對室內機(對應的WIFI)的裝置進行呼叫,室內機收到呼叫之後將對收到的資料進行UDP廣播的轉發,手機(連線對應的WIFI)收到視訊流之後,實時的展示視訊資料(手機可以接聽,結束通話,手機接聽之後,室內機不展示視訊,只是進行轉發。)

簡單點說就是手機客戶端需要做一個類似於直播平臺的軟體,可以實時的展示視訊,實時的播放接收到的聲音資料,並且實時將手機麥克風收到的聲音回傳給室內機,室內機負責轉發給門口機。

這篇文章介紹iOS怎麼進行實時的錄音和播放收到的聲音資料 

想要使用系統的框架實時播放聲音和錄音資料,就得知道音訊佇列服務,

在AudioToolbox框架中的音訊佇列服務,它完全可以做到音訊播放和錄製

一個音訊服務佇列有三個部分組成:

1.三個緩衝器Buffers:每個緩衝器都是一個儲存音訊資料的臨時倉庫。
2.一個緩衝佇列Buffer Queue:一個包含音訊緩衝器的有序佇列。
3.一個回撥CallBack:一個自定義的佇列回撥函式。

 具體怎麼運轉的還是百度吧!

我的簡單理解:

對於播放:系統會自動從緩衝佇列中迴圈取出每個緩衝器中的資料進行播放,我們需要做的就是將接收到的資料迴圈的放到緩衝器中,剩下的就交給系統去實現了。

對於錄音:  系統會自動將錄的聲音放入佇列中的每個緩衝器中,我們需要做的就是從回撥函式中將資料轉化我們自己的資料就OK了。

#pragma mark--實時播放

1. 匯入系統框架AudioToolbox.framework  AVFoundation.framework

2. 獲取麥克風許可權,在工程的Info.plist檔案中加入Privacy - Microphone Usage Description 這個key 描述:App想要訪問您的麥克風

3. 建立播放聲音的類 EYAudio

EYAudio.h

複製程式碼
#import <Foundation/Foundation.h>

@interface EYAudio : NSObject

// 播放的資料流資料
- (void)playWithData:(NSData *)data;

// 聲音播放出現問題的時候可以重置一下 - (void)resetPlay; // 停止播放 - (void)stop; @end
複製程式碼

 EYAudio.m

複製程式碼
#import "EYAudio.h"
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>

#define MIN_SIZE_PER_FRAME 1920   //每個包的大小,室內機要求為960,具體看下面的配置資訊
#define QUEUE_BUFFER_SIZE  3      //緩衝器個數
#define SAMPLE_RATE        16000  //取樣頻率

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

@implementation EYAudio

#pragma mark - 提前設定AVAudioSessionCategoryMultiRoute 播放和錄音
+ (void)initialize
{
    NSError *error = nil;
    //只想要播放:AVAudioSessionCategoryPlayback
    //只想要錄音:AVAudioSessionCategoryRecord
    //想要"播放和錄音"同時進行 必須設定為:AVAudioSessionCategoryMultiRoute 而不是AVAudioSessionCategoryPlayAndRecord(設定這個不好使)
    BOOL ret = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryMultiRoute error:&error];
    if (!ret) {
        NSLog(@"設定聲音環境失敗");
        return;
    }
    //啟用audio session
    ret = [[AVAudioSession sharedInstance] setActive:YES error:&error];
    if (!ret)
    {
        NSLog(@"啟動失敗");
        return;
    }
}

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

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

    audioQueue = nil;
    sysnLock = nil;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        sysnLock = [[NSLock alloc]init];

        //設定音訊引數 具體的資訊需要問後臺
        _audioDescription.mSampleRate = SAMPLE_RATE;
        _audioDescription.mFormatID = kAudioFormatLinearPCM;
        _audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
        //1單聲道
        _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]);
        }

        osState = AudioQueueStart(audioQueue, NULL);
        if (osState != noErr) {
            NSLog(@"AudioQueueStart Error");
        }
    }
    return self;
}

// 播放資料
-(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,向第i個緩衝器
    memcpy(audioQueueBuffers[i] -> mAudioData, bytes, len);

    // 釋放物件
    free(bytes);

    //將第i個緩衝器放到佇列中,剩下的都交給系統了
    AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffers[i], 0, NULL);

    [sysnLock unlock];
}

// ************************** 回撥 **********************************
// 回調回來把buffer狀態設為未使用
static void AudioPlayerAQInputCallback(void* inUserData,AudioQueueRef audioQueueRef, AudioQueueBufferRef audioQueueBufferRef) {

    EYAudio* audio = (__bridge EYAudio*)inUserData;

    [audio resetBufferState:audioQueueRef and:audioQueueBufferRef];
}

- (void)resetBufferState:(AudioQueueRef)audioQueueRef and:(AudioQueueBufferRef)audioQueueBufferRef {
    // 防止空資料讓audioqueue後續都不播放,為了安全防護一下
    if (tempData.length == 0) {
        audioQueueBufferRef->mAudioDataByteSize = 1;
        Byte* byte = audioQueueBufferRef->mAudioData;
        byte = 0;
        AudioQueueEnqueueBuffer(audioQueueRef, audioQueueBufferRef, 0, NULL);
    }

    for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
        // 將這個buffer設為未使用
        if (audioQueueBufferRef == audioQueueBuffers[i]) {
            audioQueueBufferUsed[i] = false;
        }
    }
}

@end
複製程式碼

外界使用: 不斷呼叫下面的方法將NSData傳遞進來

- (void)playWithData:(NSData *)data; 

 #pragma mark--實時錄音

1. 匯入系統框架AudioToolbox.framework  AVFoundation.framework

2. 建立錄音的類 EYRecord

EYRecord.h

複製程式碼
#import <Foundation/Foundation.h>

@interface ESARecord : NSObject

//開始錄音
- (void)startRecording;

//停止錄音
- (void)stopRecording;

@end
複製程式碼

EYRecord.m

複製程式碼
#import "ESARecord.h"
#import <AudioToolbox/AudioToolbox.h>

#define QUEUE_BUFFER_SIZE 3      // 輸出音訊佇列緩衝個數
#define kDefaultBufferDurationSeconds 0.03//調整這個值使得錄音的緩衝區大小為960,實際會小於或等於960,需要處理小於960的情況
#define kDefaultSampleRate 16000   //定義取樣率為16000

extern NSString * const ESAIntercomNotifationRecordString;

static BOOL isRecording = NO;

@interface ESARecord(){
    AudioQueueRef _audioQueue;                          //輸出音訊播放佇列
    AudioStreamBasicDescription _recordFormat;
    AudioQueueBufferRef _audioBuffers[QUEUE_BUFFER_SIZE]; //輸出音訊快取
}
@property (nonatomic, assign) BOOL isRecording;

@end

@implementation ESARecord

- (instancetype)init
{
    self = [super init];
    if (self) {
        //重置下
        memset(&_recordFormat, 0, sizeof(_recordFormat));
        _recordFormat.mSampleRate = kDefaultSampleRate;
        _recordFormat.mChannelsPerFrame = 1;
        _recordFormat.mFormatID = kAudioFormatLinearPCM;

        _recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        _recordFormat.mBitsPerChannel = 16;
        _recordFormat.mBytesPerPacket = _recordFormat.mBytesPerFrame = (_recordFormat.mBitsPerChannel / 8) * _recordFormat.mChannelsPerFrame;
        _recordFormat.mFramesPerPacket = 1;

        //初始化音訊輸入佇列
        AudioQueueNewInput(&_recordFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &_audioQueue);

        //計算估算的快取區大小
        int frames = (int)ceil(kDefaultBufferDurationSeconds * _recordFormat.mSampleRate);
        int bufferByteSize = frames * _recordFormat.mBytesPerFrame;

        NSLog(@"快取區大小%d",bufferByteSize);

        //建立緩衝器
        for (int i = 0; i < QUEUE_BUFFER_SIZE; i++){
            AudioQueueAllocateBuffer(_audioQueue, bufferByteSize, &_audioBuffers[i]);
            AudioQueueEnqueueBuffer(_audioQueue, _audioBuffers[i], 0, NULL);
        }
    }
    return self;
}

-(void)startRecording
{
    // 開始錄音
    AudioQueueStart(_audioQueue, NULL);
    isRecording = YES;
}

void inputBufferHandler(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime,UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc)
{
    if (inNumPackets > 0) {
        ESARecord *recorder = (__bridge ESARecord*)inUserData;
        [recorder processAudioBuffer:inBuffer withQueue:inAQ];
    }
    
    if (isRecording) {
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}

- (void)processAudioBuffer:(AudioQueueBufferRef )audioQueueBufferRef withQueue:(AudioQueueRef )audioQueueRef
{
    NSMutableData * dataM = [NSMutableData dataWithBytes:audioQueueBufferRef->mAudioData length:audioQueueBufferRef->mAudioDataByteSize];
    
    if (dataM.length < 960) { //處理長度小於960的情況,此處是補00
        Byte byte[] = {0x00};
        NSData * zeroData = [[NSData alloc] initWithBytes:byte length:1];
        for (NSUInteger i = dataM.length; i < 960; i++) {
            [dataM appendData:zeroData];
        }
    }

    // NSLog(@"實時錄音的資料--%@", dataM);
    //此處是發通知將dataM 傳遞出去
    [[NSNotificationCenter defaultCenter] postNotificationName:@"EYRecordNotifacation" object:@{@"data" : dataM}];
}

-(void)stopRecording
{
    if (isRecording)
    {
        isRecording = NO;
        
        //停止錄音佇列和移除緩衝區,以及關閉session,這裡無需考慮成功與否
        AudioQueueStop(_audioQueue, true);
        //移除緩衝區,true代表立即結束錄製,false代表將緩衝區處理完再結束
        AudioQueueDispose(_audioQueue, true);
    }
    NSLog(@"停止錄音");
}

@end