ios利用mic采集Pcm轉(zhuǎn)為AAC闰围,AudioQueue、AudioUnit(流式)

轉(zhuǎn)載既峡、原文地址:ios利用mic采集Pcm轉(zhuǎn)為AAC辫诅,AudioQueue、AudioUnit(流式)

本例需求:將Mic采集的PCM轉(zhuǎn)成AAC涧狮,可得到兩種不同數(shù)據(jù),本例采用AudioQueue/AudioUnit兩種方式存儲,即: 可采集到兩種聲音數(shù)據(jù),一種為PCM,一種為轉(zhuǎn)換后的AAC.

原理:由于公司需求更改為Mic采集的pcm一路提供給WebRTC使用炕矮,另一路將pcm轉(zhuǎn)為aac嫁赏,將aac提供給直播用的API袭蝗。因此應(yīng)該先讓Mic采集原始pcm數(shù)據(jù),采用AudioQueue/AudioUnit兩種方式采集字柠,然后在回調(diào)函數(shù)中將其轉(zhuǎn)換為aac提供給C++API


image

本例中僅包含部分代碼,建議下載代碼詳細(xì)看,在關(guān)鍵代碼中都有注釋中可以看到難理解的含義.


GitHub地址(附代碼) : PCM->AAC

簡書地址 : PCM->AAC

博客地址 : PCM->AAC

掘金地址 : PCM->AAC


實(shí)現(xiàn)方式:(下文兩種實(shí)現(xiàn)方式涉枫,挑選自己適合的)

1.AudioQueue : 若對延遲要求不高邢滑,可實(shí)現(xiàn)錄制,播放愿汰,暫停困后,回退,同步衬廷,轉(zhuǎn)換(PCM->AAC等)等功能可采用這種方式

2.AudioUnit : 比AudioQueue更加底層摇予,可實(shí)現(xiàn)高性能,低延遲吗跋,并且包括去除回聲侧戴,混音等等功能。

AudioQueue為什么會出現(xiàn)波動的情況跌宛?解決方法酗宋?這種波動的原因是在Audio Queue的底層產(chǎn)生的,之前說過疆拘,Audio ToolBox是基于Audio Unit的蜕猫,回調(diào)函數(shù)的波動要到底層才能解決。


一.本文需要基本知識點(diǎn)

C語言相關(guān)函數(shù):

1.memset:
原型: void * memset(void * __b, int __c, size_t __len);
解釋:將s中當(dāng)前位置后面的n個(gè)字節(jié)(typedef unsigned int size_t) 用ch替換并返回s
作用:在一段內(nèi)存塊中填充某個(gè)特定的值哎迄,它是對較大的結(jié)構(gòu)體或數(shù)組進(jìn)行清零操作的一種最快方法回右。

2.memcpy:
原型: void * memcpy(void * dest, const void * src, size_t n);
解釋:從源src所指的內(nèi)存地址的起始位置開始拷貝n個(gè)字節(jié)到目標(biāo)dest所指的內(nèi)存地址的起始位置中

3.void free(void *);
解釋:釋放內(nèi)存稀颁,需要將malloc出來的內(nèi)存統(tǒng)統(tǒng)釋放掉,對于結(jié)構(gòu)體要先將結(jié)構(gòu)體中malloc出來的釋放掉最后再釋放掉結(jié)構(gòu)體本身楣黍。

OC 中部分知識點(diǎn):

1.OSStaus:狀態(tài)碼匾灶,如果沒有錯(cuò)誤返回0:(即noErr)

2.AudioFormatGetPropertyInfo:

原型: 
AudioFormatGetPropertyInfo(
                            AudioFormatPropertyID   inPropertyID,
                            UInt32                  inSpecifierSize,
                            const void * __nullable inSpecifier,
                            UInt32 *                outPropertyDataSize);

* 作用:檢索給定屬性的信息,比如編碼器目標(biāo)格式的size等

3.AudioSessionGetProperty:

原型: 
extern OSStatus
AudioSessionGetProperty(    
                                AudioSessionPropertyID     inID,
                                UInt32                     *ioDataSize,
                                void                       *outData);

* 作用:獲取指定AudioSession對象的inID屬性的值(比如采樣率租漂,聲道數(shù)等等)

4.AudioUnitSetProperty

extern OSStatus
AudioUnitSetProperty(  AudioUnit               inUnit,
                            AudioUnitPropertyID     inID, 
                            AudioUnitScope         inScope,
                            AudioUnitElement           inElement,
                            const void * __nullable inData,
                            UInt32                     inDataSize)              
* 作用:設(shè)置AudioUnit特定屬性的值阶女,其中scope,element不理解可參考下文audio unit概念部分,這里可以設(shè)置音頻流的各種參數(shù),比如采樣頻率哩治、量化位數(shù)秃踩、通道個(gè)數(shù)、每包中幀的個(gè)數(shù)等等

音頻基礎(chǔ)知識

  1. AVFoundation框架中的AVAudioPlayer和AVAudioRecorder類业筏,用法簡單憔杨,但是不支持流式,也就意味著在播放音頻前蒜胖,必須等到整個(gè)音頻加載完成后消别,才能開始播放音頻;錄音時(shí)台谢,也必須等到錄音結(jié)束后才能獲得錄音數(shù)據(jù)寻狂。

  2. 在iOS和Mac OS X中,音頻隊(duì)列Audio Queues是一個(gè)用來錄制和播放音頻的軟件對象朋沮,也就是說蛇券,可以用來錄音和播放,錄音能夠獲取實(shí)時(shí)的PCM原始音頻數(shù)據(jù)樊拓。

  3. 數(shù)據(jù)介紹

(1)In CBR (constant bit rate) formats, such as linear PCM and IMA/ADPCM, all packets are the same size.

(2)In VBR (variable bit rate) formats, such as AAC, Apple Lossless, and MP3, all packets have the same number of frames but the number of bits in each sample value can vary.

(3)In VFR (variable frame rate) formats, packets have a varying number of frames. There are no commonly used formats of this type.

  1. 概念:

(1)音頻文件的組成:文件格式(或者音頻容器)+數(shù)據(jù)格式(或者音頻編碼)

知識點(diǎn):

  • 文件格式是用于形容文件本身的格式纠亚,可以通過多種不同方法為真正的音頻數(shù)據(jù)編碼,例如CAF文件便是一種文件格式筋夏,它能夠包含MP3格式蒂胞,線性PCM以及其他數(shù)據(jù)格式音頻
    線性PCM:這是表示線性脈沖編碼機(jī)制,主要是描寫用于將模擬聲音數(shù)據(jù)轉(zhuǎn)換成數(shù)組格式的技術(shù)叁丧,簡單地說也就是未壓縮的數(shù)據(jù)啤誊。因?yàn)閿?shù)據(jù)是未壓縮的岳瞭,所以我們便可以最快速地播放出音頻拥娄,而如果空間不是問題的話這便是iPhone 音頻的優(yōu)先代碼選擇

(2).音頻文件計(jì)算大小
簡述:聲卡對聲音的處理質(zhì)量可以用三個(gè)基本參數(shù)來衡量,即采樣頻率瞳筏,采樣位數(shù)和聲道數(shù)稚瘾。

知識點(diǎn):

  • 采樣頻率:單位時(shí)間內(nèi)采樣次數(shù)。采樣頻率越大姚炕,采樣點(diǎn)之間的間隔就越小摊欠,數(shù)字化后得到的聲音就越逼真丢烘,但相應(yīng)的數(shù)據(jù)量就越大,聲卡一般提供11.025kHz,22.05kHz和44.1kHz等不同的采樣頻率些椒。

  • 采樣位數(shù):記錄每次采樣值數(shù)值大小的位數(shù)播瞳。采樣位數(shù)通常有8bits或16bits兩種,采樣位數(shù)越大免糕,所能記錄的聲音變化度就越細(xì)膩赢乓,相應(yīng)的數(shù)據(jù)量就越大。

  • 聲道數(shù):處理的聲音是單聲道還是立體聲石窑。單聲道在聲音處理過程中只有單數(shù)據(jù)流牌芋,而立體聲則需要左右聲道的兩個(gè)數(shù)據(jù)流。顯然松逊,立體聲的效果要好躺屁,但相應(yīng)數(shù)據(jù)量要比單聲道數(shù)據(jù)量加倍。

  • 聲音數(shù)據(jù)量的計(jì)算公式:數(shù)據(jù)量(字節(jié) / 秒)=(采樣頻率(Hz)* 采樣位數(shù)(bit)* 聲道數(shù))/ 8
    單聲道的聲道數(shù)為1经宏,立體聲的聲道數(shù)為2. 字節(jié)B犀暑,1MB=1024KB = 1024*1024B

