一、前言

之前自己写了个扒谱助手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/downloadsndk环境变量配置样例: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文件中的方法了。