Android音视频【十三】OpenSL ES介绍&基于OpenSL ES实现音频采集

人间观察
勿再别人的心中修行自己,
勿再自己的心中强求别人。

前言

最近写文章有点偷懒了,离上次写文章大概一个月了。

一般Android音频的采集在java层使用AudioRecord类进行采集。

但是为什么要学OpenSL呢?除了C/C++的性能优势(不过其实java的效率也不低)之外,最主要是你如果使用java层的接口,还需要通过一层JNI,比较复杂,性能消耗也大。如果用OpenSL的话就能直接在C/C++里面把事情都处理了。所以有时候为了开发更加高效的 Android 音频应用需要在底层直接进行录音采集播放等,免去java和jni层的互相通信。

那这篇文章主要介绍Android在JNI层如何使用OpenSL ES进行音频的采集。

OpenSL ES介绍

此节介绍有些摘自网络,只为学习使用,只为更好更全的介绍下OpenSL ES。如有侵权将删除!

什么是OpenSL ES

这个在前一篇文章的利用OpenSL ES播放pcm数据的时候也有所介绍。这里再介绍下。

OpenSL ES全称为Open Sound Library for Embedded Systems,即嵌入式音频加速标准。OpenSL ES是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速 API。它为嵌入式移动多媒体设备上的本地 应用程序开发者提供了标准化、高性能、低响应时间的音频功能实现方法,同时还实现了软/硬件音频性能的直接跨平台部署,不仅降低了执行难度,而且促进了高级音频市场的发展。简单来说OpenSL ES是一个嵌入式跨平台免费的音频处理库。 所以它不是Android特有的。

OpenSL ES 与 Android的关系

OpenSL ES 与 Android的关系

官网介绍地址:https://source.android.com/devices/audio/latency_app.html

可以看到Android 实现的 OpenSL ES 只是 OpenSL 1.0.1 的子集,并且进行了扩展,因此,对于 OpenSL ES API 的使用,我们还需要特别留意哪些是 Android 支持的,哪些是不支持的。

OpenSL ES的特性以及优劣势

特性:

(1)C 语言接口,兼容 C++,需要在 NDK 下开发,能更好地集成在 native 应用中
(2)运行于 native 层,需要自己管理资源的申请与释放,没有 Dalvik 虚拟机的垃圾回收机制
(3)支持 PCM 数据的采集,支持的配置:16bit 位宽,16000 Hz采样率,单通道。(其他的配置不能保证兼容所有平台)
(4)支持 PCM 数据的播放,支持的配置:8bit/16bit 位宽,单通道/双通道,小端模式,采样率(8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 Hz)
(5)支持播放的音频数据来源:res 文件夹下的音频、assets 文件夹下的音频、sdcard 目录下的音频、在线网络音频、代码中定义的音频二进制数据等等

优势:

(1)避免音频数据频繁在 native 层和 Java 层拷贝,提高效率
(2)相比于 Java API,可以更灵活地控制参数
(3)由于是 C 代码,因此可以做深度优化,比如采用 NEON 优化
(4)代码细节更难被反编译

劣势:

(1)不支持版本低于 Android 2.3 (API 9) 的设备
(2)没有全部实现 OpenSL ES 定义的特性和功能
(3)不支持 MIDI 
(4)不支持直接播放 DRM 或者 加密的内容
(5)不支持音频数据的编解码,如需编解码,需要使用 MediaCodec API 或者第三方库
(6)在音频延时方面,相比于上层 API,并没有特别明显地改进

更多介绍

可以看下android 中文官网。里面也有一些demo

https://developer.android.google.cn/ndk/guides/audio/opensl

ok,了解了OpenSL ES 的 一些背景后,我们接下来开始介绍OpenSL ES 的API和实现录音采集功能。

添加权限

要想录音无论是使用java层的AudioRecord还是底层的OpenSL ES都需要在AndroidManifest.xml的配置文件里面增加录音权限。

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

引用相关库文件以及头文件

在Android中使用OpenSLES首先需要把Android 系统提供的so链接到外面自己的so。在CMakeLists.txt脚本中添加链接库OpenSLES。库的名字可以在你的ndk目录下找到,类似目录 :

/Users/guxiuzhong/Library/Android/sdk/ndk/21.1.6352462/platforms/android-19/arch-x86/usr/lib/libOpenSLES.so

然后需要去掉lib前缀即可。当工程编译的时候它会自动找这个目录的。

