Audio Queue詳解

一、前言

Audio Queue Services提供了一種簡單的、低開銷的方式來錄制、播放iOS和Mac OS X中的音頻。用于為iOS或Max OS X應用添加基本的錄制和播放功能。

Audio Queue Services 能允許你錄制和播放下面這些格式的音頻:

  • Linear PCM.
  • Apple平臺上支持的任何壓縮格式,如aac、mp3、m4a等
  • 用戶已安裝編碼器的任何其他格式

Audio Queue Services是一個上層服務。它讓你的應用使用硬件(如麥克風、揚聲器)錄制和播放,而無需了解硬件接口。它還允許你使用復雜的編解碼器,而無需了解編解碼器的工作方式。

二、什么是Audio Queue

Audio Queue是一個對象,用于錄制和播放音頻。它用AudioQueueRef數據類型表示,在AudioQueue.h頭文件中聲明

Audio Queue做以下任務:

  • Connecting to audio hardware(連接音頻硬件)
  • Managing memory(管理內存)
  • Employing codecs, as needed, for compressed audio formats(根據需要,為壓縮音頻格式使用編解碼器)
  • Mediating recording or playback(調解錄音和播放)

三、Audio Queue架構

所有的Audio Queue都有著相同的結構,由以下部分組成:

  • 一組audio queue buffers,每個buffer臨時存儲著audio data
  • 一個buffer queue,有序的管理audio queue buffers
  • 一個audio queue callback,這個需要開發者來編寫

架構取決于音頻隊列是用于錄制還是回放。 不同之處在于音頻隊列如何連接其輸入和輸出,以及回調函數的作用。

3.1 Audio Queues for recording

使用AudioQueueNewInput函數創建一個recording audio queue,其結構如下圖:

recording.png

輸入方通常連接的是音頻硬件,比如麥克風。在iOS中,音頻通常來自用戶內置的麥克風或耳機麥克風連接的設備 。在Mac OS X的默認情況下,音頻來自系統首選項中用戶設置的系統默認音頻輸入設備。

audio queue的輸出端,是開發者編寫的回調函數。當錄制到磁盤時,回調函數會將buffers中的新數據寫入到文件,buffers中的數據是從audio queue中接收到的。但是,recording audio queue還可以有其它用途。比如,你的回調函數直接向你的應用提供audio data而不是將其寫入磁盤。

每個audio queue,無論是recording或playback,都有一個或多個audio queue buffers。這些buffers按特定的順序排列。在上圖中,audio queue buffers按照它們被填充的順序進行編號,這與它們切換到回調的順序相同。 后面會詳細說明如何使用。

3.2 Audio Queues for Playback

使用AudioQueueNewOutput函數創建一個playback audio queue,其結構如下圖:

playback.png

在playback audio queue中,回調函數是在輸入端;回調函數負責從磁盤(或其他一些源)獲取音頻數據并將其交給音頻隊列。 當沒有更多數據可播放時,回調函數應該告知audio queue停止播放

playback audio queue的輸出端通常連接的audio硬件設備,如揚聲器

3.3 Audio Queue Buffers

Audio Queue Buffers是一個類型為AudioQueueBuffer結構體

