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.hextern ieee754_float32_t fast_log2(ieee754_float32_t x);=>extern float fast_log2(float x);
  • 2.id3tag.cmachine.h中将HAVE_STRCHRHAVE_MEMCPYifdef结构体删掉
  • 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.22.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.txtgradle中所有路径的都是写的相对路径。所有下面的内容因项目而异。
具体的参考谷狗的官方文档


具体的我写在注释里

# 编译本地库时,需要的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上,如果丢上去的发,就是这个地址

更多相关文章

  1. 使用MediaCodec实现H264编码「第四章,Android音视频编码那点破事
  2. 关于Android与pc通信时中文乱码的分析和解决
  3. Android实验5---通讯录(解决ListView刷新问题及一些编码规范的总
  4. android视频录制与滤镜(二)——google官方硬编demo:
  5. Android对中文url编码
  6. Android高效率编码-第三方SDK详解系列(一)——百度地图,绘制,覆盖
  7. android的编码习惯
  8. 使用多字节字符集的跨平台(PC、Android、IOS、WP)编码/解码方法
  9. 微信团队分享:微信Android版小视频编码填过的那些坑

随机推荐

  1. Android异步加载图片
  2. Android 命令行手动编译打包详解
  3. Android 不通过parent实现样式继承
  4. Android创建Native Binder Service
  5. Android(安卓)项目中文件夹作用(res文件
  6. Android(安卓)利用 APT 技术在编译期生成
  7. android 对话框AlertDialog
  8. Android(安卓)studio如何用真机调试,只需
  9. 动态实现隐藏标题栏
  10. Android 之 调用短信界面