CMake方式 在CMakeLists.txt中配置

target_link_libraries(
                OpenSLES
                            // ...省略其它需要链接的so
        )

NDK Build方式 在Makefile文件Android.mk添加链接选项

LOCAL_LDLIBS += -lOepnSLES

头文件添加

#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>

OpenSLES重要概念

Objects和Interfaces

OpenSL ES的开发中有两个必须理解的概念,一个是Object,另一个是Interface。因为很多的API都是在操作ObjectInterface。Android为了更方便的使用OpenSL ESOpenSL ES 的API设计成了类似面向对象的java的使用方式了。Object 可以想象成 Java 的 Object 类,Interface 可以想象成 Java 的 Interface,但它们并不完全相同。他们的关系:
(1) 每个 Object 可能会存在一个或者多个 Interface,官方为每一种 Object 都定义了一系列的 Interface
(2)每个 Object 对象都提供了一些最基础的操作,比如:RealizeResumeGetStateDestroy 等等,如果希望使用该对象支持的功能函数,则必须通过其 GetInterface 函数拿到 Interface 接口,然后通过 Interface 来访问功能函数
(3)并不是每个系统上都实现了 OpenSL ES 为 Object 定义的所有 Interface,所以在获取 Interface 的时候需要做一些选择和判断

所有的ObjectOpenSL里面我们拿到的都是一个SLObjectItf

typedef const struct SLObjectItf_ * const * SLObjectItf;
struct SLObjectItf_ {
    SLresult (*Realize) (
        SLObjectItf self,
        SLboolean async
    );
    SLresult (*Resume) (
        SLObjectItf self,
        SLboolean async
    );
    SLresult (*GetState) (
        SLObjectItf self,
        SLuint32 * pState
    );
    SLresult (*GetInterface) (
        SLObjectItf self,
        const SLInterfaceID iid,
        void * pInterface
    );
    SLresult (*RegisterCallback) (
        SLObjectItf self,
        slObjectCallback callback,
        void * pContext
    );
    void (*AbortAsyncOperation) (
        SLObjectItf self
    );
    void (*Destroy) (
        SLObjectItf self
    );
    SLresult (*SetPriority) (
        SLObjectItf self,
        SLint32 priority,
        SLboolean preemptable
    );
    SLresult (*GetPriority) (
        SLObjectItf self,
        SLint32 *pPriority,
        SLboolean *pPreemptable
    );
    SLresult (*SetLossOfControlInterfaces) (
        SLObjectItf self,
        SLint16 numInterfaces,
        SLInterfaceID * pInterfaceIDs,
        SLboolean enabled
    );
};

任何创建出来的Object都必须调用 Realize 方法做初始化,在不需要的时候可以使用 Destroy 方法来释放资源。

GetInterface

GetInterface可以说是OpenSL里使用频率最高的方法,通过它我们可以获取Object里面的Interface。

由于一个Object里面可能包含了多个Interface,所以GetInterface方法有个SLInterfaceID参数来指定到的需要获取Object里面的那个Interface。

比如我们通过EngineObject去获取SL_IID_ENGINE这个id的Interface,而这个id对应的Interface就是SLEngineItf:

    SLEngineItf engineEngine = NULL;
    SLObjectItf engineObject = NULL;
    // 创建引擎对象,调用全局方法创建一个引擎对象(OpenSL ES唯一入口)
    SLresult result;
    result = slCreateEngine(&engineObject, 1, pEngineOptions, 0, nullptr, nullptr);
    assert(SL_RESULT_SUCCESS == result);
    /* Realizing the SL Engine in synchronous mode. */
    //实例化这个对象
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    assert(SL_RESULT_SUCCESS == result);
    // get the engine interface, which is needed in order to create other objects
    //从这个对象里面获取引擎接口
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    assert(SL_RESULT_SUCCESS == result);

当调用每一个API后要检测其返回值是否等于 「SL_RESULT_SUCCESS」

Interface

Interface则是方法的集合,例如SLRecordItf里面包含了和录音相关的方法,SLPlayItf包含了和播放相关的方法。我们功能都是通过调用Interfaces的方法去实现的。

SLEngineItf 这个interface,SLEngineItf是OpenSL里面最重要的一个Interface,我们可以通过它去创建各种Object,例如播放器、录音器、混音器的Object,然后在用这些Object去获取各种Interface去实现各种功能。

