丢掉龟速的java媒体库,通过Lame实现Android录音同时转换为mp3格式保存
16lz
2021-01-26
0.Read Me
Android的媒体处理一直是件让人头疼的事情。Java倒是也有不少媒体第三方的处理库(
mp3agic
,musicg
等)。但是Java那一波三折的运行方式,导致在它在处理大量运算(图像,音频计算等)的时候力不从心。
为此,Java提供了native
关键字,通过jni
调用C/C++
的函数库来充分使用CPU资源。
比如这里有一个需求:Android录音机录音,然后实时
的转换成mp3
保持在SD卡上
1.Cmake
AS支持
cmake
之后,ndk的编译变得不再像Android.mk
那么痛苦(不过调试还是…
这里准备用Lame库进行音频处理
1.1.处理
lame库下载好后,解压找到libmp3lame
目录,保留调.c
、.h
等文件,复制到项目cpp
文件夹下,然后开始对头文件等做些处理。因为Android不支持。
- 1.
util.h
中extern ieee754_float32_t fast_log2(ieee754_float32_t x);
=>extern float fast_log2(float x);
- 2.
id3tag.c
、machine.h
中将HAVE_STRCHR
和HAVE_MEMCPY
的ifdef
结构体删掉 - 3.
fft.c
中去除vector/lame_intrin.h
的头文件引用 - 4.
set_get.h
中将include
改为include "lame.h"
2.jni
编写jni接口
2.1.C引用
函数名指向Java中的全路径名,用下划线 _
分隔
#include #include "lame_util.h"#include "lamemp3/lame.h"#include static lame_global_flags *glf = NULL;void Java_cos_mos_recorder_decode_ULame_close(JNIEnv *env, jclass type){ lame_close(glf); glf = NULL;}jint Java_cos_mos_recorder_decode_ULame_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;}jint Java_cos_mos_recorder_decode_ULame_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;}void Java_cos_mos_recorder_decode_ULame_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);}
2.2.C头文件
#include extern "C"{void Java_cos_mos_recorder_decode_ULame_close(JNIEnv *env, jclass type);jint Java_cos_mos_recorder_decode_ULame_encode(JNIEnv *env, jclass type, jshortArray buffer_l_, jshortArray buffer_r_, jint samples, jbyteArray mp3buf_);jint Java_cos_mos_recorder_decode_ULame_flush(JNIEnv *env, jclass type, jbyteArray mp3buf_);void Java_cos_mos_recorder_decode_ULame_init(JNIEnv *env, jclass type, jint inSampleRate, jint outChannel, jint outSampleRate, jint outBitrate, jint quality);}
2.3.Java的方法映射
各个参数的描述我直接写在注释里,这里就不多说了。
注意包名,这里和2.2
、2.3
中的函数名对应
package cos.mos.recorder.decode;/** * @Description: Lame的jni映射库 * @Author: Kosmos * @Date: 2019.05.24 23:36 * @Email: KosmoSakura@gmail.com */public class ULame { static { System.loadLibrary("lame"); } /** * @param inSamplerate 采样率(Hz) * @param inChannel 流中的通道数 * @param outSamplerate 输出采样率(Hz) * @param outBitrate 压缩比(KHz) * @param quality mp3质量 * @apiNote 初始化 * 关于质量∈[0,9] * 0->最高质量,最慢 * 9->最低质量,最快 * 通常: * 2->接近最好的质量,不太慢 * 5->质量好,速度快 * 7->音质还凑活, 非常快 */ public native static void init(int inSamplerate, int inChannel, int outSamplerate, int outBitrate, int quality); /** * @param bufferLeft 左声道的PCM数据 * @param bufferRight 右声道的PCM数据. * @param samples 每个采样通道的样本数 * @param mp3buf 指定最终编码的MP3流=>数组长度=7200+(1.25 * bufferLeft.length) * "7200 + (1.25 * buffer_l.length)" length array. * @return mp3buf中输出的字节数。可以为0。 * -1: mp3buf太小 * -2: 内存分配异常 * -3: lame初始化失败 * -4: 音质解析异常 * @apiNote 缓冲区编码为mp3 */ public native static int encode(short[] bufferLeft, short[] bufferRight, int samples, byte[] mp3buf); /** * @param mp3buf 结果编码的MP3流。您必须指定至少7200字节。 * @return 输出到encode中mp3buf的字节数,可能为0 * @apiNote flush掉lame的缓冲区 * 关于刷流: * 0.可能会返回最后的几个mp3帧列数组 * 1.将刷新lame的内部PCM编码缓冲区,不足数列用0补满最终帧 * 2.encode中的mp3buf至少>7200字节(否则可能一列都不够用) * 3.(如果有)将id3v1标签写入比特流 */ public native static int flush(byte[] mp3buf); /** * 关闭Lame. */ public native static void close();}
3.CmakeList.txt
先看看目录结构,注意,
CmakeList.txt
和gradle
中所有路径的都是写的相对路径。所有下面的内容因项目而异。
具体的参考谷狗的官方文档
具体的我写在注释里
# 编译本地库时,需要的cmake的最低版本cmake_minimum_required(VERSION 3.4.1)# 生成的so动态库最后输出的路径set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/clibs/${ANDROID_ABI})#设置变量SRC_DIR为lamemp3的所在路径set(SRC_DIR src/main/cpp/lamemp3)#指定头文件所在,可以多次调用,指定多个路径include_directories(src/main/cpp/lamemp3)#设定一个目录aux_source_directory(src/main/cpp/lamemp3 SRC_LIST)#将前面目录下所有的文件都添加进去add_library(lame SHARED src/main/cpp/lame_util.cpp ${SRC_LIST})# 添加编译本地库时所需要的依赖库,cmake已经知道系统库的路径,所以这里只需要指定使用log库find_library(clog log)target_link_libraries(lame ${clog})
4.build.gradle
见注释
...android { ..... defaultConfig { ..... //开启Cmake工具 externalNativeBuild { cmake { cppFlags "" //需要的so版本 abiFilters 'armeabi-v7a', 'arm64-v8a' } } ..... } externalNativeBuild { cmake { path "CMakeLists.txt" } } sourceSets { main { jniLibs.srcDirs = ['clibs'] } } ....}....
5.录音MP3Recorder
录音机的相关操作,还是见注释。
这里假装科普下:
- 1.Android的
AudioRecord
录音的默认格式为:PCM编码
,16Bit
、单声道
。- 2.我们平时说的音量其实是一个相对值,
- 2.1.计算方式:
volume=X*log10(P1/P2)
- 2.2.
P1
:测量值的声压、P2
:参考值的声压、X
为参考系数
- 2.3.人类所能听到的最小声压:
20微帕
- 3.人类能听到的声音频率
∈[20Hz,20KHz]
- 3.1.下面的频率计算方式为:FFT(快速傅里叶变换)
- 4.
音频帧
计算公式: int size = 采样率 * 位宽 * 采样时间 * 通道数- 5.缓冲区:大小不能低于一音频帧的大小(所以下面代码有个四舍五入到音频帧大小的步骤)
- 6.采样率:每秒钟能够采样的次数
package cos.mos.recorder.decode;import android.media.AudioFormat;import android.media.AudioRecord;import android.media.MediaRecorder;import java.io.File;import java.io.IOException;/** * @Description: 录音工具 * @Author: Kosmos * @Date: 2019.05.25 15:16 * @Email: KosmoSakura@gmail.com */public class MP3Recorder { //Recorder private static final int DEFAULT_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;//音源:麦克风 private static final int DEFAULT_SAMPLING_RATE = 44100;//采样率:模拟器仅支持从麦克风输入8kHz采样率 private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;//单声道 private static final PCMFormat DEFAULT_AUDIO_FORMAT = PCMFormat.PCM_16BIT;///PCM编码,16Bit //Lame private static final int DEFAULT_LAME_MP3_QUALITY = 7;//音质 private static final int DEFAULT_LAME_IN_CHANNEL = 1;//mono=>1 private static final int DEFAULT_LAME_MP3_BIT_RATE = 32;//编码比特率 //采样配置 private static final int FRAME_COUNT = 160;//每160帧作为一个数列周期,通知编码 private AudioRecord record; private int bufferSize; private short[] bufferPCM; private DataEncodeThread encodeThread; private boolean isRecording = false; /** * @apiNote 采样率1通道,16位pcm */ public MP3Recorder() { } public void start(File targetFile) throws IOException { if (isRecording) { return; } isRecording = true; //提早,防止init或startRecording被多次调用 initAudioRecorder(targetFile); record.startRecording(); new Thread() { @Override public void run() { //设置线程权限 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO); while (isRecording) { int readSize = record.read(bufferPCM, 0, bufferSize); if (readSize > 0) { encodeThread.addTask(bufferPCM, readSize); calculateRealVolume(bufferPCM, readSize); } } //释放 record.stop(); record.release(); record = null; // 等待完成转码 encodeThread.sendStopMessage(); } /** * @apiNote 此计算方法来自samsung开发范例 * @param buffer buffer * @param readSize readSize */ private void calculateRealVolume(short[] buffer, int readSize) { double sum = 0; for (int i = 0; i < readSize; i++) { // 这里没有做运算的优化,为了更加清晰的展示代码 sum += buffer[i] * buffer[i]; } if (readSize > 0) { double amplitude = sum / readSize; mVolume = (int) Math.sqrt(amplitude); } } }.start(); } private int mVolume; /** * @return 真实音量 * @apiNote 获取真实的音量。 [算法来自三星] */ public int getRealVolume() { return mVolume; } public void stop() { isRecording = false; } public boolean isRecording() { return isRecording; } /** * @param targetFile 目标文件 * @apiNote 初始化 */ private void initAudioRecorder(File targetFile) throws IOException { bufferSize = AudioRecord.getMinBufferSize(DEFAULT_SAMPLING_RATE, DEFAULT_CHANNEL_CONFIG, DEFAULT_AUDIO_FORMAT.getAudioFormat()); int bytesPerFrame = DEFAULT_AUDIO_FORMAT.getBytesPerFrame();//得到样本个数。计算缓冲区大小 //四舍五入到给定帧大小,方便整除,以通知 int frameSize = bufferSize / bytesPerFrame; if (frameSize % FRAME_COUNT != 0) { frameSize += (FRAME_COUNT - frameSize % FRAME_COUNT); bufferSize = frameSize * bytesPerFrame; } record = new AudioRecord(DEFAULT_AUDIO_SOURCE, DEFAULT_SAMPLING_RATE, DEFAULT_CHANNEL_CONFIG, DEFAULT_AUDIO_FORMAT.getAudioFormat(), bufferSize); bufferPCM = new short[bufferSize]; //初始化lame缓冲区mp3采样速率与所记录的pcm采样速率相同,比特率为32kbps ULame.init(DEFAULT_SAMPLING_RATE, DEFAULT_LAME_IN_CHANNEL, DEFAULT_SAMPLING_RATE, DEFAULT_LAME_MP3_BIT_RATE, DEFAULT_LAME_MP3_QUALITY); encodeThread = new DataEncodeThread(targetFile, bufferSize); encodeThread.start(); record.setRecordPositionUpdateListener(encodeThread, encodeThread.getHandler()); record.setPositionNotificationPeriod(FRAME_COUNT); }}
6.中转
- 1.过程耗时,建议交给子线程做
- 2.从缓冲区读取数据,使用
lame
编码MP3 - 3.缓冲区不足时,使用lame将所有数据刷新到文件中,并添加
MP3尾信息
/** * @return 从缓冲区中读取的数据的长度(没有数据时返回0) */ private int processData() { if (mTasks.size() > 0) { Task task = mTasks.remove(0); short[] buffer = task.getData(); int readSize = task.getReadSize(); int encodedSize = ULame.encode(buffer, buffer, readSize, mp3Buffer); if (encodedSize > 0) { try { outputStream.write(mp3Buffer, 0, encodedSize); } catch (IOException e) { e.printStackTrace(); } } return readSize; } return 0; }
private void flushAndRelease() { //将MP3尾信息写入buffer中 final int flushResult = ULame.flush(mp3Buffer); if (flushResult > 0) { try { outputStream.write(mp3Buffer, 0, flushResult); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } ULame.close(); } } }
我有时间把代码脱敏之后,会丢Github上,如果丢上去的发,就是这个地址
更多相关文章
- 使用MediaCodec实现H264编码「第四章,Android音视频编码那点破事
- 关于Android与pc通信时中文乱码的分析和解决
- Android实验5---通讯录(解决ListView刷新问题及一些编码规范的总
- android视频录制与滤镜(二)——google官方硬编demo:
- Android对中文url编码
- Android高效率编码-第三方SDK详解系列(一)——百度地图,绘制,覆盖
- android的编码习惯
- 使用多字节字符集的跨平台(PC、Android、IOS、WP)编码/解码方法
- 微信团队分享:微信Android版小视频编码填过的那些坑