(3)

  1. CoreAudio 介紹

    image

    (1). CoreAudio分為三層結(jié)構(gòu),如上圖
    1.最底層的I/O Kit, MIDI, HAL等用于直接與硬件相關(guān)操作烁兰,一般來說用不到母怜。
    2.中間層服務(wù)是對數(shù)據(jù)格式的轉(zhuǎn)換,對硬盤執(zhí)行讀寫操作缚柏,解析流苹熏,使用插件等。

  • 其中AudioConverter Services 可實(shí)現(xiàn)不同音頻格式的轉(zhuǎn)碼币喧,如PCM->AAC等
  • Audio File Services支持讀寫音頻數(shù)據(jù)從硬盤
  • Audio Unit Services and Audio Processing Graph Services 可實(shí)現(xiàn)使應(yīng)用程序處理數(shù)字信號轨域,完成一些插件功能,如均衡器和混聲器等杀餐。
  • Audio File Stream Services 可以使程序解析流干发,如播放一段來自網(wǎng)絡(luò)的音頻。
  • Audio Format Services 幫助應(yīng)用程序管理音頻格式相關(guān)操作
    3.最高層是用基于底層實(shí)現(xiàn)的部分功能史翘,使用相對簡單枉长。
  • Audio Queue Services 可實(shí)現(xiàn)錄音,播放琼讽,暫停必峰,同步音頻等功能
  • AVAudioPlayer 提供簡單地OC接口對于音頻的播放與暫停,功能較為局限钻蹬。
  • OpenAL 實(shí)現(xiàn)三維混音音頻單元頂部吼蚁,適合開發(fā)游戲

(2).Audio Data Formats:通過設(shè)置一組屬性代碼可以和操作系統(tǒng)支持的任何格式一起工作。(包括采樣率问欠,比特率)肝匆,對于AudioQueue與AudioUnit設(shè)置略有不同粒蜈。

struct AudioStreamBasicDescription
{
   Float64             mSampleRate;        // 采樣率 :Hz
   AudioFormatID       mFormatID;          // 采樣數(shù)據(jù)的類型,PCM,AAC等
   AudioFormatFlags    mFormatFlags;       // 每種格式特定的標(biāo)志旗国,無損編碼 枯怖,0表示沒有
   UInt32              mBytesPerPacket;    // 一個(gè)數(shù)據(jù)包中的字節(jié)數(shù)
   UInt32              mFramesPerPacket;   // 一個(gè)數(shù)據(jù)包中的幀數(shù),每個(gè)packet的幀數(shù)能曾。如果是未壓縮的音頻數(shù)據(jù)嫁怀,值是1。動態(tài)幀率格式借浊,這個(gè)值是一個(gè)較大的固定數(shù)字塘淑,比如說AAC的1024。如果是動態(tài)大小幀數(shù)(比如Ogg格式)設(shè)置為0蚂斤。
   UInt32              mBytesPerFrame;     // 每一幀中的字節(jié)數(shù)
   UInt32              mChannelsPerFrame;  // 每一幀數(shù)據(jù)中的通道數(shù)存捺,單聲道為1,立體聲為2
   UInt32              mBitsPerChannel;    // 每個(gè)通道中的位數(shù)曙蒸,1byte = 8bit
   UInt32              mReserved;          // 8字節(jié)對齊捌治,填0
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

---------------------------- Audio Queue ---------------------------

二.AudioQueue

.音頻隊(duì)列 — 詳細(xì)請參考 Audio Queue,該文章中已有詳細(xì)描述,不再重復(fù)介紹纽窟,不懂請參考肖油。

1.簡述:在iOS和Mac OS X中,音頻隊(duì)列是一個(gè)用來錄制和播放音頻的軟件對象臂港,他用AudioQueueRef這個(gè)不透明數(shù)據(jù)類型來表示森枪,該類型在AudioQueue.h頭文件中聲明。

2.工作:

  • 連接音頻硬件
  • 內(nèi)存管理
  • 根據(jù)需要為已壓縮的音頻格式引入編碼器
  • 媒體的錄制或播放

你可以將音頻隊(duì)列配合其他Core Audio的接口使用审孽,再加上相對少量的自定義代碼就可以在你的應(yīng)用程序中創(chuàng)建一套完整的數(shù)字音頻錄制或播放解決方案县袱。

3.結(jié)構(gòu):

  • 一組音頻隊(duì)列緩沖區(qū)(audio queue buffers),每個(gè)音頻隊(duì)列緩沖區(qū)都是一個(gè)存儲音頻數(shù)據(jù)的臨時(shí)倉庫

  • 一個(gè)緩沖區(qū)隊(duì)列(buffer queue)佑力,一個(gè)包含音頻隊(duì)列緩沖區(qū)的有序列表

  • 一個(gè)你自己編寫的音頻隊(duì)列回調(diào)函數(shù)(audio queue callback)

它的架構(gòu)很大程度上依賴于這個(gè)音頻隊(duì)列是用來錄制還是用來播放的式散。不同之處在于音頻隊(duì)列如何連接到它的輸入和輸入,還有它的回調(diào)函數(shù)所扮演的角色打颤。

4.調(diào)用步驟暴拄,首先將項(xiàng)目設(shè)置為MRC,在控制器中配置audioSession基本設(shè)置(基本設(shè)置,不會谷歌)编饺,導(dǎo)入該頭文件乖篷,直接在需要時(shí)機(jī)調(diào)用該類startRecord與stopRecord方法,另外還提供了生成錄音文件的功能反肋,具體參考github中的代碼那伐。

本例中涉及的一些宏定義,具體可以下載代碼詳細(xì)看
#define kBufferDurationSeconds              .5
#define kXDXRecoderAudioBytesPerPacket      2
#define kXDXRecoderAACFramesPerPacket       1024
#define kXDXRecoderPCMTotalPacket           512
#define kXDXRecoderPCMFramesPerPacket       1
#define kXDXRecoderConverterEncodeBitRate   64000
#define kXDXAudioSampleRate                 48000.0

(1).設(shè)置AudioStreamBasicDescription 基本信息

-(void)startRecorder {
    // Reset pcm_buffer to save convert handle, 每次開始音頻會話前初始化pcm_buffer, pcm_buffer用來在捕捉聲音的回調(diào)中存儲累加的PCM原始數(shù)據(jù)
    memset(pcm_buffer, 0, pcm_buffer_size);
    pcm_buffer_size = 0;
    frameCount      = 0;

// 是否正在錄制
    if (isRunning) {
        // log4cplus_info("pcm", "Start recorder repeat");
        return;
    }

// 本例中采用log4打印log信息,若你沒有可以不用石蔗,刪除有關(guān)Log4的語句
    // log4cplus_info("pcm", "starup PCM audio encoder");

// 設(shè)置采集的數(shù)據(jù)的類型為PCM
    [self setUpRecoderWithFormatID:kAudioFormatLinearPCM];

    OSStatus status          = 0;
    UInt32   size            = sizeof(dataFormat);

    // 編碼器轉(zhuǎn)碼設(shè)置
    [self convertBasicSetting];

    // 這個(gè)if語句用來檢測是否初始化本例對象成功,如果不成功重啟三次,三次后如果失敗可以進(jìn)行其他處理
    if (err != nil) {
        NSString *error = nil;
        for (int i = 0; i < 3; i++) {
            usleep(100*1000);
            error = [self convertBasicSetting];
            if (error == nil) break;
        }
        // if init this class failed then restart three times , if failed again,can handle at there
//        [self exitWithErr:error];
    }

    // 新建一個(gè)隊(duì)列,第二個(gè)參數(shù)注冊回調(diào)函數(shù)罕邀,第三個(gè)防止內(nèi)存泄露
    status =  AudioQueueNewInput(&dataFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &mQueue);
    // log4cplus_info("pcm","AudioQueueNewInput status:%d",(int)status);

// 獲取隊(duì)列屬性
    status = AudioQueueGetProperty(mQueue, kAudioQueueProperty_StreamDescription, &dataFormat, &size);
    // log4cplus_info("pcm","AudioQueueNewInput status:%u",(unsigned int)dataFormat.mFormatID);

// 這里將頭信息添加到寫入文件中,若文件數(shù)據(jù)為CBR,不需要添加养距,為VBR需要添加
    [self copyEncoderCookieToFile];

    //    可以計(jì)算獲得诉探,在這里使用的是固定大小
    //    bufferByteSize = [self computeRecordBufferSizeFrom:&dataFormat andDuration:kBufferDurationSeconds];

    // log4cplus_info("pcm","pcm raw data buff number:%d, channel number:%u",
                   kNumberQueueBuffers,
                   dataFormat.mChannelsPerFrame);

// 設(shè)置三個(gè)音頻隊(duì)列緩沖區(qū)
    for (int i = 0; i != kNumberQueueBuffers; i++) {
    // 注意:為每個(gè)緩沖區(qū)分配大小,可根據(jù)具體需求進(jìn)行修改,但是一定要注意必須滿足轉(zhuǎn)換器的需求,轉(zhuǎn)換器只有每次給1024幀數(shù)據(jù)才會完成一次轉(zhuǎn)換,如果需求為采集數(shù)據(jù)量較少則用本例提供的pcm_buffer對數(shù)據(jù)進(jìn)行累加后再處理
        status = AudioQueueAllocateBuffer(mQueue, kXDXRecoderPCMTotalPacket*kXDXRecoderAudioBytesPerPacket*dataFormat.mChannelsPerFrame, &mBuffers[i]);
    // 入隊(duì)
        status = AudioQueueEnqueueBuffer(mQueue, mBuffers[i], 0, NULL);
    }

