MadLib是以帧为单位解码mp3文件的,所谓同步方式是指解码函数在解码完一帧后才返回并带回出错信息,异步方式是指解码函数在调用后立即返回,通过消息传递解码状态信息。
1、mad_decoder_init()【decoder.h】
Minimad.c中给出了一个函数调用过程的实例。首先定义一个mad_decoder变量(解码器对象),这时不需要对解码器对象进行任何初始化。调用mad_decoder_init()函数对刚才创建的decoder对象进行初始化,mad_decoder_init()函数定义于decoder.h头文件中。原型如下:
void mad_decoder_init(struct mad_decoder *, //解码器对象指针
void *,//自定义消息指针,这个值被复制进mad_decoder的cb_data成员
enum mad_flow (*)(void *, struct mad_stream *),//input回调函数
enum mad_flow (*)(void *, struct mad_header const *),//header回调函数
enum mad_flow (*)(void *,struct mad_stream const *,struct mad_frame *),//filter回调函数
enum mad_flow (*)(void *,struct mad_header const *,struct mad_pcm *),//output回调函数
enum mad_flow (*)(void *,struct mad_stream *,struct mad_frame *),//error回调函数
enum mad_flow (*)(void *, void *, unsigned int *)//message回调函数
);
其中的input回到函数和output回调函数是必须定义并传递给mad_decoder_init()的,message回调函数在异步工作模式下必选,其他回调函数都可选。
2、mad_stream_buffer()【stream.h】
Input回调函数具有两个参数,第一个参数是个void指针,指向自定义消息结构,在input回调函数内部对消息进行解释并调用mad_stream_buffer()函数对输入流进行初始化,具体参考minimad.c中input函数的写法。mad_stream_buffer()函数原型如下:
void mad_stream_buffer(struct mad_stream *,//输入流指针
unsigned char const *, //文件起始地址
unsigned long//文件长度
);
第一个参数指向一个mad_stream变量,mad_stream结构定义在stream.h头文件里,用于记录文件的地址和当前处理的位置。第二、三个参数分别是mp3文件在内存中映像的起始地址和文件长度。mad_stream_buffer()函数将mp3文件与mad_stream结构进行关联。Input回调函数在解码器启动后会被调用一次,在整个解码过程中都不再被调用。
Output回调函数的原型是:
enum mad_flow (*output_func)(void *,struct mad_header const *,struct mad_pcm *)
Output回调函数将解码得到的原始PCM块作为参数传入,在这里可以进行一些解码后的操作如加入均衡器等。MadLib使用的PCM结构mad_pcm在头文件synth.h中定义:
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个PCM采样数据(左右声道共2*1152个),每个采样使用32bit存放,只使用了其中的24bit,但目前大多数的音频设备支持的是16bit量化分辨率,所以在交给声卡输出前还要自己进行转换,将24bit分辨率降低为16bit。length成员指定了当前PCM数据块的实际大小。
另外两个参数分别将自定义消息结构和已解码帧的帧头传入,以处理用户消息和获取帧信息。Output回调函数在madlib每解码完成一个帧后被调用,直到全部解码完成或出错。
Input和output回调函数的返回值是一个mad_folw枚举类型,在decoder.h头文件中定义如下:
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 */
};
3、mad_decoder_run()【decoder.h】
获取了待解吗的mp3文件后解码器开始运行。mad_decoder_run()函数可以看作是mad解码器的运行入口。其原型如下:
int mad_decoder_run(struct mad_decoder * decoder, enum mad_decoder_mode mode)
第一、二个参数将初始化好的decoder变量和解码器工作模式(同步或异步)进行关联。后面我们会看到真正完成解码工作的并不是mad_decoder_run,而是根据工作模式的不同在它的内部调用了另外不同的函数进行解码。函数返回后解码就完成了。
4、mad_decoder_finish()【decoder.h】
最后调用mad_decoder_finish()进行最后的清理工作。
根据使用平台的不同,你可以使用各种方法打开一个MP3文件,minimad.c中给出的是在UNIX下打开示例文件的方法,改写成Windows的也不难:
CFile file;
if(!file.Open("E://A.mp3",CFile::modeRead|CFile::shareDenyRead,NULL))
{
cout<<"can not open file!"<<endl;
return -1;
}
DWORD file_size=file.GetLength();
DWORD file_readerd=0;
if(file_size==0)
{
cout<<"File Error!"<<endl;
return -1;
}
unsigned char * file_buffer=(unsigned char *)malloc(file_size);
do
{
file_readerd=file.Read(file_buffer,file_size);
} while (file_readerd);
free(file_buffer);
file.Close();
整个mp3文件就被映射到了file_buffer里。这个file_buffer就可以传给解码器解码了。但是mad_decoder_init()和mad_decoder_run()的参数中并没有传入这个指针的地方。Minimad,c里使用了一个最简单的“自定义消息结构”(private message structure),通过mad_decoder_init()将buffer的start和length传给mad_decoder结构,再用mad_decoder_run()对mad_decoder进行解码:
static
int decode(unsigned char const *start, unsigned long length)
{
struct buffer buffer;
struct mad_decoder decoder;
int result;
/* initialize our private message structure */
buffer.start = start;
buffer.length = length;
/* configure input, output, and error functions */
mad_decoder_init(&decoder, &buffer,
input, 0 /* header */, 0 /* filter */, output,
error, 0 /* message */);
/* start decoding */
result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
/* release the decoder */
mad_decoder_finish(&decoder);
return result;
}
综合前面的分析,得出数据大体的走向:首先通过input回调函数调用mad_stream_buffer()函数将mp3文件绑定到一个mad_stream结构中,再由解码器从stream中分离出frame,最后解码单个frame得到pcm。
一个MP3帧由帧头和帧数据构成。Madlib分别填充mad_herder和mad_frame这两个结构,然后进行解码。mad_herder里包含mp3数据的描述信息,这个结构在frame.h头文件中定义:
layer成员的类型是enum mad_layer,这个枚举类型有3个取值(1,2,3),分别对应MPEG音频的1、2、3层;mode成员描述音频的声道数和立体声类型,取值为MAD_MODE_SINGLE_CHANNEL(单声道)、MAD_MODE_DUAL_CHANNEL(双声道)、MAD_MODE_JOINT_STEREO(联合立体声)、MAD_MODE_STEREO(普通立体声);接下来的比特率、采样率、CRC校验、播放时间等信息直接来自mp3帧。
mad_frame包含一个帧头(mad_herder)和一帧中的采样数据。该结构同样定义于frame.h头文件中:
其中的option字段来自mad_stream结构,sbsample[2][36][32]中保存的就是从mp3文件中取得的采样数据:2个声道,每声道36个采样(可播放26ms的音频,每秒帧速率大约为38fps);overlap指针成我员不了解它的用途,希望有高手不吝赐教!
Madlib每次解码循环结束时解码完成一个帧,将1152个PCM采样数据保存在数组里传递给output回调函数作输出前的处理。压缩包里的参考示例minimad.c中的output回调函数只是简单地将PCM数据在屏幕上打印显示:
在2.1-数据流向中我们总结出:一个mp3文件首先被嵌入到一个“输入流结构”中,在解码之后的PCM流存入“输出结构”中,这两个结构对应于mad_stream和mad_synth两个结构,前者用于mp3文件的输入和解码流程的控制,后者用于存放输出数据。这两个结构分别定义在stream.h和synth.h头文件中。它们在整个解码过程中起到最重要的作用:
buffer成员指向mp3文件在内存中映像的起始地址,这个地址可以去除头部的ID3V2标签,也可以不去除,由madlib自动跳过标签部分;bufferend成员指向文件映射区域的最后一个字节;skiplen成员指定从当前位置跳过的数据长度(可以不是帧长度的倍数,madlib会去寻找下一个有效帧的12bit同步字),这个成员可以用于在拖动进度条时指定播放位置;sync成员用于检查当前帧的同步状态,如果当前帧是有效帧并且正确找到了12bit同步字,该值被置1,否则置0;freerate,我不知道它的作用,从注释上看是“空闲比特率”,谁知道是什么意思?this_frame通常指向当前帧的帧头位置,next_frame则指向下一个帧的帧头位置。例外的情况是没有找到当前帧的同步字(sync=0)时会假设next_frame=this_frame+1是正确的帧同步字,以继续寻找mad_stream里的当前有效帧,这种情况不影响播放。ptr成员是当前输入流的处理位置,这个位置不一定指向帧头甚至不保证指向一个有效的mp3帧,它在解码过程中标识一个全局的位置。anc_ptr和anc_bitlen是mp3帧中附加数据的位置和长度(具体参考mp3帧的说明文章);main_data与md_len指定mp3帧的主数据区的位置和长度;options目前只有两个取值:MAD_OPTION_IGNORECRC=0x0001表示忽略CRC校验,MAD_OPTION_HALFSAMPLERATE=0x0002表示使用实际采样率的一半进行解码。error保存解码过程中的错误信息,错误处理回调函数(见第一节)以该成员作为参数进行处理。
相比之下,mad_synth的情况比较简单:
其中包含一个过滤器,一个当前处理进度计数变量和输出的pcm结构数据(见第一节-API 调用顺序)。过滤器(filter)由filter回调函数初始化,用于过滤掉杂音信号。
mad_decoder可以看作是整个解码器的“核心”结构。它用c语言有限的对象机制实现了面向对象语言的部分功能。decoder封装了一次解码过程中的所有必要成分,包括用户定义的消息、6个回调函数和相应的选项。mad_decoder结构在decoder.h头文件中定义:
其中mode成员有MAD_DECODER_MODE_SYNC和MAD_DECODER_MODE_ASYNC两个取值,分别指定同步和异步两种工作方式;options被带入到sync成员的stream中的options;async成员携带异步解码时必须的状态消息;async成员不论同步还是异步方式都要由input回调函数填充并经历整个解码过程;cb_data是用户定义的消息结构,在解码过程开始时在各处理过程(如输入、过滤、输出、错误处理等)中传递状态;下面的6个回调函数可根据需要选择填充,比如最基本的解码至少要包含input和output,其他的回调函数都是可选的。
通常使用mad_decoder_inti()函数对一个mad_decoder变量进行初始化,正如本文开头和minimad.c里做的那样(这个文件可以在压缩包里找到)。之后这个mad_decoder变量就可以使用mad_decoder_run()函数进行解码了。解码过程中output回调函数不断地被调用(每解码完成一个mp3帧),直到所有的有效帧全部被解码或遇到一个不可忽略的错误,mad_decoder_run()函数才返回。
最后使用mad_decoder_finish()函数做最后的清理工作。
mad_decoder的封装不是严格意义上的封装。其中的成员完全保持了对外的可见性,只是在处理过程中将一个解码过程看作一个整体,其中的各成分存在于mad_decoder对象中。并非严格的面向对象意义上的“is a…”和“has a…”。
最后我们又回到了原点——6个回调函数。下面的章节我们会集中讨论他们。
所谓回调函数(call-back),就是不被程序员调用而是被其他函数或过程调用的函数。通常利用函数指针实现。回调函数一般都具有指定的参数表格式以便正确地分配地址。
madlib开放给用户的唯一接口就是6个回调函数,这6个回调函数都是以enum mad_flow为返回值。enum mad_flow ,这个枚举在头文件decode.h和mad.h中均有定义:
解码器每次调用回调函数后都会检查返回值,根据返回值决定下一个动作。最基本的解码器至少要实现6个回调函数中的两个——input(decoder_init函数的第三个参数)和output(decoder_init函数的第六个参数)。其中input回调函数在解码器开始运行前将mp3文件在内存中镜像的起始指针和结束指针传递给mad_stream,然后不再起作用(为简单起见,这里使输入的mp3文件在一开始就全部映射进内存,实际当中input回调函数可以只将部分mp3帧映射进内存缓冲,而后在解码过程中被反复调用来重新填充“Refill”缓冲区);而output回调函数会在整个解码过程中被反复调用,来处理PCM的最终输出。
Shwneo原创首发CSDN技术专栏,转载请注明出处
在压缩包里提供的minimad.c示例参考中给出的input回调函数如下:
其中,buffer的类型定义如下:
这个回调函数的原型有两个参数,一个是用户自定义消息指针data,用于输入用户消息(可以包含任意消息,但消息类型必须定义明确);另一个是输入流结构mad_stream,用于输出填充好的输入流,简言之就是将mp3文件的内存镜像传递给mad_stream,这个mad_stream就是mad_decoder中sync结构的stream成员。
在上面的代码中,input回调函数调用了mad_stream_buffer()函数将文件的内存镜像映射给了mad_stream。mad_stream_buffer ()函数原型如下:
第一个参数stream是个输出参数,将input回调函数的第二个虚参数直接传递就行;第二个和第三个参数分别指定mp3文件内存镜像的起始地址和长度,对于MSVC++,可以这样获取这两个参数的值:
除此之外在input这个回调函数里我们可以做解码开始之前的任何工作比如读取并显示mp3的ID3信息,初始化回放增益等。
帧解码和错误处理
相比头部信息的解码,MP3帧主数据(main data)的结构就不那么直观了。前面我们介绍过Mad中对应于MP3帧的结构struct mad_frame:
其中包括该帧对应的帧头,该帧头由上一篇介绍的mad_header_decode()函数从MP3数据中解析并填充。
除了帧头,struct mad_frame当然要包含更重要的MP3帧采样信息。关于采样的知识各位看官可以参考信号与系统教材或者DSP相关的教材,本文后续章节也会对此进行简要介绍。
采样信息包括MP-1、MP-2和MP-3中都要使用的子带过滤器采样数据(subband filter sample)和只有MP-3才拥有的MDCT频谱因子数据(overlap data)。MP3标准中把采样分成32个等宽的频带,每个频带大约有625Hz的带宽,称作子频带(subband)。而这32个子带中的采样又分成3组,每组12个采样。MPEG Audio Layer 1只使用第一组,而Layer2和Layer3要使用全部的3组,共3*12=36个采样。等等,有点乱,咱再缕缕:
MP3将人耳能感知的20Hz~20KHz的总频带划分到32个子频带中(事实上数字音频的总频带不只是20KHz,而更常见的是44.1KHz甚至48HKz,至于为什么是这个数字请见后续章节)每个子带拥有3*12=36个采样,一个单声道MP3帧总共包含32*3*12=1152个采样!而双声道或立体声MP3帧包含的采样数就是1152的2倍。
从mad_frame结构中我们可以很容易地看出这样的事实:
sbsample 包含2个声道,每个子带36个采样,32个子带
在MPEG Audio Layer 3编码中由于种种原因,在这32各子带被组合之后往往与压缩前的原信号有较大差别。为了矫正这些差别,MP3引入了MDCT(Modified Discrete Cosine Transform,改进离散余弦转换)。具体的算法本文不做介绍,但是MP3标准中规定了两种频谱因子序列——一种是6个因子的短序列,另外一种是18个因子的长序列。长序列的音质较好但占空间较大。
从mad_frame结构中我们可以很容易地看出上述事实:
overlap 包含2个声道,32个子带,每个子带18个频谱因子
Mad函数mad_frame_decode()负责填充上述的struct mad_frame结构。前面我介绍header回调函数时曾提起过如果用户初始化该函数指针为0或NULL,mad_header_decode就推迟到主数据解码前执行,反之,mad_header_decode会被首先调用,解析并填充一个mad_header结构,把这个结构作为参数传递给用户定义的header回调函数,又用户根据header信息进行一些额外的处理工作。注意header回调函数中只能使用一个const修饰的mad_header,即不能对MP3头部信息做任何更改,因MP3头部信息为跟主数据一样重要,最终还是要被填充到帧数据中:
mad_frame_decode从stream中定位frame,首先解码header数据,再根据header中的Layer信息选择对应于Layer1、2或3的解码函数进行真数据的解码并填充mad_frame结构。
帧解码过程中可能产生错误,比如CRC校验失败等,全部已定义的错误列表在mad.h和stream.h中:
解码过程中产生的最后一个错误会保存在struct mad_stream结构中的error枚举变量中。通过检查error的枚举值检测解码过程是否有错误发生。Mad提供了一个error回调函数供我们重写以处理错误,同时也提供了一个默认的错误处理函数error_default:
error_default为我们提供了一个良好的错误处理回调函数范例,只是在编写自己的error_default版本时注意参数data的实际指向类型。
Error_default函数的参数想函数内部提供了包含了枚举变量error的stream结构指针,同时还包括可能发生错误的frame结构指针。
Mad的错误类型分为两类:可恢复错误(recoverable error)和不可恢复错误(unrecoverable error),区别就在于错误枚举值的高16位:
//file:stream.h;line:59
# define MAD_RECOVERABLE(error) ((error) & 0xff00)
当decode过程中出现一个可回复错误,那经过重新同步->丢弃该->重新同步够帧解码仍会继续;当decode过程中出现一个不可恢复错误,解码器直接退出。
如果error枚举值的高16为不位0x00xx,那么MAD_RECOVERABLE宏不为0x0000,则该错误可恢复。从error枚举值的定义可见,前4种错误为不可恢复错误,包括NO_ERROR!可以说这是个小小的不足,我们在编写自己的错误处理回调函数时一定要注意这个问题,先判断是否有错误再用MAD_RECOVERABLE()宏判断是否为可恢复错误:
到目前位置,解码过程比较清晰,各回调函数和解码函数的调用顺序为
Input回调函数-> ->mad_header_decode头解码->header回调函数(如果存在)->mad_frame_decode-> error回调函数(包括默认的error_default)。
“滤波器”回调函数filter
从MP3文件中提取出的子带数据还不能直接用于输出,之后还要经过一个“子带复合”和MDCT运算之后才会被还原为与原始波形近似的PCM编码流。在子带复合和MDCT运算之前,libmad给了我们一个操作子带数据的机会。通过重写filter回调函数我们可以利用MP3帧中未经复合的子带信息实现一些特殊的应用,比如音量调整、均衡器、音乐波形显示等。
filter回调函数的原型:
我们发现filter回调函数的原型与上一篇介绍的error错误处理回调函数几乎是一样的:
区别在于error回调函数的第二个参数没有const访问限定,这意味着如果我们要在错误处理中对待解码数据流进行重新定向,就必须使用error回调函数;如果只是简单地打印错误提示和获取错误枚举值,这种情况下filter也可以替代error函数进行简单的错误处理。但为了保持函数的专用和简洁,不推荐这样做。
关于filter函数的用法,libmad压缩包提供的minimad演示程序没用使用该回调函数,在madplay播放器的原码包里找到了这样几个函数,一个是实现文件淡入特效的:
另一个是实现音量调整的:
还有一个是实现单声道输出的:
最后一个是madplay开发者用于试验的,将frame数据直接写入文件的filter,没什么参考价值,这里不粘贴代码了。
除了上述这些应用,开篇我们提到的均衡器和示波器也可以通过直接获取/操作frame子带数据来实现,但有两个个问题:
1、通常我们编写播放器都要用到双缓冲方法以使得播放流畅,即解码一份,播放另一份,依次循环。这就导致用户输入的均衡器调整信息不能及时传送到已经完成子带复合等待最后输出的PCM数据,后果是进行均衡器调整或音量调整时出现较大的延迟!如果恰巧一份完成子带复合的数据刚开始向声卡输出(整个输出过程内存缓冲区是锁定的,强行写入可能会导致程序崩溃)另一份解码完成等待设备释放,此时用户调整了均衡器或音量,播放器需要等待最多两个缓冲区播放完成才能应用新调整的均衡器!而每个缓冲区又不能太小,否则将导致明显的播放停顿。通常每个缓冲区被设定为20个MP3帧采样的大小(即20*1152个采样/声道),这20个帧可持续播放20*26ms≈0.05s,两个缓冲区可能导致的最大延迟为2*0.05s=0.1s!
2、虽说MP3将总频带分成32等份,但是每一份的具体频率没有规定,现实中每一个子带的实际子频带也没有规律,只能实现大致的调整,并不能对每一个频段进行准确的调整。但是通过filter进行均衡器调整或示波器输出具有比较好的执行效率,另一种实现方法是通过对子带复合后的PCM进行实时的FFT(快速傅立叶变换)实现,这需要较多的CPU时间,所以一些PC播放器和绝大多数MP3随身听都采用与filter类似的做法——通过frame中的子带数据直接进行均衡器调整和获得示波器显示。
在经过filter处理后,MP3帧实际上就不复存在了——它们被送进decoder内部进行哈夫曼解码和逆MDCT变换,之后就变成了PCM采样。所谓PCM采样就是根据香农采样定理(不了解DSP的童鞋可以百度之http://baike.baidu.com/view/1961946.htm?fr=ala0_1)对声音进行数字化后的串行数据。原则上这些数据可以直接送进数/模转换电路转换成模拟信号,经过放大电路提高强度后驱动扬声器发声。但通常的计算机声卡还要进行一些更复杂的缓冲、混音工作。播放器也要保证播放过程足够流畅,这些工作我们在剩下的最后一个回调函数Output()中进行。
“头部”回调函数header()
之所以要加上引号的原因是这个函数从名字上看是用于处理MP3帧头的,实际情况是真正的帧头解码是在mad_header_decode函数中进行的,mad_header_decode函数负责填充struct mad_header结构:
decode_header函数内部是大堆的if…else,将MP3头部的数据位表示的含义填充进header结构,而mad_timer_set函数精确地计算帧的播放持续时间(duration),这点对统计MP3文件的总播放时间很有帮助,因为不论MP3文件是恒定编码率(CBR)还是可变编码率(VBR),如果逐帧计算duration的话会精确得多。
在前面的关键数据结构的章节里我们介绍过libmad帧头结构struct mad_header,下面再回过头来看看以该结构作为参数的回调函数header(void* , struct mad_header const *)可以做什么:
重写该函数并初始化到decoder结构后,我们可能做的事情有:
1、 获得音频压缩标准的层信息(layerI、II、III)
2、 获得音频文件的声道信息(单声道、立体声等)
3、 获得频文件的播放码率(bps)
4、 获得音频文件的波特率(Hz,典型的是44.1KHz)
5、 获得该帧的CRC校验计算值(如果启用了CRC的话)
6、 获得该帧的CRC校验目标值(如果有的话)
7、 最重要的是,获得该帧的播放时常!
以上的数据可以获得但不能修改,原因很简单,指针以const形式进行传递,还记得const * 与*const、const * const的区别吗?
header()回调函数如果存在(这个函数不是必须的,可以被初始化成0或NULL)那么它是紧随着input()回调函数之后被调用的,如果header()回调函数不存在,说明用户对header信息不感兴趣,那么连mad_header_decode()函数都不会被调用: