Android 使用 lame wav 轉(zhuǎn) mp3 颠区、pcm 轉(zhuǎn) mp3 (邊錄邊轉(zhuǎn));使用 mad mp3 轉(zhuǎn) wav通铲、mp3 轉(zhuǎn) pcm (邊播邊轉(zhuǎn))

前言

最近在研究wav,mp3,pcm之間的相互轉(zhuǎn)換,發(fā)現(xiàn)mp3的相關(guān)操作毕莱,都需要解碼mp3或者編碼mp3,無法直接對mp3文件做操作颅夺。下面是本文的相關(guān)知識點(diǎn)朋截。

  • wav 轉(zhuǎn) mp3
  • pcm 轉(zhuǎn) mp3 (邊錄邊轉(zhuǎn))
  • mp3 轉(zhuǎn) wav
  • mp3 轉(zhuǎn) pcm (邊播邊轉(zhuǎn))

1. Android 使用 lame wav 轉(zhuǎn)碼 mp3

1.1 準(zhǔn)備工作

下載 lame_x.xx.x 包

Lame
Lame 是最好的mp3編碼器,速度快吧黄,效果好部服,特別是中高碼率和VBR編碼方面。
http://lame.sourceforge.net/

1.2 創(chuàng)建 android 項(xiàng)目 lame

創(chuàng)建jni目錄 并 復(fù)制 lame-x.xx.x 包下的libmp3lame 目錄下的所有 .c和.h文件和 include目錄下的lame.h

1.2.1 在jni目錄下創(chuàng)建 Android.mk文件

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LAME_LIBMP3_DIR := lame_3.99.5_libmp3lame

LOCAL_MODULE    := mp3lame
LOCAL_SRC_FILES := $(LAME_LIBMP3_DIR)/bitstream.c $(LAME_LIBMP3_DIR)/fft.c $(LAME_LIBMP3_DIR)/id3tag.c $(LAME_LIBMP3_DIR)/mpglib_interface.c $(LAME_LIBMP3_DIR)/presets.c $(LAME_LIBMP3_DIR)/quantize.c $(LAME_LIBMP3_DIR)/reservoir.c $(LAME_LIBMP3_DIR)/tables.c $(LAME_LIBMP3_DIR)/util.c $(LAME_LIBMP3_DIR)/VbrTag.c $(LAME_LIBMP3_DIR)/encoder.c $(LAME_LIBMP3_DIR)/gain_analysis.c $(LAME_LIBMP3_DIR)/lame.c $(LAME_LIBMP3_DIR)/newmdct.c $(LAME_LIBMP3_DIR)/psymodel.c $(LAME_LIBMP3_DIR)/quantize_pvt.c $(LAME_LIBMP3_DIR)/set_get.c $(LAME_LIBMP3_DIR)/takehiro.c $(LAME_LIBMP3_DIR)/vbrquantize.c $(LAME_LIBMP3_DIR)/version.c lame_util.c

include $(BUILD_SHARED_LIBRARY)

1.2.2 在jni目錄下創(chuàng)建 Application.mk文件

APP_PLATFORM := android-9 
APP_ABI := all
APP_CFLAGS += -DSTDC_HEADERS

1.2.3 然后在gradle里面進(jìn)行配置(在使用該jni的gradle里進(jìn)行配置)

    sourceSets.main {
        jni.srcDirs = [] // This prevents the auto generation of Android.mk
        jniLibs.srcDir 'src/main/libs' // This is not necessary unless you have precompiled libraries in your project.
    }

1.2.4 最后ndk-build生成相應(yīng).so庫(ndk-build會生成相應(yīng)的.h頭文件)

1.3 編寫wav轉(zhuǎn)mp3的lame_util.c

大致分為兩步

  1. 通過Jstring2CStr方法將java中的jstring類型轉(zhuǎn)化成c語言的char字符串
  2. 然后再通過convert方法將wav轉(zhuǎn)碼成mp3文件
    (convert方法的參數(shù)為拗慨,wav路徑廓八,mp3路徑,采樣率赵抢,聲道數(shù)剧蹂,比特率)
    下面為大家科普一下相關(guān)參數(shù)以及知識點(diǎn)

1.3.1 Lame相關(guān)參數(shù)

  • 采樣率(sampleRate):采樣率越高聲音的還原度越好。
  • 比特率(bitrate):每秒鐘的數(shù)據(jù)量烦却,越高音質(zhì)越好宠叼。
  • 聲道數(shù)(channels):聲道的數(shù)量,通常只有單聲道和雙聲道其爵,雙聲道即所謂的立體聲冒冬。
  • 比特率控制模式:ABR、VBR摩渺、CBR简烤,這3中模式含義很容易查詢到,不在贅述

Lame采樣率支持(Hz)

MPEG1 MPEG2 MPEG2.5
44100 22050 11025
48000 24000 12000
32000 16000 8000

Lame比特率支持(bit/s)

MPEG1 MPEG2 MPEG2.5
32 8 8
40 16 16
48 24 24
56 32 32
64 40 40
80 48 48
96 56 56
112 64 64
128 80
160 96
192 112
224 128
256 144
320 160

1.3.2 編碼流程

初始化編碼參數(shù)

  • lame_init:初始化一個編碼參數(shù)的數(shù)據(jù)結(jié)構(gòu)摇幻,給使用者用來設(shè)置參數(shù)横侦。

