Android音视频开发入门(5)使用LAME编码一个PCM文件

我们之后就以小Demo来学音视频的知识。
因为音视频开发一定会使用JNI,即使用Java调用本地代码/本地代码调用Java。
所以我们也要学会去建立一个 JNI项目。

1. 使用Android Studio 3.4.2搭建一个JNI项目

在Android Studio创建好一个支持C/C++的项目后,我们创建一个 Mp3Encoder 的类,使用它来调用本地方法:

public class Mp3Encoder {
     static {
        System.loadLibrary("mp3_encode");
    }
    
    public native void encode();
}

接下来,通过命令行,进入到该class的目录下, 执行 javac Mp3Encoder.java:
在这里插入图片描述
这个时候文件目录下,就会都多出一个 Mp3Encoder.class
在这里插入图片描述
接下来是关键的一步,对这个.class进行导出,声明其为一个 JNI接口。
>javah -jni com.rikkatheworld.mp3encoder.studio.Mp3Encoder
但是却报: 错误: 找不到 ‘com.rikkatheworld.mp3encoder.studio.Mp3Encoder’ 的类文件。

这里必须要先设置好导出头文件的路径:
在这里插入图片描述
然后再在java的路径下(注意一定要在编译好的文件的父目录路径下):
在这里插入图片描述导出成功,这个时候文件夹下面多了一个头文件:
在这里插入图片描述
我们在java的路径下,通过右键->new->Folder->JniFolder 建立一个JNI文件,然后把这个头文件拖入到jni目录下。而之前那个.class文件就可以删除掉了
在这里插入图片描述
接下来我们在jni下新建一个cpp文件 Mp3Encoder,将刚刚生成文件的内容全部复制到这个文件下面,并且实现对应的方法,这里就是做一个日志打印.:

//
// Created by msn on 2019/11/17.
//

#include <jni.h>
#include <android/log.h>

#define LOG_TAG "Mp3Encoder"
#ifndef _Included_com_rikkatheworld_mp3encoder_studio_Mp3Encoder
#define _Included_com_rikkatheworld_mp3encoder_studio_Mp3Encoder
//宏定义 一个日志打印
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_com_rikkatheworld_mp3encoder_studio_Mp3Encoder_encoder
        (JNIEnv *, jobject) {
    //这里就做一个打印日志
    LOGI("encoder encode");
}

#ifdef __cplusplus
}
#endif
#endif

接着在 app build.gradle下加入:

android {
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                //设置C++标准为默认
                cppFlags ""
            }
        }

        ndk {
            //ndk模块名称
            moduleName "mp3_encoder"
            //声明log
            ldLibs "log"
        }
    }

    externalNativeBuild {
        cmake {
            //指定CMake文件路径
            path "CMakeLists.txt"
        }
    }
}

接着我们在 app下面创建 CMakeLists.txt,(以前是使用Android.mk,这个相当于代替它的)

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # 输入ndk使用的名称
        mp3_encoder

        # Sets the library as a shared library.
        SHARED

        # 输入你的cpp的路径名.
        src/main/jni/Mp3Encoder.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # 输入你的ndk模块名
        mp3_encoder

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

最后我们在MianActivity下调用这个jni:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Mp3Encoder mp3Encoder = new Mp3Encoder();
        mp3Encoder.encoder();
    }

点击运行后输出:
在这里插入图片描述
此时,就完成我们第一个jni项目的构建啦~
文件目录如下:
在这里插入图片描述

2. 交叉编译的原理和实践

交叉编译是音视频开发中必需的,因为无论在哪个移动平台下开发,第三方库都是需要进行交叉编译的。
本节会从交叉编译的原理开始介绍,然后会在两个移动平台下编译出音视频开发常用的几个库,包括 X264、 FDK_AAC、LAME,最终将以LAME库为例进行实践,完成一个将音视频的PCM裸数据编码成MP3文件的实例,以此来证明交叉编译的重要性。

2.1 交叉编译的原理

所以交叉编译,就是 在一个平台(PC)上生成另外一个平台(Android、IOS)的可执行代码

