音頻采集

1 音頻采集流程

聲音是由物體振動產(chǎn)生的聲波囱嫩,是通過介質(zhì)(空氣或固體、液體)傳播并能被人或動物聽覺器官所感知的波動現(xiàn)象。聲波是一種在時(shí)間和振幅上連續(xù)的模擬量破喻,麥克風(fēng)就是一種采集聲波并將其轉(zhuǎn)換成模擬電壓信號輸出的裝置,有了聲波的模擬電壓信號盟榴,下一步需要將模擬信號數(shù)字化曹质,即將模擬信號通過模數(shù)轉(zhuǎn)換器(A/D)后轉(zhuǎn)換成數(shù)字信號,最常見的模數(shù)轉(zhuǎn)換方式就是脈沖編碼調(diào)制PCM(Pulse Code Modulation)擎场,PCM編碼過程如下圖所示:


PCM編碼過程

從上圖中可以看到PCM編碼主要有三個(gè)過程:采樣羽德、量化、編碼迅办。

1> 采樣
將時(shí)間連續(xù)的模擬信號按照采樣率提取樣值宅静,變?yōu)闀r(shí)間軸上離散的抽樣信號的過程。采樣率是每秒從模擬信號中提取樣值的次數(shù)站欺。Nyquist–Shannon(奈奎斯特-香農(nóng))采樣定律表明如果至少以模擬信號最高頻率2倍的采樣率對模擬信號進(jìn)行均勻采樣姨夹,那么原始模擬信號才能不失真的從采樣產(chǎn)生的離散值中完全恢復(fù)。人耳可以聽到的聲波頻率范圍是 20Hz~22.05kHz矾策,因此44.1kHz/16bit的音頻數(shù)據(jù)被認(rèn)為是無損音頻磷账。

2> 量化
抽樣信號雖然是時(shí)間軸上離散的信號,但仍然是模擬信號贾虽,其樣值在一定的取值范圍內(nèi)逃糟,可有無限多個(gè)值。顯然,對無限個(gè)樣值給出數(shù)字碼組來對應(yīng)是不可能的绰咽。為了實(shí)現(xiàn)以數(shù)字碼表示樣值菇肃,必須采用“四舍五入”的方法把樣值分級“取整”,使一定取值范圍內(nèi)的樣值由無限多個(gè)值變?yōu)橛邢迋€(gè)值剃诅。這一過程稱為量化巷送。

量化后的抽樣信號與量化前的抽樣信號相比較,當(dāng)然有所失真矛辕,且不再是模擬信號笑跛。這種量化失真在接收端還原模擬信號時(shí)表現(xiàn)為噪聲,并稱為量化噪聲聊品。量化噪聲的大小取決于把樣值分級“取整”的方式飞蹂,分的級數(shù)越多,即量化級差或間隔越小翻屈,量化噪聲也越小陈哑。

3> 編碼
量化后的抽樣信號就轉(zhuǎn)化為按抽樣時(shí)序排列的一串十進(jìn)制數(shù)字碼流,即十進(jìn)制數(shù)字信號伸眶。簡單高效的數(shù)據(jù)系統(tǒng)是二進(jìn)制碼系統(tǒng)惊窖,因此應(yīng)將十進(jìn)制數(shù)字代碼變換成二進(jìn)制編碼。這種把量化的抽樣信號變換成給定字長(采樣位數(shù))的二進(jìn)制碼流的過程稱為編碼

經(jīng)過上面的PCM編碼過程得到的數(shù)字信號就是 PCM音頻數(shù)據(jù)厘贼。

在PCM編碼過程中主要用3個(gè)參數(shù)表現(xiàn)PCM音頻數(shù)據(jù):采樣率界酒、采樣位數(shù)以及聲道數(shù),
其中采樣率嘴秸、采樣位數(shù)上面已經(jīng)講解過毁欣,通道數(shù)即采集聲音的通道數(shù),有單聲道(mono)和立體聲(雙聲道stereo)等岳掐,聲道數(shù)越多越能體現(xiàn)聲音的空間立體效果凭疮。

