关于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:70x200000+byte1:70x4000+byte2:7*0x80+byte3:7
扩展标签头 扩展标签头大小 4 size=byte00x200000+byte10x4000+byte2*0x80+byte3
扩展标志位 2 xx
补空大小 4 可以在所有的标签帧后边添加补空的数据,也可以预留空间存放额外的帧,是的整个标签大小比标签头的大小更大,一般不用
标签帧 帧标识 4 固定四个字符,每个标签帧都有一个10个自己的固定的头和至少一个字节的不固定长度的内容组成,也就是下边的帧大小和帧标志必须有,而帧数据的内容不得小于1.
帧大小 4 出去帧头的所有长度,size=byte00x200000+byte10x4000+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下载。

安卓平台编译

官方并没有提供专门的编译文件,不过我们可以自己采用多种方式编译: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]

主要有四点:

  1. 将libmp3lame文件夹下的所有内容拷贝到一个指定的地方,然后再讲lame.h文件考进来

  2. 找到util.h文件,将其中的extern ieee754_float32_t fast_log2(ieee754_float32_t x);替换为 extern float fast_log2(float x);

  3. 找到set_get.h文件。替换 #include <lame.h>#include “lame.h”

  4. 假如出现bcopy unrefrence的错误,在Application.mk文件中添加一个flag,最后添加一行,内容为APP_CFLAGS += -DSTDC_HEADERS

这样就可以直接编译生成so文件了。 假如配置好了ndk的全局变量,只需要运行ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk就生成了对应的so文件了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.
├── arm64-v8a
│   └── libmp3lame.so
├── armeabi
│   └── libmp3lame.so
├── armeabi-v7a
│   └── libmp3lame.so
├── mips
│   └── libmp3lame.so
├── mips64
│   └── libmp3lame.so
├── x86
│   └── libmp3lame.so
└── x86_64
    └── libmp3lame.so

下边是两个文件

  1. Application.mk
1
2
3
4
APP_PLATFORM := android-18
APP_ABI := all
APP_BUILD_SCRIPT := Android.mk
APP_CFLAGS += -DSTDC_HEADERS
  1. Android.mk
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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中添加生成该库的代码:

1
2
3
4
5
6
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文档:

  1. 获取版本信息(可选的) const char * get_lame_version(void);

  2. 错误信息 默认情况下lame会输出错误信息到标准错误流中,但是我们需要获取错误信息的话,可以调用如下方法来设置:

1
2
3
lame_set_errorf(gfp,error_handler_function);
lame_set_debugf(gfp,error_handler_function);
lame_set_msgf(gfp,error_handler_function);

通过这种方式,就可以将调试或者错误信息发送到我们自己的handler中。这个handler函数一般如下:

1
2
3
4
 void my_debugf(const char *format, va_list ap)
 {
     (void) vfprintf(stdout, format, ap);
 }
  1. 初始化编码器 初始化编码器并设置默认值:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#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。

  1. 设置参数
1
zret_code = lame_init_params(gfp);

这个需要检查错误,因为可能会有错误的参数。

  1. 编码 输出源时PCM数据,输出时mp3的帧,我们需要先设置一个缓冲区,来存放编码后的mp3数据,这个数据的大小可以根据采样率和采样数来计算。一个公式如下:
1
mp3buffer_size (in bytes) = 1.25*num_samples + 7200.

接下来是将采样数据生成为mp3数据,存入上边分配的缓冲区:

1
2
3
int lame_encode_buffer(lame_global_flags *gfp,
     short int leftpcm[], short int rightpcm[],
     int num_samples,char *mp3buffer,int  mp3buffer_size);

编码成功的话会返回编码的数量,有可能为0.假如编码不成功就会返回一个负数。

  1. 编码结束 编码器可能会持有最后几个数据,需要调用这个函数:
1
int lame_encode_flush(lame_global_flags *,char *mp3buffer, int mp3buffer_size);

函数的返回值是最后的数据,大多数情况下是0。

  1. 写入tag

这个地方主要是写入上边提到的一些ID3等帧信息

1
void lame_mp3_tags_fid(lame_global_flags *,FILE* fid);
  1. 释放资源 最后我们需要调用
1
void lame_close(lame_global_flags *);

最后附上demo的github地址: https://github.com/rangaofei/AudioApplication

写在最后

现代技术发展,MP3终究会被时代抛弃,流媒体格式一般不会采用MP3传输,这篇文章的目的是介绍一些基本的音频知识,而且AudioRecorder本身有一定的局限性,不如OpenSLES灵活,所以算是抛砖引玉,后边将会介绍如何直接将PCM转码为mp3. 参考:

  1. 音视频开发进阶指南
  2. 维基百科-mp3
  3. (http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090)[http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090]