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

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];

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

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

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

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

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

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

最后

总之啊,家里没矿的同学们,如果你们想以后的日子过得好一些,多想想你们的业余时间怎么安排吧;

技术方面的提升肯定是重中之重,但是技术外的一些“软实力”也不能完全忽视,很多时候升职确实是因为你的技术足够强,但也与你的“软实力”密切相关

在这我也分享一份大佬自己收录整理的 Android学习PDF+架构视频+面试文档+源码笔记 ,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅并给下属员工学习的精品资料。在脑图中,每个知识点专题都配有相对应的实战项目,可以有效的帮助大家掌握知识点。

总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习

相信自己,没有做不到的,只有想不到的

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

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

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

握知识点。

总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习

[外链图片转存中…(img-P8gvYrX7-1712126020345)]

[外链图片转存中…(img-3aUFQush-1712126020346)]

相信自己,没有做不到的,只有想不到的

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

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

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

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,你需要使用 DirectShow 采集音视频数据,并使用 x264 和 LAME 进行编码。然后,你需要使用 FFmpeg 封装音视频数据为 MP4 文件。下面是一个简单的示例代码,展示如何实现音视频同步: ```c++ // 初始化 DirectShow 采集器 CoInitialize(NULL); IGraphBuilder* pGraph = NULL; IMediaControl* pControl = NULL; ICaptureGraphBuilder2* pBuilder = NULL; IBaseFilter* pAudioCaptureFilter = NULL; IBaseFilter* pVideoCaptureFilter = NULL; IBaseFilter* pAudioEncoderFilter = NULL; IBaseFilter* pVideoEncoderFilter = NULL; IBaseFilter* pMuxFilter = NULL; CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)&pGraph); CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)&pBuilder); pBuilder->SetFiltergraph(pGraph); // 添加音频采集器和编码器 CoCreateInstance(CLSID_AudioCapture, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pAudioCaptureFilter); CoCreateInstance(CLSID_AudioEncoder, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pAudioEncoderFilter); pGraph->AddFilter(pAudioCaptureFilter, L"Audio Capture Filter"); pGraph->AddFilter(pAudioEncoderFilter, L"Audio Encoder Filter"); // 添加视频采集器和编码器 CoCreateInstance(CLSID_VideoCapture, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pVideoCaptureFilter); CoCreateInstance(CLSID_VideoEncoder, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pVideoEncoderFilter); pGraph->AddFilter(pVideoCaptureFilter, L"Video Capture Filter"); pGraph->AddFilter(pVideoEncoderFilter, L"Video Encoder Filter"); // 添加 MP4 封装器 CoCreateInstance(CLSID_MPEG4Muxer, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pMuxFilter); pGraph->AddFilter(pMuxFilter, L"MP4 Mux Filter"); // 连接音频采集器和编码器 IPin* pAudioCaptureOutPin = GetPin(pAudioCaptureFilter, PINDIR_OUTPUT); IPin* pAudioEncoderInPin = GetPin(pAudioEncoderFilter, PINDIR_INPUT); pGraph->Connect(pAudioCaptureOutPin, pAudioEncoderInPin); // 连接视频采集器和编码器 IPin* pVideoCaptureOutPin = GetPin(pVideoCaptureFilter, PINDIR_OUTPUT); IPin* pVideoEncoderInPin = GetPin(pVideoEncoderFilter, PINDIR_INPUT); pGraph->Connect(pVideoCaptureOutPin, pVideoEncoderInPin); // 连接音频编码器和 MP4 封装器 IPin* pAudioEncoderOutPin = GetPin(pAudioEncoderFilter, PINDIR_OUTPUT); IPin* pMuxInAudioPin = GetPin(pMuxFilter, PINDIR_INPUT, MEDIATYPE_Audio); pGraph->Connect(pAudioEncoderOutPin, pMuxInAudioPin); // 连接视频编码器和 MP4 封装器 IPin* pVideoEncoderOutPin = GetPin(pVideoEncoderFilter, PINDIR_OUTPUT); IPin* pMuxInVideoPin = GetPin(pMuxFilter, PINDIR_INPUT, MEDIATYPE_Video); pGraph->Connect(pVideoEncoderOutPin, pMuxInVideoPin); // 开始采集和编码 pControl->Run(); // 等待直到采集结束 while (true) { if (CheckCaptureEnded(pAudioCaptureFilter) && CheckCaptureEnded(pVideoCaptureFilter)) { break; } Sleep(10); } // 停止采集和编码 pControl->Stop(); // 释放资源 pMuxFilter->Release(); pVideoEncoderFilter->Release(); pVideoCaptureFilter->Release(); pAudioEncoderFilter->Release(); pAudioCaptureFilter->Release(); pBuilder->Release(); pControl->Release(); pGraph->Release(); CoUninitialize(); ``` 在上面的代码中,我们使用 DirectShow 采集音频和视频数据,并将它们分别传递给音频和视频编码器进行编码。然后,我们将编码后的音频和视频数据传递给 MP4 封装器,将它们封装为 MP4 文件。在这个过程中,我们需要确保音频和视频数据的时间戳是同步的,以确保最终的 MP4 文件中的音视频是同步的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值