Fork me on GitHub
0%

Libmad详解

简单来讲,libmad就是一个MP3文件的解码库。如果想要深入理解其中的实现需要对MP3文件格式有详细的了解,关于MP3文件格式的内容在这里我不赘述,之前的文章当中有过详细的讲解 click here

What

Libmad 详解:https://www.underbit.com/products/mad/

libmad 是一个高质量的 MPEG 音频解码器。目前支持 MPEG-1 和 MPEG-2 对较低采样频率的扩展,以及所谓的 MPEG 2.5 格式,三个音频层都在代码上做了实现。优势:

  • 24 位 PCM 输出
  • 100% 定点(整数)计算
  • 基于 ISO/IEC 标准的全新实施
  • 根据 GNU 通用公共许可证 (GPL) 的条款分发

How

代码下载

https://sourceforge.net/projects/mad/files/

https://www.linuxfromscratch.org/blfs/view/svn/multimedia/libmad.html

代码结构

< 基于 libmad-0.15.1b 版本>

源文件 bit.c decoder.c fixed.c frame.c huffman.c layer12.c layer3.c stream.c synth.c timer.c version.c minimad.c(demo)
头文件 bit.h decoder.h fixed.h frame.h huffman.h layer12.h layer3.h stream.h synth.h timer.h version.h global.h mad.h(API)
dat文件 sf_table.dat imdct_s.dat qc_table.dat D.dat rq_table.dat
其他文件 Makefile config …… 大部分内容不参与编译

具体使用方法可以参考minimad.c文件,这里面对api的使用有进一步的说明

同时也可以参考我基于Linux libmad写出来的一个音频播放器,源码链接 click here

编译方法

< 仅介绍linux环境下编译方法 >

  1. 手写Makefile / CMakeLists.txt
  2. 系统提供的config
    • 执行 sed -i ‘/-fforce-mem/d’ configure , 这条命令是为了适配高版本的gcc,因为高版本的gcc已经将-fforce-mem去除了
    • 执行 ./configure , 文件夹下会生成Makefile
    • 执行 sudo make; sudo install;
    • 至此静态库和动态库已经生成,目录在/usr/local/lib

核心API

  1. mad_decoder_init( )

    1
    2
    3
    4
    5
    6
    7
    8
    //mad.h
    void mad_decoder_init(struct mad_decoder *, void *,
    /* input func */ enum mad_flow (*)(void *, struct mad_stream *),
    /* header func */ enum mad_flow (*)(void *, struct mad_header const *),
    /* filter func */ enum mad_flow (*)(void *, struct mad_stream const *, struct mad_frame *),
    /* output func */ enum mad_flow (*)(void *, struct mad_header const *, struct mad_pcm *),
    /* error func */ enum mad_flow (*)(void *, struct mad_stream *, struct mad_frame *),
    /* message func*/ enum mad_flow (*)(void *, void *, unsigned int *));

    mad_decoder_init( )是libmad中最终重要的函数之一,其作用是将line3~line8的六个回调函数注册到mad_decoder实例出来的decoder中。

    • 必须自定义的参数:参数1:用户自己实例的一个解码器结构体;参数2:用户自定义的结构体指针,这个指针将用于整个解码的过程在回调函数之间进行数据的传输;参数3:输入的回调函数,该回调用于用户自定义将数据输入编码器的逻辑;参数6:输出的回调函数,同input callback func。自定义输出的分辨率吧:24bit / 16bit,可以存成文件亦可以直接通过pcm接口播放。Output回调函数在madlib每解码完成一个帧后被调用,直到全部解码完成或出错。参数8(异步工作模式下必选):输出信息。

    • 选择性定义参数:其他参数属于自定义参数比如进行头解析、过滤筛选的回调函数等等,如果自己没有需求置0即可。

  2. mad_decoder_run( )

    1
    2
    //mad.h
    int mad_decoder_run(struct mad_decoder *, enum mad_decoder_mode);

    该函数是解码的主流程函数,即解码器的入口函数,如果要追代码可以从此处开始进行剖析。

    • 参数1:用户自定义实例化并初始化的解码器decoder;
    • 参数2:选择解码模式(SYNC / ASYNC)深入代码可以发现这个选择使得解码器进入不同的函数进行工作。

    所谓同步方式是指解码函数在解码完一帧后才返回并带回出错信息,异步方式是指解码函数在调用后立即返回,通过消息传递解码状态信息。(故异步方式必须定义message回调函数)

  3. mad_decoder_finish( )

    1
    2
    //mad.h
    int mad_decoder_finish(struct mad_decoder *);

    解码结束,用于清理工作,释放与流相关的任何动态内存。

  4. mad_stream_buffer( )

    1
    2
    //mad.h
    void mad_stream_buffer(struct mad_stream *, unsigned char const *, unsigned long);

    该函数是input回调函数中最重要的函数,其作用是按照参数2、参数3即MP3文件在内存映像的起始地址和本次要传递的文件长度与mad_stream进行关联,形成libmad可识别的stream流

    需要注意,这里传递多少数据完全是自定义的。如果一次性送入则整个解码过程调用一次input;如果一次性传递若干,output根据传递的数据进行解码,如果没有数据可解继续调用input。所以自己定义好输入逻辑即可。