    isRunning  = YES;
    hostTime   = 0;

    status     =  AudioQueueStart(mQueue, NULL);
    log4cplus_info("pcm","AudioQueueStart status:%d",(int)status);
}

初始化輸出流的結(jié)構(gòu)體描述

struct AudioStreamBasicDescription
{
   Float64             mSampleRate;        // 采樣率 :Hz
   AudioFormatID       mFormatID;          // 采樣數(shù)據(jù)的類型棍厌,PCM,AAC等
   AudioFormatFlags    mFormatFlags;       // 每種格式特定的標(biāo)志肾胯,無損編碼 ,0表示沒有
   UInt32              mBytesPerPacket;    // 一個(gè)數(shù)據(jù)包中的字節(jié)數(shù)
   UInt32              mFramesPerPacket;   // 一個(gè)數(shù)據(jù)包中的幀數(shù)耘纱,每個(gè)packet的幀數(shù)敬肚。如果是未壓縮的音頻數(shù)據(jù),值是1束析。動態(tài)幀率格式艳馒,這個(gè)值是一個(gè)較大的固定數(shù)字,比如說AAC的1024员寇。如果是動態(tài)大小幀數(shù)(比如Ogg格式)設(shè)置為0弄慰。
   UInt32              mBytesPerFrame;     // 每一幀中的字節(jié)數(shù)
   UInt32              mChannelsPerFrame;  // 每一幀數(shù)據(jù)中的通道數(shù),單聲道為1蝶锋,立體聲為2
   UInt32              mBitsPerChannel;    // 每個(gè)通道中的位數(shù)陆爽,1byte = 8bit
   UInt32              mReserved;          // 8字節(jié)對齊,填0
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

注意: kNumberQueueBuffers扳缕,音頻隊(duì)列可以使用任意數(shù)量的緩沖區(qū)慌闭。你的應(yīng)用程序制定它的數(shù)量。一般情況下這個(gè)數(shù)字是3躯舔。這樣就可以讓給一個(gè)忙于將數(shù)據(jù)寫入磁盤贡必,同時(shí)另一個(gè)在填充新的音頻數(shù)據(jù),第三個(gè)緩沖區(qū)在需要做磁盤I/O延遲補(bǔ)償?shù)臅r(shí)候可用

如何使用AudioQueue:

  1. 創(chuàng)建輸入隊(duì)列AudioQueueNewInput
  2. 分配buffers
  3. 入隊(duì):AudioQueueEnqueueBuffer
  4. 回調(diào)函數(shù)采集音頻數(shù)據(jù)
  5. 出隊(duì)

AudioQueueNewInput

// 作用:創(chuàng)建一個(gè)音頻隊(duì)列為了錄制音頻數(shù)據(jù)
原型:extern OSStatus             
        AudioQueueNewInput( const AudioStreamBasicDescription   *inFormat, 同上
                            AudioQueueInputCallback             inCallbackProc, // 注冊回調(diào)函數(shù)
                            void * __nullable                   inUserData,     
                            CFRunLoopRef __nullable             inCallbackRunLoop,
                            CFStringRef __nullable              inCallbackRunLoopMode,
                            UInt32                              inFlags,
                            AudioQueueRef __nullable            * __nonnull outAQ)庸毫;

// 這個(gè)函數(shù)的第四個(gè)和第五個(gè)參數(shù)是有關(guān)于線程的仔拟,我設(shè)置成null,代表它默認(rèn)使用內(nèi)部線程去錄音飒赃,而且還是異步的

(2).設(shè)置采集數(shù)據(jù)的格式利花,采集PCM必須按照如下設(shè)置,參考蘋果官方文檔载佳,不同需求自己另行修改

 -(void)setUpRecoderWithFormatID:(UInt32)formatID {
     // Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設(shè)置為官方推薦設(shè)置,可根據(jù)具體需求修改部分設(shè)置
    //setup auido sample rate, channel number, and format ID
    memset(&dataFormat, 0, sizeof(dataFormat));

    UInt32 size = sizeof(dataFormat.mSampleRate);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
                            &size,
                            &dataFormat.mSampleRate);
    dataFormat.mSampleRate = kXDXAudioSampleRate; // 設(shè)置采樣率

    size = sizeof(dataFormat.mChannelsPerFrame);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
                            &size,
                            &dataFormat.mChannelsPerFrame);
    dataFormat.mFormatID = formatID;

    // 關(guān)于采集PCM數(shù)據(jù)是根據(jù)蘋果官方文檔給出的Demo設(shè)置炒事,至于為什么這么設(shè)置可能與采集回調(diào)函數(shù)內(nèi)部實(shí)現(xiàn)有關(guān),修改的話請謹(jǐn)慎
    if (formatID == kAudioFormatLinearPCM)
    {
         /*
          為保存音頻數(shù)據(jù)的方式的說明蔫慧,如可以根據(jù)大端字節(jié)序或小端字節(jié)序挠乳,
          浮點(diǎn)數(shù)或整數(shù)以及不同體位去保存數(shù)據(jù)
          例如對PCM格式通常我們?nèi)缦略O(shè)置:kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked等
          */
        dataFormat.mFormatFlags     = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        // 每個(gè)通道里,一幀采集的bit數(shù)目
        dataFormat.mBitsPerChannel  = 16;
        // 8bit為1byte,即為1個(gè)通道里1幀需要采集2byte數(shù)據(jù)睡扬,再*通道數(shù)盟蚣,即為所有通道采集的byte數(shù)目
        dataFormat.mBytesPerPacket  = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
        // 每個(gè)包中的幀數(shù),采集PCM數(shù)據(jù)需要將dataFormat.mFramesPerPacket設(shè)置為1卖怜,否則回調(diào)不成功
        dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket;
    }
}

(3).將PCM轉(zhuǎn)成AAC一些基本設(shè)置

-(NSString *)convertBasicSetting {
    // 此處目標(biāo)格式其他參數(shù)均為默認(rèn)葛虐,系統(tǒng)會自動計(jì)算蛾娶,否則無法進(jìn)入encodeConverterComplexInputDataProc回調(diào)

    AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
    AudioStreamBasicDescription targetDes;              // 轉(zhuǎn)碼后格式

    // 設(shè)置目標(biāo)格式及基本信息
    memset(&targetDes, 0, sizeof(targetDes));
    targetDes.mFormatID           = kAudioFormatMPEG4AAC;
    targetDes.mSampleRate         = kXDXAudioSampleRate;
    targetDes.mChannelsPerFrame   = dataFormat.mChannelsPerFrame;
    targetDes.mFramesPerPacket    = kXDXRecoderAACFramesPerPacket; // 采集的為AAC需要將targetDes.mFramesPerPacket設(shè)置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個(gè)樣點(diǎn)才開始編碼,這與回調(diào)函數(shù)中inNumPackets有關(guān)烦粒,不可隨意更改

    OSStatus status     = 0;
    UInt32 targetSize   = sizeof(targetDes);
    status              = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &targetSize, &targetDes);
    // log4cplus_info("pcm", "create target data format status:%d",(int)status);

    memset(&_targetDes, 0, sizeof(_targetDes));
    // 賦給全局變量
    memcpy(&_targetDes, &targetDes, targetSize);

    // 選擇軟件編碼
    AudioClassDescription audioClassDes;
    status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                        sizeof(targetDes.mFormatID),
                                        &targetDes.mFormatID,
                                        &targetSize);
    // log4cplus_info("pcm","get kAudioFormatProperty_Encoders status:%d",(int)status);

    // 計(jì)算編碼器容量
    UInt32 numEncoders = targetSize/sizeof(AudioClassDescription);
    // 用數(shù)組存放編碼器內(nèi)容
    AudioClassDescription audioClassArr[numEncoders];
    // 將編碼器屬性賦給數(shù)組
    AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                           sizeof(targetDes.mFormatID),
                           &targetDes.mFormatID,
                           &targetSize,
                           audioClassArr);
    // log4cplus_info("pcm","wrirte audioClassArr status:%d",(int)status);

 // 遍歷數(shù)組祝高,設(shè)置軟編
    for (int i = 0; i < numEncoders; i++) {
        if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
            memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
            break;
        }
    }

    // 防止內(nèi)存泄露   
    if (_encodeConvertRef == NULL) {
        // 新建一個(gè)編碼對象恬偷,設(shè)置原供炎,目標(biāo)格式
        status          = AudioConverterNewSpecific(&sourceDes, &targetDes, 1,
                                                    &audioClassDes, &_encodeConvertRef);

        if (status != noErr) {
//            log4cplus_info("Audio Recoder","new convertRef failed status:%d \n",(int)status);
            return @"Error : New convertRef failed \n";
        }
    }    

