手把手教你Android如何使用NDK实现一个MP3转码库

通过本文你可以学到以下知识:

  • 如何实现一个Android MP3转码库
  • 一些和音频转码相关的基础知识
  • 如何使用NDK将c/c++项目移植到Android端,并使用Java调用c/c++代码
  • 如何使用CMake构建NDK项目
  • 如何生成不同CPU架构所需的动态链接库

工具简介

Lame

LAME 是最好的MP3编码器,速度快,效果好,特别是中高码率和VBR编码方面。

NDK

原生开发工具包,即帮助开发原生代码的一系列工具,包括但不限于编译工具、一些公共库、开发IDE等。它提供了完整的一套将 c/c++ 代码编译成静态/动态库的工具,而 Android.mkApplication.mk 你可以认为是描述编译参数和一些配置的文件。比如指定使用c++11还是c++14编译,会引用哪些共享库,并描述关系等,还会指定编译的abi。只有有了这些 NDK 中的编译工具才能准确的编译 c/c++ 代码。

CMake简介

CMake是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefile 或 project 文件,然后再调用底层的编译。Android Studio 2.2以后开始支持CMake,所以现在我们有2种方式来编译c/c++ 代码。一个是 ndk-build + Android.mk + Application.mk 组合,另一个是 CMake + CMakeLists.txt 组合,它们都不会影响我们的android代码和c/c++代码,只是构建方式和结构不同。

CMake相对传统ndk-build的优点在于:无需手动生成Java的头文件、相对于mk文件配置更简单、可以自动生成对应abi*.so动态链接库、支持设置断点调试(我认为这是最方便的地方)、可以引用其他已经生成的so库。

准备工作

  1. 在Android Studio 上安装好NDK和CMake,网上教程很多这里就不在赘述。
  2. 下载Lame源码。

项目结构

通过这张项目结构可以先帮助我们更形象整体的理解CMake构建NDK的方式。

这里写图片描述

Tips:如果你对CMake刚接触,可以先用Android Studio创建一个项目,然后勾选上include c++选项,去看下demo的结构,帮助理解,我就是这样做的,效果还不错。

Lame源码移植

  1. 首先在src/main/目录下新建一个cpp文件夹,我们可以将Lame源码中libmp3lame拷贝到cpp文件夹下,当然这里我们也可以重命名,例如我命名为lamemp3(以下介绍我将沿用此名)。
  2. 将Lame源码中的include文件夹下的lame.h复制到lamemp3文件夹中。
  3. 剔除lamemp3中不必要的文件和目录,只保留.c.h文件,因为其他文件大多都是批处理文件,对于Android不是必需的。
  4. 修改util.h的源码。在570行找到ieee754_float32_t数据类型,将其修改为float类型,因为ieee754_float32_t是Linux或者是Unix下支持的数据类型,在Android下并不支持。
  5. set_get.h中24行将include <lame.h>改为include "lame.h"
  6. id3tag.cmachine.h两个文件里,將HAVE_STRCHRHAVE_MEMCPY的ifdef结构体注释掉,不然编译会报错。
#ifdef STDC_HEADERS
# include <stdlib.h>
# include <string.h>
#else
/*
# ifndef HAVE_STRCHR
#  define strchr index
#  define strrchr rindex
# endif
*/
char   *strchr(), *strrchr();
/*
# ifndef HAVE_MEMCPY
#  define memcpy(d, s, n) bcopy ((s), (d), (n))
#  define memmove(d, s, n) bcopy ((s), (d), (n))
# endif
*/
#endif

CMakeLists编写

src中新建一个名为CMakeLists.txt的文件(注意,这里的CMakeLists.txt不一定非要放到这里,只要它的位置和build.gradle文件的配置相对应就行)。

我们看下CMakeLists.txt的内容,这里我把注释已经写得很详细了,大家看下就很明白了:

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.4.1)

# 定义常量
set(SRC_DIR main/cpp/lamemp3)

# 指定关联的头文件目录
include_directories(main/cpp/lamemp3)

# 查找在某个路径下的所有源文件
aux_source_directory(main/cpp/lamemp3 SRC_LIST)

# 设置 *.so 文件输出路径,要放在在add_library之前,不然不会起作用
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