設(shè)置編碼參數(shù)

  • lame_set_in_samplerate:設(shè)置被輸入編碼器的原始數(shù)據(jù)的采樣率。
  • lame_set_out_samplerate:設(shè)置最終mp3編碼輸出的聲音的采樣率囚企,如果不設(shè)置則和輸入采樣率一樣丈咐。
  • lame_set_num_channels :設(shè)置被輸入編碼器的原始數(shù)據(jù)的聲道數(shù)。
  • lame_set_mode :設(shè)置最終mp3編碼輸出的聲道模式龙宏,如果不設(shè)置則和輸入聲道數(shù)一樣棵逊。參數(shù)是枚舉,STEREO代表雙聲道银酗,MONO代表單聲道辆影。
  • lame_set_VBR:設(shè)置比特率控制模式徒像,默認(rèn)是CBR,但是通常我們都會設(shè)置VBR蛙讥。參數(shù)是枚舉锯蛀,vbr_off代表CBR,vbr_abr代表ABR(因?yàn)锳BR不常見次慢,所以本文不對ABR做講解)vbr_mtrh代表VBR旁涤。
  • lame_set_brate:設(shè)置CBR的比特率,只有在CBR模式下才生效迫像。
  • lame_set_VBR_mean_bitrate_kbps:設(shè)置VBR的比特率劈愚,只有在VBR模式下才生效。

其中每個參數(shù)都有默認(rèn)的配置闻妓,如非必要可以不設(shè)置菌羽。這里只介紹了幾個關(guān)鍵的設(shè)置接口,還有其他的設(shè)置接口可以參考lame.h(lame的文檔里只有命令行程序的用法由缆,沒有庫接口的用法)注祖。

初始化編碼器器

lame_init_params:根據(jù)上面設(shè)置好的參數(shù)建立編碼器

編碼PCM數(shù)據(jù)

  • lame_encode_bufferlame_encode_buffer_interleaved:將PCM數(shù)據(jù)送入編碼器,獲取編碼出的mp3數(shù)據(jù)均唉。這些數(shù)據(jù)寫入文件就是mp3文件是晨。
  • 其中lame_encode_buffer輸入的參數(shù)中是雙聲道的數(shù)據(jù)分別輸入的,lame_encode_buffer_interleaved輸入的參數(shù)中雙聲道數(shù)據(jù)是交錯在一起輸入的浸卦。具體使用哪個需要看采集到的數(shù)據(jù)是哪種格式的署鸡,不過現(xiàn)在的設(shè)備采集到的數(shù)據(jù)大部分都是雙聲道數(shù)據(jù)是交錯在一起案糙。
  • 單聲道輸入只能使用lame_encode_buffer限嫌,把單聲道數(shù)據(jù)當(dāng)成左聲道數(shù)據(jù)傳入,右聲道傳NULL即可时捌。
  • 調(diào)用這兩個函數(shù)時(shí)需要傳入一塊內(nèi)存來獲取編碼器出的數(shù)據(jù)怒医,這塊內(nèi)存的大小lame給出了一種建議的計(jì)算方式:采樣率/20+7200。

結(jié)束編碼

lame_encode_flush:刷新編碼器緩沖奢讨,獲取殘留在編碼器緩沖里的數(shù)據(jù)稚叹。這部分?jǐn)?shù)據(jù)也需要寫入mp3文件

銷毀編碼器

lame_close銷毀編碼器,釋放資源拿诸。

#include "lame_3.99.5_libmp3lame/lame.h"
#include "com_czt_mp3recorder_util_LameUtil.h"
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
#include <sys/stat.h>

/**
 * 返回值 char* 這個代表char數(shù)組的首地址
 *  Jstring2CStr 把java中的jstring的類型轉(zhuǎn)化成一個c語言中的char 字符串
 */
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String
    jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一個java字符串 "GB2312"
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
            "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,
            strencode); // String .getByte("GB2312");
    jsize alen = (*env)->GetArrayLength(env, barr); // byte數(shù)組的長度
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    if (alen > 0) {
        rtn = (char*) malloc(alen + 1); //"\0"
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //
    return rtn;
}

int flag = 0;
/**
 * wav轉(zhuǎn)換mp3
 */
JNIEXPORT void JNICALL Java_com_czt_mp3recorder_util_LameUtil_convert
(JNIEnv * env, jobject obj, jstring jwav, jstring jmp3, jint inSamplerate, jint inChannel, jint outBitrate) {

    char* cwav =Jstring2CStr(env,jwav) ;
    char* cmp3=Jstring2CStr(env,jmp3);

    //1.打開 wav,MP3文件
    FILE* fwav = fopen(cwav,"rb");
    FILE* fmp3 = fopen(cmp3,"wb+");

    int channel = inChannel;//聲道數(shù)

    short int wav_buffer[8192*channel];
    unsigned char mp3_buffer[8192];

    //1.初始化lame的編碼器
    lame_t lameConvert =  lame_init();

    //2. 設(shè)置lame mp3編碼的采樣率
    lame_set_in_samplerate(lameConvert , inSamplerate);
    lame_set_out_samplerate(lameConvert, inSamplerate);
    lame_set_num_channels(lameConvert,channel);
    lame_set_mode(lameConvert, MONO);
    // 3. 設(shè)置MP3的編碼方式
    lame_set_VBR(lameConvert, vbr_default);
    lame_init_params(lameConvert);
    int read ; int write; //代表讀了多少個次 和寫了多少次
    int total=0; // 當(dāng)前讀的wav文件的byte數(shù)目
    do{
        if(flag==404){
            return;
        }
        read = fread(wav_buffer,sizeof(short int)*channel, 8192,fwav);
        total +=  read* sizeof(short int)*channel;
        if(read!=0){
            write = lame_encode_buffer(lameConvert, wav_buffer, NULL, read, mp3_buffer, 8192);
            //write = lame_encode_buffer_interleaved(lame,wav_buffer,read,mp3_buffer,8192);
        }else{
        write = lame_encode_flush(lameConvert,mp3_buffer,8192);
        }
        //把轉(zhuǎn)化后的mp3數(shù)據(jù)寫到文件里
        fwrite(mp3_buffer,1,write,fmp3);

    }while(read!=0);
    lame_close(lameConvert);
    fclose(fwav);
    fclose(fmp3);
}

