• 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 

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

    #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库,声明代码示例如下:

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

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

    1. #include <jni.h>
    2. #include "libmp3lame/lame.h"
    3. static lame_global_flags *glf = NULL;
    4. extern "C"
    5. JNIEXPORT jstring
    6. JNICALL
    7. Java_com_example_audio_util_LameUtil_version(JNIEnv *env, jclass type) {
    8.     return env->NewStringUTF(get_lame_version());
    9. }
    10. extern "C"
    11. JNIEXPORT void JNICALL
    12. Java_com_example_audio_util_LameUtil_init(JNIEnv *env, jclass type, jint inSampleRate,
    13.               jint outChannel, jint outSampleRate, jint outBitrate, jint quality) {
    14.     if (glf != NULL) {
    15.         lame_close(glf);
    16.         glf = NULL;
    17.     }
    18.     glf = lame_init();
    19.     lame_set_in_samplerate(glf, inSampleRate);
    20.     lame_set_num_channels(glf, outChannel);
    21.     lame_set_out_samplerate(glf, outSampleRate);
    22.     lame_set_brate(glf, outBitrate);
    23.     lame_set_quality(glf, quality);
    24.     lame_init_params(glf);
    25. }
    26. extern "C"
    27. JNIEXPORT jint JNICALL
    28. Java_com_example_audio_util_LameUtil_encode(JNIEnv *env, jclass type, jshortArray buffer_l_,
    29.                jshortArray buffer_r_, jint samples, jbyteArray mp3buf_) {
    30.     jshort *buffer_l = env->GetShortArrayElements(buffer_l_, NULL);
    31.     jshort *buffer_r = env->GetShortArrayElements(buffer_r_, NULL);
    32.     jbyte *mp3buf = env->GetByteArrayElements(mp3buf_, NULL);
    33.     const jsize mp3buf_size = env->GetArrayLength(mp3buf_);
    34.     int result = lame_encode_buffer(glf, buffer_l, buffer_r, samples, (u_char*)mp3buf, mp3buf_size);
    35.     env->ReleaseShortArrayElements(buffer_l_, buffer_l, 0);
    36.     env->ReleaseShortArrayElements(buffer_r_, buffer_r, 0);
    37.     env->ReleaseByteArrayElements(mp3buf_, mp3buf, 0);
    38.     return result;
    39. }
    40. extern "C"
    41. JNIEXPORT jint JNICALL
    42. Java_com_example_audio_util_LameUtil_flush(JNIEnv *env, jclass type, jbyteArray mp3buf_) {
    43.     jbyte *mp3buf = env->GetByteArrayElements(mp3buf_, NULL);
    44.     const jsize  mp3buf_size = env->GetArrayLength(mp3buf_);
    45.     int result = lame_encode_flush(glf, (u_char*)mp3buf, mp3buf_size);
    46.     env->ReleaseByteArrayElements(mp3buf_, mp3buf, 0);
    47.     return result;
    48. }
    49. extern "C"
    50. JNIEXPORT void JNICALL
    51. Java_com_example_audio_util_LameUtil_close(JNIEnv *env, jclass type) {
    52.     lame_close(glf);
    53.     glf = NULL;
    54. }

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

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

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

    1. // 此处指定mk文件的路径
    2. externalNativeBuild {
    3.     // 下面使用cmake方式编译
    4.     cmake {
    5.         path file('src/main/jni/CMakeLists.txt')
    6.     }
    7. }
    8. 再给defaultConfig节点补充下面的cmake规则配置:
    9.     externalNativeBuild {
    10.         cmake {
    11.             cppFlags "-frtti -fexceptions"
    12.             cFlags "-DSTDC_HEADERS"
    13.         }
    14.         ndkBuild {
    15.             abiFilters "arm64-v8a", "armeabi-v7a"
    16.         }
    17.     }

    完成以上集成步骤之后,依次点击菜单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转码线程发个停止消息。录音线程的关键代码示例如下:

    1. private File mRecordFile; // 音频文件的保存路径
    2. private int mFrequence = 16000; // 音频的采样频率,单位赫兹
    3. private int mChannel = AudioFormat.CHANNEL_IN_MONO; // 音频的声道类型
    4. private int mFormat = AudioFormat.ENCODING_PCM_16BIT; // 音频的编码格式
    5. private static final int FRAME_COUNT = 160; // 时间周期,单位毫秒
    6. public Mp3RecordTask(Activity act, String filePath, OnRecordListener listener) {
    7.     mRecordFile = new File(filePath);
    8.     // 最后一个参数表示录音质量,取值为09。 其中0最好,但转换慢;9是最差。
    9.     LameUtil.init(mFrequence, 1, mFrequence, 32, 5);
    10. }
    11. // 根据样本数重新计算缓冲区大小
    12. private int calculateBufferSize() {
    13.     // 根据定义好的几个配置,来获取合适的缓冲大小
    14.     int bufferSize = AudioRecord.getMinBufferSize(mFrequence, mChannel, mFormat);
    15.     int bytesPerFrame = 2;
    16.     // 通过样本数重新计算缓冲区大小(能够整除样本数),以便周期性通知
    17.     int frameSize = bufferSize / bytesPerFrame;
    18.     if (frameSize % FRAME_COUNT != 0) {
    19.         frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT);
    20.         bufferSize = frameSize * bytesPerFrame;
    21.     }
    22.     return bufferSize;
    23. }
    24. @Override
    25. public void run() {
    26.     int bufferSize = calculateBufferSize(); // 根据样本数重新计算缓冲区大小
    27.     short[] buffer = new short[bufferSize];
    28.     try {
    29.         // 构建MP3转码线程
    30.         Mp3EncodeTask encodeTask = new Mp3EncodeTask(mRecordFile, bufferSize);
    31.         encodeTask.start(); // 启动MP3转码线程
    32.         // 根据音频配置和缓冲区构建原始音频录制实例
    33.         AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.MIC,
    34.                 mFrequence, mChannel, mFormat, bufferSize);
    35.         // 设置需要通知的时间周期
    36.         record.setPositionNotificationPeriod(FRAME_COUNT);
    37.         // 设置录制位置变化的监听器
    38.         record.setRecordPositionUpdateListener(encodeTask, encodeTask.getHandler());
    39.         record.startRecording(); // 开始录制原始音频
    40.         while (!isCancel) { // 没有取消录制,则持续读取缓冲区
    41.             int readSize = record.read(buffer, 0, buffer.length);
    42.             if (readSize > 0) {
    43.                 encodeTask.addTask(buffer, readSize); // 添加MP3转码任务
    44.             }
    45.         }
    46.         record.stop(); // 停止原始音频录制
    47.         encodeTask.sendStopMessage(); // 发送停止消息
    48.     } catch (Exception e) {
    49.         e.printStackTrace();
    50.     }
    51. }

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

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

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

     

     


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

  • 相关阅读:
    Python编程语言学习笔记
    SDI-12协议与STM32 进行uart通信
    大模型与数据库:AI 时代的双向助推力
    杭电oj 2034 人见人爱A-B C语言
    提示工程(Prompt Engineering)指南(入门篇)
    Flutter报错RenderBox was not laid out: RenderRepaintBoundary的解决方法
    【SpringCloud微服务全家桶学习笔记-GateWay网关(微服务入口)】
    Go gRPC 入门
    Centos8安装docker并配置Kali Linux图形化界面
    RabbitMQ消息的可靠性
  • 原文地址:https://blog.csdn.net/aqi00/article/details/127706472