比如:

    SLresult (*CreateAudioPlayer) (
        SLEngineItf self,
        SLObjectItf * pPlayer,
        SLDataSource *pAudioSrc,
        SLDataSink *pAudioSnk,
        SLuint32 numInterfaces,
        const SLInterfaceID * pInterfaceIds,
        const SLboolean * pInterfaceRequired
    );
    SLresult (*CreateAudioRecorder) (
        SLEngineItf self,
        SLObjectItf * pRecorder,
        SLDataSource *pAudioSrc,
        SLDataSink *pAudioSnk,
        SLuint32 numInterfaces,
        const SLInterfaceID * pInterfaceIds,
        const SLboolean * pInterfaceRequired
    );
// 还有很多其它的,可以看下OpenSLES.h的头文件

Object的生命周期

OpenSL ES 的 Object 一般有三种状态,分别是:UNREALIZED (不可用),REALIZED(可用),SUSPENDED(挂起)。

Object 处于 UNREALIZED (不可用)状态时,系统不会为其分配资源;调用 Realize 方法后便进入 REALIZED(可用)状态,此时对象的各个功能和资源可以正常访问;当系统音频相关的硬件设备被其他进程占用时,OpenSL ES Object 便会进入 SUSPENDED (挂起)状态,随后调用 Resume 方法可使对象重回 REALIZED(可用)状态;当 Object 使用结束后,调用 Destroy 方法释放资源,是对象重回 UNREALIZED (不可用)状态。

录音

demo就简单的把采集到的pcm文件保存到sd卡目录的文件下了,主要是如何用OpenSL ES实现音频pcm数据的采集。

明白了上面的OpenSL ES的设计思想和Object和Interface的API使用流程代码就容易看得懂了。

创建引擎对象

需要调用slCreateEngine()这个全局方法来创建

SL_API SLresult SLAPIENTRY slCreateEngine(
    SLObjectItf             *pEngine, // 引擎对象地址,用于传出对象,是一个输出参数
    SLuint32                numOptions, //配置参数数量,传1
    const SLEngineOption    *pEngineOptions,// 配置参数,为枚举数组
    SLuint32                numInterfaces,//支持的接口数量
    const SLInterfaceID     *pInterfaceIds,//具体的要支持的接口,是枚举的数组
    const SLboolean         * pInterfaceRequired//具体的要支持的接口是开放的还是关闭的,也是一个数组,后三个参数长度是一致的
);

创建之后按照上面介绍的标准流程三步走,调用Realize来实例化这个对象,之后通过GetInterface从这个对象里面获取引擎接口。所以创建引擎对象的代码片段为:

// 成员变量
SLEngineItf engineEngine = NULL;
SLObjectItf engineObject = NULL;

void AudioRecorder::createEngine() {
    SLEngineOption pEngineOptions[] = {(SLuint32) SL_ENGINEOPTION_THREADSAFE,
                                       (SLuint32) SL_BOOLEAN_TRUE};
    // 创建引擎对象,//调用全局方法创建一个引擎对象(OpenSL ES唯一入口)
    SLresult result;
    result = slCreateEngine(
            &engineObject, //对象地址,用于传出对象
            1, //配置参数数量
            pEngineOptions, //配置参数,为枚举数组
            0,  //支持的接口数量
            nullptr, //具体的要支持的接口,是枚举的数组
            nullptr//具体的要支持的接口是开放的还是关闭的,也是一个数组,这三个参数长度是一致的
            );
    assert(SL_RESULT_SUCCESS == result);
    /* Realizing the SL Engine in synchronous mode. */
    //实例化这个对象
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    assert(SL_RESULT_SUCCESS == result);
    // get the engine interface, which is needed in order to create other objects
    //从这个对象里面获取引擎接口
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    assert(SL_RESULT_SUCCESS == result);
}

