音频(六)-安卓ndk将pcm转换为mp3

安卓不支持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: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

扫描二维码关注公众号,回复: 2229013 查看本文章
偏移地址 位数(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文件格式全解

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文件了

.
├── 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
APP_PLATFORM := android-18
APP_ABI := all
APP_BUILD_SCRIPT := Android.mk
APP_CFLAGS += -DSTDC_HEADERS
复制代码
  1. 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文档:

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

  2. 错误信息 默认情况下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);
 }
复制代码
  1. 初始化编码器 初始化编码器并设置默认值:
#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. 设置参数
zret_code = lame_init_params(gfp);
复制代码

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

  1. 编码 输出源时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.假如编码不成功就会返回一个负数。

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

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

  1. 写入tag

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

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

最后附上demo的github地址: github.com/rangaofei/A…

参考:

  1. 音视频开发进阶指南
  2. 维基百科-mp3
  3. developer.samsung.com/technical-d…

最后的最后,说一下最近自己的一点事。我普通211非计算机专业,11年毕业,毕业之后一直在央企工作,后来因为兴趣原因转行做安卓开发,已过而立之年,目前在江苏一个小城市做安卓开发,没有大公司工作背景,想去上海试一试机会,经历了无数次失败了,包括阿里内推,中通等,这些经历都使我认清了自己现在的劣势。这也是一个非常沮丧的过程,因为多年的努力被一个人轻轻松松否定确实很丧气。不过我有我自己的优势,我的技能不一定会匹配所有人的技能要求,运气不在的时候需要练好内功,提升自己,现在的不认可不等于将来的不认可,留给我的时间不多了,但我的路还很长,希望和我一样的小伙伴也能像我一样,尽快调整过来。

猜你喜欢

转载自juejin.im/post/5b4f254bf265da0f91560633