# 声明库名称、类型、源码文件
add_library(lame-mp3-utils SHARED main/cpp/lame-mp3-utils.cpp ${SRC_LIST})

# 定位某个NDK库,这里定位的是log库
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# 将NDK库链接到native库中,这样native库才能调用NDK库中的函数
target_link_libraries( # Specifies the target library.
                       lame-mp3-utils

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

build.gradle配置

android {
    ......
    defaultConfig {
        ......
        externalNativeBuild {
            cmake {
                cppFlags ""
                abiFilters 'armeabi-v7a','arm64-v8a','mips','mips64','x86','x86_64' //要支持的abi
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "src/CMakeLists.txt"//配置文件路径
        }
    }
}

编写Java native方法

这里我在代码中注释已经写得非常详细了,关于一些参数我会在下面做更详细的解释。

public class Mp3Converter {

    static  {
        System.loadLibrary("lame-mp3-utils");
    }

    /**
     * init lame
     * @param inSampleRate
     *              input sample rate in Hz
     * @param channel
     *              number of channels
     * @param mode
     *              0 = CBR, 1 = VBR, 2 = ABR.  default = 0
     * @param outSampleRate
     *              output sample rate in Hz
     * @param outBitRate
     *              rate compression ratio in KHz
     * @param quality
     *              quality=0..9. 0=best (very slow). 9=worst.<br />
     *              recommended:<br />
     *              2 near-best quality, not too slow<br />
     *              5 good quality, fast<br />
     *              7 ok quality, really fast
     */
    public native static void init(int inSampleRate, int channel, int mode,
                                   int outSampleRate, int outBitRate, int quality);


    /**
     * file convert to mp3
     * it may cost a lot of time and better put it in a thread
     * @param input
     *          file path to be converted
     * @param mp3
     *          mp3 output file path
     */
    public native  static void convertMp3(String input, String mp3);


    /**
     * get converted bytes in inputBuffer
     * @return
     *          converted bytes in inputBuffer
     *          to ignore the deviation of the file size,when return to -1 represents convert complete
     */
    public native static long getConvertBytes();

    /**
     * get library lame version
     * @return
     */
    public native static String getLameVersion();

}

编写调用C/C++的cpp

先看一个上面Java文件中native init(args...) 方法在这里是如何实现的:

extern "C" JNIEXPORT void JNICALL
Java_jaygoo_library_converter_Mp3Converter_init(JNIEnv *env, jclass type, jint inSampleRate,
                                               jint channel, jint mode, jint outSampleRate,
                                               jint outBitRate, jint quality) {
    lameInit(inSampleRate, channel, mode, outSampleRate, outBitRate, quality);
}
  • extern "C"因为我们写的是cpp是c++文件,所以当我们调用一些c文件的方法时需要加上extern "C",不然会提示找不到方法。
  • Java_jaygoo_library_converter_Mp3Converter_init这里方法名是和Java文件中的native方法一一对应的,这样才能让native方法找到对应的cpp方法。格式是:Java_包名_类名_方法名,这里包名的._代替,所以我们native的方法名命名尽量不要包含_,但如果真的包含了,那么在cpp文件中用1代替Java native 中的_
  • JNIEXPORT void JNICALL是固定的格式,也是辅助native方法找到对应的cpp方法。
  • JNIEnv *envJNIEnv是指向JNINativeInterface结构的指针,当我们需要调用JNI方法时,都需要通过这个指针才能进行调用。

其实我们还可以通过Android Studio来自动生成这些方法和参数,在Android Studio中点击native方法名,快捷键alt+enter即可自动生成了。

看到这里,大家基本对如何编写cpp代码有一定的了解,接下来我来介绍下lame-mp3-utils.cpp的实现,由于篇幅有限,就不全上代码了,这里介绍几个比较关键的方法。


init

这里主要是对Lame进行一些初始化,主要的参数包括:

  1. inSampleRate 要转换的音频文件采样率
  2. mode 音频编码模式,包括VBR、ABR、CBR
  3. outSampleRate 转换后音频文件采样率
  4. outBitRate 输出的码率
  5. quality 压缩质量(具体数值上面注释已经写的很清楚了)

这里的代码没什么可看的,主要是调用一些lame自带的方法设置一些配置参数,最后调用lame_init_params(lame)完成初始化,这里我对上面几个参数出现的名词做下解释:

  • 采样率每秒从连续信号中提取并组成离散信号的采样个数,单位Hz。数值越高,音质越好,常见的如8000Hz、11025Hz、22050Hz、32000Hz、44100Hz等。
  • 码率又称比特率是指每秒传送的比特(bit)数,单位kbps,越高音质越好(相同编码格式下)。
  • CBR常数比特率编码,码率固定,速度较快,但压缩的文件相比其他模式较大,音质也不会有很大提高,适用于流式播放方案,lame默认的方案是这种。
  • VBR动态比特率编码,码率不固定。适用于下载后在本地播放或者在读取速度有限的设备播放,体积和为CBR的一半左右,但是输出码率不可控
  • ABR平均比特率编码,是Lame针对CBR不佳的文件体积比和VBR生成文件大小不定的特点独创的编码模式。是一种折中方案,码率基本可控,但是好像用的不多。

convertMp3(jstring jInputPath, jstring jMp3Path)

首先我们要将jstring转换为c++中的char*后才可以使用,我们可以通过JNI提供的GetStringUTFChars方法完成转换:

const char* cInput = env->GetStringUTFChars(jInputPath, 0);
const char* cMp3 = env->GetStringUTFChars(jMp3Path, 0);

然后我们通过fopen来打开需要操作的文件,用rb来读取输入文件,用wb来写转换后的文件。

 FILE* fInput = fopen(cInputPath,"rb");
 FILE* fMp3 = fopen(cMp3Path,"wb");

接下来我们申请两个buffer来缓存文件数据,我们边读边转换,然后再将转换后的数据写入文件。由于Lame的要求,这里的buffer数据必须要不小于7200,下面是具体的转换代码:

 //convert to mp3
    do{
        //这里将输入文件内容读取到inputBuffer中,当全部读取会返回0
        read = static_cast<int>(fread(inputBuffer, sizeof(short int) * 2, 8192, fInput));
        //这里用于计算读取的原文件的byte数,可以用于计算转换的进度
        total +=  read * sizeof(short int)*2;
        nowConvertBytes = total;
        if(read != 0){
            //这里用lame将inputBuffer转换为MP3格式的数据放入mp3Buffer中
            write = lame_encode_buffer_interleaved(lame, inputBuffer, read, mp3Buffer, BUFFER_SIZE);
            //将转换好的mp3Buffer的数据写入文件
            fwrite(mp3Buffer, sizeof(unsigned char), static_cast<size_t>(write), fMp3);
        }
        //最后全部读取完成后及时flush
        if(read == 0){
            lame_encode_flush(lame,mp3Buffer, BUFFER_SIZE);
        }
    }while(read != 0);

最后记得转换后释放资源:

    resetLame();
    fclose(fInput);
    fclose(fMp3);
    env->ReleaseStringUTFChars(jInputPath, cInput);
    env->ReleaseStringUTFChars(jMp3Path, cMp3);

生成不同ABI下的so库

为了支持不同的设备,我们需要根据不同的ABI生成不同的so库来调用,我们可以通过Android Studio的Make来调用CMakeList.txt脚本生成支持各种ABI版本的so库。文件输出路径可以通过配置CMakeList.txt来修改:

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

其中PROJECT_SOURCE_DIR是指脚本所在目录,ANDROID_ABI是指在build.gradle中配置的abiFilters

ABI扩展知识

ABI(Application binary interface)应用程序二进制接口。不同的CPU 与指令集的每种组合都有定义的 ABI (应用程序二进制接口),一段程序只有遵循这个接口规范才能在该 CPU 上运行,所以同样的程序代码为了兼容多个不同的CPU,需要为不同的 ABI 构建不同的库文件。当然对于CPU来说,不同的架构并不意味着一定互不兼容。

  • armeabi设备只兼容armeabi
  • armeabi-v7a设备兼容armeabi-v7a、armeabi
  • arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi
  • x86设备兼容X86、armeabi
  • mips64设备兼容mips64、mips
  • mips只兼容mips;

GitHub

https://github.com/Jay-Goo/Mp3Converter

参考文献

https://blog.csdn.net/allen315410/article/details/42456661
https://www.jianshu.com/p/6332418b12b1

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容