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 <lame.h>改为include "lame.h"

2.jni

编写jni接口

2.1.C引用

函数名指向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.C头文件

#include <jni.h>

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]
  • 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上,如果丢上去的发,就是这个地址


职业移动开发,业余整点音乐