Q:Android为什么要进行交叉编译呢?
A:即使是Android设备具有越来越强的计算能力,但是有两个原因不能在 这种嵌入式设备上进行本地编译:

  1. 还是计算能力的问题,不够全面,不够极致
  2. ARM平台上没有较好的编译环境,这导致整个编译过程异常繁琐

所以大部分的嵌入式开发平台都是提供了 本身平台交叉编译所需要的交叉工具编译链(Android提供了 Eclipse SDK、Android Studio编译器),这样开发者就能在 PC上编译出可以运行在ARM平台下的程序了。

无论是自行安装PC上的编译器,还是下载其他平台的交叉编译链,它们都会提供下面几个工具:

  • CC
    编译器,对C源文件进行编译处理,生成汇编文件
  • AS
    将汇编文件生成目标文件
  • AR
    打包器,用于库操作
  • LD
    链接器,为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者是可执行文件
  • GDB
    调试工具
  • STRIP
    最终生成的可执行文件或者库文件作为输入,然后消除掉其中的源码
  • NM
    查看静态库文件中的符号表
  • Objdump
    查看静态库或者动态库中的方法名

2.2 Android Studio平台交叉编译工具

在编译之前,我们先看看LAME、FDK_ACC等这些的概念简介:

  • LAME
    是目前非常优秀的一种MP3编译引擎,在业界,转码成 MP3格式的音频文件时,最常用的编码器就是LAME库。当达到320Kbits/s以上时,LAME编码出来的音频质量几乎可以CD的音质相媲美。并且保证整个音频文件的体积非常小。
    因此若要在移动平台上编码 MP3文件,使用LAME便成为唯一选择。
  • FDK_ACC
    FDK_ACC 是用来编码和解码的AAC格式音频文件的开源库。
  • X264
    X264是一个开源的H.264/MPEG-4 AVC视频编码函数库,是最好的有损视频编码器之一。一般的输入的视频帧是YUV,输出是编码之后的 H264的数据包,并且支持 CBR、VBR模式,可以在编码的过程中直接改变码率的设置,这点在直播的场景中是非常实用的(直播场景下利用该特点可以做码率自适应)

了解完这些后,我们在来看看Android NDK下一些经常会用到的组件:

  • ARM、x86的交叉编译器
  • 构建系统
  • Java原生接口文件
  • C库
  • Math库
  • 最小的C++库
  • ZLib压缩库
  • POSIX线程
  • Android日志库
  • Android原生应用Api
  • OpenGL ES库
  • OpenSL ES库

2.3 AS交叉编译LAME

先去 传送门 下载好LAME的源码然后解压缩。
解压完后将 libmp3lame 文件夹下的所有的 带 .h 和带 .c的 C/C++文件 和 include 下的lame.h 复制到 JNI目录下(最好再统一放到一个新的子目录下,这边就放到了 lame子目录下),因为添加了这么多的文件,那么需要把这些文件写入到CMake的 add_library

add_library( # Sets the name of the library.
        mp3_encoder

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        src/main/jni/Mp3Encoder.cpp
        src/main/jni/lame/bitstream.c src/main/jni/lame/encoder.c
        src/main/jni/lame/fft.c src/main/jni/lame/gain_analysis.c
        src/main/jni/lame/id3tag.c src/main/jni/lame/lame.c
        src/main/jni/lame/mpglib_interface.c src/main/jni/lame/newmdct.c
        src/main/jni/lame/presets.c src/main/jni/lame/psymodel.c
        src/main/jni/lame/quantize.c src/main/jni/lame/quantize_pvt.c
        src/main/jni/lame/reservoir.c src/main/jni/lame/set_get.c
        src/main/jni/lame/tables.c src/main/jni/lame/takehiro.c
        src/main/jni/lame/util.c src/main/jni/lame/vbrquantize.c
        src/main/jni/lame/VbrTag.c src/main/jni/lame/version.c)

ok,lame的源码就已经添加到我们的项目中了。但是因为文件里面一些引入的路径已经变了,所以我们要对这些引入的路径进行更改:

  1. 删除 fft.c 文件的 47 行的 include“vector/lame_intrin.h”
  2. 删除掉set_get.h的第24行
  3. 修改 util.h 文件的 570 行的 extern ieee754_float32_t fast_log2(ieee754_float32_t x)extern float fast_log2(float x)
  4. 此时还有很多文件报错,因为没有定义宏 STDC_HEADERS ,在build.gradle中添加宏定义:cFlags “-DSTDC_HEADERS”:
    在这里插入图片描述