2 PCM音頻數(shù)據(jù)的存儲方式

采集的PCM音頻數(shù)據(jù)是需要保存到本地文件中,如果用單聲道采集的串述,則按時(shí)間的先后順序依次存入执解,如果是雙聲道的話則按時(shí)間先后順序交叉地存入,如下圖所示:


PCM音頻數(shù)據(jù)存儲格式

PCM音頻數(shù)據(jù)一般無法通過播放器直接播放剖煌〔酿校可以使用ffplay工具進(jìn)行播放

ffplay -f s16le -ar 44100 -ac 1 -i raw.pcm
參數(shù)解釋
-f s16le: 設(shè)置音頻格式為有符號16位小端格式(signed 16 bits little endian),對應(yīng)Android中的AudioFormat.ENCODING_PCM_16BIT
-ar 44100 :設(shè)置音頻采樣率(audiorate)為44100
-ac 1:設(shè)置聲道數(shù)(audiochannels)1,單聲道為1耕姊,雙聲道為2
-i raw.pcm :設(shè)置輸入的pcm音頻文件

通常將PCM音頻數(shù)據(jù)轉(zhuǎn)化為WAVE文件就可以用播放器直接解析播放桶唐,WAVE是微軟公司專門為Windows開發(fā)的一種標(biāo)準(zhǔn)數(shù)字音頻文件,該文件能記錄各種單聲道或立體聲的聲音信息茉兰,并能保證聲音不失真尤泽。它符合資源互換文件格式(RIFF)規(guī)范

RIFF文件(符合RIFF規(guī)范的文件)是windows環(huán)境下大部分多媒體文遵循的一種文件結(jié)構(gòu),RIFF文件所包含的數(shù)據(jù)類型由該文件的擴(kuò)展名來標(biāo)識,能以RIFF文件存儲的數(shù)據(jù)包括:音頻視頻交錯(cuò)格式數(shù)據(jù)(.AVI)坯约、 波形格式數(shù)據(jù)(.WAV) 熊咽、位圖格式數(shù)據(jù)(.RDI)、 MIDI格式數(shù)據(jù)(.RMI)闹丐、調(diào)色板格式(.PAL)横殴、多媒體電影(.RMN)、動畫光標(biāo)(.ANI)等卿拴,RIFF文件結(jié)構(gòu)如下圖所示:

RIFF文件結(jié)構(gòu)

如上圖所示衫仑,chunk是構(gòu)成RIFF文件的基本單元,RIFF文件是由chunk嵌套構(gòu)成堕花,RIFF文件首先存放的必須是一個(gè)RIFF chunk文狱,并且只能有這一個(gè)標(biāo)志為RIFF的chunk。chunk的詳細(xì)說明如下:

ID: 塊的唯一標(biāo)識缘挽,其值可為RIFF,LIST,fmt,fact,data等瞄崇。
Size: 塊中Data的大小,以字節(jié)為單位壕曼。
Data: 塊中的實(shí)際數(shù)據(jù)苏研。

只有ID為RIFF或者LIST的chunk才能包含其他的chunk,ID為RIFF的chunk中Data的起始位置的FormType用于標(biāo)識Data中的chunk的數(shù)據(jù)類型腮郊。

WAVE文件中chunk的排列方式依次是:RIFF chunk(FormType 為 WAVE)楣富,F(xiàn)ormat sub-chunk,F(xiàn)act sub-chunk(附加塊伴榔,可選,采用壓縮編碼的WAVE文件庄萎,必須要有Fact chunk踪少,該塊中只有一個(gè)數(shù)據(jù),為每個(gè)聲道的采樣總數(shù))糠涛,Data chunk援奢。接下來我們看看WAVE文件結(jié)構(gòu),如下圖所示:


