前言
最近在研究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
大致分为两步
- 通过Jstring2CStr方法将java中的jstring类型转化成c语言的char字符串
- 然后再通过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_buffer
或lame_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
#include
#include
#include
/**
* 返回值 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();
那么转码成功后就大功告成了吗?
遗憾的告诉你并没有那么简单,因为转码出来的音频最开始会啪的一声,没错,每一个音频都有,无一幸免,意不意外,惊不惊喜!!!
这里啪的一声是什么原因呢?
是因为转码的时候把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);
}
然后再转码之后,就没有啪的一声了,开不开心。
然而,你仔细观察,发现是不是秒数不对了,想不想哭
那秒数没有对是为啥呢?
因为需要写入相关的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 mTasks = Collections.synchronizedList(new ArrayList());
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
#include
#include "mad/mad.h"
#include "NativeMP3Decoder.h"
#include
#include
#include
#include
#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