实现录音功能

我们知道AndroidApI提供了MediaRecorder和AudioRecord两个类给开发者来很方便地实现音视频的录制(前者可以实现音频和视频的录制,后者只能实现音频的录制)。这两个类都提供了start()和stop()方法用于开始和结束音频或视频的录制,但令人费解的是这两个类都没有提供pause()方法用于暂停录制音视频,因为在实际应用当中,暂停录制的功能是非常有必要的,暂不清楚Google工程师们在设计API时是如何考量的而没有添加这个方法,可能另有玄机吧。那既然android自身没有提供这样一个方法,就只有我们自己来实现了,那么问题就来了,就是到底如何实现音频录制的暂停方法呢?别急,先讲一下我在工作中所遇到的需求,如下:需实现音频录制的暂停功能,并且生成的音频文件格式必须是m4a格式。为什么项目中音频文件一定要采用m4a格式的呢?有以下几点原因:

1. 录制相同时间的音频,使用m4a格式存储的文件的大小要比使用其它格式类型存储的文件的大小要小(通过实验多次,在相同采样率16000的情况下,一般录制5分钟的音频,采用m4a格式存储的音频文件只有1.2Mb,而采用arm、mp3及其它格式的一般都有2-5Mb),这样当用户需要下载或上传录制的音频文件时,可以节省流量,并且相同压缩率的前提下,m4a格式音频的音质相比其它格式的也更高;

2.产品同时拥有Android客户端和iOS客户端,那为了避免使用Android客户端的用户录制的音频上传到服务器之后,使用ios客户端的用户下载下来发生无法播放的问题,我们需统一录制音频的存储格式。由于Iphone手机官方推荐的音频格式是m4a且对m4a格式的音频文件支持度较高,再综合第一点来看,于是我们选择m4a格式作为音频文件的存储格式。

好了,解释了为什么音频录制文件必须使用m4a存储格式之后,接下来我们来解决如何实现音频的录制的暂停功能。前面讲了,Android SDK

API提供了MediaRecorder和AudioRecord两个类来完成音视频的录制方法,我们看下它们两者之间的特点和区别:

MediaRecorder:

特性:该类集成了录音、编码和压缩等功能,可根据设置的编码格式的参数直接生成各种格式的音频文件(如arm、

mp3或m4a等),由于集成度较高,因此使用起来简单,但灵活度不高,不能实现像AudioRecord那样进行音

频的实时处理。

AudioRecord:

特性:该类录制的音频为原始的PCM二进制音频数据,没有文件头和文件尾,生成的PCM文件不能直接使用

Mediaplayer播放,只能使用AudioTrack播放。使用AudioRecord可以实现边录边播的音频实时处理。

了解了这两个类的特性之后,起初我决定使用MediaRecorder类来解决录制暂停的问题,具体的思路如下:

(1)每次触发开始录制和暂停录制音频的事件时都单独保存一个m4a格式的音频文件,直到最后触发停止录制音频的事件时,将之前录制的若干m4a格式的音频文件合并成一个文件。如图下:

这种方法比较好理解,也容易想到,不过在实现过程中遇到了一个技术难点,那就是多个m4a格式的音频文件的合并并不是简单地将文件的内容拷贝到一个文件中,而是要通过分析每一个m4a格式的音频文件,计算出每个文件头的结构大小,并将文件头去掉,再将文件进行拷贝合并。通过查阅资料,发现m4a格式的音频文件头是由多个包含关系的ATOM结构组成,且每个不同的m4a格式的音频文件的文件头的大小都不一样,这样使得多个m4a文件头文件解析和合并变得较为复杂,若有多个m4a文件需要合并,那么会变得较为耗时。再者,对于没有足够音视频文件解析和编解码经验的开发者来讲,要精准地得解析一个m4a文件,挑战性太大(网上这方面的资料也寥寥无几),有兴趣的读者可以进行深入研究。

上述方法行不通,于是只好作罢,后来又想到了另外一种方法,也是我解决问题的最终方案,具体的思路如下:

(2)由于使用AudioRecord类提供的方法录制的音频是原始的PCM格式的二进制数据,该格式的文件没有文件头信息,那么我们在进行文件合并时就就无需解析文件结构去掉对应的文件头,这样就变成了二进制数据地简单拷贝和合并。我在这里实现的方式是在录制音频的过程中采用边录制边写入的方式不断地向同一个文件写入录制的二进制音频数据。当触发暂停录音事件时,停止录制停止写入二进制数据,当触发继续录音事件时,则继续录制和向文件中写入数据。最后停止写入数据时,将PCM二进制音频文件编码成m4a格式的音频文件。如图下:

上面方法描述中,实现边录制边写入的功能倒比较简单,关键难点是如何将PCM二进制数据编码成目标的m4a格式的音频数据,要实现音视频的编解码,一般都是使用第三方开源的编解码库,比较著名的有FFMpeg和Speex,这些库都提供了录制、转换以及流化音视频的完整解决方案,不过在此我的需求只是需要简单地实现编码工作,使用这些开源库体积太大,有点杀鸡用牛刀的感觉。因此,通过研究和查阅资料,我在github上找到了一个非常有用的编解码开源项目android-aac-enc(地址:https://github.com/timsu/android-aac-enc),该开源项目能完美地实现将原始的pcm格式的二进制数据编码成m4a格式的数据文件,相比于FFmpeg库,这个库有以下几点优点:

1. aac-enc库的体积比FFmpeg库的体积更小;

2. 相比FFMpeg, aac-enc实现格式转换更加简单和快速;

3. aac-enc比FFmpeg需要编译更少的底层的代码。

该开源项目使用起来也非常地简单,通过分析其示例代码我们可以通过以下四个步骤来实现音频的编码工作,代码如下:

/**

* 1.初始化编码配置

*

* 32000 : 音频的比特率

* 2 : 音频的声道

* sampleRateInHz : 音频采样率

* 16 :音频数据格式,PCM 16位每个样本

* FileUtils.getAAcFilePath(mAudioRecordFileName) : aac音频文件的存储路径

*/

encoder.init(32000, 2, sampleRateInHz, 16, FileUtils.

getAAcFilePath(mAudioRecordFileName));

/**

* 2.对二进制代码进行编码

*

* b :需要编码的二进制音频流

*/

encoder.encode(b);

/**

* 3. 从pcm二进制数据转aac音频文件编码完成

*

*/

encoder.uninit();

/**

* 4. 将aac文件转码成m4a文件

*

* FileUtils.getAAcFilePath(mAudioRecordFileName) :需要编码的aac文件路径

* FileUtils.getM4aFilePath(mAudioRecordFileName) :编码成m4a文件的目标路径

*/

new AACToM4A().convert(mContext, FileUtils.getAAcFilePath(mAudioRecordFileName),

FileUtils.getM4aFilePath(mAudioRecordFileName));

使用起来是不是很简单方便,我们无需对音频文件格式和文件头进行判断和解析,只需要通过该开源项目封装的api方法直接调用就可以很快速的将原始的二进制PCM音频数据转换成m4a格式的音频数据文件。感兴趣的读者可以去研究一下该项目的源码,了解一下其内部的实现,这里暂且不深入探究。

基本上明确好思路和编码的实现方法后,接下来就是具体的实现过程了,我们将依据上面的思路和方法来实现一个具有暂停功能的音频录制Demo

如何使用AudioRecord类来实现音频的录制,这方面的资料很多,读者可以先学习,简单地入一下门。接下来我们先运行一下Demo,来看一下效果图

(1)初始界面
(2)正在录制界面
(3)暂停界面

                                                                       

(4)播放界面
           (5)暂停播放界面                              

粗略看了Demo的运行效果图后,接下来我们就要来实现,这里由于要使用aac-encode项目来实现音频的编码,则需将该项目以library的形式集成到我们的Demo中,做完该项工作后,我们就可以在Demo工程  中写其它相关的逻辑代码了,下面看一下实现demo的关键代码,首先是RecordAct.Java文件中的代码,该类为主界面类,主要实现了界面的初始化、音频的录制和音频播放的功能,具体的代码如下:

publicclassRecordActextendsActivityimplementsOnClickListener{

/**

* Status:录音初始状态

*/

privatestaticfinalintSTATUS_PREPARE =0;

/**

* Status:正在录音中

*/

privatestaticfinalintSTATUS_RECORDING =1;

/**

* Status:暂停录音

*/

privatestaticfinalintSTATUS_PAUSE =2;

/**

* Status:播放初始状态

*/

privatestaticfinalintSTATUS_PLAY_PREPARE =3;

/**

* Status:播放中

*/

privatestaticfinalintSTATUS_PLAY_PLAYING =4;

/**

* Status:播放暂停

*/

privatestaticfinalintSTATUS_PLAY_PAUSE =5;

privateintstatus = STATUS_PREPARE;

/**

* 录音时间

*/

privateTextView tvRecordTime;

/**

* 录音按钮

*/

privateImageView btnRecord;// 录音按钮

privatePopupWindow popAddWindow;

/**

* 试听界面

*/

privateLinearLayout layoutListen;

/**

* 录音长度

*/

privateTextView tvLength;

privateTextView recordContinue;

/**

* 重置按钮

*/

privateView resetRecord;

/**

* 结束录音

*/

privateView recordOver;

privateImageView audioRecordNextImage;

privateTextView audioRecordNextText;

/**

* 音频播放进度

*/

privateTextView tvPosition;

longstartTime =0;

/**

* 最大录音长度

*/

privatestaticfinalintMAX_LENGTH =300*1000;

privateHandler handler =newHandler();

privateRunnable runnable;

/**

* 音频录音的总长度

*/

privatestaticintvoiceLength;

/**

* 音频录音帮助类

*/

privateAudioRecordUtils mRecordUtils;

/**

* 播放进度条

*/

privateSeekBar seekBar;

/**

* 音频播放类

*/

privatePlayer player;

/**

* 录音文件名

*/

privateString audioRecordFileName;

@Override

protectedvoidonCreate(Bundle savedInstanceState) {

// TODO Auto-generated method stub

super.onCreate(savedInstanceState);

setContentView(R.layout.pop_add_record);

initView();

}

publicvoidinitView(){

//音频录音的文件名称

audioRecordFileName = TimeUtils.getTimestamp();

//初始化音频录音对象

mRecordUtils =newAudioRecordUtils(this,audioRecordFileName);

View view = LayoutInflater.from(this).inflate(R.layout.pop_add_record,null);

tvRecordTime = (TextView)findViewById(R.id.tv_time);

btnRecord = (ImageView)findViewById(R.id.iv_btn_record);

btnRecord.setOnClickListener(this);

recordContinue = (TextView)findViewById(R.id.record_continue_txt);

resetRecord = findViewById(R.id.btn_record_reset);

recordOver = findViewById(R.id.btn_record_complete);

resetRecord.setOnClickListener(this);

recordOver.setOnClickListener(this);

audioRecordNextImage = (ImageView)findViewById(R.id.recrod_complete_img);

audioRecordNextText = (TextView)findViewById(R.id.record_complete_txt);

layoutListen = (LinearLayout)findViewById(R.id.layout_listen);

tvLength = (TextView)findViewById(R.id.tv_length);

tvPosition = (TextView)findViewById(R.id.tv_position);

seekBar = (SeekBar)findViewById(R.id.seekbar_play);

seekBar.setOnSeekBarChangeListener(newSeekBarChangeEvent());

seekBar.setEnabled(false);

player =newPlayer(seekBar, tvPosition);

player.setMyPlayerCallback(newMyPlayerCallback() {

@Override

publicvoidonPrepared() {

seekBar.setEnabled(true);

}

@Override

publicvoidonCompletion() {

status = STATUS_PLAY_PREPARE;

seekBar.setEnabled(false);

seekBar.setProgress(0);

tvPosition.setText("00:00");

recordContinue.setBackgroundResource(R.drawable.record_audio_play);

}

});

popAddWindow =newPopupWindow(view, LayoutParams.MATCH_PARENT,

LayoutParams.MATCH_PARENT);

popAddWindow.setFocusable(true);

popAddWindow.setAnimationStyle(R.style.pop_anim);

popAddWindow.setBackgroundDrawable(newBitmapDrawable());

}

publicvoidhandleRecord(){

switch(status){

caseSTATUS_PREPARE:

mRecordUtils.startRecord();

btnRecord.setBackgroundResource(R.drawable.record_round_red_bg);

status = STATUS_RECORDING;

voiceLength =0;

timing();

break;

caseSTATUS_RECORDING:

pauseAudioRecord();

resetRecord.setVisibility(View.VISIBLE);

recordOver.setVisibility(View.VISIBLE);

btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg);

recordContinue.setVisibility(View.VISIBLE);

status = STATUS_PAUSE;

break;

caseSTATUS_PAUSE:

mRecordUtils.startRecord();

resetRecord.setVisibility(View.INVISIBLE);

recordOver.setVisibility(View.INVISIBLE);

btnRecord.setBackgroundResource(R.drawable.record_round_red_bg);

recordContinue.setVisibility(View.INVISIBLE);

status = STATUS_RECORDING;

timing();

break;

caseSTATUS_PLAY_PREPARE:

player.playUrl(FileUtils.getM4aFilePath(audioRecordFileName));

recordContinue.setBackgroundResource(R.drawable.record_audio_play_pause);

status = STATUS_PLAY_PLAYING;

break;

caseSTATUS_PLAY_PLAYING:

player.pause();

recordContinue.setBackgroundResource(R.drawable.record_audio_play);

status = STATUS_PLAY_PAUSE;

break;

caseSTATUS_PLAY_PAUSE:

player.play();

recordContinue.setBackgroundResource(R.drawable.record_audio_play_pause);

status = STATUS_PLAY_PLAYING;

break;

}

}

/**

* 暂停录音

*/

publicvoidpauseAudioRecord(){

mRecordUtils.pauseRecord();

if(handler !=null&& runnable !=null) {

handler.removeCallbacks(runnable);

runnable =null;

}

}

/**

* 停止录音

*/

publicvoidstopAudioRecord(){

pauseAudioRecord();

mRecordUtils.stopRecord();

status = STATUS_PLAY_PREPARE;

showListen();

}

/**

* 重新录音参数初始化

*/

@SuppressLint("NewApi")

publicvoidresetAudioRecord(){

//停止播放音频

player.stop();

pauseAudioRecord();

mRecordUtils.reRecord();

status = STATUS_PREPARE;

voiceLength =0;

tvRecordTime.setTextColor(Color.WHITE);

tvRecordTime.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength));