// 獲取原始格式大小
    targetSize      = sizeof(sourceDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentInputStreamDescription, &targetSize, &sourceDes);
    // log4cplus_info("pcm","get sourceDes status:%d",(int)status);

// 獲取目標(biāo)格式大小
    targetSize      = sizeof(targetDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &targetSize, &targetDes);;
    // log4cplus_info("pcm","get targetDes status:%d",(int)status);

    // 設(shè)置碼率,需要和采樣率對應(yīng)
    UInt32 bitRate  = kXDXRecoderConverterEncodeBitRate;
    targetSize      = sizeof(bitRate);
    status          = AudioConverterSetProperty(_encodeConvertRef,
                                                kAudioConverterEncodeBitRate,
                                                targetSize, &bitRate);
    // log4cplus_info("pcm","set covert property bit rate status:%d",(int)status);
        if (status != noErr) {
//        log4cplus_info("Audio Recoder","set covert property bit rate status:%d",(int)status);
        return @"Error : Set covert property bit rate failed";
    }

    return nil;

}

AudioFormatGetProperty:

原型: 
extern OSStatus

AudioFormatGetProperty( AudioFormatPropertyID    inPropertyID,
                            UInt32                      inSpecifierSize,
                            const void * __nullable  inSpecifier,
                            UInt32   * __nullable  ioPropertyDataSize,
                            void * __nullabl         outPropertyData);
作用:檢索某個(gè)屬性的值

AudioClassDescription:

指的是一個(gè)能夠?qū)σ粋€(gè)信號或者一個(gè)數(shù)據(jù)流進(jìn)行變換的設(shè)備或者程序妙啃。這里指的變換既包括將 信號或者數(shù)據(jù)流進(jìn)行編碼(通常是為了傳輸档泽、存儲或者加密)或者提取得到一個(gè)編碼流的操作,也包括為了觀察或者處理從這個(gè)編碼流中恢復(fù)適合觀察或操作的形式的操作彬祖。編解碼器經(jīng)常用在視頻會議和流媒體等應(yīng)用中茁瘦。

默認(rèn)情況下,Apple會創(chuàng)建一個(gè)硬件編碼器储笑,如果硬件不可用甜熔,會創(chuàng)建軟件編碼器。

經(jīng)過我的測試突倍,硬件AAC編碼器的編碼時(shí)延很高腔稀,需要buffer大約2秒的數(shù)據(jù)才會開始編碼。而軟件編碼器的編碼時(shí)延就是正常的羽历,只要喂給1024個(gè)樣點(diǎn)焊虏,就會開始編碼。

AudioConverterNewSpecific:
原型: extern OSStatus
AudioConverterNewSpecific(  const AudioStreamBasicDescription * inSourceFormat,
                            const AudioStreamBasicDescription * inDestinationFormat,
                            UInt32                              inNumberClassDescriptions,
                            const AudioClassDescription *       inClassDescriptions,
                            AudioConverterRef __nullable * __nonnull outAudioConverter)秕磷;

解釋:創(chuàng)建一個(gè)轉(zhuǎn)換器
作用:設(shè)置一些轉(zhuǎn)碼基本信息          

AudioConverterSetProperty:
原型:extern OSStatus 
AudioConverterSetProperty(  AudioConverterRef           inAudioConverter,
                            AudioConverterPropertyID    inPropertyID,
                            UInt32                      inPropertyDataSize,
                            const void *                inPropertyData)诵闭;
作用:設(shè)置碼率,需要注意澎嚣,AAC并不是隨便的碼率都可以支持疏尿。比如如果PCM采樣率是44100KHz,那么碼率可以設(shè)置64000bps易桃,如果是16K褥琐,可以設(shè)置為32000bps。

(4).設(shè)置最終音頻文件的頭部信息(此類寫法為將pcm轉(zhuǎn)為AAC的寫法)

-(void)copyEncoderCookieToFile
{
    // Grab the cookie from the converter and write it to the destination file.
    UInt32 cookieSize = 0;
    OSStatus error = AudioConverterGetPropertyInfo(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);

    // If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not.
    // log4cplus_info("cookie","cookie status:%d %d",(int)error, cookieSize);
    if (error == noErr && cookieSize != 0) {
        char *cookie = (char *)malloc(cookieSize * sizeof(char));
        //        UInt32 *cookie = (UInt32 *)malloc(cookieSize * sizeof(UInt32));
        error = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, cookie);
        // log4cplus_info("cookie","cookie size status:%d",(int)error);

        if (error == noErr) {
            error = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie);
            // log4cplus_info("cookie","set cookie status:%d ",(int)error);
            if (error == noErr) {
                UInt32 willEatTheCookie = false;
                error = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
                printf("Writing magic cookie to destination file: %u\n   cookie:%d \n", (unsigned int)cookieSize, willEatTheCookie);
            } else {
                printf("Even though some formats have cookies, some files don't take them and that's OK\n");
            }
        } else {
            printf("Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n");
        }

        free(cookie);
    }
}

Magic cookie 是一種不透明的數(shù)據(jù)格式晤郑,它和壓縮數(shù)據(jù)文件與流聯(lián)系密切敌呈,如果文件數(shù)據(jù)為CBR格式(無損)贸宏,則不需要添加頭部信息,如果為VBR需要添加,// if collect CBR needn't set magic cookie , if collect VBR should set magic cookie, if needn't to convert format that can be setting by audio queue directly.

(5).AudioQueue中注冊的回調(diào)函數(shù)

// AudioQueue中注冊的回調(diào)函數(shù)
static void inputBufferHandler(void *                                 inUserData,
                               AudioQueueRef                          inAQ,
                               AudioQueueBufferRef                    inBuffer,
                               const AudioTimeStamp *                 inStartTime,
                               UInt32                                 inNumPackets,
                               const AudioStreamPacketDescription*    inPacketDesc) {
    // 相當(dāng)于本類對象實(shí)例
    TVURecorder *recoder        = (TVURecorder *)inUserData;

 /*
     inNumPackets 總包數(shù):音頻隊(duì)列緩沖區(qū)大小 (在先前估算緩存區(qū)大小為kXDXRecoderAACFramesPerPacket*2)/ (dataFormat.mFramesPerPacket (采集數(shù)據(jù)每個(gè)包中有多少幀磕洪,此處在初始化設(shè)置中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個(gè)字節(jié)吭练,此處在初始化設(shè)置中為每一幀中兩個(gè)字節(jié))),所以可以根據(jù)該公式計(jì)算捕捉PCM數(shù)據(jù)時(shí)inNumPackets褐鸥。
     注意:如果采集的數(shù)據(jù)是PCM需要將dataFormat.mFramesPerPacket設(shè)置為1线脚,而本例中最終要的數(shù)據(jù)為AAC,因?yàn)楸纠惺褂玫霓D(zhuǎn)換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設(shè)置為1024.也就是采集到的inNumPackets為1赐稽,在轉(zhuǎn)換器中傳入的inNumPackets應(yīng)該為AAC格式下默認(rèn)的1叫榕,在此后寫入文件中也應(yīng)該傳的是轉(zhuǎn)換好的inNumPackets,如果有特殊需求需要將采集的數(shù)據(jù)量小于1024,那么需要將每次捕捉到的數(shù)據(jù)先預(yù)先存儲在一個(gè)buffer中,等到攢夠1024幀再進(jìn)行轉(zhuǎn)換。
     */

    // collect pcm data姊舵,可以在此存儲

    // First case : collect data not is 1024 frame, if collect data not is 1024 frame, we need to save data to pcm_buffer untill 1024 frame
    memcpy(pcm_buffer+pcm_buffer_size, inBuffer->mAudioData, inBuffer->mAudioDataByteSize);
    pcm_buffer_size = pcm_buffer_size + inBuffer->mAudioDataByteSize;
    if(inBuffer->mAudioDataByteSize != kXDXRecoderAACFramesPerPacket*2)
        NSLog(@"write pcm buffer size:%d, totoal buff size:%d", inBuffer->mAudioDataByteSize, pcm_buffer_size);

    frameCount++;

     // Second case : If the size of the data collection is not required, we can let mic collect 1024 frame so that don't need to write firtst case, but it is recommended to write the above code because of agility 

    // if collect data is added to 1024 frame
    if(frameCount == totalFrames) {
        AudioBufferList *bufferList = convertPCMToAAC(recoder);
        pcm_buffer_size = 0;
        frameCount      = 0;

        // free memory
        free(bufferList->mBuffers[0].mData);
        free(bufferList);
        // begin write audio data for record audio only

        // 出隊(duì)
        AudioQueueRef queue = recoder.mQueue;
        if (recoder.isRunning) {
            AudioQueueEnqueueBuffer(queue, inBuffer, 0, NULL);
        }
    }
}

