Android 使用 lame wav 转 mp3 、pcm 转 mp3 (边录边转);使用 mad mp3 转 wav、mp3 转 pcm (边播边转)

前言

最近在研究wav,mp3,pcm之间的相互转换,发现mp3的相关操作,都需要解码mp3或者编码mp3,无法直接对mp3文件做操作。下面是本文的相关知识点。

  • wav 转 mp3
  • pcm 转 mp3 (边录边转)
  • mp3 转 wav
  • mp3 转 pcm (边播边转)

1. Android 使用 lame wav 转码 mp3

1.1 准备工作

下载 lame_x.xx.x 包

Lame
Lame 是最好的mp3编码器,速度快,效果好,特别是中高码率和VBR编码方面。
http://lame.sourceforge.net/

1.2 创建 android 项目 lame

创建jni目录 并 复制 lame-x.xx.x 包下的libmp3lame 目录下的所有 .c和.h文件和 include目录下的lame.h

1.2.1 在jni目录下创建 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目录下创建 Application.mk文件

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

1.2.3 然后在gradle里面进行配置(在使用该jni的gradle里进行配置)

    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生成相应.so库(ndk-build会生成相应的.h头文件)

1.3 编写wav转mp3的lame_util.c

大致分为两步

  1. 通过Jstring2CStr方法将java中的jstring类型转化成c语言的char字符串
  2. 然后再通过convert方法将wav转码成mp3文件
    (convert方法的参数为,wav路径,mp3路径,采样率,声道数,比特率)
    下面为大家科普一下相关参数以及知识点

1.3.1 Lame相关参数

  • 采样率(sampleRate):采样率越高声音的还原度越好。
  • 比特率(bitrate):每秒钟的数据量,越高音质越好。
  • 声道数(channels):声道的数量,通常只有单声道和双声道,双声道即所谓的立体声。
  • 比特率控制模式: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 编码流程

初始化编码参数

  • lame_init:初始化一个编码参数的数据结构,给使用者用来设置参数。

设置编码参数

  • lame_set_in_samplerate:设置被输入编码器的原始数据的采样率。
  • lame_set_out_samplerate:设置最终mp3编码输出的声音的采样率,如果不设置则和输入采样率一样。
  • lame_set_num_channels :设置被输入编码器的原始数据的声道数。
  • lame_set_mode :设置最终mp3编码输出的声道模式,如果不设置则和输入声道数一样。参数是枚举,STEREO代表双声道,MONO代表单声道。
  • lame_set_VBR:设置比特率控制模式,默认是CBR,但是通常我们都会设置VBR。参数是枚举,vbr_off代表CBR,vbr_abr代表ABR(因为ABR不常见,所以本文不对ABR做讲解)vbr_mtrh代表VBR。
  • lame_set_brate:设置CBR的比特率,只有在CBR模式下才生效。
  • lame_set_VBR_mean_bitrate_kbps:设置VBR的比特率,只有在VBR模式下才生效。

其中每个参数都有默认的配置,如非必要可以不设置。这里只介绍了几个关键的设置接口,还有其他的设置接口可以参考lame.h(lame的文档里只有命令行程序的用法,没有库接口的用法)。

初始化编码器器

lame_init_params:根据上面设置好的参数建立编码器

编码PCM数据

  • lame_encode_bufferlame_encode_buffer_interleaved:将PCM数据送入编码器,获取编码出的mp3数据。这些数据写入文件就是mp3文件。
  • 其中lame_encode_buffer输入的参数中是双声道的数据分别输入的,lame_encode_buffer_interleaved输入的参数中双声道数据是交错在一起输入的。具体使用哪个需要看采集到的数据是哪种格式的,不过现在的设备采集到的数据大部分都是双声道数据是交错在一起。
  • 单声道输入只能使用lame_encode_buffer,把单声道数据当成左声道数据传入,右声道传NULL即可。
  • 调用这两个函数时需要传入一块内存来获取编码器出的数据,这块内存的大小lame给出了一种建议的计算方式:采样率/20+7200。

结束编码

lame_encode_flush:刷新编码器缓冲,获取残留在编码器缓冲里的数据。这部分数据也需要写入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数组的首地址
 *  Jstring2CStr 把java中的jstring的类型转化成一个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数组的长度
    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转换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;//声道数

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

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

    //2. 设置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. 设置MP3的编码方式
    lame_set_VBR(lameConvert, vbr_default);
    lame_init_params(lameConvert);
    int read ; int write; //代表读了多少个次 和写了多少次
    int total=0; // 当前读的wav文件的byte数目
    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);
        }
        //把转化后的mp3数据写到文件里
        fwrite(mp3_buffer,1,write,fmp3);

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

1.3.3 导入库以及创建native方法