typedef struct AudioQueueBuffer {
    const UInt32   mAudioDataBytesCapacity;
    void *const    mAudioData;
    UInt32         mAudioDataByteSize;
    void           *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;

mAudioData:指向了存儲音頻數據的內存塊
mAudioDataByteSize:audio data的字節數,在錄制的時候audio queue會設置此值;在播放的時候,需要開發者來設置

Audio Queue可以使用任意數量的buffers。 你的應用程序指定了多少。 通常是三個。 一個用來寫入磁盤,而另一個用來填充新的音頻數據。 如果需要,可以使用第三個緩沖區來補償磁盤I / O延遲等問題

Audio Queue為其buffers執行內存管理。

  • 使用AudioQueueAllocateBuffer函數創建一個buffer
  • 使用AudioQueueDispose函數釋放一個Audio Queue,Audio Queue會釋放掉它的buffers

四、The Buffer Queue and Enqueuing

下面將分析Audio Queue對象如何在錄制和播放期間管理buffer queue,以及enqueuing

4.1 錄制過程
錄制過程.png

步驟1:開始錄制,audio queue填充數據到buffer1
步驟2:buffer1填充滿后,audio queue調用回調函數處理buffer1(步驟3);與此同時audio queue填充數據到buffer2
步驟4:將用過的buffer1重新入隊,再將填充好的buffer2給回調使用(步驟6),與此同時audio queue填充數據到其它的buffer(步驟5);依此循環,直到停止錄制

4.2 播放過程

播放時,一個audio queue buffer 被發送到輸出設備,如揚聲器。在queue buffer中的剩余buffers排在當前buffer后面,等待依此播放。

Audio queue會按照播放順序將播放的音頻數據buffer交給回調函數,回調函數將新的音頻數據讀入buffer,然后將其入隊

播放過程.png

步驟1:應用程序啟動playback audio queue。應用程序為每個audio queue buffers調用一次回調,填充它們并將它們添加到buffer queue。當你的應用程序調用AudioQueueStart函數時確保能夠啟動。
步驟3:audio queue 發送buffer1到輸出設備
一旦播放了第一個buffer,playback audio queue進入一個循環狀態。audio queue開始播放下一個buffer(buffer2 步驟4)并調用回調處理剛剛播放完的buffer(步驟5),填充buffer,并將其入隊(步驟6)

4.3 控制播放過程

Audio queue buffers始終按照入隊順序進行播放,但是audio queue可以使用AudioQueueEnqueueBufferWithParameters函數對播放過程進行一些控制

  • Set the precise playback time for a buffer. This lets you support synchronization.
  • Trim frames at the start or end of an audio queue buffer. This lets you remove leading or trailing silence.
  • Set the playback gain at the granularity of a buffer

五、The Audio Queue Callback Function

通常,使用Audio Queue Services大部分工作是編寫回調函數。在錄制和播放時,audio queue會重復的調用callback。調用之間的時間取決于audio queue buffers的容量,通常為半秒到幾秒

5.1 The Recording Audio Queue Callback Function
typedef void (*AudioQueueInputCallback)(
        void * __nullable               inUserData,
        AudioQueueRef                   inAQ,
        AudioQueueBufferRef             inBuffer,
        const AudioTimeStamp *          inStartTime,
        UInt32                          inNumberPacketDescriptions,
        const AudioStreamPacketDescription * __nullable inPacketDescs
);

當recording audio queue調用callback時,會提供下一組音頻數據所需的內容

  • inUserData:通常是傳入一個包含audio queue及其buffer的狀態信息的對象/結構體
  • inAQ:調用callback的audio queue
  • inBuffer:audio queue buffer,有audio queue刷新填充,其包含了你的回調需要寫入磁盤的新數據。該數據已根據在inUserData中指定的格式進行格式化。
  • inStartTime:buffer第一個樣本的采樣時間,對應基本錄制,你的callback不使用該參數
  • inNumberPacketDescriptions:是inPacketDescs參數中的數據包描述個數。如果要錄制為VBR(可變比特率)格式,音頻隊列會為您的回調提供此參數的值,然后將其傳遞給AudioFileWritePackets函數。 CBR(恒定比特率)格式不使用數據包描述。 對于CBR記錄,音頻隊列將此設置和inPacketDescs參數設置為NULL。
  • inPacketDescs:buffer中的樣本對應的數據包描述集。 同樣,如果音頻數據是VBR格式,音頻隊列將提供此參數的值,并且您的回調將其傳遞給AudioFileWritePackets函數
5.2 The Playback Audio Queue Callback Function
typedef void (*AudioQueueOutputCallback)(
        void * __nullable       inUserData,
        AudioQueueRef           inAQ,
        AudioQueueBufferRef     inBuffer
);

在調用callback是,playback audio queue提供callback讀取下一組音頻數據所需的內容

  • inUserData:通常傳入一個包含audio queue及其buffers的狀態信息對象/結構體
  • inAQ:調用callback的audio queue
  • inBuffer:由audio queue提供,你的callback將填充從正在播放的文件中讀取下一組數據

如果你的應用程序播放VBR數據,callback需要獲取audio data的packet information。使用AudioFileReadPacketData函數,然后將packet information放入inUserData中,以使其可用于playback audio queue

六、使用Codecs and Audio Data Formats

Audio Queue Services根據需要使用編解碼器(音頻數據編碼/解碼組件)來在音頻格式之間進行轉換。 你的錄制或播放應用程序可以使用已安裝編解碼器的任何音頻格式。 你無需編寫自定義代碼來處理各種音頻格式。 具體來說,你的回調不需要了解數據格式。

每個Audio queue具有audio data format,使用結構體AudioStreamBasicDescription來表示。當你指定foramt結構體中的mFromatID字段的格式時,audio queue會使用合適的編解碼器。然后,你可以指定采樣率和聲道數,這就是它的全部內容。

recording_use_codec.png

錄制使用Codec:
步驟1:應用程序開始recording,并告知audio queue使用何種format來編碼
步驟2:根據fromat選擇合適的codec得到壓縮數據提交給callback
步驟3:callback調用寫入磁盤

playback_use_codec.png

播放使用Codec:
步驟1:應用程序告知audio queue接收何種format的數據,并啟動playing
步驟2:audio queue調用callback,從audio file中讀取數據,callback將原始數據傳遞給audio queue。
步驟3:audio queue使用合適的codec將數據轉為未壓縮的數據發送到目的地

七、Audio Queue Control and State

Audio queue在creation和disposal之間具有生命周期。你的應用程序可以使用以下函數來管理它的生命周期。

  • Start(AudioQueueStart)在初始化recording或playback時調用
  • Prime(AudioQueuePrime)對于playback,需要在AudioQueueStart之前調用,以確保立即有可用的數據提供給audio queue播放。該函數與recording無關
  • Stop(AudioQueueStop)用來重置audio queue,然后停止recording或playback。當playback的callback沒有數據來播放時調用該函數
  • Pause(AudioQueuePause)用來暫停recording或playback,不會影響buffers和重置audio queue;調用AudioQueueStart函數恢復
  • Flush(AudioQueueFlush)在最后一個入隊的buffer之后調用,用來確保所有的buffer數據,也包括處理中的數據,得到播放或錄制
  • Reset(AudioQueueReset)調用該函數會讓audio queue立即靜音,它會清除掉所有的buffers、重置所有的編解碼器和DSP狀態

AudioQueuesStop函數有同步異步兩種調用方式:

  • 同步調用會立即停止,不考慮已緩沖的audio data
  • 異步調用會等到隊列中所有的buffer全部播放或錄完再停止

八、Audio Queue Parameters

可以給audio queue設置相關的參數,這些參數通常是針對playback,而不是recording
有兩種方式來設置參數:

  • 對于audio queue,使用AudioQueueSetParameter函數
  • 對于audio queue buffer,使用AudioQueueEnqueueBufferWithParameters函數

九、錄制音頻

下面以錄制一個aac音頻格式到本地磁盤為例。

1、創建一個對象LXAudioRecoder來管理audio queue狀態、存儲dataformat、路徑等信息

static const int kNumberBuffers = 3;

@interface LXAudioRecoder () {
    // 音頻隊列
    AudioQueueRef               queueRef;
    // buffers數量
    AudioQueueBufferRef         buffers[kNumberBuffers];
    // 音頻數據格式
    AudioStreamBasicDescription dataformat;
}

@property (nonatomic, assign) SInt64 currPacket;
// 錄制的文件
@property (nonatomic, assign) AudioFileID mAudioFile;
// 當前錄制文件的大小
@property (nonatomic, assign) UInt32 bufferBytesSize;

2、配置datafromat

    Float64 sampleRate = 44100.0;
    UInt32 channel = 2;
    // 音頻格式
    dataformat.mFormatID = kAudioFormatMPEG4AAC;
    // 采樣率
    dataformat.mSampleRate = sampleRate;
    // 聲道數
    dataformat.mChannelsPerFrame = channel;
    UInt32 formatSize = sizeof(dataformat);
    AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &formatSize, &dataformat);
    // 采樣位數