recordContinue.setText(R.string.record_continue);

recordContinue.setBackground(null);

recordContinue.setVisibility(View.GONE);

layoutListen.setVisibility(View.GONE);

tvRecordTime.setVisibility(View.VISIBLE);

audioRecordNextImage.setImageResource(R.drawable.btn_record_icon_complete);

audioRecordNextText.setText(R.string.record_over);

btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg);

resetRecord.setVisibility(View.INVISIBLE);

recordOver.setVisibility(View.INVISIBLE);

}

/**

* 计时功能

*/

privatevoidtiming() {

runnable =newRunnable() {

@Override

publicvoidrun() {

voiceLength +=100;

if(voiceLength >= (MAX_LENGTH -10*1000)) {

tvRecordTime.setTextColor(getResources().getColor(

R.color.red_n));

}else{

tvRecordTime.setTextColor(Color.WHITE);

}

if(voiceLength > MAX_LENGTH) {

stopAudioRecord();

}else{

tvRecordTime.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength));

handler.postDelayed(this,100);

}

}

};

handler.postDelayed(runnable,100);

}

@Override

publicvoidonClick(View v) {

// TODO Auto-generated method stub

switch(v.getId()) {

caseR.id.iv_btn_record:

handleRecord();

break;

caseR.id.btn_record_reset:

resetAudioRecord();

break;

caseR.id.btn_record_complete:

stopAudioRecord();

break;

default:

break;

}

}

