都說iOS最惡心的部分是流媒體,其中惡心的惡心之處更在即時語音。
所以我們先不談即時語音,研究一下,iOS中聲音采集與播放的實現。
要在iOS設備上實現錄音和播放功能,蘋果提供了簡單的做法,那就是利用AVAudioRecorder和AVAudioPlayer。度娘大多數也是如此。但是這種方法有很大的局限性。單說說這種做法:錄音,首先得設置錄音文件路徑,然后錄音數據直接寫入了文件。播放也是首先給出文件路徑,等到音頻整個加載完成了,才能開始播放。這相當不靈活。
我的做法是利用音頻隊列AudioQueue,將聲音暫存至緩沖區,然后從緩沖區取出音頻數據,進行播放。
聲音采集:
使用AudioQueue框架以隊列的形式處理音頻數據。因此使用時需要給隊列分配緩存空間,由回調(Callback)函數完成向隊列緩存讀寫音頻數據的功能。
一個Recording Audio Queue,包括Buffer(緩沖器)組成的Buffer Queue(緩沖隊列),以及一個Callback(回調)。實現主要步驟為:
- 設置音頻的參數
- 准備並啟動聲音采集的音頻隊列
- 在回調函數中處理采集到的音頻Buffer,在這里是暫存在了一個Byte數組里,提供給播放端使用
Record.h #import <Foundation/Foundation.h> #import <AudioToolbox/AudioToolbox.h> #import <CoreAudio/CoreAudioTypes.h> #import "AudioConstant.h" // use Audio Queue typedef struct AQCallbackStruct { AudioStreamBasicDescription mDataFormat; AudioQueueRef queue; AudioQueueBufferRef mBuffers[kNumberBuffers]; AudioFileID outputFile; unsigned long frameSize; long long recPtr; int run; } AQCallbackStruct; @interface Record : NSObject { AQCallbackStruct aqc; AudioFileTypeID fileFormat; long audioDataLength; Byte audioByte[999999]; long audioDataIndex; } - (id) init; - (void) start; - (void) stop; - (void) pause; - (Byte *) getBytes; - (void) processAudioBuffer:(AudioQueueBufferRef) buffer withQueue:(AudioQueueRef) queue; @property (nonatomic, assign) AQCallbackStruct aqc; @property (nonatomic, assign) long audioDataLength; @end
Record.mm #import "Record.h" @implementation Record @synthesize aqc; @synthesize audioDataLength; static void AQInputCallback (void * inUserData, AudioQueueRef inAudioQueue, AudioQueueBufferRef inBuffer, const AudioTimeStamp * inStartTime, unsigned long inNumPackets, const AudioStreamPacketDescription * inPacketDesc) { Record * engine = (__bridge Record *) inUserData; if (inNumPackets > 0) { [engine processAudioBuffer:inBuffer withQueue:inAudioQueue]; } if (engine.aqc.run) { AudioQueueEnqueueBuffer(engine.aqc.queue, inBuffer, 0, NULL); } } - (id) init { self = [super init]; if (self) { aqc.mDataFormat.mSampleRate = kSamplingRate; aqc.mDataFormat.mFormatID = kAudioFormatLinearPCM; aqc.mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger |kLinearPCMFormatFlagIsPacked; aqc.mDataFormat.mFramesPerPacket = 1; aqc.mDataFormat.mChannelsPerFrame = kNumberChannels; aqc.mDataFormat.mBitsPerChannel = kBitsPerChannels; aqc.mDataFormat.mBytesPerPacket = kBytesPerFrame; aqc.mDataFormat.mBytesPerFrame = kBytesPerFrame; aqc.frameSize = kFrameSize; AudioQueueNewInput(&aqc.mDataFormat, AQInputCallback, (__bridge void *)(self), NULL, kCFRunLoopCommonModes,0, &aqc.queue); for (int i=0;i<kNumberBuffers;i++) { AudioQueueAllocateBuffer(aqc.queue, aqc.frameSize, &aqc.mBuffers[i]); AudioQueueEnqueueBuffer(aqc.queue, aqc.mBuffers[i], 0, NULL); } aqc.recPtr = 0; aqc.run = 1; } audioDataIndex = 0; return self; } - (void) dealloc { AudioQueueStop(aqc.queue, true); aqc.run = 0; AudioQueueDispose(aqc.queue, true); } - (void) start { AudioQueueStart(aqc.queue, NULL); } - (void) stop { AudioQueueStop(aqc.queue, true); } - (void) pause { AudioQueuePause(aqc.queue); } - (Byte *)getBytes { return audioByte; } - (void) processAudioBuffer:(AudioQueueBufferRef) buffer withQueue:(AudioQueueRef) queue { NSLog(@"processAudioData :%ld", buffer->mAudioDataByteSize); //處理data:忘記oc怎么copy內存了,於是采用的C++代碼,記得把類后綴改為.mm。同Play memcpy(audioByte+audioDataIndex, buffer->mAudioData, buffer->mAudioDataByteSize); audioDataIndex +=buffer->mAudioDataByteSize; audioDataLength = audioDataIndex; } @end
聲音播放:
同采集一樣,播放主要步驟如下:
- 設置音頻參數(需和采集時設置參數一樣)
- 取得緩存的音頻Buffer
- 准備並啟動聲音播放的音頻隊列
- 在回調函數中處理Buffer
Play.h #import <Foundation/Foundation.h> #import <AudioToolbox/AudioToolbox.h> #import "AudioConstant.h" @interface Play : NSObject { //音頻參數 AudioStreamBasicDescription audioDescription; // 音頻播放隊列 AudioQueueRef audioQueue; // 音頻緩存 AudioQueueBufferRef audioQueueBuffers[QUEUE_BUFFER_SIZE]; } -(void)Play:(Byte *)audioByte Length:(long)len; @end
Play.mm #import "Play.h" @interface Play() { Byte *audioByte; long audioDataIndex; long audioDataLength; } @end @implementation Play //回調函數(Callback)的實現 static void BufferCallback(void *inUserData,AudioQueueRef inAQ,AudioQueueBufferRef buffer){ NSLog(@"processAudioData :%u", (unsigned int)buffer->mAudioDataByteSize); Play* player=(__bridge Play*)inUserData; [player FillBuffer:inAQ queueBuffer:buffer]; } //緩存數據讀取方法的實現 -(void)FillBuffer:(AudioQueueRef)queue queueBuffer:(AudioQueueBufferRef)buffer { if(audioDataIndex + EVERY_READ_LENGTH < audioDataLength) { memcpy(buffer->mAudioData, audioByte+audioDataIndex, EVERY_READ_LENGTH); audioDataIndex += EVERY_READ_LENGTH; buffer->mAudioDataByteSize =EVERY_READ_LENGTH; AudioQueueEnqueueBuffer(queue, buffer, 0, NULL); } } -(void)SetAudioFormat { ///設置音頻參數 audioDescription.mSampleRate = kSamplingRate;//采樣率 audioDescription.mFormatID = kAudioFormatLinearPCM; audioDescription.mFormatFlags = kAudioFormatFlagIsSignedInteger;//|kAudioFormatFlagIsNonInterleaved; audioDescription.mChannelsPerFrame = kNumberChannels; audioDescription.mFramesPerPacket = 1;//每一個packet一偵數據 audioDescription.mBitsPerChannel = kBitsPerChannels;//av_get_bytes_per_sample(AV_SAMPLE_FMT_S16)*8;//每個采樣點16bit量化 audioDescription.mBytesPerFrame = kBytesPerFrame; audioDescription.mBytesPerPacket = kBytesPerFrame; [self CreateAudioQueue]; } -(void)CreateAudioQueue { [self Cleanup]; //使用player的內部線程播 AudioQueueNewOutput(&audioDescription, BufferCallback, (__bridge void *)(self), nil, nil, 0, &audioQueue); if(audioQueue) { ////添加buffer區 for(int i=0;i<QUEUE_BUFFER_SIZE;i++) { int result = AudioQueueAllocateBuffer(audioQueue, EVERY_READ_LENGTH, &audioQueueBuffers[i]); ///創建buffer區,MIN_SIZE_PER_FRAME為每一偵所需要的最小的大小,該大小應該比每次往buffer里寫的最大的一次還大 NSLog(@"AudioQueueAllocateBuffer i = %d,result = %d",i,result); } } } -(void)Cleanup { if(audioQueue) { NSLog(@"Release AudioQueueNewOutput"); [self Stop]; for(int i=0; i < QUEUE_BUFFER_SIZE; i++) { AudioQueueFreeBuffer(audioQueue, audioQueueBuffers[i]); audioQueueBuffers[i] = nil; } audioQueue = nil; } } -(void)Stop { NSLog(@"Audio Player Stop"); AudioQueueFlush(audioQueue); AudioQueueReset(audioQueue); AudioQueueStop(audioQueue,TRUE); } -(void)Play:(Byte *)byte Length:(long)len { [self Stop]; audioByte = byte; audioDataLength = len; NSLog(@"Audio Play Start >>>>>"); [self SetAudioFormat]; AudioQueueReset(audioQueue); audioDataIndex = 0; for(int i=0; i<QUEUE_BUFFER_SIZE; i++) { [self FillBuffer:audioQueue queueBuffer:audioQueueBuffers[i]]; } AudioQueueStart(audioQueue, NULL); } @end
以上,實現了通過內存緩存,聲音的采集和播放,包括了聲音采集,暫停,結束,播放等主要流程。
PS:由於本人水品有限加之這方面資料較少,只跑通了正常流程,暫時沒做異常處理。采集的聲音Buffer限定大小每次只有十來秒鍾的樣子,這個留給需要的人自己去優化了。