点个锤子后,我们打开之前 写过的Mp3Encoder.java下,进行如下修改

public class Mp3Encoder {
    static {
        System.loadLibrary("mp3_encoder");
    }
    public native int init(String pcmFile,int audioChannels,int bitRate,int sampleRate, String mp3Path);
    public native void encoder();
    public native void destroy();
}

然后给其编译,然后javah(重复上一节的操作)
产生的新的 Mp3Encoder.h替换旧的,接着在 Mp3Encoder.cpp中重写方法,它作为JNI层,是被Java层调用的:

#include "mp3_encoder.h"
#include "com_rikkatheworld_mp3encoder_studio_Mp3Encoder.h"

Mp3Encoder *encoder = NULL;
extern "C" {
#define LOG_TAG "Mp3Encoder"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
//实例化Mp3Encoder,然后调用初始方法
JNIEXPORT jint JNICALL Java_com_rikkatheworld_mp3encoder_studio_Mp3Encoder_init
        (JNIEnv *env, jobject, jstring pcmPathParam, jint channels, jint bitRate, jint sampleRate,
         jstring mp3PathParam) {
    const char *pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);
    const char *mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);
    encoder = new Mp3Encoder();
    int ret = encoder->Init(pcmPath, mp3Path, sampleRate, channels, bitRate);
    env->ReleaseStringUTFChars(mp3PathParam, mp3Path);
    env->ReleaseStringUTFChars(pcmPathParam, pcmPath);
    return ret;
}

JNIEXPORT void JNICALL Java_com_rikkatheworld_mp3encoder_studio_Mp3Encoder_encoder
        (JNIEnv *, jobject) {
    encoder->Encode();
}

JNIEXPORT void JNICALL Java_com_rikkatheworld_mp3encoder_studio_Mp3Encoder_destroy
        (JNIEnv *, jobject) {
    encoder->Destory();
}
}

我们在JNI层中调用了 native层的代码,我们要去 jni下创建两个文件 mp3_encoder.hmp3_encoder.cpp
我们先来编写 mp3_encoder.h,定义变量和方法:

#ifndef MP3ENCODER_MP3_ENCODER_H
#define MP3ENCODER_MP3_ENCODER_H

#include "lame/lame.h"

extern "C" {
class Mp3Encoder {
private:
    FILE *pcmFile;
    FILE *mp3File;
    lame_t lameClient;

public:
    Mp3Encoder();

    ~Mp3Encoder();

    int Init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels,
             int bitRat);

    void Encode();

    void Destory();
};


#endif //MP3ENCODER_MP3_ENCODER_H
}

接着我们编写 mp3_encoder.cpp, 它会使用到 lame库 里的一些方法:

#include "mp3_encoder.h"
#include <jni.h>
extern "C"
/**
 * 以二进制文件的方式打开PCM文件,以写入二进制文件的方式打开MP3文件,然后初始化LAME
 */
int
Mp3Encoder::Init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels,
                  int bitRate) {
    int ret = -1;
    pcmFile = fopen(pcmFilePath, "rb");
    if (pcmFile) {
        mp3File = fopen(mp3FilePath, "wb");
        if (mp3File) {
            lameClient = lame_init();
            lame_set_in_samplerate(lameClient, sampleRate);
            lame_set_out_samplerate(lameClient, sampleRate);
            lame_set_num_channels(lameClient, channels);
            lame_set_brate(lameClient, bitRate);
            lame_init_params(lameClient);
            ret = 0;
        }
    }
    return ret;
}

/**
 * 函数主体是一个循环,每次都会读取一段bufferSize大小的PCM数据buffer,然后再编码该buffer
 * 但是在编码buffer之前得把该buffer的左右声道拆分开,再送入到 lame编码器
 * 最后将编码的数据写入到mp3文件中
 */