解析回調(diào)函數(shù):相當(dāng)于中斷服務(wù)函數(shù)晰绎,每次錄取到音頻數(shù)據(jù)就進(jìn)入這個(gè)函數(shù)

注意:inNumPackets 總包數(shù):音頻隊(duì)列緩沖區(qū)大小 (在先前估算緩存區(qū)大小為2048)/ (dataFormat.mFramesPerPacket (采集數(shù)據(jù)每個(gè)包中有多少幀,此處在初始化設(shè)置中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個(gè)字節(jié)括丁,此處在初始化設(shè)置中為每一幀中兩個(gè)字節(jié)))

  • inAQ 是調(diào)用回調(diào)函數(shù)的音頻隊(duì)列
  • inBuffer 是一個(gè)被音頻隊(duì)列填充新的音頻數(shù)據(jù)的音頻隊(duì)列緩沖區(qū)荞下,它包含了回調(diào)函數(shù)寫入文件所需要的新數(shù)據(jù)
  • inStartTime 是緩沖區(qū)中的一采樣的參考時(shí)間,對于基本的錄制史飞,你的毀掉函數(shù)不會使用這個(gè)參數(shù)
  • inNumPackets是inPacketDescs參數(shù)中包描述符(packet descriptions)的數(shù)量尖昏,如果你正在錄制一個(gè)VBR(可變比特率(variable bitrate))格式, 音頻隊(duì)列將會提供這個(gè)參數(shù)給你的回調(diào)函數(shù),這個(gè)參數(shù)可以讓你傳遞給AudioFileWritePackets函數(shù). CBR (常量比特率(constant bitrate)) 格式不使用包描述符构资。對于CBR錄制抽诉,音頻隊(duì)列會設(shè)置這個(gè)參數(shù)并且將inPacketDescs這個(gè)參數(shù)設(shè)置為NULL,官方解釋為The number of packets of audio data sent to the callback in the inBuffer parameter.
// PCM -> AAC
AudioBufferList* convertPCMToAAC (AudioQueueBufferRef inBuffer, XDXRecorder *recoder) {

    UInt32   maxPacketSize    = 0;
    UInt32   size             = sizeof(maxPacketSize);
    OSStatus status;

    status = AudioConverterGetProperty(_encodeConvertRef,
                                       kAudioConverterPropertyMaximumOutputPacketSize,
                                       &size,
                                       &maxPacketSize);
    // log4cplus_info("AudioConverter","kAudioConverterPropertyMaximumOutputPacketSize status:%d \n",(int)status);

// 初始化一個(gè)bufferList存儲數(shù)據(jù)
    AudioBufferList *bufferList             = (AudioBufferList *)malloc(sizeof(AudioBufferList));
    bufferList->mNumberBuffers              = 1;
    bufferList->mBuffers[0].mNumberChannels = _targetDes.mChannelsPerFrame;
    bufferList->mBuffers[0].mData           = malloc(maxPacketSize);
    bufferList->mBuffers[0].mDataByteSize   = pcm_buffer_size;

    AudioStreamPacketDescription outputPacketDescriptions;

    /*     
    inNumPackets設(shè)置為1表示編碼產(chǎn)生1幀數(shù)據(jù)即返回吐绵,官方:On entry, the capacity of outOutputData expressed in packets in the converter's output format. On exit, the number of packets of converted data that were written to outOutputData. 在輸入表示輸出數(shù)據(jù)的最大容納能力 在轉(zhuǎn)換器的輸出格式上迹淌,在轉(zhuǎn)換完成時(shí)表示多少個(gè)包被寫入
    */
    UInt32 inNumPackets = 1;
    status = AudioConverterFillComplexBuffer(_encodeConvertRef,
                                             encodeConverterComplexInputDataProc,   // 填充數(shù)據(jù)的回調(diào)函數(shù)
                                             pcm_buffer,        // 音頻隊(duì)列緩沖區(qū)中數(shù)據(jù)
                                             &inNumPackets,     
                                             bufferList,            // 成功后將值賦給bufferList
                                             &outputPacketDescriptions);    // 輸出包包含的一些信息
    log4cplus_info("AudioConverter","set AudioConverterFillComplexBuffer status:%d",(int)status);

    if (recoder.needsVoiceDemo) {
        // if inNumPackets set not correct, file will not normally play. 將轉(zhuǎn)換器轉(zhuǎn)換出來的包寫入文件中,inNumPackets表示寫入文件的起始位置
        OSStatus status = AudioFileWritePackets(recoder.mRecordFile,
                                                FALSE,
                                                bufferList->mBuffers[0].mDataByteSize,
                                                &outputPacketDescriptions,
                                                recoder.mRecordPacket,
                                                &inNumPackets,
                                                bufferList->mBuffers[0].mData);
//        log4cplus_info("write file","write file status = %d",(int)status);
        recoder.mRecordPacket += inNumPackets;  // Used to record the location of the write file,用于記錄寫入文件的位置
    }

    return bufferList;
}

解析

outputPacketDescriptions數(shù)組是每次轉(zhuǎn)換的AAC編碼后各個(gè)包的描述,但這里每次只轉(zhuǎn)換一包數(shù)據(jù)(由傳入的packetSize決定)己单。調(diào)用AudioConverterFillComplexBuffer觸發(fā)轉(zhuǎn)碼唉窃,他的第二個(gè)參數(shù)是填充原始音頻數(shù)據(jù)的回調(diào)。轉(zhuǎn)碼完成后纹笼,會將轉(zhuǎn)碼的數(shù)據(jù)存放在它的第五個(gè)參數(shù)中(bufferList).

// 錄制聲音功能
-(void)startVoiceDemo
{
   NSArray *searchPaths    = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
   NSString *documentPath  = [[searchPaths objectAtIndex:0] stringByAppendingPathComponent:@"VoiceDemo"];
   OSStatus status;

   // Get the full path to our file.
   NSString *fullFileName  = [NSString stringWithFormat:@"%@.%@",[[XDXDateTool shareXDXDateTool] getDateWithFormat_yyyy_MM_dd_HH_mm_ss],@"caf"];
   NSString *filePath      = [documentPath stringByAppendingPathComponent:fullFileName];
   [mRecordFilePath release];
   mRecordFilePath         = [filePath copy];;
   CFURLRef url            = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)filePath, NULL);

   // create the audio file
   status                  = AudioFileCreateWithURL(url, kAudioFileMPEG4Type, &_targetDes, kAudioFileFlags_EraseFile, &mRecordFile);
   if (status != noErr) {
       // log4cplus_info("Audio Recoder","AudioFileCreateWithURL Failed, status:%d",(int)status);
   }

   CFRelease(url);

   // add magic cookie contain header file info for VBR data
   [self copyEncoderCookieToFile];

   mNeedsVoiceDemo         = YES;
   NSLog(@"%s",__FUNCTION__);
}

--------------------------- Audio Unit -----------------------------

1. What is Audio Unit 纹份? AudioUnit官方文檔, 優(yōu)秀博客1

1). AudioUnit是 iOS提供的為了支持混音,均衡廷痘,格式轉(zhuǎn)換蔓涧,實(shí)時(shí)輸入輸出用于錄制,回放牍疏,離線渲染和實(shí)時(shí)回話(VOIP)蠢笋,這讓我們可以動態(tài)加載和使用,即從iOS應(yīng)用程序中接收這些強(qiáng)大而靈活的插件鳞陨。它是iOS音頻中最低層昨寞,所以除非你需要合成聲音的實(shí)時(shí)播放瞻惋,低延遲的I/O,或特定聲音的特定特點(diǎn)援岩。
Audio unit scopes and elements :

