本文章是阅读《音视频开发进阶指南基于android与ios平台的实践》一书的学习笔记。
本章主要目标:使用 LAME 这个开源的 MP3 编码库在 Android 平台上将一个 PCM 文件编码为 MP3 文件,最终将编码后的 MP3文件发送到电脑上即可进行播放。
LAME 是目前非常优秀的一种 MP3 编码引擎,在业界,转码成 MP3 格式的音频文件时,最常用的编码器就是 LAME 库。当达到320Kbit/s 以上时,LAME 编码出来的音频质量几乎可以和 CD 的音质相媲美,并且还能保证整个音频文件的体积非常小,因此若要在移动端平台上编码 MP3 文件,使用 LAME 便成为唯一的选择。
LAME 官网:https://lame.sourceforge.io
由于我之前弄过很久的 NDK,所以这块就不做赘述,可以根据文档和 NDK 的知识去做交叉编译。或者直接使用别人已经编译好的 lame android 库:https://github.com/naman14/TAndroidLame
FDK_AAC 是用来编码和解码 AAC 格式音频文件的开源库,Android 系统编码和解码 AAC 所用的就是这个库。开发者 Fraunhofer IIS 是 AAC 音频规范的核心制定者(MP3 时代 Fraunhofer IIS 也是 MP3 规范的制定者)。前面章节中已经介绍过 AAC 有很多种Profile,而 FDK_AAC 几乎支持大部分的 Profile,并且支持 CBR 和 VBR 这两种模式,根据笔者个人的听感和频谱分析,在同等码率下 FDK_AAC 比 NeroAAC 以及 faac 和 voaac 的音质都要好一些。
这边也有 Android 编译好的现成库:https://github.com/mstorsjo/fdk-aac
X264 是一个开源的 H.264/MPEG-4 AVC 视频编码函数库,是最好的有损视频编码器之一。一般的输入是视频帧的 YUV 表示,输出是编码之后的 H264 的数据包,并且支持 CBR、VBR 模式,可以在编码的过程中直接改变码率的设置,这在直播的场景中是非常实用的 (直播场景下利用该特点可以做码率自适应)。
x264 源码:http://www.videolan.org/developers/x264.html
现成库:https://github.com/sszhangpengfei/android_x264_encoder
github: https://github.com/zhanxiaokai/Android-Mp3Encoder
要实现的目标是,在添加好 C++ 支持的项目中加入编码 MP3 文件的功能。当点击按钮的时候,输入的是一个 PCM 文件的路径和一个 MP3 的路径,等运行完毕,电脑上的播放器直接就可以播放该 MP3 文件。
Java 代码:
public class MainActivity extends Activity {
static {
System.loadLibrary("audioencoder");
}
private final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.mp3_encoder_btn).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Mp3Encoder encoder = new Mp3Encoder();
// 通道数
int audioChannels = 2;
// 比特率
int bitRate = 128 * 1024;
// 采样率
int sampleRate = 44100;
String audioPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
// 进行编码初始化
int ret = encoder.init(audioPath + "vocal.pcm", audioChannels, bitRate, sampleRate, audioPath + "vocal.mp3");
if (ret >= 0) {
// 编码
encoder.encode();
// 销毁
encoder.destroy();
Log.i(TAG, "Encode Mp3 Success");
} else {
Log.i(TAG, "Encoder Initialized Failed...");
}
}
});
}
}
JNI 类:
package com.phuket.tour.studio;
public class Mp3Encoder {
public native int init(String pcmPath, int audioChannels, int bitRate, int sampleRate, String mp3Path);
public native void encode();
public native void destroy();
}
C++ 实现:
#include "mp3_encoder.h"
Mp3Encoder::Mp3Encoder() {
}
Mp3Encoder::~Mp3Encoder() {
}
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 / 1000);
lame_init_params(lameClient);
ret = 0;
}
}
return ret;
}
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];
uint8_t* mp3_buffer = new uint8_t[bufferSize];
int 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];
}
}
int wroteSize = lame_encode_buffer(lameClient, (short int *) leftBuffer, (short int *) rightBuffer, 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);
lame_close(lameClient);
}
}