Android开发笔记(一百八十九)利用LAME录制MP3音频
Posted aqi00
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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开发笔记(一百八十九)利用LAME录制MP3音频的主要内容,如果未能解决你的问题,请参考以下文章
Android开发笔记(一百八十九)利用LAME录制MP3音频