设置采集设备(麦克风)输入输出配置

    SLDataLocator_IODevice ioDevice = {
            SL_DATALOCATOR_IODEVICE,  //类型 这里只能是SL_DATALOCATOR_IODEVICE
            SL_IODEVICE_AUDIOINPUT,//device类型  选择了音频输入类型
            SL_DEFAULTDEVICEID_AUDIOINPUT, //deviceID 对应的是SL_DEFAULTDEVICEID_AUDIOINPUT
            NULL//device实例
    };
    // 输入,SLDataSource 表示音频数据来源的信息
    SLDataSource recSource = {
            &ioDevice,//SLDataLocator_IODevice配置输入
            NULL//输入格式,采集的并不需要
    };
    // 数据源简单缓冲队列定位器,输出buffer队列
    SLDataLocator_AndroidSimpleBufferQueue recBufferQueue = {
            SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, //类型 这里只能是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE
            NUM_BUFFER_QUEUE //buffer的数量
    };
    // PCM 数据源格式 //设置输出数据的格式
    SLDataFormat_PCM pcm = {
            SL_DATAFORMAT_PCM, //输出PCM格式的数据
            2,  //  //输出的声道数量2 个声道(立体声)
            SL_SAMPLINGRATE_44_1, //输出的采样频率,这里是44100Hz
            SL_PCMSAMPLEFORMAT_FIXED_16, //输出的采样格式,这里是16bit
            SL_PCMSAMPLEFORMAT_FIXED_16,//一般来说,跟随上一个参数
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//双声道配置,如果单声道可以用 SL_SPEAKER_FRONT_CENTER
            SL_BYTEORDER_LITTLEENDIAN //PCM数据的大小端排列
    };
    // 输出,SLDataSink 表示音频数据输出信息
    SLDataSink dataSink = {
            &recBufferQueue, //SLDataFormat_PCM配置输出
            &pcm //输出数据格式
    };

OpenSL ES 中的 SLDataSourceSLDataSink 结构体,主要用于构建 audio player 和 recorder 对象,其中 SLDataSource 表示音频数据来源的信息,SLDataSink 表示音频数据输出信息。

创建录音器-创建录音对象和获取录音相关的接口