//    dataformat.mBitsPerChannel = 16;
//    // 每個包中的字節數
//    dataformat.mBytesPerPacket = channel * sizeof(SInt16);
//    // 每個幀中的字節數
//    dataformat.mBytesPerFrame = channel * sizeof(SInt16);
//    // 每個包中的幀數
//    dataformat.mFramesPerPacket = 1;
//    // flags
//    dataformat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;

3、創建record Audio Queue

OSStatus status = AudioQueueNewInput(&dataformat, recoderCallBack, (__bridge void *)self, NULL, NULL, 0, &queueRef);

4、編寫recoderCallBack

static void recoderCallBack(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *timestamp, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {
    LXAudioRecoder *recoder = (__bridge LXAudioRecoder *)aqData;
    
    if (inNumPackets == 0 && recoder->dataformat.mBytesPerPacket != 0) {
        inNumPackets = inBuffer->mAudioDataByteSize / recoder->dataformat.mBytesPerPacket;
    }
    // 將音頻數據寫入文件
    if (AudioFileWritePackets(recoder.mAudioFile, false, inBuffer->mAudioDataByteSize, inPacketDesc, recoder.currPacket, &inNumPackets, inBuffer->mAudioData) == noErr) {
        recoder.currPacket += inNumPackets;
    }
    if (recoder.isRunning) {
        // 入隊
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}

5、計算Audio Queue Buffers大小

/** 
 *   獲取AudioQueueBuffer大小
 *   seconds:每個buffer保存的音頻秒數,一般設置為半秒
 */
void deriveBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription streamDesc, Float64 seconds, UInt32 *outBufferSize) {
    // 音頻隊列數據大小的上限
    static const int maxBufferSize = 0x50000;
    
    int maxPacketSize = streamDesc.mBytesPerPacket;
    if (maxPacketSize == 0) {  // VBR
        UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
        AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, &maxVBRPacketSize);
    }
    // 獲取音頻數據大小
    Float64 numBytesForTime = streamDesc.mSampleRate * maxPacketSize * seconds;
    *outBufferSize = (UInt32)(numBytesForTime < maxBufferSize? numBytesForTime : maxBufferSize);
}