void Mp3Encoder::Encode() {
    int bufferSize = 1024 * 256;
    short *buffer = new short[bufferSize / 2];
    short *leftBuffer = new short[bufferSize / 4];
    short *rightBuffer = new short[bufferSize / 4];
    unsigned char *mp3_buffer = new unsigned char[bufferSize];
    size_t readBufferSize = 0;
    while ((readBufferSize = fread(buffer, 2, bufferSize / 2, pcmFile)) > 0) {
        for (int i = 0; i < readBufferSize; i++) {
            if (i % 2 == 0) {
                leftBuffer[i / 2] = buffer[i];
            } else {
                rightBuffer[i / 2] = buffer[i];
            }
        }
        size_t wroteSize = lame_encode_buffer(lameClient, leftBuffer, rightBuffer,
                                              (int) (readBufferSize / 2), mp3_buffer, bufferSize);
        fwrite(mp3_buffer, 1, wroteSize, mp3File);
    }
    delete[] buffer;
    delete[] leftBuffer;
    delete[] rightBuffer;
    delete[] mp3_buffer;
}

void Mp3Encoder::Destory() {
    if (pcmFile) {
        fclose(pcmFile);
    }
    if (mp3File) {
        fclose(mp3File);
    }
}

Mp3Encoder::Mp3Encoder() {

}

Mp3Encoder::~Mp3Encoder() {

}

这些代码其实我们不用特别的了解,只需了解并且跑通就可以了。

到这里,我们的 JNI层、Native层都编译完了。
接下来就是Java层的调用了,我们先下载一个 PCM文件到手机下,然后在MainActivity下这样写:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private String[] permissions = new String[]{
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    private List<String> mPermissionList = new ArrayList<>();
    private static final int MY_PERMISSIONS_REQUEST = 1001;

    //采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。
    public static final int SAMPLE_RATE_INHZ = 44100;
    //声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。
    public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    //返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT.
    public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkPermissions();

        String pcmPath, mp3Path;
        pcmPath = "/storage/emulated/0/16k.pcm";//pcm文件路径,文件要存在!
        mp3Path = "/storage/emulated/0/16k1.mp3";//转换后mp3文件的保存路径

        Mp3Encoder mp3Encoder = new Mp3Encoder();
        if (mp3Encoder.init(pcmPath, CHANNEL_CONFIG, 128, SAMPLE_RATE_INHZ, mp3Path) == 0) {
            Log.d(TAG, "onCreate: encoder-init:success");
            mp3Encoder.encoder();
            mp3Encoder.destroy();
            Log.d(TAG, "onCreate:encode finish");
        } else {
            Log.d(TAG, "onCreate: encoder-init:failed");
        }
    }

    private void checkPermissions() {
        // Marshmallow开始才用申请运行时权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            for (int i = 0; i < permissions.length; i++) {
                if (ContextCompat.checkSelfPermission(this, permissions[i]) !=
                        PackageManager.PERMISSION_GRANTED) {
                    mPermissionList.add(permissions[i]);
                }
            }
            if (!mPermissionList.isEmpty()) {
                String[] permissions = mPermissionList.toArray(new String[mPermissionList.size()]);
                ActivityCompat.requestPermissions(this, permissions, MY_PERMISSIONS_REQUEST);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == MY_PERMISSIONS_REQUEST) {
            for (int i = 0; i < grantResults.length; i++) {
                if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                    Log.i(TAG, permissions[i] + " 权限被用户禁止!");
                }
            }
        }
    }
}

如果顺畅跑完,则会输出结果:
在这里插入图片描述
这样就可以去听一下解析出来的MP3文件啦。

3. 总结

从这么一个小小的demo中,我们就已经明确看出了 NDK开发中 三层。

  • Java层
    Java层的代码就只有 native的声明,以及 loadLibrary()加载so库。
    然后在使用的时候调用声明出来的函数就可以了,不涉及实现。
  • JNI层
    JNI有两个重要的文件:
    一个是由Java 有声明native的类 通过 javac编译、javah导出而生成的 头文件
    另外一个是 根据该头文件而编写的 同名.cpp文件,它就是一个中间态,它被Java调用后,就去调用 native层的代码。
    它几乎也没有什么实现。
  • Native层
    native层充满着 .h头文件,和 .cpp文件,他们都时都是实现特定的功能和写出来的。
    Jni层调用的就是这一些文件里面的方法。
发布了248 篇原创文章 · 获赞 99 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/103106402