1.3.3 導(dǎo)入庫以及創(chuàng)建native方法

創(chuàng)建LameUtil類扒袖,并導(dǎo)入相應(yīng)的庫,并創(chuàng)建convert方法

public class LameUtil {
    static {
        System.loadLibrary("mp3lame");
    }
    public native static void convert(String wavFile, String mp3File, int inSamplerate, int inChannel, int outBitrate);
}

1.3.4 調(diào)用native方法

                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            WavFileReader reader = new WavFileReader();
                            try {
                                if (reader.openFile(mWAVPathEt.getText().toString())) {
                                    //讀取wav文件的頭信息
                                    WavFileHeader wavFileHeader = reader.getmWavFileHeader();
                                    //把獲取到的wav頭信息傳入natvie方法
                                    LameUtil.convert(mWAVPathEt.getText().toString(), mMP3PathEt.getText().toString(), wavFileHeader.getmSampleRate(), wavFileHeader.getmNumChannel(), wavFileHeader.getmByteRate());
                                }


                                if (mFile.exists()) {
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(WAVTransMP3Activity.this, "轉(zhuǎn)碼成功:\t" + mFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                } else {
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(WAVTransMP3Activity.this, "轉(zhuǎn)碼失敗", Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();

那么轉(zhuǎn)碼成功后就大功告成了嗎亩码?
遺憾的告訴你并沒有那么簡單季率,因?yàn)檗D(zhuǎn)碼出來的音頻最開始會啪的一聲,沒錯描沟,每一個音頻都有飒泻,無一幸免鞭光,意不意外,驚不驚喜!!!


not_easy.jpg

這里啪的一聲是什么原因呢泞遗?
是因?yàn)檗D(zhuǎn)碼的時(shí)候把wav文件的頭信息也一起轉(zhuǎn)了惰许,才會出現(xiàn)這種情況。那要怎么解決呢史辙?
當(dāng)然是跳過這個頭信息汹买,直接從數(shù)據(jù)開始讀取。

fseek(fwav, 4*1024, SEEK_CUR);

對聊倔,就是這一句話就可以了卦睹,完整代碼是這樣的

/**
 * 返回值 char* 這個代表char數(shù)組的首地址
 *  Jstring2CStr 把java中的jstring的類型轉(zhuǎn)化成一個c語言中的char 字符串
 */
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String
    jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一個java字符串 "GB2312"
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
            "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,
            strencode); // String .getByte("GB2312");
    jsize alen = (*env)->GetArrayLength(env, barr); // byte數(shù)組的長度
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    if (alen > 0) {
        rtn = (char*) malloc(alen + 1); //"\0"
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //
    return rtn;
}

int flag = 0;
/**
 * wav轉(zhuǎn)換mp3
 */
JNIEXPORT void JNICALL Java_com_czt_mp3recorder_util_LameUtil_convert
(JNIEnv * env, jobject obj, jstring jwav, jstring jmp3, jint inSamplerate, jint inChannel, jint outBitrate) {

    char* cwav =Jstring2CStr(env,jwav) ;
    char* cmp3=Jstring2CStr(env,jmp3);

    //1.打開 wav,MP3文件
    FILE* fwav = fopen(cwav,"rb");
    fseek(fwav, 4*1024, SEEK_CUR);
    FILE* fmp3 = fopen(cmp3,"wb+");

    int channel = inChannel;//單聲道

    short int wav_buffer[8192*channel];
    unsigned char mp3_buffer[8192];

    //1.初始化lame的編碼器
    lame_t lameConvert =  lame_init();

    //2. 設(shè)置lame mp3編碼的采樣率
    lame_set_in_samplerate(lameConvert , inSamplerate);
    lame_set_out_samplerate(lameConvert, inSamplerate);
    lame_set_num_channels(lameConvert,channel);
    lame_set_mode(lameConvert, MONO);
    // 3. 設(shè)置MP3的編碼方式
    lame_set_VBR(lameConvert, vbr_default);
    lame_init_params(lameConvert);
    int read ; int write; //代表讀了多少個次 和寫了多少次
    int total=0; // 當(dāng)前讀的wav文件的byte數(shù)目
    do{
        if(flag==404){
            return;
        }
        read = fread(wav_buffer,sizeof(short int)*channel, 8192,fwav);
        total +=  read* sizeof(short int)*channel;
        if(read!=0){
            write = lame_encode_buffer(lameConvert, wav_buffer, NULL, read, mp3_buffer, 8192);
            //write = lame_encode_buffer_interleaved(lame,wav_buffer,read,mp3_buffer,8192);
        }else{
        write = lame_encode_flush(lameConvert,mp3_buffer,8192);
        }
        //把轉(zhuǎn)化后的mp3數(shù)據(jù)寫到文件里
        fwrite(mp3_buffer,1,write,fmp3);

    }while(read!=0);
    lame_close(lameConvert);
    fclose(fwav);
    fclose(fmp3);
}

然后再轉(zhuǎn)碼之后,就沒有啪的一聲了方库,開不開心结序。
然而,你仔細(xì)觀察纵潦,發(fā)現(xiàn)是不是秒數(shù)不對了徐鹤,想不想哭

cry.jpeg

那秒數(shù)沒有對是為啥呢?
因?yàn)樾枰獙懭胂嚓P(guān)的VBRTAG邀层,也可以理解為mp3的頭信息返敬。

