Android音视频开发入门(5)使用LAME编码一个PCM文件,Android-Camera内存问题剖析

点击运行后输出:

在这里插入图片描述

此时,就完成我们第一个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 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”);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结

最后小编想说:不论以后选择什么方向发展,目前重要的是把Android方面的技术学好,毕竟其实对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里附上我整理的几十套腾讯、字节跳动,京东,小米,头条、阿里、美团等公司19年的Android面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

技术进阶之路很漫长,一起共勉吧~

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

整理的几十套腾讯、字节跳动,京东,小米,头条、阿里、美团等公司19年的Android面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-g8R3aTz6-1712162554507)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

技术进阶之路很漫长,一起共勉吧~

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值