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阅读 160,585评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,923评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,314评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,346评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,718评论 3 291
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,828评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,020评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,758评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,486评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,722评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,196评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,546评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,211评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,132评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,916评论 0 200
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,904评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,758评论 2 274

推荐阅读更多精彩内容