寫入VBRTAG

  • lame_mp3_tags_fid:向一個文件指針中寫入規(guī)范的VBRTAG。

  • VBRTAG的作用是記錄整個mp3的一些信息寥院,通常用于VBR模式下的編碼劲赠,因?yàn)閂BR模式下比特率不固定,無法直接計(jì)算出播放的時(shí)長和跳躍點(diǎn)秸谢,所以在mp3的開頭部分插入一個VBRTAG凛澎。

  • VBRTAG有幾種規(guī)范,但是lame支持的是最通用的規(guī)范估蹄。

  • 注意lame_mp3_tags_fid函數(shù)的參數(shù)需要一個FILE *類型代表要寫入的文件塑煎,這個文件一定是之前編碼時(shí)寫入了mp3數(shù)據(jù)的文件,VBRTAG是需要卸載mp3的開頭的臭蚁,之前的編碼過程中會自動空出寫入VBRTAG所需要的空間最铁,這個函數(shù)內(nèi)會自動尋找合適的文件偏移然后覆蓋,所以當(dāng)前的文件偏移是無關(guān)緊要的垮兑,但是打開文件的時(shí)候一定要以讀寫模式打開冷尉。

  • 注意我提到了之前的編碼過程中會自動空出寫入VBRTAG所需要的空間,所以如果結(jié)束編碼后不調(diào)用lame_mp3_tags_fid寫入VBRTAG就會導(dǎo)致這部分內(nèi)容為空系枪,雖然不影響播放雀哨,但是會影響很多播放器對于時(shí)長和跳躍點(diǎn)的計(jì)算。

  • 那么對于非VBR模式也需要寫入VBRTAG嗎嗤无?是的震束,lame對于非VBR模式也會預(yù)留出VBRTAG的空間怜庸,所以非VBR模式的編碼最后也需要寫入VBRTAG。

說了那么多垢村,意思就是這個函數(shù)是應(yīng)該在lame_encode_flush()之后調(diào), 當(dāng)所有數(shù)據(jù)都寫入完畢了再調(diào)用割疾。仔細(xì)想想也很合理, 這時(shí)才能確定文件的總幀數(shù)。

于是嘉栓,我們就這樣寫

/**
 * 返回值 char* 這個代表char數(shù)組的首地址
 *  Jstring2CStr 把java中的jstring的類型轉(zhuǎn)化成一個c語言中的char 字符串
 */
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env, "java/lang/String"); //String
    jstring strencode = (*env)->NewStringUTF(env, "GB2312"); // 得到一個java字符串 "GB2312"
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
            "(Ljava/lang/String;)[B"); //[ String.getBytes("gb2312");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,
            strencode); // String .getByte("GB2312");
    jsize alen = (*env)->GetArrayLength(env, barr); // byte數(shù)組的長度
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    if (alen > 0) {
        rtn = (char*) malloc(alen + 1); //"\0"
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0); //
    return rtn;
}

int flag = 0;
/**
 * wav轉(zhuǎn)換mp3
 */
JNIEXPORT void JNICALL Java_com_czt_mp3recorder_util_LameUtil_convert
(JNIEnv * env, jobject obj, jstring jwav, jstring jmp3, jint inSamplerate, jint inChannel, jint outBitrate) {

    char* cwav =Jstring2CStr(env,jwav) ;
    char* cmp3=Jstring2CStr(env,jmp3);

    //1.打開 wav,MP3文件
    FILE* fwav = fopen(cwav,"rb");
    fseek(fwav, 4*1024, SEEK_CUR);
    FILE* fmp3 = fopen(cmp3,"wb+");

    int channel = inChannel;//單聲道

    short int wav_buffer[8192*channel];
    unsigned char mp3_buffer[8192];

    //1.初始化lame的編碼器
    lame_t lameConvert =  lame_init();

    //2. 設(shè)置lame mp3編碼的采樣率
    lame_set_in_samplerate(lameConvert , inSamplerate);
    lame_set_out_samplerate(lameConvert, inSamplerate);
    lame_set_num_channels(lameConvert,channel);
    lame_set_mode(lameConvert, MONO);
    // 3. 設(shè)置MP3的編碼方式
    lame_set_VBR(lameConvert, vbr_default);
    lame_init_params(lameConvert);
    int read ; int write; //代表讀了多少個次 和寫了多少次
    int total=0; // 當(dāng)前讀的wav文件的byte數(shù)目
    do{
        if(flag==404){
            return;
        }
        read = fread(wav_buffer,sizeof(short int)*channel, 8192,fwav);
        total +=  read* sizeof(short int)*channel;
        if(read!=0){
            write = lame_encode_buffer(lameConvert, wav_buffer, NULL, read, mp3_buffer, 8192);
            //write = lame_encode_buffer_interleaved(lame,wav_buffer,read,mp3_buffer,8192);
        }else{
        write = lame_encode_flush(lameConvert,mp3_buffer,8192);
        }
        //把轉(zhuǎn)化后的mp3數(shù)據(jù)寫到文件里
        fwrite(mp3_buffer,1,write,fmp3);

    }while(read!=0);
    lame_mp3_tags_fid(lameConvert,fmp3);
    lame_close(lameConvert);
    fclose(fwav);
    fclose(fmp3);
}

