Android开发笔记(一百八十九)利用LAME录制MP3音频

Android常用的录音工具有两种,分别是MediaRecorder和AudioRecord,前者用于录制普通音频,后者用于录制原始音频。然而无论是普通音频的amr和aac格式,还是原始音频的pcm格式,都不能在电脑上直接播放,也不能在苹果手机上播放,因为它们属于安卓手机的定制格式,并非通用的音频格式。若想让录音文件放之四海而皆能播放,就得事先将其转为通用的MP3格式,虽然Android官方的开发包不支持MP3转换,不过借助第三方的LAME库,能够将原始音频转存为MP3文件。
LAME是一个高质量的MP3编码器,它采用C/C++代码开发,需要通过JNI技术引入到App工程。LAME源码的下载页面为https://lame.sourceforge.io/download.php,笔者找到的最新版本是3.100,先解压下载完成的源码包,再按照下列步骤依次调整源码细节:
1、把源码包里面的libmp3lame目录整个复制到App模块的jni目录下;
2、把include目录下的lame.h头文件复制到jni\libmp3lame目录下;
3、打开jni\libmp3lame下面的set_get.h,把这行代码

#include <lame.h>

改为下面这样,也就是尖括号改为双引号:

#include "lame.h"

4、打开jni\libmp3lame下面的util.h,把这行代码

    extern ieee754_float32_t fast_log2(ieee754_float32_t x);

改为下面这样,也就是把参数类型改为float:

    extern float fast_log2(float x);

接着给App模块添加LAME支持,具体步骤说明如下:
1、在App代码中声明几个来自JNI的原生方法,同时准备加载NDK编译生成的so库,声明代码示例如下:

public class LameUtil {
    static {
        System.loadLibrary("lamemp3"); // 加载so库
    }
    // 查看Lame版本号
    public native static String version();
    // 初始化Lame
    public native static void init(int inSampleRate, int inChannel, int outSampleRate, int outBitrate, int quality);
    // 开始MP3转码
    public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);
    // 写入缓冲区
    public native static int flush(byte[] mp3buf);
    // 关闭Lame
    public native static void close();
}

2、在jni目录下新建lame-lib.cpp,编写与第一步对应的原生函数,注意函数名称内部的包名、类名与方法名都要跟App模块保持一致。CPP代码内容如下所示:

#include <jni.h>
#include "libmp3lame/lame.h"

static lame_global_flags *glf = NULL;

extern "C"
JNIEXPORT jstring

