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閱讀 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

推薦閱讀更多精彩內容