WAV音頻文件結(jié)構(gòu)

Data sub-chunk中的Data中存放具體的音頻數(shù)據(jù)忍捡,將PCM音頻數(shù)據(jù)轉(zhuǎn)換成WAV音頻文件實(shí)際上就是把PCM音頻數(shù)據(jù)放到該位置集漾。

3 Android上采集和播放PCM音頻數(shù)據(jù)

有了上面的理論基礎(chǔ),接下來就在Android手機(jī)上實(shí)現(xiàn)一下砸脊,使用AudioRecord采集PCM音頻數(shù)據(jù)的代碼實(shí)現(xiàn):

private var audioRecord: AudioRecord? = null
private const val sampleRateInHz: Int = 44100
private const val bitsPerSample: Int = 16
private const val channelConfig = AudioFormat.CHANNEL_IN_MONO
private const val audioFormat = AudioFormat.ENCODING_PCM_16BIT
private val bufferSize =
    AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)

private var pcmFile: File? = null
private var mScope: CoroutineScope? = null

/**
 * 創(chuàng)建音頻錄制器
 *
 * @author cytmxk
 * @since 2021/10/12
 */
private fun createAudioRecord(): AudioRecord {
    Log.d(TAG, "createAudioRecord: bufferSize = $bufferSize");

    // audioSource: 音頻來源具篇,MediaRecorder.AudioSource.MIC 代表來源于麥克風(fēng)
    // sampleRateInHz: 采樣率,每秒取得聲音樣本的次數(shù)凌埂,采樣頻率越高驱显,聲音的質(zhì)量也就越好,還原的聲音就越真實(shí),但同時(shí)它占用的資源越多埃疫。常見的采樣率為44100 即44.1KHZ
    // channelConfig: 聲道配置伏恐,分為單聲道和立體聲道,CHANNEL_IN_MONO代表單聲道栓霜,CHANNEL_IN_STEREO代表立體聲道
    // audioFormat: 音頻格式翠桦,ENCODING_PCM_16BIT代表通過PCM進(jìn)行采樣編碼,采樣的大小為16位
    // bufferSizeInBytes: 音頻采集緩沖區(qū)大小,計(jì)算公式為 采樣率 x 位寬 x 采樣時(shí)間 x 通道數(shù)申屹,采樣時(shí)間一般取 2.5ms~120ms 之間坪蚁,
    // 由廠商或者具體的應(yīng)用決定,采樣時(shí)間取得越短碎片化的數(shù)據(jù)也就會越多闻鉴,開發(fā)中使用getMinBufferSize()方法的返回值,
    // 使用比getMinBufferSize()小的值則會導(dǎo)致初始化失敗茂洒。
    return AudioRecord(
        MediaRecorder.AudioSource.MIC, sampleRate,
        channelConfig, audioFormat,
        bufferSize
    )
}

/**
 * 開始音頻錄制
 *
 * @author cytmxk
 * @since 2021/10/12
 */
public fun startRecord() {
    stopRecord()

    audioRecord = createAudioRecord()
    Log.d(TAG, "captureByAudioRecord: state = ${audioRecord!!.state}")
    // 判斷視頻錄制器是否初始化成功
    if (AudioRecord.STATE_INITIALIZED != audioRecord!!.state) {
        Log.d(TAG, "AudioRecord無法初始化孟岛,請檢查錄制權(quán)限或者是否其他app沒有釋放錄音器")
    }

    // 創(chuàng)建用于保存采集的pcm音頻數(shù)據(jù)的文件
    pcmFile = MediaFileUtils.getAudioFile("test.pcm")
    Log.d(TAG, "initPCMFile: pcmFile=$pcmFile")
    pcmFile ?: return

    if (pcmFile!!.exists()) {
        pcmFile!!.delete()
    }

    // 開始采集pcm音頻數(shù)據(jù)
    val buffer = ByteArray(bufferSize)
    audioRecord!!.startRecording()

    // 在IO線程中采集pcm音頻數(shù)據(jù)
    GlobalScope.launch(Dispatchers.IO) {
        mScope = this
        var fileOutputStream: FileOutputStream? = null
        try {
            fileOutputStream = FileOutputStream(pcmFile)
            while (isActive) {
                val readStatus = audioRecord!!.read(buffer, 0, bufferSize)
                Log.d(TAG, "scope: readStatus = $readStatus")
                fileOutputStream.write(buffer)
            }
        } catch (exception: IOException) {
            Log.d(TAG, "scope: exception = $exception")
        } finally {
            fileOutputStream?.also {
                try {
                    it.close()
                } catch (exception: IOException) {
                }
            }
        }
    }
}

