安卓不支持 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 | 一些列的帧,个数由文件的大小和帧长度决定 < br ztid="118" ow="0" oh="0"> 每个 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:70x200000+byte1:7 0x4000+byte2:7*0x80+byte3:7 | |
扩展标签头 | 扩展标签头大小 | 4 | size=byte00x200000+byte1 0x4000+byte2*0x80+byte3 |
扩展标志位 | 2 | xx | |
补空大小 | 4 | 可以在所有的标签帧后边添加补空的数据,也可以预留空间存放额外的帧,是的整个标签大小比标签头的大小更大,一般不用 | |
标签帧 | 帧标识 | 4 | 固定四个字符,每个标签帧都有一个 10 个自己的固定的头和至少一个字节的不固定长度的内容组成,也就是下边的帧大小和帧标志必须有,而帧数据的内容不得小于 1. |
帧大小 | 4 | 出去帧头的所有长度,size=byte00x200000+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 | 强调位 |
这个地方的内容较多, 此处我不一一列举, 附上一个写的比较详细的博客:
MP3 文件格式全解 https://blog.csdn.net/u013904227/article/details/52184038
LAME 的使用
Lame 是一个专门用编码 MP3 的开源库, 它可以提供多种不同比特率的支持, 并且提供了各个平台下的编译源码包, 可以直接在 SourceForge https://sourceforge.net/projects/lame/files/lame/ 下载.
安卓平台编译
官方并没有提供专门的编译文件, 不过我们可以自己采用多种方式编译: 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 地址: https://github.com/rangaofei/AudioApplication
参考:
音视频开发进阶指南
维基百科 - mp3
http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090
来源: https://juejin.im/post/5b4f254bf265da0f91560633