Android的媒体处理一直是件让人头疼的事情。Java倒是也有不少媒体第三方的处理库(mp3agic,musicg等)。但是Java那一波三折的运行方式,导致在它在处理大量运算(图像,音频计算等)的时候力不从心。 为此,Java提供了native关键字,通过jni调用C/C++的函数库来充分使用CPU资源。 比如这里有一个需求:Android录音机录音,然后实时的转换成mp3保持在SD卡上
AS支持cmake之后,ndk的编译变得不再像Android.mk那么痛苦(不过调试还是… 这里准备用Lame库进行音频处理
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 <lame.h>改为include "lame.h"编写jni接口
函数名指向Java中的全路径名,用下划线 _分隔
#include <cwchar> #include "lame_util.h" #include "lamemp3/lame.h" #include <jni.h> 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、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(); }先看看目录结构,注意,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})见注释
... android { ..... defaultConfig { ..... //开启Cmake工具 externalNativeBuild { cmake { cppFlags "" //需要的so版本 abiFilters 'armeabi-v7a', 'arm64-v8a' } } ..... } externalNativeBuild { cmake { path "CMakeLists.txt" } } sourceSets { main { jniLibs.srcDirs = ['clibs'] } } .... } ....录音机的相关操作,还是见注释。
这里假装科普下:
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); } }我有时间把代码脱敏之后,会丢Github上,如果丢上去的发,就是这个地址