/**
 * 暫停音頻錄制
 *
 * @author cytmxk
 * @since 2021/10/12
 */
public fun stopRecord() {
    mScope ?: return

    mScope!!.cancel()
    mScope = null

    if (AudioRecord.STATE_UNINITIALIZED != audioRecord!!.state) {
        audioRecord!!.stop()
        // 調(diào)用release方法之后該對象不可以再次被使用,因此必須將該對象置null
        audioRecord!!.release()
        audioRecord = null
    }
}

上面的代碼都有注釋,就不在這里詳細(xì)講解了督勺,執(zhí)行完成之后會生成一個(gè)用于保存PCM音頻數(shù)據(jù)的test.pcm文件渠羞。

為了讓手機(jī)上的播放器可以播放采集的PCM音頻數(shù)據(jù),那么接下來通過下面的代碼就可將test.pcm文件中保存的PCM音頻數(shù)據(jù)轉(zhuǎn)換成WAVE文件格式并且保存到convert.wav文件中:

private fun convertPcmToWav() {
    val wavFile = MediaFileUtils.getAudioFile("convert.wav")
    wavFile ?: return

    if (wavFile.exists()) {
        wavFile.delete()
    }

    var fileInputStream: FileInputStream? = null
    var fileOutputStream: FileOutputStream? = null
    try {
        fileInputStream = FileInputStream(pcmFile)
        fileOutputStream = FileOutputStream(wavFile)
        val audioByteLen = fileInputStream.channel.size()
        val wavByteLen = audioByteLen + 36
        addWavHeader(fileOutputStream, audioByteLen, wavByteLen)
        val buffer = ByteArray(bufferSize)
        while (fileInputStream.read(buffer) != -1) {
            fileOutputStream.write(buffer)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    } finally {
        try {
            fileInputStream?.close()
            fileOutputStream?.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
}

private fun addWavHeader(
    fileOutputStream: FileOutputStream, audioByteLen: Long, wavByteLen: Long
) {
    val header = ByteArray(44)
    // WAVE chunk
    // header[0] ~ header[3] 內(nèi)容為"RIFF"
    header[0] = 'R'.code.toByte()
    header[1] = 'I'.code.toByte()
    header[2] = 'F'.code.toByte()
    header[3] = 'F'.code.toByte()
    // header[4] ~ header[7] 存儲文件的字節(jié)數(shù)(不包含ChunkID和ChunkSize這8個(gè)字節(jié))
    header[4] = (wavByteLen and 0xff).toByte()
    header[5] = (wavByteLen shr 8 and 0xff).toByte()
    header[6] = (wavByteLen shr 16 and 0xff).toByte()
    header[7] = (wavByteLen shr 24 and 0xff).toByte()
    // header[8] ~ header[11] 內(nèi)容為"WAVE"
    header[8] = 'W'.code.toByte()
    header[9] = 'A'.code.toByte()
    header[10] = 'V'.code.toByte()
    header[11] = 'E'.code.toByte()

    // "fmt " 子chunk 4個(gè)字節(jié)
    // header[12] ~ header[15] 內(nèi)容為 "fmt "
    header[12] = 'f'.code.toByte()
    header[13] = 'm'.code.toByte()
    header[14] = 't'.code.toByte()
    header[15] = ' '.code.toByte()
    // header[16] ~ header[19] 存儲該子塊的字節(jié)數(shù)(不包含Subchunk1ID和Subchunk1Size這8個(gè)字節(jié))
    header[16] = 16
    header[17] = 0
    header[18] = 0
    header[19] = 0
    // header[20] ~ header[21] 存儲音頻文件的編碼格式智哀,例如若為PCM則其存儲值為1次询,若為其他非PCM格式的則有一定的壓縮。
    header[20] = 1
    header[21] = 0
    // header[22] ~ header[23] 通道數(shù)瓷叫,單通道(CHANNEL_IN_MONO)值為1屯吊,雙通道(CHANNEL_IN_STEREO)值為2
    val channelSize = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2
    header[22] = channelSize.toByte()
    header[23] = 0
    // header[24] ~ header[27] 采樣頻率
    header[24] = (sampleRateInHz and 0xff).toByte()
    header[25] = (sampleRateInHz shr 8 and 0xff).toByte()
    header[26] = (sampleRateInHz shr 16 and 0xff).toByte()
    header[27] = (sampleRateInHz shr 24 and 0xff).toByte()
    // header[28] ~ header[31] 每秒采集的音頻字節(jié)數(shù)
    val byteRate = (audioFormat * sampleRateInHz * channelSize).toLong()
    header[28] = (byteRate and 0xff).toByte()
    header[29] = (byteRate shr 8 and 0xff).toByte()
    header[30] = (byteRate shr 16 and 0xff).toByte()
    header[31] = (byteRate shr 24 and 0xff).toByte()
    // header[32] ~ header[33] 塊對齊大小,每個(gè)采樣(包含所有聲道)需要的字節(jié)數(shù)
    header[32] = (channelSize * bitsPerSample / 8).toByte()
    header[33] = 0
    // header[34] ~ header[35] 每個(gè)采樣需要的 bit 數(shù)
    header[34] = bitsPerSample.toByte()
    header[35] = 0

    //data 子chunk
    // header[36] ~ header[39] 內(nèi)容為“data”
    header[36] = 'd'.code.toByte()
    header[37] = 'a'.code.toByte()
    header[38] = 't'.code.toByte()
    header[39] = 'a'.code.toByte()
    // header[40] ~ header[43] pcm字節(jié)數(shù)
    header[40] = (audioByteLen and 0xff).toByte()
    header[41] = (audioByteLen shr 8 and 0xff).toByte()
    header[42] = (audioByteLen shr 16 and 0xff).toByte()
    header[43] = (audioByteLen shr 24 and 0xff).toByte()
    try {
        fileOutputStream.write(header, 0, 44)
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

通過系統(tǒng)文件夾應(yīng)用找到convert.wav文件的位置摹菠,點(diǎn)擊就可以播放了盒卸。

其實(shí)PCM音頻數(shù)據(jù)也可以直接使用AudioTrack播放,實(shí)現(xiàn)代碼如下:

private var audioTrack: AudioTrack? = null
private const val sampleRateInHz: Int = 44100
private const val channelConfig = AudioFormat.CHANNEL_OUT_MONO // 錯(cuò)誤的寫成了CHANNEL_IN_MONO
private const val audioFormat = AudioFormat.ENCODING_PCM_16BIT
private val bufferSize =
    AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)

private var pcmFile: File? = null
private var mScope: CoroutineScope? = null


public fun playAudioByAudioTrack() {
    pcmFile = MediaFileUtils.getAudioFile("test.pcm")
    Log.d(TAG, "initPCMFile: pcmFile=${pcmFile}")
    pcmFile ?: return

    if (!pcmFile!!.exists()) {
        return
    }

    stopPlayAudio()
    initAudioTrackWithMode(AudioTrack.MODE_STREAM)
    if (audioTrack!!.state == AudioTrack.STATE_UNINITIALIZED) {
        Log.e(TAG, "state is uninit")
        return
    }

    // 在IO線程中播放采集的pcm音頻數(shù)據(jù)
    GlobalScope.launch(Dispatchers.IO) {
        mScope = this
        var fileInputStream: FileInputStream? = null
        try {
            fileInputStream = FileInputStream(pcmFile)
            val buffer = ByteArray(bufferSize / 2)
            //stream模式次氨,可以先調(diào)用play
            audioTrack!!.play()
            while (isActive && fileInputStream.available() > 0) {
                val readCount = fileInputStream.read(buffer)
                if (readCount == AudioTrack.ERROR_BAD_VALUE || readCount == AudioTrack.ERROR_INVALID_OPERATION) {
                    continue
                }

                if (readCount > 0 && audioTrack!!.playState == AudioTrack.PLAYSTATE_PLAYING && audioTrack!!.state == AudioTrack.STATE_INITIALIZED) {
                    audioTrack!!.write(buffer, 0, readCount)
                }
            }
        } catch (exception: IOException) {
            Log.d(TAG, "scope: exception = $exception")
        } finally {
            fileInputStream?.also {
                try {
                    it.close()
                } catch (exception: IOException) {
                }
            }
        }
    }
}

private fun initAudioTrackWithMode(mode: Int) {
    audioTrack = AudioTrack(
        AudioAttributes.Builder()
            .setLegacyStreamType(AudioManager.STREAM_MUSIC)
            .build(),
        AudioFormat.Builder()
            .setChannelMask(channelConfig)
            .setEncoding(audioFormat)
            .setSampleRate(sampleRateInHz)
            .build(),
        bufferSize,
        mode, AudioManager.AUDIO_SESSION_ID_GENERATE
    )
}

public fun stopPlayAudio() {
    mScope ?: return
    mScope!!.cancel()

    if (AudioTrack.STATE_UNINITIALIZED != audioTrack!!.state) {
        audioTrack!!.stop()
        audioTrack!!.release()
    }
}

通過執(zhí)行上面playAudioByAudioTrack方法就可以播放上面生成的test.pcm音頻文件蔽介。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市煮寡,隨后出現(xiàn)的幾起案子虹蓄,更是在濱河造成了極大的恐慌,老刑警劉巖幸撕,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薇组,死亡現(xiàn)場離奇詭異,居然都是意外死亡杈帐,警方通過查閱死者的電腦和手機(jī)体箕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門专钉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人累铅,你說我怎么就攤上這事跃须。” “怎么了娃兽?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵菇民,是天一觀的道長。 經(jīng)常有香客問我投储,道長第练,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任玛荞,我火速辦了婚禮娇掏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘勋眯。我一直安慰自己婴梧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布客蹋。 她就那樣靜靜地躺著塞蹭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讶坯。 梳的紋絲不亂的頭發(fā)上番电,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音辆琅,去河邊找鬼漱办。 笑死,一個(gè)胖子當(dāng)著我的面吹牛婉烟,可吹牛的內(nèi)容都是我干的洼冻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼隅很,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了率碾?” 一聲冷哼從身側(cè)響起叔营,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎所宰,沒想到半個(gè)月后绒尊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仔粥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年婴谱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟹但。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谭羔,死狀恐怖华糖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瘟裸,我是刑警寧澤客叉,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站话告,受9級特大地震影響兼搏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沙郭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一佛呻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧病线,春花似錦吓著、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至让虐,卻和暖如春紊撕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赡突。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工对扶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惭缰。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓浪南,卻偏偏與公主長得像,于是被迫代替她去往敵國和親漱受。 傳聞我的和親對象是個(gè)殘疾皇子络凿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

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