JNICALL
Java_com_example_audio_util_LameUtil_version(JNIEnv *env, jclass type) {
    return env->NewStringUTF(get_lame_version());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_audio_util_LameUtil_init(JNIEnv *env, jclass type, jint inSampleRate,
              jint outChannel, jint outSampleRate, jint outBitrate, jint quality) {
    if (glf != NULL) {
        lame_close(glf);
        glf = NULL;
    }
    glf = lame_init();
    lame_set_in_samplerate(glf, inSampleRate);
    lame_set_num_channels(glf, outChannel);
    lame_set_out_samplerate(glf, outSampleRate);
    lame_set_brate(glf, outBitrate);
    lame_set_quality(glf, quality);
    lame_init_params(glf);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_audio_util_LameUtil_encode(JNIEnv *env, jclass type, jshortArray buffer_l_,
               jshortArray buffer_r_, jint samples, jbyteArray mp3buf_) {
    jshort *buffer_l = env->GetShortArrayElements(buffer_l_, NULL);
    jshort *buffer_r = env->GetShortArrayElements(buffer_r_, NULL);
    jbyte *mp3buf = env->GetByteArrayElements(mp3buf_, NULL);
    const jsize mp3buf_size = env->GetArrayLength(mp3buf_);
    int result = lame_encode_buffer(glf, buffer_l, buffer_r, samples, (u_char*)mp3buf, mp3buf_size);
    env->ReleaseShortArrayElements(buffer_l_, buffer_l, 0);
    env->ReleaseShortArrayElements(buffer_r_, buffer_r, 0);
    env->ReleaseByteArrayElements(mp3buf_, mp3buf, 0);
    return result;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_audio_util_LameUtil_flush(JNIEnv *env, jclass type, jbyteArray mp3buf_) {
    jbyte *mp3buf = env->GetByteArrayElements(mp3buf_, NULL);
    const jsize  mp3buf_size = env->GetArrayLength(mp3buf_);
    int result = lame_encode_flush(glf, (u_char*)mp3buf, mp3buf_size);
    env->ReleaseByteArrayElements(mp3buf_, mp3buf, 0);
    return result;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_example_audio_util_LameUtil_close(JNIEnv *env, jclass type) {
    lame_close(glf);
    glf = NULL;
}

3、在jni目录下新建MakeLists.txt,编写LAME库的编译规则,指定so文件名,以及要编译哪些代码,编译规则内容示例如下:

cmake_minimum_required(VERSION 3.6) # 指定CMake的最低要求版本号
set(target lamemp3) # 设置环境变量的名称(target)及其取值(lamemp3)
project(${target}) # 指定项目的名称
aux_source_directory(libmp3lame SRC_LIST) # 查找在某个路径下的所有源文件
add_library(${target} SHARED lame-lib.cpp ${SRC_LIST}) # 生成动态库(共享库)

4、打开模块的build.gradle,先给android节点补充下面的cmake文件配置:

// 此处指定mk文件的路径
externalNativeBuild {
    // 下面使用cmake方式编译
    cmake {
        path file('src/main/jni/CMakeLists.txt')
    }
}
再给defaultConfig节点补充下面的cmake规则配置:
    externalNativeBuild {
        cmake {
            cppFlags "-frtti -fexceptions"
            cFlags "-DSTDC_HEADERS"
        }
        ndkBuild {
            abiFilters "arm64-v8a", "armeabi-v7a"
        }
    }

完成以上集成步骤之后,依次点击菜单Build→Make module 'audio',等待编译完成即可在模块目录的build\intermediates\cmake\debug\obj\arm64-v8a下面找到liblamemp3.so了。
不过要想让App真正实现MP3转码功能,还得在代码中调用LameUtil类的初始化、转码、写入、关闭等方法。MP3的转换过程又有两种形式,一种是把PCM文件转成MP3文件,另一种是在录音时将原始数据直接转存为MP3文件,也就是边录边转。由于PCM保存着原始音频数据,该格式的文件较大,一次性转成MP3较费时间,因此通常采取边录边转以便提高转换效率。具体而言,则需构建录音线程,在其构造方法中初始化LAME;然后开启录音线程,同时启动MP3转码线程,录音线程由AudioRecord获得原始音频数据,马上转交给MP3转码线程处理;录音结束时,也给MP3转码线程发个停止消息。录音线程的关键代码示例如下:

private File mRecordFile; // 音频文件的保存路径
private int mFrequence = 16000; // 音频的采样频率,单位赫兹
private int mChannel = AudioFormat.CHANNEL_IN_MONO; // 音频的声道类型
private int mFormat = AudioFormat.ENCODING_PCM_16BIT; // 音频的编码格式
private static final int FRAME_COUNT = 160; // 时间周期,单位毫秒

public Mp3RecordTask(Activity act, String filePath, OnRecordListener listener) {
    mRecordFile = new File(filePath);
    // 最后一个参数表示录音质量,取值为0~9。 其中0最好,但转换慢;9是最差。
    LameUtil.init(mFrequence, 1, mFrequence, 32, 5);
}

// 根据样本数重新计算缓冲区大小
private int calculateBufferSize() {
    // 根据定义好的几个配置,来获取合适的缓冲大小
    int bufferSize = AudioRecord.getMinBufferSize(mFrequence, mChannel, mFormat);
    int bytesPerFrame = 2;
    // 通过样本数重新计算缓冲区大小(能够整除样本数),以便周期性通知
    int frameSize = bufferSize / bytesPerFrame;
    if (frameSize % FRAME_COUNT != 0) {
        frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT);
        bufferSize = frameSize * bytesPerFrame;
    }
    return bufferSize;
}

@Override
public void run() {
    int bufferSize = calculateBufferSize(); // 根据样本数重新计算缓冲区大小
    short[] buffer = new short[bufferSize];
    try {
        // 构建MP3转码线程
        Mp3EncodeTask encodeTask = new Mp3EncodeTask(mRecordFile, bufferSize);
        encodeTask.start(); // 启动MP3转码线程
        // 根据音频配置和缓冲区构建原始音频录制实例
        AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.MIC,
                mFrequence, mChannel, mFormat, bufferSize);
        // 设置需要通知的时间周期
        record.setPositionNotificationPeriod(FRAME_COUNT);
        // 设置录制位置变化的监听器
        record.setRecordPositionUpdateListener(encodeTask, encodeTask.getHandler());
        record.startRecording(); // 开始录制原始音频
        while (!isCancel) { // 没有取消录制,则持续读取缓冲区
            int readSize = record.read(buffer, 0, buffer.length);
            if (readSize > 0) {
                encodeTask.addTask(buffer, readSize); // 添加MP3转码任务
            }
        }
        record.stop(); // 停止原始音频录制
        encodeTask.sendStopMessage(); // 发送停止消息
    } catch (Exception e) {
        e.printStackTrace();
    }
}

启动MP3录音线程很简单,跟启动原始音频录制线程一样,只要下面两行代码就搞定了。

    // 创建一个MP3录制线程,并设置录制事件监听器
    mRecordTask = new Mp3RecordTask(this, mRecordFilePath, this);
    mRecordTask.start(); // 启动MP3录制线程

运行测试App,观察到MP3录音效果如下面两图所示,其中第一张图为MP3录音完成时的截图,第二张图为正在播放MP3时的截图。

 


点此查看Android开发笔记的完整目录

猜你喜欢

转载自blog.csdn.net/aqi00/article/details/127706472