创建LameUtil类,并导入相应的库,并创建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 调用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, "转码成功:\t" + mFile.getAbsolutePath(), Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                } else {
                                    runOnUiThread(new Runnable() {
                                        @Override
                                        public void run() {
                                            Toast.makeText(WAVTransMP3Activity.this, "转码失败", Toast.LENGTH_SHORT).show();
                                        }
                                    });
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();

那么转码成功后就大功告成了吗?
遗憾的告诉你并没有那么简单,因为转码出来的音频最开始会啪的一声,没错,每一个音频都有,无一幸免,意不意外,惊不惊喜!!!


not_easy.jpg

这里啪的一声是什么原因呢?
是因为转码的时候把wav文件的头信息也一起转了,才会出现这种情况。那要怎么解决呢?
当然是跳过这个头信息,直接从数据开始读取。

fseek(fwav, 4*1024, SEEK_CUR);

对,就是这一句话就可以了,完整代码是这样的

/**
 * 返回值 char* 这个代表char数组的首地址
 *  Jstring2CStr 把java中的jstring的类型转化成一个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数组的长度
    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转换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. 设置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. 设置MP3的编码方式
    lame_set_VBR(lameConvert, vbr_default);
    lame_init_params(lameConvert);
    int read ; int write; //代表读了多少个次 和写了多少次
    int total=0; // 当前读的wav文件的byte数目
    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);
        }
        //把转化后的mp3数据写到文件里
        fwrite(mp3_buffer,1,write,fmp3);

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

然后再转码之后,就没有啪的一声了,开不开心。
然而,你仔细观察,发现是不是秒数不对了,想不想哭

cry.jpeg

那秒数没有对是为啥呢?
因为需要写入相关的VBRTAG,也可以理解为mp3的头信息。

写入VBRTAG

  • lame_mp3_tags_fid:向一个文件指针中写入规范的VBRTAG。

  • VBRTAG的作用是记录整个mp3的一些信息,通常用于VBR模式下的编码,因为VBR模式下比特率不固定,无法直接计算出播放的时长和跳跃点,所以在mp3的开头部分插入一个VBRTAG。

  • VBRTAG有几种规范,但是lame支持的是最通用的规范。

  • 注意lame_mp3_tags_fid函数的参数需要一个FILE *类型代表要写入的文件,这个文件一定是之前编码时写入了mp3数据的文件,VBRTAG是需要卸载mp3的开头的,之前的编码过程中会自动空出写入VBRTAG所需要的空间,这个函数内会自动寻找合适的文件偏移然后覆盖,所以当前的文件偏移是无关紧要的,但是打开文件的时候一定要以读写模式打开。

  • 注意我提到了之前的编码过程中会自动空出写入VBRTAG所需要的空间,所以如果结束编码后不调用lame_mp3_tags_fid写入VBRTAG就会导致这部分内容为空,虽然不影响播放,但是会影响很多播放器对于时长和跳跃点的计算。

  • 那么对于非VBR模式也需要写入VBRTAG吗?是的,lame对于非VBR模式也会预留出VBRTAG的空间,所以非VBR模式的编码最后也需要写入VBRTAG。

说了那么多,意思就是这个函数是应该在lame_encode_flush()之后调, 当所有数据都写入完毕了再调用。仔细想想也很合理, 这时才能确定文件的总帧数。

于是,我们就这样写

/**
 * 返回值 char* 这个代表char数组的首地址
 *  Jstring2CStr 把java中的jstring的类型转化成一个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数组的长度
    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转换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. 设置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. 设置MP3的编码方式
    lame_set_VBR(lameConvert, vbr_default);
    lame_init_params(lameConvert);
    int read ; int write; //代表读了多少个次 和写了多少次
    int total=0; // 当前读的wav文件的byte数目
    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);
        }
        //把转化后的mp3数据写到文件里
        fwrite(mp3_buffer,1,write,fmp3);

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

再重新ndk-build,再编译一次,重新安装,再转码一次,大功终于告成了

2. Android 使用 lame pcm 转码 mp3(边录边转)

边录边转的原理就是,拿到pcm数据,马上转成mp3数据并写入相关文件,当录制结束,转换也同时结束。

2.1 DataEncodeThread的编写

那么肯定需要跑一个线程来进行解码和写入的工作,但是每次写入和转换肯定有很多次,这里使用HandlerThread来进行。

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) {
                //处理缓冲区中的数据
                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();
    }
    /**
     * 从缓冲区中读取并处理数据,使用lame编码MP3
     * @return  从缓冲区中读取的数据的长度
     *          缓冲区中没有数据时返回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结尾信息写入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;
        }
    }
}

下面是调用录音的代码

    /**
     * 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被多次调用
        initAudioRecorder();
        mAudioRecord.startRecording();
        new Thread() {
            @Override
            public void run() {
                //设置线程权限
                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();
            }
            /**
             * 此计算方法来自samsung开发范例
             * 
             * @param buffer buffer
             * @param readSize readSize
             */
            private void calculateRealVolume(short[] buffer, int readSize) {
                double sum = 0;
                for (int i = 0; i < readSize; i++) {  
                    // 这里没有做运算的优化,为了更加清晰的展示代码  
                    sum += buffer[i] * buffer[i]; 
                } 
                if (readSize > 0) {
                    double amplitude = sum / readSize;
                    mVolume = (int) Math.sqrt(amplitude);
                }
            }
        }.start();
    }

这样也就实现了相关的功能,具体转换的方法上面已经提过,这里就不再赘述了。

3. mp3 转 wav

既然是要实现mp3转换为wav格式,那么必须先解码mp3为pcm数据,再将pcm数据写入相关的头信息,这样就实现了mp3转换为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");// 随机读取文件
        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 {// 否则完全复制文件
            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 {// 否则完全复制内容至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();
    }

然后再进行pcm文件写入头信息:

    /**
     * pcm文件转wav文件
     *
     * @param inFilename  源文件路径
     * @param outFilename 目标文件路径
     */
    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转换为wav 的方法。

4. mp3 转 pcm (边播边转)

其实和pcm转mp3边录边转的原理是一样的,也是拿到数据再进行解码,不过这次要用到的mad库来进行解码工作。

    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);是调用的mad的库的方法,具体方法网上都有提供,这里只是贴出相应的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;
 }

最后

感谢大家的支持和阅读,完整项目代码已经上传,再次感谢大家
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
https://www.jianshu.com/p/971fff236881

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

推荐阅读更多精彩内容