再重新ndk-build,再編譯一次宏榕,重新安裝,再轉(zhuǎn)碼一次侵佃,大功終于告成了

2. Android 使用 lame pcm 轉(zhuǎn)碼 mp3(邊錄邊轉(zhuǎn))

邊錄邊轉(zhuǎn)的原理就是麻昼,拿到pcm數(shù)據(jù),馬上轉(zhuǎn)成mp3數(shù)據(jù)并寫入相關(guān)文件馋辈,當(dāng)錄制結(jié)束抚芦,轉(zhuǎn)換也同時(shí)結(jié)束。

2.1 DataEncodeThread的編寫

那么肯定需要跑一個線程來進(jìn)行解碼和寫入的工作迈螟,但是每次寫入和轉(zhuǎn)換肯定有很多次叉抡,這里使用HandlerThread來進(jìn)行。

public class DataEncodeThread extends HandlerThread implements AudioRecord.OnRecordPositionUpdateListener {
    private StopHandler mHandler;
    private static final int PROCESS_STOP = 1;
    private byte[] mMp3Buffer;
    private FileOutputStream mFileOutputStream;

    private static class StopHandler extends Handler {
        
        private DataEncodeThread encodeThread;
        
        public StopHandler(Looper looper, DataEncodeThread encodeThread) {
            super(looper);
            this.encodeThread = encodeThread;
        }

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == PROCESS_STOP) {
                //處理緩沖區(qū)中的數(shù)據(jù)
                while (encodeThread.processData() > 0);
                // Cancel any event left in the queue
                removeCallbacksAndMessages(null);
                encodeThread.flushAndRelease();
                getLooper().quit();
            }
        }
    }

    /**
     * Constructor
     * @param file file
     * @param bufferSize bufferSize
     * @throws FileNotFoundException file not found
     */
    public DataEncodeThread(File file, int bufferSize) throws FileNotFoundException {
        super("DataEncodeThread");
        this.mFileOutputStream = new FileOutputStream(file);
        mMp3Buffer = new byte[(int) (7200 + (bufferSize * 2 * 1.25))];
    }

    @Override
    public synchronized void start() {
        super.start();
        mHandler = new StopHandler(getLooper(), this);
    }

    private void check() {
        if (mHandler == null) {
            throw new IllegalStateException();
        }
    }

    public void sendStopMessage() {
        check();
        mHandler.sendEmptyMessage(PROCESS_STOP);
    }
    public Handler getHandler() {
        check();
        return mHandler;
    }

    @Override
    public void onMarkerReached(AudioRecord recorder) {
        // Do nothing       
    }

    @Override
    public void onPeriodicNotification(AudioRecord recorder) {
        processData();
    }
    /**
     * 從緩沖區(qū)中讀取并處理數(shù)據(jù)答毫,使用lame編碼MP3
     * @return  從緩沖區(qū)中讀取的數(shù)據(jù)的長度
     *          緩沖區(qū)中沒有數(shù)據(jù)時(shí)返回0 
     */
    private int processData() { 
        if (mTasks.size() > 0) {
            Task task = mTasks.remove(0);
            short[] buffer = task.getData();
            int readSize = task.getReadSize();
            int encodedSize = LameUtil.encode(buffer, buffer, readSize, mMp3Buffer);
            if (encodedSize > 0){
                try {
                    mFileOutputStream.write(mMp3Buffer, 0, encodedSize);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return readSize;
        }
        return 0;
    }
    
    /**
     * Flush all data left in lame buffer to file
     */
    private void flushAndRelease() {
        //將MP3結(jié)尾信息寫入buffer中
        final int flushResult = LameUtil.flush(mMp3Buffer);
        if (flushResult > 0) {
            try {
                mFileOutputStream.write(mMp3Buffer, 0, flushResult);
            } catch (IOException e) {
                e.printStackTrace();
            }finally{
                if (mFileOutputStream != null) {
                    try {
                        mFileOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                LameUtil.close();
            }
        }
    }
    private List<Task> mTasks = Collections.synchronizedList(new ArrayList<Task>());
    public void addTask(short[] rawData, int readSize){
        mTasks.add(new Task(rawData, readSize));
    }
    private class Task{
        private short[] rawData;
        private int readSize;
        public Task(short[] rawData, int readSize){
            this.rawData = rawData.clone();
            this.readSize = readSize;
        }
        public short[] getData(){
            return rawData;
        }
        public int getReadSize(){
            return readSize;
        }
    }
}

下面是調(diào)用錄音的代碼

    /**
     * Start recording. Create an encoding thread. Start record from this
     * thread.
     * 
     * @throws IOException  initAudioRecorder throws
     */
    public void start() throws IOException {
        if (mIsRecording) {
            return;
        }
        mIsRecording = true; // 提早褥民,防止init或startRecording被多次調(diào)用
        initAudioRecorder();
        mAudioRecord.startRecording();
        new Thread() {
            @Override
            public void run() {
                //設(shè)置線程權(quán)限
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
                while (mIsRecording) {
                    int readSize = mAudioRecord.read(mPCMBuffer, 0, mBufferSize);
                    if (readSize > 0) {
                        mEncodeThread.addTask(mPCMBuffer, readSize);
                        calculateRealVolume(mPCMBuffer, readSize);
                    }
                }
                // release and finalize audioRecord
                mAudioRecord.stop();
                mAudioRecord.release();
                mAudioRecord = null;
                // stop the encoding thread and try to wait
                // until the thread finishes its job
                mEncodeThread.sendStopMessage();
            }
            /**
             * 此計(jì)算方法來自samsung開發(fā)范例
             * 
             * @param buffer buffer
             * @param readSize readSize
             */
            private void calculateRealVolume(short[] buffer, int readSize) {
                double sum = 0;
                for (int i = 0; i < readSize; i++) {  
                    // 這里沒有做運(yùn)算的優(yōu)化,為了更加清晰的展示代碼  
                    sum += buffer[i] * buffer[i]; 
                } 
                if (readSize > 0) {
                    double amplitude = sum / readSize;
                    mVolume = (int) Math.sqrt(amplitude);
                }
            }
        }.start();
    }

這樣也就實(shí)現(xiàn)了相關(guān)的功能洗搂,具體轉(zhuǎn)換的方法上面已經(jīng)提過消返,這里就不再贅述了。

3. mp3 轉(zhuǎn) wav

既然是要實(shí)現(xiàn)mp3轉(zhuǎn)換為wav格式耘拇,那么必須先解碼mp3為pcm數(shù)據(jù)撵颊,再將pcm數(shù)據(jù)寫入相關(guān)的頭信息,這樣就實(shí)現(xiàn)了mp3轉(zhuǎn)換為wav驼鞭。

下面是解碼mp3文件為pcm文件:

    public static String fenLiData(String path, String newPath) throws IOException {
        File file = new File(path);// 原文件
        File file1 = new File(path + "01");// 分離ID3V2后的文件,這是個中間文件秦驯,最后要被刪除
        File file2 = new File(newPath);// 分離id3v1后的文件
        RandomAccessFile rf = new RandomAccessFile(file, "rw");// 隨機(jī)讀取文件
        FileOutputStream fos = new FileOutputStream(file1);
        byte ID3[] = new byte[3];
        rf.read(ID3);
        String ID3str = new String(ID3);
        // 分離ID3v2
        if (ID3str.equals("ID3")) {
            rf.seek(6);
            byte[] ID3size = new byte[4];
            rf.read(ID3size);
            int size1 = (ID3size[0] & 0x7f) << 21;
            int size2 = (ID3size[1] & 0x7f) << 14;
            int size3 = (ID3size[2] & 0x7f) << 7;
            int size4 = (ID3size[3] & 0x7f);
            int size = size1 + size2 + size3 + size4 + 10;
            rf.seek(size);
            int lens = 0;
            byte[] bs = new byte[1024 * 4];
            while ((lens = rf.read(bs)) != -1) {
                fos.write(bs, 0, lens);
            }
            fos.close();
            rf.close();
        } else {// 否則完全復(fù)制文件
            int lens = 0;
            rf.seek(0);
            byte[] bs = new byte[1024 * 4];
            while ((lens = rf.read(bs)) != -1) {
                fos.write(bs, 0, lens);
            }
            fos.close();
            rf.close();
        }
        RandomAccessFile raf = new RandomAccessFile(file1, "rw");
        byte TAG[] = new byte[3];
        raf.seek(raf.length() - 128);
        raf.read(TAG);
        String tagstr = new String(TAG);
        if (tagstr.equals("TAG")) {
            FileOutputStream fs = new FileOutputStream(file2);
            raf.seek(0);
            byte[] bs = new byte[(int) (raf.length() - 128)];
            raf.read(bs);
            fs.write(bs);
            raf.close();
            fs.close();
        } else {// 否則完全復(fù)制內(nèi)容至file2
            FileOutputStream fs = new FileOutputStream(file2);
            raf.seek(0);
            byte[] bs = new byte[1024 * 4];
            int len = 0;
            while ((len = raf.read(bs)) != -1) {
                fs.write(bs, 0, len);
            }
            raf.close();
            fs.close();
        }
        if (file1.exists())// 刪除中間文件
        {
            file1.delete();

        }
        return file2.getAbsolutePath();
    }

然后再進(jìn)行pcm文件寫入頭信息:

    /**
     * pcm文件轉(zhuǎn)wav文件
     *
     * @param inFilename  源文件路徑
     * @param outFilename 目標(biāo)文件路徑
     */
    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = mSampleRate;
        int channels = 2;
        long byteRate = 16 * mSampleRate * channels / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;

            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 加入wav文件頭
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF/WAVE header
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';  //WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f'; // 'fmt ' chunk
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1;   // format = 1
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        header[32] = (byte) (2 * 16 / 8); // block align
        header[33] = 0;
        header[34] = 16;  // bits per sample
        header[35] = 0;
        header[36] = 'd'; //data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }

以上便是mp3轉(zhuǎn)換為wav 的方法。

4. mp3 轉(zhuǎn) pcm (邊播邊轉(zhuǎn))

其實(shí)和pcm轉(zhuǎn)mp3邊錄邊轉(zhuǎn)的原理是一樣的挣棕,也是拿到數(shù)據(jù)再進(jìn)行解碼,不過這次要用到的mad庫來進(jìn)行解碼工作亲桥。

    private void startDecode() {
        if (ret == -1) {
            Log.i("conowen", "Couldn't open file '" + mMP3PathEt.getText().toString() + "'");

        } else {
            mThreadFlag = true;
            initAudioPlayer();

            audioBuffer = new short[1024 * 1024];
            mThread = new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    while (mThreadFlag) {
                        if (null != mAudioTrack && mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PAUSED) {
                            // ****從libmad處獲取data******/
                            MP3Decoder.getAudioBuf(audioBuffer,
                                    mAudioMinBufSize);
                            if(null != mAudioTrack){
                                mAudioTrack.write(audioBuffer, 0, mAudioMinBufSize);
                            }
                        } else {
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }

                }
            });

        }
    }

其中洛心,MP3Decoder.getAudioBuf(audioBuffer,mAudioMinBufSize);是調(diào)用的mad的庫的方法,具體方法網(wǎng)上都有提供题篷,這里只是貼出相應(yīng)的c代碼词身。

#define LOG_TAG "NativeMP3Decoder"
#include <fcntl.h>
#include <jni.h>
#include "mad/mad.h"
#include "NativeMP3Decoder.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <android/log.h>
#include "FileSystem.h"


#define INPUT_BUFFER_SIZE   (8192/4)
#define OUTPUT_BUFFER_SIZE  8192 /* Must be an integer multiple of 4. */


#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, fmt, ##args)
//int g_size;


/**
 * Struct holding the pointer to a wave file.
 */
typedef struct
{
    int size;
    int64_t fileStartPos;
    T_pFILE file;
    struct mad_stream stream;
    struct mad_frame frame;
    struct mad_synth synth;
    mad_timer_t timer;
    int leftSamples;
    int offset;
    unsigned char inputBuffer[INPUT_BUFFER_SIZE];
} MP3FileHandle;

/** static WaveFileHandle array **/
static inline int readNextFrame( MP3FileHandle* mp3 );

static MP3FileHandle* Handle;
unsigned int g_Samplerate;

/**
 * Seeks a free handle in the handles array and returns its index or -1 if no handle could be found
 */

extern int file_open(const char *filename, int flags);
extern int file_read(T_pFILE fd, unsigned char *buf, int size);
extern int file_write(T_pFILE fd, unsigned char *buf, int size);
extern int64_t file_seek(T_pFILE fd, int64_t pos, int whence);
extern int file_close(T_pFILE fd);

static inline void closeHandle()
{
    file_close( Handle->file);
    mad_synth_finish(&Handle->synth);
    mad_frame_finish(&Handle->frame);
    mad_stream_finish(&Handle->stream);
    free(Handle);
    Handle = NULL;
}

static inline signed short fixedToShort(mad_fixed_t Fixed)
{
    if(Fixed>=MAD_F_ONE)
        return(SHRT_MAX);
    if(Fixed<=-MAD_F_ONE)
        return(-SHRT_MAX);

    Fixed=Fixed>>(MAD_F_FRACBITS-15);
    return((signed short)Fixed);
}


int  NativeMP3Decoder_init(char * filepath,unsigned long start/*,unsigned long size*/)
{
    LOGI("bfp----->NativeMP3Decoder_init start filepath: %s",filepath);
    LOGI("bfp----->NativeMP3Decoder_init  start: %ld",start);
    T_pFILE fileHandle = file_open( filepath, _FMODE_READ);
    LOGI("bfp----->NativeMP3Decoder_init fileHandle: %ld",fileHandle);
    if( fileHandle <= 0 )
        return -1;

    MP3FileHandle* mp3Handle = (MP3FileHandle*)malloc(sizeof(MP3FileHandle));
    memset(mp3Handle, 0, sizeof(MP3FileHandle));
    mp3Handle->file = fileHandle;

    mp3Handle->fileStartPos= start;

    file_seek( mp3Handle->file, start, SEEK_SET);

    mad_stream_init(&mp3Handle->stream);
    mad_frame_init(&mp3Handle->frame);
    mad_synth_init(&mp3Handle->synth);
    mad_timer_reset(&mp3Handle->timer);

    Handle = mp3Handle;

    readNextFrame( Handle );

    g_Samplerate = Handle->frame.header.samplerate;
    LOGI("bfp----->NativeMP3Decoder_init fileHandle: end");
    return 1;
}

static inline int readNextFrame( MP3FileHandle* mp3 )
{
    do
    {
        if( mp3->stream.buffer == 0 || mp3->stream.error == MAD_ERROR_BUFLEN )
        {
            int inputBufferSize = 0;

            if( mp3->stream.next_frame != 0 )
            {

                int leftOver = mp3->stream.bufend - mp3->stream.next_frame;
                int i;
                for(  i= 0; i < leftOver; i++ )
                    mp3->inputBuffer[i] = mp3->stream.next_frame[i];
                int readBytes = file_read( mp3->file, mp3->inputBuffer + leftOver, INPUT_BUFFER_SIZE - leftOver);
                if( readBytes == 0 )
                    return 0;
                inputBufferSize = leftOver + readBytes;
            }
            else
            {

                int readBytes = file_read( mp3->file, mp3->inputBuffer, INPUT_BUFFER_SIZE);
                if( readBytes == 0 )
                    return 0;
                inputBufferSize = readBytes;
            }

            mad_stream_buffer( &mp3->stream, mp3->inputBuffer, inputBufferSize );
            mp3->stream.error = MAD_ERROR_NONE;

        }

        if( mad_frame_decode( &mp3->frame, &mp3->stream ) )
        {

            if( mp3->stream.error == MAD_ERROR_BUFLEN ||(MAD_RECOVERABLE(mp3->stream.error)))
                continue;
            else
                return 0;
        }
        else
            break;
    }
    while( 1 );

    mad_timer_add( &mp3->timer, mp3->frame.header.duration );
    mad_synth_frame( &mp3->synth, &mp3->frame );
    mp3->leftSamples = mp3->synth.pcm.length;
    mp3->offset = 0;

    return -1;
}



int NativeMP3Decoder_readSamples(short *target, int size)
{
    LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioBuf start size %d",size);
    MP3FileHandle* mp3 = Handle;
    int pos=0;
    int idx = 0;
    while( idx != size )
    {
        if( mp3->leftSamples > 0 )
        {
            for( ; idx < size && mp3->offset < mp3->synth.pcm.length; mp3->leftSamples--, mp3->offset++ )
            {
                int value = fixedToShort(mp3->synth.pcm.samples[0][mp3->offset]);

                if( MAD_NCHANNELS(&mp3->frame.header) == 2 )
                {
                    value += fixedToShort(mp3->synth.pcm.samples[1][mp3->offset]);
                    value /= 2;
                }

                target[idx++] = value;
            }
        }
        else
        {

            pos = file_seek( mp3->file, 0, SEEK_CUR);

            int result = readNextFrame( mp3);
            if( result == 0 )
                return 0;
        }

    }

    if( idx > size )
        return 0;
     LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioBuf end pos %d",pos);
     return pos;

}

int NativeMP3Decoder_getAduioSamplerate()
{
    LOGI("bfp----->NativeMP3Decoder_getAduioSamplerate g_Samplerate %d",g_Samplerate);
    return g_Samplerate;

}


void  NativeMP3Decoder_closeAduioFile()
{
    LOGI("bfp----->NativeMP3Decoder_closeAduioFile start Handle:%d",Handle->size);
    if( Handle != 0 )
    {
        closeHandle();
        Handle = 0;
    }
    LOGI("bfp----->NativeMP3Decoder_closeAduioFile end");
}

jint Java_com_czt_mp3recorder_NativeMP3Decoder_initAudioPlayer(JNIEnv *env, jobject obj, jstring file,jint startAddr)
{
     LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_initAudioPlayer start");
     char*  fileString = (*env)->GetStringUTFChars(env,file, 0);
     LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_initAudioPlayer end");
    return  NativeMP3Decoder_init(fileString,startAddr);

}

 jint Java_com_czt_mp3recorder_NativeMP3Decoder_getAudioBuf(JNIEnv *env, jobject obj ,jshortArray audioBuf,jint len)
{
    LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioBuf start len:%d",len);
    int bufsize = 0;
    int ret = 0;
    if (audioBuf != NULL) {
        bufsize = (*env)->GetArrayLength(env, audioBuf);
        jshort *_buf = (*env)->GetShortArrayElements(env, audioBuf, 0);
        memset(_buf, 0, bufsize*2);
        ret = NativeMP3Decoder_readSamples(_buf, len);
        (*env)->ReleaseShortArrayElements(env, audioBuf, _buf, 0);
    }
    else{
         LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioBuf getAudio failed");
        }
    LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioBuf end ret:%d",ret);
    return ret;
}

 jint Java_com_czt_mp3recorder_NativeMP3Decoder_getAudioSamplerate(JNIEnv *env, jobject obj)
{
     LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioSamplerate  start");
    return NativeMP3Decoder_getAduioSamplerate();
    LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_getAudioSamplerate  end");
}


 void Java_com_czt_mp3recorder_NativeMP3Decoder_closeAduioFile(JNIEnv *env, jobject obj)

{
    LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_closeAduioFile str");
    NativeMP3Decoder_closeAduioFile();
    LOGI("bfp----->Java_com_mediatek_factorymode_NativeMP3Decoder_closeAduioFile end");

}

 jint JNI_OnLoad(JavaVM* vm, void* reserved) {
     void *venv;
     LOGI("bfp----->JNI_OnLoad!");
     if ((*vm)->GetEnv(vm, (void**) &venv, JNI_VERSION_1_4) != JNI_OK) {
         LOGE("bfp--->ERROR: GetEnv failed");
         return -1;
     }
     return JNI_VERSION_1_4;
 }

最后

感謝大家的支持和閱讀,完整項(xiàng)目代碼已經(jīng)上傳番枚,再次感謝大家
https://pan.baidu.com/s/1faWwLbvQhd7v1m-woXtHvA

十分感謝以下博客的分享:

https://www.imooc.com/article/27041?block_id=tuijian_wz
https://blog.csdn.net/aiyh0202/article/details/52815374
https://blog.csdn.net/qq634416025/article/details/51424556
http://www.cnblogs.com/ct2011/p/4080193.html
https://blog.csdn.net/bjrxyz/article/details/73435407?locationNum=15&fps=1
https://blog.csdn.net/haovip123/article/details/52356024
http://www.reibang.com/p/971fff236881

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末法严,一起剝皮案震驚了整個濱河市损敷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌深啤,老刑警劉巖拗馒,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溯街,居然都是意外死亡诱桂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門呈昔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挥等,“玉大人,你說我怎么就攤上這事堤尾「尉ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵郭宝,是天一觀的道長涡相。 經(jīng)常有香客問我,道長剩蟀,這世上最難降的妖魔是什么催蝗? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮育特,結(jié)果婚禮上丙号,老公的妹妹穿的比我還像新娘。我一直安慰自己缰冤,他們只是感情好犬缨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著棉浸,像睡著了一般怀薛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上迷郑,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天枝恋,我揣著相機(jī)與錄音,去河邊找鬼嗡害。 笑死焚碌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的霸妹。 我是一名探鬼主播十电,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鹃骂?” 一聲冷哼從身側(cè)響起台盯,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎畏线,沒想到半個月后静盅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡象踊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年温亲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杯矩。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡栈虚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出史隆,到底是詐尸還是另有隱情魂务,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布泌射,位于F島的核電站粘姜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏熔酷。R本人自食惡果不足惜孤紧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拒秘。 院中可真熱鬧号显,春花似錦、人聲如沸躺酒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽羹应。三九已至揽碘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間园匹,已是汗流浹背雳刺。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留偎肃,地道東北人煞烫。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像累颂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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