6、創建Audio Queue Buffers

deriveBufferSize(queueRef, dataformat, 0.5, &_bufferBytesSize);
// 為Audio Queue準備指定數量的buffer
for (int i = 0; i < kNumberBuffers; i++) {
    AudioQueueAllocateBuffer(queueRef, self.bufferBytesSize, &buffers[i]);
    AudioQueueEnqueueBuffer(queueRef, buffers[i], 0, NULL);
}

7、創建音頻文件

NSURL *fileURL = [NSURL URLWithString:filePath];
AudioFileCreateWithURL((__bridge CFURLRef)fileURL, kAudioFileCAFType, &dataformat, kAudioFileFlags_EraseFile, &_mAudioFile);

8、設置magic cookie for an audio file
默寫壓縮音頻格式,如MPEG 4 AAC,使用一個結構來包含audio的元數據。這種結構叫做magic cookie。當使用audio queue services錄制這種格式時,你必須從audio queue中獲取magic cookie并在開始錄制前添加到音頻文件中。
注意:下面方法需在recording之前和在停止recording時調用,因為某些編解碼器在recording停止時更新magic cookie數據

- (OSStatus)setupMagicCookie {
    UInt32 cookieSize;
    OSStatus status = noErr;
    if (AudioQueueGetPropertySize(queueRef, kAudioQueueProperty_MagicCookie, &cookieSize) == noErr) {
        char *magicCookie = (char *)malloc(cookieSize);
        if (AudioQueueGetProperty(queueRef, kAudioQueueProperty_MagicCookie, magicCookie, &cookieSize) == noErr) {
            status = AudioFileSetProperty(_mAudioFile, kAudioFilePropertyMagicCookieData, cookieSize, magicCookie);
        }
        free(magicCookie);
    }
    return status;
}

9、record audio

- (void)recoder {
    
    if (self.isRunning) {
        return;
    }
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    [[AVAudioSession sharedInstance] setActive:true error:nil];
    
    OSStatus status = AudioQueueStart(queueRef, NULL);
    if (status != noErr) {
        NSLog(@"start queue failure");
        return;
    }
    _isRunning = true;
}