重点数据结构

  1. mad_stream

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //mad.h && stream.h
    struct mad_stream {
    unsigned char const *buffer; /* input bitstream buffer */ unsigned char const *bufend; /* end of buffer */
    unsigned long skiplen; /* bytes to skip before next frame */
    int sync; /* stream sync found */
    unsigned long freerate; /* free bitrate (fixed) */
    unsigned char const *this_frame; /* start of current frame */
    unsigned char const *next_frame; /* start of next frame */
    struct mad_bitptr ptr; /* current processing bit pointer */
    struct mad_bitptr anc_ptr; /* ancillary bits pointer */
    unsigned int anc_bitlen; /* number of ancillary bits */
    unsigned char (*main_data)[MAD_BUFFER_MDLEN]; /* Layer III main_data() */
    unsigned int md_len; /* bytes in main_data */
    int options; /* decoding options (see below) */
    enum mad_error error; /* error code (see above) */
    };

    该结构体记录了文件的地址、当前所处理的位置和解码前的Bitstream数据

    mad_stream.bufend – mad_stream.next_frame就是剩余的未被解码的 MPEG 帧的数据的字节数量(假设此帧在缓冲区中不完整)

  2. mad_header

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct mad_header {
    enum mad_layer layer; /* audio layer (1, 2, or 3) */
    enum mad_mode mode; /* channel mode (see above) */
    int mode_extension; /* additional mode info */ enum mad_emphasis emphasis; /* de-emphasis to use (see above) */
    unsigned long bitrate; /* stream bitrate (bps) */
    unsigned int samplerate; /* sampling frequency (Hz) */
    unsigned short crc_check; /* frame CRC accumulator */
    unsigned short crc_target; /* final target CRC checksum */
    int flags; /* flags (see below) */
    int private_bits; /* private bits (see below) */
    mad_timer_t duration; /* audio playing time of frame */
    };

    通过注释很容易看出,该结构体记录了MPEG 帧的基本信息,比如MPEG 层数、声道模式、流比特率、采样率、比特率以及某些校验位等等。

    Tips:(bitrate % 32) 如果是整数说明该文件格式是CBR(constant bitrate,恒定比特率),否则是VBR(variable bitrate,可变比特率),这对文件播放时长是有影响的。

  3. mad_pcm

    1
    2
    3
    4
    5
    struct mad_pcm {
    unsigned int samplerate; /* sampling frequency (Hz) */ unsigned short channels; /* number of channels */
    unsigned short length; /* number of samples per channel */
    mad_fixed_t samples[2][1152]; /* PCM output samples [ch][sample] */
    };

    madlib解码器是以帧为单位进行解码的,mad_pcm每次最多解码出(1152 * channels)个PCM数据,每个采样点用int(32bit)表征但是只用了其中的24bit,至此可以直接输出数据保存文件或者直接调用音频播放的API进行播放。但目前大多数codec支持的是16bit量化分辨率,所以在输出时将数据饱和到16bit进行输出。

  4. mad_flow

    1
    2
    3
    4
    5
    enum mad_flow {
    MAD_FLOW_CONTINUE = 0x0000, /* continue normally */ MAD_FLOW_STOP = 0x0010, /* stop decoding normally */
    MAD_FLOW_BREAK = 0x0011, /* stop decoding and signal an error */
    MAD_FLOW_IGNORE = 0x0020 /* ignore the current frame */
    };

    所有回调函数的返回值均为该枚举类型,在解码的主流程中会通过不断判断解码器所调用的回调函数的状态来确认下一步的动作

相关调试经验

  1. 配置参数未选择

    Q:编译正常通过,流程正确,但是输出的声音能听出是所需要的音频但是整体数据并不正确?

    A:在mad.h中有若干的函数是根据平台的不同有不同的实现,由于编译过程中没有注意警告直接将其注释导致没有选择正确函数实现,导致最终的数据都是错误的。编译时添加相应的平台的参数即可,如果手动写cmake,参考给出的Makefile做参数选择!

  2. 线程栈空间大小分配不足

    Q:在PC上测试正常播放,但是移植到小系统中经常出现stack overflow?

    A:由于MP3一帧数据的采样点数为1152,使用 int 类型进行存储,所以核心函数 “ Ⅲ_decode” 至少需要6k的栈空间,如果是单独一个线程的话再加上其他的局部变量、函数跳转等等可能就超过8k。

    ​ 当时使用c++ 的thread进行的线程创建并不能配置栈空间大小,经过两天左右的debug查到是核心函数栈空间的溢出,最终使用pthread加大栈空间的大小运行即可。

  3. 动态解码的实现

    Q:刚开始百度libmad的库,很多博客说只能调用一次input回调函数即一次加载所有的源数据,这对于实时控制造成不便?

    A: 深入源码可以看出输入的的数据解码完成只要不返回STOP是可以继续填充数据的,这样临时的buffer就小一些并且可以试试控制完成播放器的功能。(需要注意如果一次输入的数据是几帧多一点,而多出来的需要放保存下次再此进行解码)

  4. 获得音频参数

    Q:能够很快的获得音频信息?

    A: 对于wav头很容易的可以找到音频的参数,但是MP3文件的信息保存在帧头中,所以可以进行一步预解码。即读入若干(512即可)byte数据解一帧的头就可以获得全部信息,包括:采样率、比特率、声道、音频时长等等。

  5. 获得播放进度 / 时长出现异常

    Q:使用公式:当前文件位置 / 文件大小 × 文件总时长,获得当前播放时间出现异常?

    A: 对于duration 和 fpos是用int的数据类型进行保存的,但是对于wav文件一般比较大在计算的时候超出了数据类型所能存储的最大长度。