Android 录音详解(二)—— 录制 mp3 格式音频( lame 库的编译及使用)
2018-11-27 liuyingcong 安卓开发
众多周知,mp3 是跨平台性最好的音频格式,由于采用了压缩率更高的有损压缩算法,文件大小是大约每分钟1M,使其在网络中传输更快,占用存储空间也更少;与此同时,它的声音质量也不错,尤其是人声(相声、评书、脱口秀),当然追求无损音乐的除外。
Android 中没有提供录制 mp3 的 API,需要使用开源库 lame,lame 是专门用于编码 mp3 的轻量高效的 c 代码库。由于采用 c 语言编写,故需要用到 jni。且听我慢慢道来~
一、lame 库下载
https://sourceforge.net/projects/lame/files/lame/
我们使用最新的版本:3.100
二、lame 引入
1、创建 jni 文件夹:
首先把 Android Studio 中的项目切换到 Project 视图,在 main 目录下新建 jni 文件夹,在jni文件夹里面新建 lame 文件夹;
2、复制要编译的文件:
解压下载的 lame 库,打开 libmp3lame 文件夹,把里面所有后缀名为 .c .h 的文件(不包括两个文件夹下的)复制到上述新建的 lame 文件夹内;返回上一级,打开 include 文件夹,把 lame.h 文件同样复制到上述新建的 lame 文件夹内,此时 lame 文件夹内应包含 42 个文件。
3、修改库文件:
打开项目中刚刚拷贝过来的 util.h 文件,把 570 行的两处 ieee754_float32_t 改为 float
打开 set_get.h 文件,把头部的 #include <lame.h> 改为 #include "lame.h"
打开 fft.c 文件,删除第47行 #include "vector/lame_intrin.h"
4、新建 Android.mk 文件
在 jni 目录下新建文件 Android.mk,将下面代码拷贝到文件中:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := mp3lame LOCAL_SRC_FILES := lame/bitstream.c lame/fft.c lame/id3tag.c lame/mpglib_interface.c lame/presets.c lame/quantize.c lame/reservoir.c lame/tables.c lame/util.c lame/VbrTag.c lame/encoder.c lame/gain_analysis.c lame/lame.c lame/newmdct.c lame/psymodel.c lame/quantize_pvt.c lame/set_get.c lame/takehiro.c lame/vbrquantize.c lame/version.c MP3Recorder.c include $(BUILD_SHARED_LIBRARY)
5、新建 Application.mk 文件:
在 jni 目录下新建文件 Application.mk,将下面代码拷贝到文件中:
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 APP_CFLAGS += -DSTDC_HEADERS APP_PLATFORM := android-19
三、编写 java 类和 c 文件
1、新建 MP3Recorder 类:
在项目包名(如 com.demo.lame)下新建 MP3Recorder 类,存放本地方法:
package com.demo.lame; /** * 18.11.30 8:51 Yingcong Liu */ public class MP3Recorder { static {System.loadLibrary("mp3lame");}/** * 初始化 lame编码器 * * @param inSampleRate * 输入采样率 * @param outChannel * 声道数 * @param outSampleRate * 输出采样率 * @param outBitrate * 比特率(kbps) * @param quality * 0~9,0最好 */ public static native void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality); /** * 编码,把 AudioRecord 录制的 PCM 数据转换成 mp3 格式 * * @param buffer_l * 左声道输入数据 * @param buffer_r * 右声道输入数据 * @param samples * 输入数据的size * @param mp3buf * 输出数据 * @return * 输出到mp3buf的byte数量 */ public static native int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf); /** * 刷写 * * @param mp3buf * mp3数据缓存区 * @return * 返回刷写的数量 */ public static native int flush(byte[] mp3buf); /** * 关闭 lame 编码器,释放资源 */ public static native void close(); }
2、生成 MP3Recorder.h
打开Android studio的terminal 命令行,输入cd app/src/main/java 命令切换到java目录下,输入javah -o MP3Recorder.h com.demo.lame.MP3Recorder (注意,后面是MP3Recorder的全类名,修改成你的)生成MP3Recorder.h文件,生成过程中有可能出现:
错误: 编码GBK的不可映射字符
不要在意(原因讲一下,JDK默认是用Unicode编码的,由于文件中出现了中文注释,Unicode不支持,故出现这种错误,强迫症者可以用javah -encoding UTF-8 -o MP3Recorder),同步一下文件:File - Sync with File System,就会在java文件夹下发现 MP3Recorder.h文件,把它移动到jni文件夹下;
3、新建 MP3Recorder.c
在jni目录下新建 MP3Recorder.c 文件,把下面代码拷贝到里面,注意改方法名,参照MP3Recorder中的方法名:
#include "lame/lame.h" #include "MP3Recorder.h" static lame_global_flags *glf = NULL; JNIEXPORT void JNICALL Java_com_demo_myrecord2_MP3Recorder_init(JNIEnv *env, jobject instance, 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); } JNIEXPORT jint JNICALL Java_com_demo_myrecord2_MP3Recorder_encode(JNIEnv *env, jobject instance, jshortArray buffer_l, jshortArray buffer_r, jint samples, jbyteArray mp3buf) { jshort* j_buffer_l = (*env)->GetShortArrayElements(env, buffer_l, NULL); jshort* j_buffer_r = (*env)->GetShortArrayElements(env, buffer_r, NULL); const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf); jbyte* j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL); int result = lame_encode_buffer(glf, j_buffer_l, j_buffer_r, samples, j_mp3buf, mp3buf_size); (*env)->ReleaseShortArrayElements(env, buffer_l, j_buffer_l, 0); (*env)->ReleaseShortArrayElements(env, buffer_r, j_buffer_r, 0); (*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0); return result; } JNIEXPORT jint JNICALL Java_com_demo_myrecord2_MP3Recorder_flush(JNIEnv *env, jobject instance, jbyteArray mp3buf) { const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf); jbyte* j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL); int result = lame_encode_flush(glf, j_mp3buf, mp3buf_size); (*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0); return result; } JNIEXPORT void JNICALL Java_com_demo_myrecord2_MP3Recorder_close(JNIEnv *env, jobject instance) { lame_close(glf); glf = NULL; }
四、编译
1、确认是否有 NDK:
打开 Android Studio 的 File - Settings,打开 Appearance & Behavior - System Settings - Android SDK,切换到 SDK Tools 便签,勾选 NDK 并下载;
2、确认项目是否已关联 NDK:
打开 File - Project Structure,在 Android NDK location 中填入 NDK 路径,如:E:\sdk\ndk-bundle;同时,要把该路径配置成环境变量,后面会用到。
3、编译生成 so库
打开Android studio的terminal命令行,切换到jni目录下,输入命令:ndk-build,执行完毕后同步一下文件,就可以看到libs文件夹了(细心的同学会发现还多了obj文件夹,里面的内容几乎和libs文件夹夹是相同的,只是它里面带有调试信息,没用,可以删掉),里面就是各平台的so库。
如果编译失败,请下载最新版本的NDK试一下。
五、使用
1、在 app 下的 build-gradle 文件 android 项目里添加:
externalNativeBuild { ndkBuild { path file('src/main/jni/Android.mk') } }
2、录音的代码:
// 录音状态 private boolean isRecording; //开始录音 private void record() { new Thread() { @Override public void run() { // 音源 int audioSource = MediaRecorder.AudioSource.MIC; // 采样率 int sampleRate = 44100; // 声道 int channelConfig = AudioFormat.CHANNEL_IN_MONO;//单声道 // 采样位数 int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 录音缓存区大小 int bufferSizeInBytes; // 文件输出流 FileOutputStream fos; // 录音最小缓存大小 bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); AudioRecord audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes); try { fos = new FileOutputStream(getExternalCacheDir() + "/demo.mp3"); MP3Recorder.init(sampleRate, 2, sampleRate, 128, 5); short[] buffer = new short[bufferSizeInBytes]; byte[] mp3buffer = new byte[(int) (7200 + buffer.length * 1.25)]; audioRecord.startRecording(); isRecording = true; while (isRecording && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { int readSize = audioRecord.read(buffer, 0, bufferSizeInBytes); if (readSize > 0) { int encodeSize = MP3Recorder.encode(buffer, buffer, readSize, mp3buffer); if (encodeSize > 0) { try { fos.write(mp3buffer, 0, encodeSize); } catch (IOException e) { e.printStackTrace(); } } } } int flushSize = MP3Recorder.flush(mp3buffer); if (flushSize > 0) { try { fos.write(mp3buffer, 0, flushSize); } catch (IOException e) { e.printStackTrace(); } } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } audioRecord.stop(); audioRecord.release(); MP3Recorder.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } } }.start(); }
// 停止录音 private void stop() { isRecording = false; }