/**

* 显示播放界面

*/

privatevoidshowListen() {

layoutListen.setVisibility(View.VISIBLE);

tvLength.setText(TimeUtils.convertMilliSecondToMinute2(voiceLength));

tvRecordTime.setVisibility(View.GONE);

resetRecord.setVisibility(View.VISIBLE);

recordOver.setVisibility(View.INVISIBLE);

recordContinue.setVisibility(View.VISIBLE);

seekBar.setProgress(0);

tvPosition.setText("00:00");

btnRecord.setBackgroundResource(R.drawable.record_round_blue_bg);

recordContinue.setText(null);

recordContinue.setBackgroundResource(R.drawable.record_audio_play);

}

/**

*

* SeekBar进度条改变事件监听类

*/

classSeekBarChangeEventimplementsSeekBar.OnSeekBarChangeListener {

intprogress;

@Override

publicvoidonProgressChanged(SeekBar seekBar,intprogress,

booleanfromUser) {

if(null!= player && player.mediaPlayer !=null) {

this.progress = progress * player.mediaPlayer.getDuration()

/ seekBar.getMax();

tvPosition.setText(TimeUtils

.convertMilliSecondToMinute2(player.currentPosition));

}

}

@Override

publicvoidonStartTrackingTouch(SeekBar seekBar) {

}

@Override

publicvoidonStopTrackingTouch(SeekBar seekBar) {

if(player.mediaPlayer !=null) {

player.mediaPlayer.seekTo(progress);

}

}

}