- (void)stop {
    if (self.isRunning) {
        AudioQueueStop(queueRef, true);
        _isRunning = false;
        
        [[AVAudioSession sharedInstance] setActive:false withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
    }
}

10、clean up

- (void)dealloc {
    AudioQueueDispose(queueRef, true);
    AudioFileClose(_mAudioFile);
}

十、播放音頻

以播放本地音頻文件為例
1、創建一個對象用來管理audio queue狀態、datafromat、buffers等

static const int kNumberBuffers = 3;

@interface LXAudioPlayer () {
    AudioStreamBasicDescription   dataFormat;
    AudioQueueRef                 queueRef;
    AudioQueueBufferRef           mBuffers[kNumberBuffers];
}

@property (nonatomic, assign) AudioFileID mAudioFile;
@property (nonatomic, assign) UInt32 bufferByteSize;
@property (nonatomic, assign) SInt64 mCurrentPacket;
@property (nonatomic, assign) UInt32 mPacketsToRead;
@property (nonatomic, assign) AudioStreamPacketDescription *mPacketDescs;
@property (nonatomic, assign) bool isRunning;
@end

2、打開文件

NSURL *fileURL = [NSURL URLWithString:filePath];
OSStatus status = AudioFileOpenURL((__bridge CFURLRef)fileURL, kAudioFileReadPermission, kAudioFileCAFType, &_mAudioFile);

3、獲取文件格式

// 獲取文件格式
UInt32 dataFromatSize = sizeof(dataFormat);
AudioFileGetProperty(_mAudioFile, kAudioFilePropertyDataFormat, &dataFromatSize, &dataFormat);

4、創建playback audio queue

// 創建播放音頻隊列
AudioQueueNewOutput(&dataFormat, playCallback, (__bridge void *)self, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &queueRef);

5、編寫play callback

static void playCallback(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
    LXAudioPlayer *player = (__bridge LXAudioPlayer *)aqData;
    
    UInt32 numBytesReadFromFile = player.bufferByteSize;
    UInt32 numPackets = player.mPacketsToRead;
    AudioFileReadPacketData(player.mAudioFile, false, &numBytesReadFromFile, player.mPacketDescs, player.mCurrentPacket, &numPackets, inBuffer->mAudioData);
    if (numPackets > 0) {
        inBuffer->mAudioDataByteSize = numBytesReadFromFile;
        player.mCurrentPacket += numPackets;
        AudioQueueEnqueueBuffer(player->queueRef, inBuffer, player.mPacketDescs ? numPackets : 0, player.mPacketDescs);
    } else {
        NSLog(@"play end");
        AudioQueueStop(player->queueRef, false);
        player.isRunning = false;
    }
}

6、計算buffer的大小

void playBufferSize(AudioStreamBasicDescription basicDesc, UInt32 maxPacketSize, Float64 seconds, UInt32 *outBufferSize, UInt32 *outNumPacketsToRead) {
    static const int maxBufferSize = 0x50000;
    static const int minBufferSize = 0x4000;
    
    if (basicDesc.mFramesPerPacket != 0) {
        Float64 numPacketsForTime = basicDesc.mSampleRate / basicDesc.mFramesPerPacket * seconds;
        *outBufferSize = numPacketsForTime * maxPacketSize;
    } else {
        *outBufferSize = maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize;
    }
    
    if (*outBufferSize > maxBufferSize && *outBufferSize > maxPacketSize) {
        *outBufferSize = maxBufferSize;
    } else {
        if (*outBufferSize < minBufferSize) {
            *outBufferSize = minBufferSize;
        }
    }
    *outNumPacketsToRead = *outBufferSize / maxPacketSize;
}

7、為數據包描述分配內存

bool isFormatVBR = dataFormat.mBytesPerPacket == 0 || dataFormat.mFramesPerPacket == 0;
if (isFormatVBR) {
    _mPacketDescs = (AudioStreamPacketDescription *)malloc(_mPacketsToRead * sizeof(AudioStreamPacketDescription));
} else {
    _mPacketDescs = NULL;
}

8、set magic cookie

- (void)setupMagicCookie {
    // magic cookie
    UInt32 cookieSize = sizeof(UInt32);
    if (AudioFileGetPropertyInfo(_mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, NULL) == noErr && cookieSize) {
        char *magicCookie = (char *)malloc(cookieSize);
        if (AudioFileGetProperty(_mAudioFile, kAudioFilePropertyMagicCookieData, &cookieSize, magicCookie) == noErr) {
            AudioQueueSetProperty(queueRef, kAudioQueueProperty_MagicCookie, magicCookie, cookieSize);
        }
        free(magicCookie);
    }
}

9、創建buffer

UInt32 maxPacketSize;
UInt32 propertySize = sizeof(maxPacketSize);
AudioFileGetProperty(_mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize);
playBufferSize(dataFormat, maxPacketSize, 0.5, &_bufferByteSize, &_mPacketsToRead);
// 分配音頻隊列
for (int i = 0; i < kNumberBuffers; i++) {
    AudioQueueAllocateBuffer(queueRef, _bufferByteSize, &mBuffers[i]);
    playCallback((__bridge void *)self, queueRef, mBuffers[i]);
}

10、play audio

- (void)play {
    if (self.isRunning) {
        return;
    }
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
    //[[AVAudioSession sharedInstance] setActive:YES error:nil];
    [[AVAudioSession sharedInstance] setActive:true withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
    
    OSStatus status = AudioQueueStart(queueRef, NULL);
    if (status != noErr) {
        NSLog(@"play error");
        return;
    }
    self.isRunning = true;
}

11、stop play

- (void)stop {
    if (self.isRunning) {
        self.isRunning = false;
        AudioQueueStop(queueRef, true);
        
        [[AVAudioSession sharedInstance] setActive:false withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
    }
}

12、clean

- (void)dealloc {
    AudioFileClose(_mAudioFile);
    AudioQueueDispose(queueRef, true);
    if (_mPacketDescs) {
        free(_mPacketDescs);
    }
}

播放和錄音demo已上傳Github
參考文章:
1、Audio Queue Services Programing Guide

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容