一、前言
之前自己写了个扒谱助手apk,想把录音得到的pcm转成mp3,百度发现需要使用so文件实现,然后在踩了一堆坑后,终于实现了pcm转mp3的方法。
包含如何生成so文件、如何使用so文件两个主要内容。
现在记录如下。
二、流程
1.目标是android实现pcm转mp3,首先从这个网址找到了大概的方法:
2.从网上下载了lame-3.100.tar.gz,直接解压就行;lame下载地址:
https://lame.sourceforge.io/
下载android-ndk-r23-windows.zip,解压后,配置个环境变量就行(修改path啥的)ndk下载地址:
https://developer.android.google.cn/ndk/downloads
ndk环境变量配置样例:
Path F:\android-ndk-r23
3.打开cmd,执行下ndk-build
,看看环境变量配好了没有。
4.按照教程,创建一个mp3lame文件夹,例如F:\mp3lame
,然后创建个jni文件夹,例如F:\mp3lame\jni
,然后把下载的lame源码中的libmp3lame整个拷贝到jni下,例如F:\mp3lame\jni\libmp3lame
。
5.然后在jni目录下创建Android.mk文件,位置例如F:\mp3lame\jni\Android.mk
,内容例如:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libmp3lame
LOCAL_CFLAGS := -DSTDC_HEADERS
LOCAL_SRC_FILES := \
./libmp3lame/bitstream.c \
./libmp3lame/encoder.c \
./libmp3lame/fft.c \
./libmp3lame/gain_analysis.c \
./libmp3lame/id3tag.c \
./libmp3lame/lame.c \
./libmp3lame/mpglib_interface.c \
./libmp3lame/newmdct.c \
./libmp3lame/presets.c \
./libmp3lame/psymodel.c \
./libmp3lame/quantize.c \
./libmp3lame/quantize_pvt.c \
./libmp3lame/reservoir.c \
./libmp3lame/set_get.c \
./libmp3lame/tables.c \
./libmp3lame/takehiro.c \
./libmp3lame/util.c \
./libmp3lame/vbrquantize.c \
./libmp3lame/VbrTag.c \
./libmp3lame/version.c \
./wrapper.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
6.然后创建Application.mk文件,位置例如:F:\mp3lame\jni\Application.mk
,内容例如:
APP_PLATFORM := android-19
7.然后在mp3lame目录下执行ndk-build命令,例如:
F:\mp3lame>ndk-build
8.如果报错,看清报错信息,如果报的是缺少lame.h
错误,就把lame源码中的lame.h放到jni目录下,例如放到F:\mp3lame\jni\lame.h
9.如果报错jni/util.h:574:5: error: unknown type name ‘ieee754_float32_t‘
等这种类型的错误,
就修改jni中的util.h文件,把extern ieee754_float32_t fast_log2(ieee754_float32_t x);
替换为extern float fast_log2(float x);
注意该变量可能有多个,
因此要把util.h中的多个地方的ieee754_float32_t
都替换为float
。
10.编写一个wrapper.c文件,供java实际调用用,位置例如:F:\Z\mp3lame\jni\wrapper.c
,代码例如:
#include <stdio.h>
#include <stdlib.h>
#include <jni.h>
#include <android/log.h>
#include "libmp3lame/lame.h"
#define LOG_TAG "LAME ENCODER"
#define LOGD(format, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, format, ##args);
#define BUFFER_SIZE 8192
#define be_short(s) ((short) ((unsigned short) (s) << 8) | ((unsigned short) (s) >> 8))
lame_t lame;
int read_samples(FILE *input_file, short *input) {
int nb_read;
nb_read = fread(input, 1, sizeof(short), input_file) / sizeof(short);
int i = 0;
while (i < nb_read) {
input[i] = be_short(input[i]);
i++;
}
return nb_read;
}
void Java_com_example_myapplication_Record_RecordToMP3_initEncoder(JNIEnv *env,
jobject jobj, jint in_num_channels, jint in_samplerate, jint in_brate,
jint in_mode, jint in_quality) {
lame = lame_init();
LOGD("Encoding Init parameters:");
lame_set_num_channels(lame, in_num_channels);
LOGD("Encoding Number of channels: %d", in_num_channels);
lame_set_in_samplerate(lame, in_samplerate);
LOGD("Encoding Sample rate: %d", in_samplerate);
lame_set_brate(lame, in_brate);
LOGD("Encoding Bitrate: %d", in_brate);
lame_set_mode(lame, in_mode);
LOGD("Encoding Mode: %d", in_mode);
lame_set_quality(lame, in_quality);
LOGD("Encoding Quality: %d", in_quality);
int res = lame_init_params(lame);
LOGD("Encoding Init returned: %d", res);
}
void Java_com_example_myapplication_Record_RecordToMP3_destroyEncoder(
JNIEnv *env, jobject jobj) {
int res = lame_close(lame);
LOGD("Encoding Deinit returned: %d", res);
}
void Java_com_example_myapplication_Record_RecordToMP3_encodeFile(JNIEnv *env,
jobject jobj, jstring in_source_path, jstring in_target_path) {
const char *source_path, *target_path;
source_path = (*env)->GetStringUTFChars(env, in_source_path, NULL);
target_path = (*env)->GetStringUTFChars(env, in_target_path, NULL);
FILE *input_file, *output_file;
input_file = fopen(source_path, "rb");
output_file = fopen(target_path, "wb");
short input[BUFFER_SIZE];
char output[BUFFER_SIZE];
int nb_read = 0;
int nb_write = 0;
int nb_total = 0;
LOGD("Encoding started");
while (nb_read = read_samples(input_file, input)) {
nb_write = lame_encode_buffer(lame, input, input, nb_read, output,
BUFFER_SIZE);
fwrite(output, nb_write, 1, output_file);
nb_total += nb_write;
}
LOGD("Encoded %d bytes", nb_total);
nb_write = lame_encode_flush(lame, output, BUFFER_SIZE);
fwrite(output, nb_write, 1, output_file);
LOGD("Encoded Flushed %d bytes", nb_write);
fclose(input_file);
fclose(output_file);
}
11.这里需要注意下,这个wrapper.c文件中,有3个方法:
Java_com_example_myapplication_Record_RecordToMP3_initEncoder
Java_com_example_myapplication_Record_RecordToMP3_destroyEncoder
Java_com_example_myapplication_Record_RecordToMP3_encodeFile
这3个方法名,是有特殊含义
的,不能乱写,需要根据需要自己改。
解释如下:
其中的`com_example_myapplication_Record`是说,我有一个java文件,它在`com.example.myapplication.Record`包里;(这个java文件下面会提到)
`RecordToMP3`是java文件名,对应我的android项目中的RecordToMP3.java文件;
`initEncoder`、`destroyEncoder`、`encodeFile`对应RecordToMP3.java文件中的3个方法名。
这些需要根据自己的需要修改。
对应的java文件下方会讲到。
12.重新在F:\mp3lame
下执行ndk-build
命令,F:\mp3lame\ndk-build
,得到F:\Z\mp3lame\libs
文件夹及其中的文件。
13.在自己的Android项目中写一个java文件,包名例如:com.example.myapplication.Record
,文件名例如:RecordToMP3.java
,内容例如:
package com.example.myapplication.Record;
import android.os.Environment;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
public class RecordToMP3 {
static {
//libmp3lame
System.loadLibrary("mp3lame");
}
private String filePath;
public static final File PATH = Environment.getExternalStorageDirectory();
private String fileName = "txtRecord";
private String fileLast = ".mp3";
public RecordToMP3(){
int numChannels = 2;
int sampleRate = 44100;
int bitRate = 16;
int quality = 2;
int mode = 3;
initMP3();
initEncoder(numChannels, sampleRate, bitRate, mode, quality);
}
public String getFilePath(){
return filePath;
}
/**
*
* @param numChannels 声道数
* @param sampleRate 采样率
* @param bitRate 比特率
* @param mode 模式
* @param quality
*/
public native void initEncoder(int numChannels, int sampleRate, int bitRate, int mode, int quality);
public native void destroyEncoder();
public native int encodeFile(String sourcePath, String targetPath);
public void pcmToMP3(String pcmPath, String mp3Path){
try {
File file1 = bigtolittle(pcmPath);
encodeFile(file1.getPath(),mp3Path);
file1.delete();
}catch (Exception e){ }
}
/**
* 注意:直接转mp3会出现噪音。。因为安卓字节是小端排序,lame是大端排序,所以需要转换,转换代码如下:
*
* 大小端字节转换
* @param fileName
* @return
* @throws IOException
*/
private static File bigtolittle( String fileName) throws IOException {
File file = new File(fileName); //filename为pcm文件,请自行设置
InputStream in = null;
byte[] bytes = null;
in = new FileInputStream(file);
bytes = new byte[in.available()];//in.available()是得到文件的字节数
int length = bytes.length;
while (length != 1) {
long i = in.read(bytes, 0, bytes.length);
if (i == -1) {
break;
}
length -= i;
}
int dataLength = bytes.length;
int shortlength = dataLength / 2;
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, 0, dataLength);
ShortBuffer shortBuffer = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();//此处设置大小端
short[] shorts = new short[shortlength];
shortBuffer.get(shorts, 0, shortlength);
File file1 = File.createTempFile("pcm", null);//输出为临时文件
String pcmtem = file1.getPath();
FileOutputStream fos1 = new FileOutputStream(file1);
BufferedOutputStream bos1 = new BufferedOutputStream(fos1);
DataOutputStream dos1 = new DataOutputStream(bos1);
for (int i = 0; i < shorts.length; i++) {
dos1.writeShort(shorts[i]);
}
dos1.close();
Log.d("gg", "bigtolittle: " + "=" + shorts.length);
return file1;
}
private void initMP3(){
try {
int i = 0;
filePath = PATH + "/" + fileName + i + fileLast;
File f = new File(filePath);
while (f.exists()) {
i++;
filePath = PATH + "/" + fileName + i + fileLast;
f = new File(filePath);
}
f.createNewFile();
}catch (Exception e){}
}
}
说明:
●static方法中的System.loadLibrary方法会加载lib下的文件(这个下面提到)
●其中3个native方法实际会调用lib中的方法,可以供其它java调用,名称与wrapper.c中的要对应
●这个java使用时,可以new一个对象,调用其中的方法即可,例如:
//pcm文件路径
String filePath = "/abc.pcm";
//pcm文件对象
File filePathFile = new(filePath);
//new对象
RecordToMP3 recordToMP3 = new RecordToMP3();
//pcm转mp3
recordToMP3.pcmToMP3(filePath, recordToMP3.getFilePath());
//得到的mp3的路径
String finalPath = recordToMP3.getFilePath();
//删除pcm文件
filePathFile.delete();
//释放资源
recordToMP3.destroyEncoder();
14.现在,终于讲到so文件使用方法了。
●在android项目中,与java文件夹同级,新建一个libs文件夹;例如F:\myapp\app\src\main\libs
●在这个libs文件夹中,把刚才ndk-build得到的libs文件夹下的内容直接放过来
即可。这样so文件就放到android项目中了。路径例如:
F:\myapp\app\src\main\libs\arm64-v8a\libmp3lame.so
F:\myapp\app\src\main\libs\armeabi-v7a\libmp3lame.so
F:\myapp\app\src\main\libs\x86\libmp3lame.so
F:\myapp\app\src\main\libs\x86_64\libmp3lame.so
共4个同名文件,在不同的文件夹中,调用时会自动选择到底用哪一个。
●修改build.gradle文件,位置例如F:\myapp\app\build.gradle
,增加如下内容:
sourceSets {
main {
jniLibs.srcDirs = ['src/main/libs']
}
}
整个build.gradle样例:
apply plugin: 'com.android.application'
android {
lintOptions {
checkReleaseBuilds false
abortOnError false
}
compileSdkVersion 28
defaultConfig {
applicationId "com.z.bapuzhushou"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/libs']
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:percent:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
这样,so文件就放入了android项目中,调用native方法就可以使用了。
三、总结
●so文件使用方法:
1.在android项目中,项目名/app/src/main/
目录下(与java文件夹同级),创建libs文件夹,在libs文件夹中放so文件(中间可以有x86_64等其它文件夹)
2.修改build.gradle文件,增加sourceSets
配置,指明libs的路径。
3.编写java类,使用System.loadLibrary
方法读取so文件,使用native
方法调用so文件中的方法;需要注意类的包路径、类名、方法名,要与so文件中的方法名保持一致
。
4.编写其它java类,调用native方法,就是调用so文件中的方法了。