image
  • 上圖是一個(gè)AudioUnit的組成結(jié)構(gòu)歼狼,A scope 主要使用到的輸入kAudioUnitScope_Input和輸出kAudioUnitScope_Output。Element 是嵌套在audio unit scope的編程上下文享怀。

    image
  • AudioUnit 的Remote IO有2個(gè)element羽峰,大部分代碼和文獻(xiàn)都用bus代替element,兩者同義添瓷,bus0就是輸出,bus 1代表輸入梅屉,播放音頻文件就是在bus 0傳送數(shù)據(jù),bus 1輸入在Remote IO 默認(rèn)是關(guān)閉的鳞贷,在錄音的狀態(tài)下 需要把bus 1設(shè)置成開啟狀態(tài)坯汤。

  • 我們能使用(kAudioOutputUnitProperty_EnableIO)屬性獨(dú)立地開啟或禁用每個(gè)element,Element 1 直接與音頻輸入硬件相連(麥克風(fēng))搀愧,Element 1 的input scope對我們是不透明的惰聂,來自輸入硬件的音頻數(shù)據(jù)只能在Element 1的output scope中訪問。

  • 同樣的element 0直接和輸出硬件相連(揚(yáng)聲器)咱筛,我們可以將audio數(shù)據(jù)傳輸?shù)絜lement 0的input scope中搓幌,但是output scope對我們是不透明的。

  • 注意:每個(gè)element本身都有一個(gè)輸入范圍和輸出范圍迅箩,因此在代碼中如果不理解可能會比較懵逼溉愁,比如你從input element的 output scope 中受到音頻,并將音頻發(fā)送到output element的intput scope中沙热,如果代碼中不理解叉钥,可以再看看上圖。

2.相關(guān)概念解析

2 - 1. I/O Units : iOS提供了3種I/O Units.

  • The Remote I/O unit 是最常用的篙贸,它連接音頻硬件的輸入和輸出并且提供單個(gè)傳入和傳出音頻樣本值得低延遲訪問投队。還支持硬件音頻格式和應(yīng)用程序音頻格式的轉(zhuǎn)換,通過包含F(xiàn)ormat Converter unit來實(shí)現(xiàn)爵川。
  • The Voice-Processing I/O unit 繼承了the Remote I/O unit 并且增加回聲消除用于VOIP或語音聊天應(yīng)用敷鸦。它還提供了自動增益校正,語音處理的質(zhì)量調(diào)整和靜音的功能寝贡。(本例中用此完成回聲消除)
  • The Generic Output unit 不連接音頻硬件扒披,而是一共一種將處理鏈的輸出發(fā)送到應(yīng)用程序的機(jī)制。通常用來進(jìn)行脫機(jī)音頻處理圃泡。

3. 使用步驟:

1). 導(dǎo)入所需動態(tài)庫與頭文件(At runtime, obtain a reference to the dynamically-linkable library that defines an audio unit you want to use.)

2). 實(shí)例化audio unit(Instantiate the audio unit.)

3). 配置audioUnit的類型去完成特定的需求(Configure the audio unit as required for its type and to accomodate the intent of your app.)

4). 初始化uandio unit(Initialize the audio unit to prepare it to handle audio.
)

5). 開始audio flow(Start audio flow.)

6). 控制audio unit(Control the audio unit.)

7). 結(jié)束后回收audio unit(When finished, deallocate the audio unit.)

4.代碼解析

  • 1). init.
- (void)initAudioComponent {
   OSStatus status;
   // 配置AudioUnit基本信息
   AudioComponentDescription audioDesc;
   audioDesc.componentType         = kAudioUnitType_Output;
   // 如果你的應(yīng)用程序需要去除回聲將componentSubType設(shè)置為kAudioUnitSubType_VoiceProcessingIO碟案,否則根據(jù)需求設(shè)置為其他,在博客中有介紹
   audioDesc.componentSubType      = kAudioUnitSubType_VoiceProcessingIO;//kAudioUnitSubType_VoiceProcessingIO;
   // 蘋果自己的標(biāo)志
   audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
   audioDesc.componentFlags        = 0;
   audioDesc.componentFlagsMask    = 0;

   AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
   // 新建一個(gè)AudioComponent對象颇蜡,只有這步完成才能進(jìn)行后續(xù)步驟价说,所以順序不可顛倒
   status = AudioComponentInstanceNew(inputComponent, &_audioUnit);
   if (status != noErr)  {
       _audioUnit = NULL;
//        log4cplus_info("Audio Recoder", "couldn't create a new instance of AURemoteIO, status : %d \n",status);
   }
}

解析

  • To find an audio unit at runtime, start by specifying its type, subtype, and manufacturer keys in an audio component description data structure. You do this whether using the audio unit or audio processing graph API.
  • 要在運(yùn)行時(shí)找到AudioUnit辆亏,首先要在AudioComponentDescription中指定它的類型,子類型和制作商,AudioComponentFindNext參數(shù)inComponent一般設(shè)置為NULL鳖目,從系統(tǒng)中找到第一個(gè)符合inDesc描述的Component扮叨,如果為其賦值,則從其之后進(jìn)行尋找领迈。AudioUnit實(shí)際上就是一個(gè)AudioComponentInstance實(shí)例對象
  • componentSubType一般可設(shè)置為kAudioUnitSubType_RemoteIO彻磁,如果有特別需求,如本例中要去除回聲狸捅,則使用kAudioUnitSubType_VoiceProcessingIO衷蜓,每種類型作用在2-1中均有描述,不再重復(fù)薪贫。
- (void)initBuffer {
   // 禁用AudioUnit默認(rèn)的buffer而使用我們自己寫的全局BUFFER,用來接收每次采集的PCM數(shù)據(jù)恍箭,Disable AU buffer allocation for the recorder, we allocate our own.
   UInt32 flag     = 0;
   OSStatus status = AudioUnitSetProperty(_audioUnit,
                                          kAudioUnitProperty_ShouldAllocateBuffer,
                                          kAudioUnitScope_Output,
                                          INPUT_BUS,
                                          &flag,
                                          sizeof(flag));
   if (status != noErr) {
//        log4cplus_info("Audio Recoder", "couldn't AllocateBuffer of AudioUnitCallBack, status : %d \n",status);
   }
   _buffList = (AudioBufferList*)malloc(sizeof(AudioBufferList));
   _buffList->mNumberBuffers               = 1;
   _buffList->mBuffers[0].mNumberChannels  = dataFormat.mChannelsPerFrame;
   _buffList->mBuffers[0].mDataByteSize    = kTVURecoderPCMMaxBuffSize * sizeof(short);
   _buffList->mBuffers[0].mData            = (short *)malloc(sizeof(short) * kTVURecoderPCMMaxBuffSize);
}

解析

本例通過禁用AudioUnit默認(rèn)的buffer而使用我們自己寫的全局BUFFER,用來接收每次采集的PCM數(shù)據(jù)刻恭,Disable AU buffer allocation for the recorder, we allocate our own.還有一種寫法是可以使用回調(diào)中提供的ioData存儲采集的數(shù)據(jù)瞧省,這里使用全局的buff是為了供其他地方使用,可根據(jù)需要自行決定采用哪種方式鳍贾,若不采用全局buffer則不可采用上述禁用操作鞍匾。

// 因?yàn)楸纠蛔鲣浺艄δ埽磳?shí)現(xiàn)播放功能骑科,所以沒有設(shè)置播放相關(guān)設(shè)置橡淑。
- (void)setAudioUnitPropertyAndFormat {
    OSStatus status;
    [self setUpRecoderWithFormatID:kAudioFormatLinearPCM];

    // 應(yīng)用audioUnit設(shè)置的格式
    status = AudioUnitSetProperty(_audioUnit,
                                  kAudioUnitProperty_StreamFormat,
                                  kAudioUnitScope_Output,
                                  INPUT_BUS,
                                  &dataFormat,
                                  sizeof(dataFormat));
    if (status != noErr) {
//        log4cplus_info("Audio Recoder", "couldn't set the input client format on AURemoteIO, status : %d \n",status);
    }
    // 去除回聲開關(guān)
    UInt32 echoCancellation;
    AudioUnitSetProperty(_audioUnit,
                         kAUVoiceIOProperty_BypassVoiceProcessing,
                         kAudioUnitScope_Global,
                         0,
                         &echoCancellation,
                         sizeof(echoCancellation));

    // AudioUnit輸入端默認(rèn)是關(guān)閉,需要將他打開
    UInt32 flag = 1;
    status      = AudioUnitSetProperty(_audioUnit,
                                       kAudioOutputUnitProperty_EnableIO,
                                       kAudioUnitScope_Input,
                                       INPUT_BUS,
                                       &flag,
                                       sizeof(flag));
    if (status != noErr) {
//        log4cplus_info("Audio Recoder", "could not enable input on AURemoteIO, status : %d \n",status);
    }
}

-(void)setUpRecoderWithFormatID:(UInt32)formatID {
    // Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設(shè)置為官方推薦設(shè)置,可根據(jù)具體需求修改部分設(shè)置
    //setup auido sample rate, channel number, and format ID
    memset(&dataFormat, 0, sizeof(dataFormat));

    UInt32 size = sizeof(dataFormat.mSampleRate);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
                            &size,
                            &dataFormat.mSampleRate);
    dataFormat.mSampleRate = kXDXAudioSampleRate;

    size = sizeof(dataFormat.mChannelsPerFrame);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
                            &size,
                            &dataFormat.mChannelsPerFrame);
    dataFormat.mFormatID = formatID;
    dataFormat.mChannelsPerFrame = 1;

    if (formatID == kAudioFormatLinearPCM) {
        if (self.releaseMethod == XDXRecorderReleaseMethodAudioQueue) {
            dataFormat.mFormatFlags     = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        }else if (self.releaseMethod == XDXRecorderReleaseMethodAudioQueue) {
            dataFormat.mFormatFlags     = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
        }

        dataFormat.mBitsPerChannel  = 16;
        dataFormat.mBytesPerPacket  = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
        dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket; // 用AudioQueue采集pcm需要這么設(shè)置
    }
}

解析

上述操作針對錄音功能需要對Audio Unit做出對應(yīng)設(shè)置咆爽,首先設(shè)置ASBD采集數(shù)據(jù)為PCM的格式梁棠,需要注意的是如果是使用AudioQueue與AudioUnit的dataFormat.mFormatFlags設(shè)置略有不同,經(jīng)測試必須這樣設(shè)置斗埂,原因暫不詳符糊,設(shè)置完后使用AudioUnitSetProperty應(yīng)用設(shè)置,這里只做錄音呛凶,所以對kAudioOutputUnitProperty_EnableIO 的 kAudioUnitScope_Input 開啟男娄,而對kAudioUnitScope_Output 輸入端輸出的音頻格式進(jìn)行設(shè)置,如果不理解可參照1中概念解析進(jìn)行理解漾稀,kAUVoiceIOProperty_BypassVoiceProcessing則是回聲的開關(guān)模闲。

-(NSString *)convertBasicSetting {
    // 此處目標(biāo)格式其他參數(shù)均為默認(rèn),系統(tǒng)會自動計(jì)算崭捍,否則無法進(jìn)入encodeConverterComplexInputDataProc回調(diào)

    AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
    AudioStreamBasicDescription targetDes;              // 轉(zhuǎn)碼后格式

    // 設(shè)置目標(biāo)格式及基本信息
    memset(&targetDes, 0, sizeof(targetDes));
    targetDes.mFormatID           = kAudioFormatMPEG4AAC;
    targetDes.mSampleRate         = kXDXAudioSampleRate;
    targetDes.mChannelsPerFrame   = dataFormat.mChannelsPerFrame;
    targetDes.mFramesPerPacket    = kXDXRecoderAACFramesPerPacket; // 采集的為AAC需要將targetDes.mFramesPerPacket設(shè)置為1024尸折,AAC軟編碼需要喂給轉(zhuǎn)換器1024個(gè)樣點(diǎn)才開始編碼,這與回調(diào)函數(shù)中inNumPackets有關(guān)殷蛇,不可隨意更改

    OSStatus status     = 0;
    UInt32 targetSize   = sizeof(targetDes);
    status              = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &targetSize, &targetDes);
    // log4cplus_info("pcm", "create target data format status:%d",(int)status);

    memset(&_targetDes, 0, sizeof(_targetDes));
    // 賦給全局變量
    memcpy(&_targetDes, &targetDes, targetSize);

    // 選擇軟件編碼
    AudioClassDescription audioClassDes;
    status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                        sizeof(targetDes.mFormatID),
                                        &targetDes.mFormatID,
                                        &targetSize);
    // log4cplus_info("pcm","get kAudioFormatProperty_Encoders status:%d",(int)status);

    // 計(jì)算編碼器容量
    UInt32 numEncoders = targetSize/sizeof(AudioClassDescription);
    // 用數(shù)組存放編碼器內(nèi)容
    AudioClassDescription audioClassArr[numEncoders];
    // 將編碼器屬性賦給數(shù)組
    AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                           sizeof(targetDes.mFormatID),
                           &targetDes.mFormatID,
                           &targetSize,
                           audioClassArr);
    // log4cplus_info("pcm","wrirte audioClassArr status:%d",(int)status);

 // 遍歷數(shù)組实夹,設(shè)置軟編
    for (int i = 0; i < numEncoders; i++) {
        if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
            memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
            break;
        }
    }

    // 防止內(nèi)存泄露   
    if (_encodeConvertRef == NULL) {
        // 新建一個(gè)編碼對象拣播,設(shè)置原,目標(biāo)格式
        status          = AudioConverterNewSpecific(&sourceDes, &targetDes, 1,
                                                    &audioClassDes, &_encodeConvertRef);

        if (status != noErr) {
//            log4cplus_info("Audio Recoder","new convertRef failed status:%d \n",(int)status);
            return @"Error : New convertRef failed \n";
        }
    }    

// 獲取原始格式大小
    targetSize      = sizeof(sourceDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentInputStreamDescription, &targetSize, &sourceDes);
    // log4cplus_info("pcm","get sourceDes status:%d",(int)status);

// 獲取目標(biāo)格式大小
    targetSize      = sizeof(targetDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &targetSize, &targetDes);;
    // log4cplus_info("pcm","get targetDes status:%d",(int)status);

    // 設(shè)置碼率收擦,需要和采樣率對應(yīng)
    UInt32 bitRate  = kXDXRecoderConverterEncodeBitRate;
    targetSize      = sizeof(bitRate);
    status          = AudioConverterSetProperty(_encodeConvertRef,
                                                kAudioConverterEncodeBitRate,
                                                targetSize, &bitRate);
    // log4cplus_info("pcm","set covert property bit rate status:%d",(int)status);
        if (status != noErr) {
//        log4cplus_info("Audio Recoder","set covert property bit rate status:%d",(int)status);
        return @"Error : Set covert property bit rate failed";
    }

    return nil;

}

解析

設(shè)置原格式與轉(zhuǎn)碼格式并創(chuàng)建_encodeConvertRef轉(zhuǎn)碼器對象完成相關(guān)初始化操作贮配,值得注意的是targetDes.mFramesPerPacket設(shè)置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個(gè)樣點(diǎn)才開始編碼塞赂,不可隨意更改泪勒,原因如下圖,由AAC編碼器決定。

image
- (void)initRecordeCallback {
    // 設(shè)置回調(diào)宴猾,有兩種方式圆存,一種是采集pcm的BUFFER使用系統(tǒng)回調(diào)中的參數(shù),另一種是使用我們自己的仇哆,本例中使用的是自己的沦辙,所以回調(diào)中的ioData為空。

    // 方法1:
    AURenderCallbackStruct recordCallback;
    recordCallback.inputProc        = RecordCallback;
    recordCallback.inputProcRefCon  = (__bridge void *)self;
    OSStatus status                 = AudioUnitSetProperty(_audioUnit,
                                                           kAudioOutputUnitProperty_SetInputCallback,
                                                           kAudioUnitScope_Global,
                                                           INPUT_BUS,
                                                           &recordCallback,
                                                           sizeof(recordCallback));

        // 方法2:
      AURenderCallbackStruct renderCallback;
      renderCallback.inputProc        = RecordCallback;
      renderCallback.inputProcRefCon   = (__bridge void *)self;
      AudioUnitSetProperty(_rioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, & RecordCallback, sizeof(RecordCallback));

    if (status != noErr) {
//        log4cplus_info("Audio Recoder", "Audio Unit set record Callback failed, status : %d \n",status);
    }
}

解析

以上為設(shè)置采集回調(diào)讹剔,有兩種方式油讯,1種為使用我們自己的buffer,這樣需要先在上述initBuffer中禁用系統(tǒng)的buffer延欠,則回調(diào)函數(shù)中每次渲染的為我們自己的buffer陌兑,另一種則是使用系統(tǒng)的buffer,對應(yīng)需要在回調(diào)函數(shù)中將ioData放進(jìn)渲染的函數(shù)中由捎。

static OSStatus RecordCallback(void *inRefCon,
                               AudioUnitRenderActionFlags *ioActionFlags,
                               const AudioTimeStamp *inTimeStamp,
                               UInt32 inBusNumber,
                               UInt32 inNumberFrames,
                               AudioBufferList *ioData) {
/*
      注意:如果采集的數(shù)據(jù)是PCM需要將dataFormat.mFramesPerPacket設(shè)置為1兔综,而本例中最終要的數(shù)據(jù)為AAC,因?yàn)楸纠惺褂玫霓D(zhuǎn)換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設(shè)置為1024.也就是采集到的inNumPackets為1,在轉(zhuǎn)換器中傳入的inNumPackets應(yīng)該為AAC格式下默認(rèn)的1狞玛,在此后寫入文件中也應(yīng)該傳的是轉(zhuǎn)換好的inNumPackets,如果有特殊需求需要將采集的數(shù)據(jù)量小于1024,那么需要將每次捕捉到的數(shù)據(jù)先預(yù)先存儲在一個(gè)buffer中,等到攢夠1024幀再進(jìn)行轉(zhuǎn)換软驰。
 */

    XDXRecorder *recorder = (XDXRecorder *)inRefCon;

    // 將回調(diào)數(shù)據(jù)傳給_buffList
    AudioUnitRender(recorder->_audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, recorder->_buffList);

    void    *bufferData = recorder->_buffList->mBuffers[0].mData;
    UInt32   bufferSize = recorder->_buffList->mBuffers[0].mDataByteSize;
    //    printf("Audio Recoder Render dataSize : %d \n",bufferSize);

    // 由于PCM轉(zhuǎn)成AAC的轉(zhuǎn)換器每次需要有1024個(gè)采樣點(diǎn)(每一幀2個(gè)字節(jié))才能完成一次轉(zhuǎn)換,所以每次需要2048大小的數(shù)據(jù)心肪,這里定義的pcm_buffer用來累加每次存儲的bufferData
    memcpy(pcm_buffer+pcm_buffer_size, bufferData, bufferSize);
    pcm_buffer_size = pcm_buffer_size + bufferSize;

    if(pcm_buffer_size >= kTVURecoderPCMMaxBuffSize) {
        AudioBufferList *bufferList = convertPCMToAAC(recorder);

        // 因?yàn)椴蓸硬豢赡苊看味季珳?zhǔn)的采集到1024個(gè)樣點(diǎn)锭亏,所以如果大于2048大小就先填滿2048,剩下的跟著下一次采集一起送給轉(zhuǎn)換器
        memcpy(pcm_buffer, pcm_buffer + kTVURecoderPCMMaxBuffSize, pcm_buffer_size - kTVURecoderPCMMaxBuffSize);
        pcm_buffer_size = pcm_buffer_size - kTVURecoderPCMMaxBuffSize;

        // free memory
        if(bufferList) {
            free(bufferList->mBuffers[0].mData);
            free(bufferList);
        }
    }
    return noErr;
}

解析

在該回調(diào)中如果采用我們自己定義的全局buffer蒙畴,則回調(diào)函數(shù)參數(shù)中的ioData為NULL,不再使用贰镣,如果想使用ioData按照上述設(shè)置并將其放入AudioUnitRender函數(shù)中進(jìn)行渲染,回調(diào)函數(shù)中采用pcm_buffer存儲滿2048個(gè)字節(jié)的數(shù)組傳給轉(zhuǎn)換器膳凝,這是編碼器的特性碑隆,所以如果采集的數(shù)據(jù)小于2048先取pcm_buffer的前2048個(gè)字節(jié),后面的數(shù)據(jù)與下次采集的PCM數(shù)據(jù)累加在一起蹬音。上述轉(zhuǎn)換過程在AudioQueue中已經(jīng)有介紹上煤,邏輯完全相同,可在上文中閱讀著淆。

-(void)copyEncoderCookieToFile
{
    // Grab the cookie from the converter and write it to the destination file.
    UInt32 cookieSize = 0;
    OSStatus error = AudioConverterGetPropertyInfo(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);

    // If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not.
    // log4cplus_info("cookie","cookie status:%d %d",(int)error, cookieSize);
    if (error == noErr && cookieSize != 0) {
        char *cookie = (char *)malloc(cookieSize * sizeof(char));
        //        UInt32 *cookie = (UInt32 *)malloc(cookieSize * sizeof(UInt32));
        error = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, cookie);
        // log4cplus_info("cookie","cookie size status:%d",(int)error);

        if (error == noErr) {
            error = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie);
            // log4cplus_info("cookie","set cookie status:%d ",(int)error);
            if (error == noErr) {
                UInt32 willEatTheCookie = false;
                error = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
                printf("Writing magic cookie to destination file: %u\n   cookie:%d \n", (unsigned int)cookieSize, willEatTheCookie);
            } else {
                printf("Even though some formats have cookies, some files don't take them and that's OK\n");
            }
        } else {
            printf("Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n");
        }

        free(cookie);
    }
}

解析

Magic cookie 是一種不透明的數(shù)據(jù)格式劫狠,它和壓縮數(shù)據(jù)文件與流聯(lián)系密切拴疤,如果文件數(shù)據(jù)為CBR格式(無損),則不需要添加頭部信息独泞,如果為VBR需要添加,// if collect CBR needn't set magic cookie , if collect VBR should set magic cookie, if needn't to convert format that can be setting by audio queue directly.

- (void)startAudioUnitRecorder {
    OSStatus status;

    if (isRunning) {
//        log4cplus_info("Audio Recoder", "Start recorder repeat \n");
        return;
    }

    [self initGlobalVar];

//    log4cplus_info("Audio Recoder", "starup PCM audio encoder \n");

    status = AudioOutputUnitStart(_audioUnit);
//    log4cplus_info("Audio Recoder", "AudioOutputUnitStart status : %d \n",status);
    if (status == noErr) {
        isRunning  = YES;
        hostTime   = 0;
    }
}

-(void)stopAudioUnitRecorder {
    if (isRunning == NO) {
//        log4cplus_info("Audio Recoder", "Stop recorder repeat \n");
        return;
    }

//    log4cplus_info("Audio Recoder","stop pcm encoder \n");

    isRunning = NO;

    [self copyEncoderCookieToFile];
    OSStatus status = AudioOutputUnitStop(_audioUnit);
    if (status != noErr){
//        log4cplus_info("Audio Recoder", "stop AudioUnit failed. \n");
    }

    AudioFileClose(mRecordFile);
}

解析

由于AudioUnit的初始化在本類中初始化方法中完成呐矾,所以只需要調(diào)用start,stop方法即可控制錄制轉(zhuǎn)碼過程。切記不可在start方法中完成audio unit對象的創(chuàng)建和初始化懦砂,否則會發(fā)生異常蜒犯。

總結(jié):開始寫這篇文章是在三月初剛剛接觸音頻相關(guān)項(xiàng)目,當(dāng)時(shí)直接使用AudioQueue來進(jìn)行操作荞膘,可慢慢發(fā)現(xiàn)由于公司項(xiàng)目對直播要求很高罚随,AudioQueue中有些致命缺點(diǎn)比如:回調(diào)時(shí)間無法精確控制,采集出來的數(shù)據(jù)大小問題羽资,以及無法消除回聲問題淘菩,所以二次重新開發(fā)采用AudioUnit,在本例中我已經(jīng)將兩種寫法都總結(jié)出來屠升,可根據(jù)需求決定到底使用哪種潮改,Demo中也有兩套API的封裝,轉(zhuǎn)碼邏輯基本相同弥激,但也有略微差別进陡,后續(xù)如果有問題也可以問我,簡信我就好微服,如果幫到你可以幫忙在gitHub里點(diǎn)顆星星,歡迎轉(zhuǎn)載缨历。

參考:CoreAudio, Audio Unit, 轉(zhuǎn)碼操作, AudioUnit, Audio Unit, 回聲消除, AudioQueue, 直播基礎(chǔ)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末以蕴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雄可,更是在濱河造成了極大的恐慌粗恢,老刑警劉巖轩娶,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宝与,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)冶匹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門习劫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嚼隘,你說我怎么就攤上這事诽里。” “怎么了飞蛹?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵谤狡,是天一觀的道長灸眼。 經(jīng)常有香客問我,道長墓懂,這世上最難降的妖魔是什么焰宣? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮捕仔,結(jié)果婚禮上宛徊,老公的妹妹穿的比我還像新娘。我一直安慰自己逻澳,他們只是感情好闸天,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斜做,像睡著了一般苞氮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓤逼,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天笼吟,我揣著相機(jī)與錄音,去河邊找鬼霸旗。 笑死贷帮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诱告。 我是一名探鬼主播撵枢,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼精居!你這毒婦竟也來了锄禽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤靴姿,失蹤者是張志新(化名)和其女友劉穎沃但,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體佛吓,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宵晚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了维雇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淤刃。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖谆沃,靈堂內(nèi)的尸體忽然破棺而出钝凶,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布耕陷,位于F島的核電站掂名,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哟沫。R本人自食惡果不足惜饺蔑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嗜诀。 院中可真熱鬧猾警,春花似錦、人聲如沸隆敢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拂蝎。三九已至穴墅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間温自,已是汗流浹背玄货。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悼泌,地道東北人松捉。 一個(gè)月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像馆里,于是被迫代替她去往敵國和親隘世。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容