@Override

protectedvoidonDestroy() {

// TODO Auto-generated method stub

super.onDestroy();

player.stop();

}

}

上面代码注释比较清楚,且好理解,因此不多分析,读者自行学习。下面再来看一下AudioRecordUtils类的代码,该类是音频录制功能的主要实现代码,里面简单地封装了开始录音、暂停录音、停止录音和重新录音几个方法,在开发中只要调用就行,来看看具体的实现代码,如下:

[java]view plaincopy

publicclassAudioRecordUtils {

privatefinalintaudioSource = MediaRecorder.AudioSource.MIC;

// 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025

privatefinalintsampleRateInHz =16000;

// 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道

privatefinalintchannelConfig = AudioFormat.CHANNEL_IN_STEREO;

// 音频数据格式:PCM 16位每个样本。保证设备支持。PCM 8位每个样本。不一定能得到设备支持。

privatefinalintaudioFormat = AudioFormat.ENCODING_PCM_16BIT;

privateintinBufSize =0;

privateAudioRecord audioRecord;

privateAACEncoder encoder =null;

privateProgressDialog mProgressDialog =null;

privatebooleanisRecord =false;

privateContext mContext;

/**

* 录制的音频文件名称

*/

privateString mAudioRecordFileName;

privatestaticfinalintRECORDED_INIT_DELETE =0;

privatestaticfinalintRECORDED_COMPLETED_DELETE =1;

publicAudioRecordUtils(Context context,String audioRecordFileName){

mContext = context;

mAudioRecordFileName = audioRecordFileName;

initAudioRecord();

}

/**

* 初始化对象

*/

privatevoidinitAudioRecord(){

inBufSize = AudioRecord.getMinBufferSize(

sampleRateInHz,

channelConfig,

audioFormat);

audioRecord  =newAudioRecord(

audioSource,

sampleRateInHz,

channelConfig,

audioFormat,

inBufSize);

encoder =newAACEncoder();

deleteAllFiles(RECORDED_INIT_DELETE);

mProgressDialog =newProgressDialog(mContext);

mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);

mProgressDialog.setCanceledOnTouchOutside(false);

mProgressDialog.setCancelable(false);

mProgressDialog.setTitle("提示");

mProgressDialog.setMessage("正在保存录音,请耐心等候......");

}

/**

* 开始录音

*/

publicvoidstartRecord(){

newAudioRecordTask().execute();

}

/**

* 暂停录音

*/

publicvoidpauseRecord(){

isRecord =false;

}

/**

* 停止录音

*/

publicvoidstopRecord(){

newAudioEncoderTask().execute();

}

/**

* 重新录制

*/

publicvoidreRecord(){

//重新录制时,删除录音文件夹中的全部文件

deleteAllFiles(RECORDED_INIT_DELETE);

}

privatevoidencodeAudio(){

try{

//读取录制的pcm音频文件

DataInputStream mDataInputStream =newDataInputStream(newFileInputStream(

FileUtils.getPcmFilePath(mAudioRecordFileName)));

byte[] b =newbyte[(int)newFile(FileUtils.

getPcmFilePath(mAudioRecordFileName)).length()];

mDataInputStream.read(b);

//初始化编码配置

encoder.init(32000,2, sampleRateInHz,16, FileUtils.

getAAcFilePath(mAudioRecordFileName));

//对二进制代码进行编码

encoder.encode(b);

//编码完成

encoder.uninit();

//关闭流

mDataInputStream.close();

try{

//将aac文件转码成m4a文件

newAACToM4A().convert(mContext, FileUtils.getAAcFilePath(mAudioRecordFileName),

FileUtils.getM4aFilePath(mAudioRecordFileName));

}catch(IOException e) {

Log.e("ERROR","error converting", e);

}

deleteAllFiles(RECORDED_COMPLETED_DELETE);

}catch(FileNotFoundException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}catch(IOException e1) {

// TODO Auto-generated catch block

e1.printStackTrace();

}

}

classAudioRecordTaskextendsAsyncTask{

@Override

protectedVoid doInBackground(Void... params) {

// TODO Auto-generated method stub

if(audioRecord ==null){

initAudioRecord();

}

RandomAccessFile mRandomAccessFile =null;

try{

mRandomAccessFile =newRandomAccessFile(newFile(

FileUtils.getPcmFilePath(mAudioRecordFileName)),"rw");

byte[] b =newbyte[inBufSize/4];

//开始录制音频

audioRecord.startRecording();

//判断是否正在录制

isRecord =true;

while(isRecord){

audioRecord.read(b,0, b.length);

//向文件中追加内容

mRandomAccessFile.seek(mRandomAccessFile.length());

mRandomAccessFile.write(b,0, b.length);

}

//停止录制

audioRecord.stop();

mRandomAccessFile.close();

}catch(FileNotFoundException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}catch(IOException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

returnnull;

}

}

classAudioEncoderTaskextendsAsyncTask{

@Override

protectedvoidonPreExecute() {

// TODO Auto-generated method stub

super.onPreExecute();

if(mProgressDialog !=null&& !mProgressDialog.isShowing()){

mProgressDialog.show();

}

}

@Override

protectedLong doInBackground(Void... params) {

// TODO Auto-generated method stub

encodeAudio();

returnnull;

}

@Override

protectedvoidonPostExecute(Long result) {

// TODO Auto-generated method stub

super.onPostExecute(result);

if(mProgressDialog.isShowing()){

mProgressDialog.cancel();

mProgressDialog.dismiss();

}

}

}

/**

* 清空音频录制文件夹中的所有文件

* @param isRecorded

*/

publicvoiddeleteAllFiles(intisRecorded){

File[] files =newFile(FileUtils.getAudioRecordFilePath()).listFiles();

switch(isRecorded) {

caseRECORDED_INIT_DELETE:

for(File file: files){

file.delete();

}

break;

caseRECORDED_COMPLETED_DELETE:

for(File file: files){

if(!file.getName().equals(mAudioRecordFileName + Constants.M4A_SUFFIX)){

file.delete();

}

}

break;

default:

break;

}

}

}

上面代码关键处都有注释,读者可自行学习。自此,我们基本熟悉了实现能够暂停录音功能的关键代码,代码没有全部贴出,想要完整的Demo可在文章末尾下载来仔细研究。最后我再补充一点,就是若读者对录制的音频格式没有严格的要求话,如录制的音频格式是arm格式,则没有必要考虑到音频的编解码问题,因为arm格式的音频文件的文件头信息固定是6个字节的大小,那这种情况读者可以采用文章开头所说的第一种方法,就是每次点击暂停事件都录制成一个arm文件,在最后合并的时候,只需要去掉第2至n个文件的前6个字节,然后进行文件的拷贝合并就行。

demo 下载地址 

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

推荐阅读更多精彩内容