安卓不支持mp3格式的录制,但是可以解码mp3格式文件,lame库是一个通用的编码mp3库,用c语言实现。这篇文章自制了lame库的cmake脚本,实现了在安卓上将PCM数据转换为MP3。
关于mp3
Mp3曾经以它优秀的压缩率和较低的失真一横行音乐行业,在那个存储介质昂贵的时代大放光彩,随着技术的发展,存储已经不是瓶颈了,现在的音乐爱好者也开始追求音质,出现了高保真音乐,复古黑胶唱片等。但是作为一个音频开发者,基本的mp3知识还是需要掌握的。
MP3是一种有损压缩格式,对它进行解码不能还原PCM。一般CD品质的音频文件是1411.2kbps(16bitpersample、*44100samplerate、*2channels),这个需要较高的带宽才能保证传输的稳定性,但是经过MP3编码后比特率基本结余128kbps~320kbps,压缩率为12:1-10:1,这样回放的质量低了,但是文件大小得到了控制。 本篇文章讨论的并非是音乐播放器,而是一种编码格式,并且以lame编码器来讲解文章格式,事实上lame编码器被认为是最好的MP3编码器。
MP3文件格式
MP3一般包含3个主要部分ID3v2、frame、ID3v1。其形式如下:
帧 | 说明 |
---|---|
ID3v2 | 包含了作者,作曲,专辑信息等,长度不固定,扩展了ID3v1的信息量 |
Frame |
一些列的帧,个数由文件的大小和帧长度决定
每个frame包含帧头和实体数据两部分,帧头记录了mp3的位宽,采样率,版本信息等,每个帧之间相互独立,但是每个帧的长度不固定,由bitrate决定 |
ID3v1 | 包含了作者,作曲,专辑等信息,长度固定是123Byte |
下面分别说一下各个格式的信息
ID3v2结构图
ID3V2共有4个版本,但实际上用的最多的是ID3V2.3
数据块 | 数据描述 | 字节数(Byte) | 内容 |
---|---|---|---|
标签头 | ID3V2标识 | 3 | 固定字符"ID3",表示是ID3v2标签 |
ID3v2的子版本号 | 2 | 0x0300表示是主版本号为3,副版本号为0,也就是ID3v2.3 | |
ID3v2标志位 | 1 | abc00000,a-非同步编码,b-扩展标签头,c-测试指示位,当这三位置是1时表示有效,一般情况都是0 | |
ID3v2大小 | 4 | 每个字节只有后七位有效,size=byte0:7 0x200000+byte1:7 0x4000+byte2:7*0x80+byte3:7 | |
扩展标签头 | 扩展标签头大小 | 4 | size=byte0 0x200000+byte1 0x4000+byte2*0x80+byte3 |
扩展标志位 | 2 | xx | |
补空大小 | 4 | 可以在所有的标签帧后边添加补空的数据,也可以预留空间存放额外的帧,是的整个标签大小比标签头的大小更大,一般不用 | |
标签帧 | 帧标识 | 4 | 固定四个字符,每个标签帧都有一个10个自己的固定的头和至少一个字节的不固定长度的内容组成,也就是下边的帧大小和帧标志必须有,而帧数据的内容不得小于1. |
帧大小 | 4 | 出去帧头的所有长度,size=byte0 0x200000+byte1 0x4000+byte2*0x80+byte3 | |
标志 | 2 | 标志位,只定义6bit,abc00000 ijk00000一般为0 | |
帧数据 | size | 存放的数据 | |
补空 | 补空大小 |
介绍一下常用的帧标识:
标识内容 | 描述 |
---|---|
TIT2 | 标题 |
TPE1 | 作者 |
TALB | 专辑 |
TRCK | 音轨N/M格式 |
TYER | 年代 |
TCON | 类型 |
COMM | 备注 |
有效数据帧
有效数据帧的编码在lame共有三种,CBR、VBR和ABR。
- CBR:帧长度固定,数据平均分配在各个帧,这种方式有利于计算播放时长,但是文件稍微大
- VBR:帧长度不固定,要获取真个播放时长必须知道帧的总数,文件较小
- ABR:帧长度不固定,介于CBR和VBR之间
有效数据帧头为四个字节: 此处是1-32
偏移地址 | 位数(bits) | 内容 |
---|---|---|
1 | 12 | 帧同步标识,一般标识数据帧的开始,全部为1 |
13 | 1 | MPEG音频版本号 |
14 | 2 | Layer版本 |
16 | 1 | 保护位 |
17 | 4 | 比特率 |
21 | 2 | 采样率 |
23 | 1 | 补空位大小 |
24 | 1 | 不知道啥 |
25 | 2 | 模式 |
27 | 2 | 模式拓展位 |
29 | 1 | 版权位 |
30 | 1 | 原始位 |
31 | 2 | 强调位 |
这个地方的内容较多,此处我不一一列举,附上一个写的比较详细的博客:
LAME的使用
Lame是一个专门用编码MP3的开源库,它可以提供多种不同比特率的支持,并且提供了各个平台下的编译源码包,可以直接在 SourceForge 下载。
安卓平台编译
官方并没有提供专门的编译文件,不过我们可以自己采用多种方式编译:ndk-build和cmake,两种方式都非常简单。首先要下载源码,然后解压到一个文件夹内。
ndk-build方式构建lame
我们需要编写两个文件,Android.mk和Application.mk。一个参考网址可以少走一些坑(http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090)[http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090]
主要有四点:
-
将libmp3lame文件夹下的所有内容拷贝到一个指定的地方,然后再将lame.h文件考进来
-
找到
util.h
文件,将其中的extern ieee754_float32_t fast_log2(ieee754_float32_t x);
替换为extern float fast_log2(float x);
-
找到
set_get.h
文件。替换#include <lame.h>
为#include “lame.h”
-
假如出现bcopy unrefrence的错误,在Application.mk文件中添加一个flag,最后添加一行,内容为
APP_CFLAGS += -DSTDC_HEADERS
这样就可以直接编译生成so文件了。
假如配置好了ndk的全局变量,只需要运行
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk
就生成了对应的so文件了
.
├── arm64-v8a
│ └── libmp3lame.so
├── armeabi
│ └── libmp3lame.so
├── armeabi-v7a
│ └── libmp3lame.so
├── mips
│ └── libmp3lame.so
├── mips64
│ └── libmp3lame.so
├── x86
│ └── libmp3lame.so
└── x86_64
└── libmp3lame.so
复制代码
下边是两个文件
- Application.mk
APP_PLATFORM := android-18
APP_ABI := all
APP_BUILD_SCRIPT := Android.mk
APP_CFLAGS += -DSTDC_HEADERS
复制代码
- Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libmp3lame
LOCAL_SRC_FILES := \
./libmp3lame/bitstream.c \
./libmp3lame/encoder.c \
./libmp3lame/fft.c \
./libmp3lame/gain_analysis.c \
./libmp3lame/id3tag.c \
./libmp3lame/lame.c \
./libmp3lame/mpglib_interface.c \
./libmp3lame/newmdct.c \
./libmp3lame/presets.c \
./libmp3lame/psymodel.c \
./libmp3lame/quantize.c \
./libmp3lame/quantize_pvt.c \
./libmp3lame/reservoir.c \
./libmp3lame/set_get.c \
./libmp3lame/tables.c \
./libmp3lame/takehiro.c \
./libmp3lame/util.c \
./libmp3lame/vbrquantize.c \
./libmp3lame/VbrTag.c \
./libmp3lame/version.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
复制代码
cmake方式构建lame
cmake构建更加简单,只需要将刚才的libmp3lame文件夹和lame.h文件添加到src/main/cpp文件夹下,此处我和源文件夹保持一致,起名为libmp3lame,然后编写一个CMakeLists.txt文件如下:
add_definitions("-DSTDC_HEADERS")
add_library(mp3lame bitstream.c
encoder.c
fft.c
gain_analysis.c
id3tag.c
lame.c
mpglib_interface.c
newmdct.c
presets.c
psymodel.c
quantize.c
quantize_pvt.c
reservoir.c
set_get.c
tables.c
takehiro.c
util.c
vbrquantize.c
VbrTag.c
version.c)
复制代码
然后在主文件夹下的CMakeList.txt中添加生成该库的代码:
set(LIB_MP3 Mp3Codec)
include_directories(
src/main/cpp/include #将lame.h文件复制到这个文件夹下,更加清晰一些,可以作为一个接口文件
)
add_subdirectory(src/main/cpp/libmp3lame)
复制代码
假如要使用这个库的话只需要假如target_link命令来连接即可。
lame转码pcm格式为mp3
我做了一个非常简单的实例程序,首先是通过AudioRecorder录制PCM数据,然后封装为wav格式,这个格式在安卓手机上是可以直接播放的。然后在将wav文件通过jni层的lame调用转码为MP3。
首先了解一下lame的api文档:
-
获取版本信息(可选的) const char * get_lame_version(void);
-
错误信息 默认情况下lame会输出错误信息到标准错误流中,但是我们需要获取错误信息的话,可以调用如下方法来设置:
lame_set_errorf(gfp,error_handler_function);
lame_set_debugf(gfp,error_handler_function);
lame_set_msgf(gfp,error_handler_function);
复制代码
通过这种方式,就可以将调试或者错误信息发送到我们自己的handler中。这个handler函数一般如下:
void my_debugf(const char *format, va_list ap)
{
(void) vfprintf(stdout, format, ap);
}
复制代码
- 初始化编码器 初始化编码器并设置默认值:
#include "lame.h"
lame_global_flags *gfp;
gfp = lame_init();
/*The default (if you set nothing) is a J-Stereo, 44.1khz
128kbps CBR mp3 file at quality 5. */
lame_set_num_channels(gfp,2);
lame_set_in_samplerate(gfp,44100);
lame_set_brate(gfp,128);
lame_set_mode(gfp,1);
lame_set_quality(gfp,2); /* 2=high 5 = medium 7=low */
复制代码
在lame.h文件中定义了lame_glob_flags的一种简写形式:
typedef lame_global_flags *lame_t;
我们就可以使用lame_t。
- 设置参数
zret_code = lame_init_params(gfp);
复制代码
这个需要检查错误,因为可能会有错误的参数。
- 编码 输出源时PCM数据,输出时mp3的帧,我们需要先设置一个缓冲区,来存放编码后的mp3数据,这个数据的大小可以根据采样率和采样数来计算。一个公式如下:
mp3buffer_size (in bytes) = 1.25*num_samples + 7200.
复制代码
接下来是将采样数据生成为mp3数据,存入上边分配的缓冲区:
int lame_encode_buffer(lame_global_flags *gfp,
short int leftpcm[], short int rightpcm[],
int num_samples,char *mp3buffer,int mp3buffer_size);
复制代码
编码成功的话会返回编码的数量,有可能为0.假如编码不成功就会返回一个负数。
- 编码结束 编码器可能会持有最后几个数据,需要调用这个函数:
int lame_encode_flush(lame_global_flags *,char *mp3buffer, int mp3buffer_size);
复制代码
函数的返回值是最后的数据,大多数情况下是0。
- 写入tag
这个地方主要是写入上边提到的一些ID3等帧信息
void lame_mp3_tags_fid(lame_global_flags *,FILE* fid);
复制代码
- 释放资源 最后我们需要调用
void lame_close(lame_global_flags *);
复制代码
最后附上demo的github地址: github.com/rangaofei/A…
参考:
- 音视频开发进阶指南
- 维基百科-mp3
- developer.samsung.com/technical-d…
最后的最后,说一下最近自己的一点事。我普通211非计算机专业,11年毕业,毕业之后一直在央企工作,后来因为兴趣原因转行做安卓开发,已过而立之年,目前在江苏一个小城市做安卓开发,没有大公司工作背景,想去上海试一试机会,经历了无数次失败了,包括阿里内推,中通等,这些经历都使我认清了自己现在的劣势。这也是一个非常沮丧的过程,因为多年的努力被一个人轻轻松松否定确实很丧气。不过我有我自己的优势,我的技能不一定会匹配所有人的技能要求,运气不在的时候需要练好内功,提升自己,现在的不认可不等于将来的不认可,留给我的时间不多了,但我的路还很长,希望和我一样的小伙伴也能像我一样,尽快调整过来。