SLEngineItf来创建录音器,调用CreateAudioRecorder方法

    //创建录制的对象,并且指定开放SL_IID_ANDROIDSIMPLEBUFFERQUEUE这个接口
    SLInterfaceID iids[NUM_RECORDER_EXPLICIT_INTERFACES] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                                                            SL_IID_ANDROIDCONFIGURATION};
    SLboolean required[NUM_RECORDER_EXPLICIT_INTERFACES] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
    /* Create the audio recorder */
    // 创建 audio recorder 对象
    result = (*engineEngine)->CreateAudioRecorder(engineEngine, //引擎接口
                                                  &recorderObject, //录制对象地址,用于传出对象
                                                  &recSource,//输入配置
                                                  &dataSink,//输出配置
                                                  NUM_RECORDER_EXPLICIT_INTERFACES,//支持的接口数量
                                                  iids, //具体的要支持的接口
                                                  required //具体的要支持的接口是开放的还是关闭的
    );
    assert(SL_RESULT_SUCCESS == result);

    assert(SL_RESULT_SUCCESS == result);
    /* Realize the recorder in synchronous mode. */ //实例化这个录制对象
    result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
    assert(SL_RESULT_SUCCESS == result);
    /* Get the buffer queue interface which was explicitly requested *///获取Buffer接口
    result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
                                             (void *) &recorderBuffQueueItf);
    assert(SL_RESULT_SUCCESS == result);

    /* get the record interface */ //获取录制接口
    (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
    assert(SL_RESULT_SUCCESS == result);

还是先创建出来的Object后,然后调用 Realize 方法做初始化,再调用GetInterface来获取Object里面的那个Interface。SL_IID_RECORD就是录音器的接口方法的id,输出参数recorderRecord就是录音接口,后续的启动录音停止录音都是对它操作。

设置数据回调方法并且开始录制

通过回调函数RegisterCallback后并设置开始录制状态来获取录制的音频 PCM 数据。

    buffer = new uint8_t[BUFFER_SIZE]; //数据缓存区,
    bufferSize = BUFFER_SIZE;
    //设置数据回调接口AudioRecorderCallback,最后一个参数是可以传输自定义的上下文引用
    (*recorderBuffQueueItf)->RegisterCallback(recorderBuffQueueItf, AudioRecorderCallback, this);
    assert(SL_RESULT_SUCCESS == result);
    /* Start recording */
    // 开始录制音频,//设置录制器为录制状态 SL_RECORDSTATE_RECORDING
    result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
    assert(SL_RESULT_SUCCESS == result);

    // 在设置完录制状态后一定需要先Enqueue一次,这样的话才会开始采集回调
    /* Enqueue buffers to map the region of memory allocated to store the recorded data */
    (*recorderBuffQueueItf)->Enqueue(recorderBuffQueueItf, buffer, BUFFER_SIZE);
    assert(SL_RESULT_SUCCESS == result);
    LOGD("Starting recording tid=%ld", syscall(SYS_gettid));//线程id

其中RegisterCallback的第1个参数是获取Buffer接口,第2个参数是一个函数的地址,录音时

OpenSL ES 会自动进行回调,需要注意的是回调方法AudioRecorderCallback不是在UI线程回调,是一个子线程。第3个参数是给回调方法传入的参数,可以传任何数据,这里就传入了this,方便在回调方法里获取一些成员变量等。回调函数如下:

void AudioRecorderCallback(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {
    //注意这个是另外一条采集线程回调
    AudioRecorder *recorderContext = (AudioRecorder *) context;
    assert(recorderContext != NULL);
    if (recorderContext->buffer != NULL) {
        fwrite(recorderContext->buffer, recorderContext->bufferSize, 1, recorderContext->pfile);
        LOGD("save a frame audio data,pid=%ld", syscall(SYS_gettid));
        SLresult result;
        SLuint32 state;
        result = (*(recorderContext->recorderRecord))->GetRecordState(
                recorderContext->recorderRecord, &state);
        assert(SL_RESULT_SUCCESS == result);
        (void) result;
        LOGD("state=%d", state);
        if (state == SL_RECORDSTATE_RECORDING) {
            //取完数据,需要调用Enqueue触发下一次数据回调
            result = (*bufferQueueItf)->Enqueue(bufferQueueItf, recorderContext->buffer,
                                                recorderContext->bufferSize);
            assert(SL_RESULT_SUCCESS == result);
            (void) result;
        }
    }
}

需要注意的点如注释所写: 在设置完录制状态后一定需要先Enqueue一次,这样的话才会开始采集回调,然后每处理完一次后需要再次Enqueue来达到循环录音。

停止录音

对于停止录音,就是对录音接口SLRecordItf修改录制状态为SL_RECORDSTATE_STOPPED即可

void AudioRecorder::stopRecord() {
    // 停止录制
    if (recorderRecord != nullptr) {
        //设置录制器为停止状态 SL_RECORDSTATE_STOPPED
        SLresult result = result = (*recorderRecord)->SetRecordState(recorderRecord,
                                                                     SL_RECORDSTATE_STOPPED);
        assert(SL_RESULT_SUCCESS == result);
        fclose(pfile);
        pfile = nullptr;
        delete buffer;
        LOGD("stopRecord done");
    }
}

释放录音和OpenSL ES的资源

// 释放资源,释放OpenSL ES资源
void AudioRecorder::release() {
    //只需要销毁OpenSL ES对象,接口不需要做Destroy处理。
    if (recorderObject != nullptr) {
        (*recorderObject)->Destroy(recorderObject);
        recorderObject = NULL;
        recorderRecord = NULL;
        recorderBuffQueueItf = NULL;
        configItf = NULL;
    }
    // destroy engine object, and invalidate all associated interfaces
    if (engineObject != NULL) {
        // 释放引擎对象的资源
        (*engineObject)->Destroy(engineObject);
        engineObject = NULL;
        engineEngine = NULL;
    }
    LOGD("release done");

调用测试

有个Audacity软件可以进行播放未压缩的音频pcm文件。当然你也可以用前一篇文章我们介绍的AudioTrack和OpenSL ES进行播放来验证声音是否ok。

JNI层如下代码,比较简单就贴一下了:

AudioRecorder *audioRecorder;
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_pcmplay_AudioRecorder_startRecord(JNIEnv *env, jobject thiz) {
    if (audioRecorder == nullptr) {
        audioRecorder = new AudioRecorder();
        audioRecorder->startRecord();
    }
}
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_pcmplay_AudioRecorder_stopRecord(JNIEnv *env, jobject thiz) {
    if (audioRecorder != nullptr) {
        audioRecorder->stopRecord();
    }
}
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_pcmplay_AudioRecorder_release(JNIEnv *env, jobject thiz) {
    if (audioRecorder != nullptr) {
        audioRecorder->release();
        delete audioRecorder;
        audioRecorder = nullptr;
    }
}

对应java层:

/**
 * Created by guxiuzhong on 2021/06/05 2:03 下午
 */
public class AudioRecorder {
    static {
        System.loadLibrary("native-lib");
    }

    public native void startRecord();

    public native void stopRecord();

    public native void release();
}

源码

https://github.com/ta893115871/PCMPlay

总结

学习了OpenSL ES的一些基础知识,以及它的优缺点,使用场景。

学习了OpenSL ES采集音频的流程。

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

推荐阅读更多精彩内容