level 10
二、这只是一个核心,对于解码调用很简单,二个初始化
_mp3dec.make_decode_tables(32768);//这个数值可以用来减少音量
_mp3dec.init_layer3(SBLIMIT);
解码时,只要读取
正确的
一帧数据,拷贝到set_decode_buf()返回的解码缓冲区里,然后调用do_layer3解码既可返回wav波型数据,当解码到超过一定的大小AUDIOBUFSIZE时,就送到声卡处进行播放。详情见
包内播放代码中的readFrame和decode两个函数处的代码,虽然是C++的代码,其实是可以很简单的改写成C代码的。主要过程就这些,解码帧信息的过程和文件读写过程,包括同步帧的代码(它的作用就是查找帧,跳过可能有坏帧的部分继续解码),可能各人在编程上的不一样,这些代码完全可以自己尝试着查找帧信息相关文章写出来,有兴趣的可以完全可以根据这些原理,自己动手写C程序来实现调用这个解码器,或许当你写出来以后,听着从音箱里传出来的音乐的那个时间,会发现音乐播放的底层原来是这样的。
2019年05月26日 15点05分
3
level 10
第三,包里是两份代码,一份是使用老式的C语言的文件读取函数,由于不打算将文件全部读入内存,使用的是小内存块模式,而且该缓冲区可以自行调整大小,默认每次读取1.5MB大小,所以整个程序内存占用是2.5MB,超级小,但代价嘛,就是代码的复杂度增加,特别是拷贝帧数据这部分代码,效率不高,因为缓冲区的数据有可能不足。见下面的代码:
bool MPEGL3::readFrame()
{
byte head[5];
for(int i = 0; i < 4; i++)
{
if(!get_byte(head[i])) return false;
}
if(check_frame(head) && is_sync_frame())
{
int frame_size = getFrameLength() - 4;
if(frm.crc) //直接跳过CRC 2字节
{
get_byte(head[4]);
get_byte(head[4]);
frame_size -= 2;
}
byte *p = mp3dec.set_decode_buf(frame_size);
for(int i = 0; i < frame_size; i++) //读取main_data数据到待解码区
{
get_byte(*(p + i) );
}
return true;
}
if(sync_frame()) return true;
return false;
}
所以引出本章的第二个问题,内存文件映射。学习过操作系统的人,不论是linux还是windows,都会知道系统提供的一个有价值的功能,文件映射,改变了我们C程序文件读取程序的这个令人尴尬的问题,让我们写文件部分代码更加简单,关于这个涉及的相关内容可以自行百度或是下载windows核心编程第三章中有详细的解说。简单来说,文件映射就象我们将文件内容全部加载到内存中一样,不过是以系统内存页面的形式,而且可以映射大于4G的文件,四个步子:用CreateFile函数打开文件,用CreatefileMapping根据CreateFile返回的句柄创建一个内存映射对象,然后用MapViewofFile将文件数据映射到进程的地址空间,返回的就是一个指向文件数据的指针,访问内存就和我们读取文件一样了,使用完后,用UnmapViewofFile取消映射,关闭文件句柄和映射对象。非常简单,用C++写成类以方便以后的使用,代码如下:
2019年05月28日 01点05分
5
level 10
本类写文件部分没有测试,读取部分通过测试,也可以用于上一篇的图片解码的文件操作,经测试,解码速度还会加快。
#pragma once
//写没有测试
#include <Windows.h>
class FileMap
{
public:
enum fmode {read_only, read_write};
FileMap();
~FileMap();
void open(const char *file_name, fmode mode, DWORD len = 0);
size_t getSize(unsigned long *high=0);
unsigned char * getView() const;
bool flushView(size_t length);
void close();
// 或许要禁用复制和赋值
private:
HANDLE _file;
HANDLE _filemap;
LPVOID _pbuf;
};
然后,我们再来看使用文件内存文件映射后,readFrame的代码变化:
bool MpegLayer3::readFrame()
{
if(_check_frame(mpeg_data) && _is_sync_frame())
{
int frame_size = getFrameLength() - 4;
mpeg_data +=4;//跳过帧头四字节
if(frm.crc)//跳过CRC 2字节
{
mpeg_data += 2;
frame_size -= 2;
}
byte *p = _mp3dec.set_decode_buf(frame_size);
memcpy(p, mpeg_data, frame_size);//读取main_data数据到待解码区
mpeg_data += frame_size; //更新到下一帧位置
return true;
}
if(_sync_frame()) return true;
return false;
}
代码是不是很精简了?同步帧的代码也由于文件映射的使用,从42行,减少到26行,一些相关的函数也由于文件映射而用不上了,速度也快了很多。当然,第二份代码我只用了半天改写主要部分,可以播放,没有信息显示,感兴趣的可以根据第一份代码继续完成信息显示,做为自己的编码练习吧。
再谈一下同步帧的设计思路,这部分和源代码不同,源代码使用的是三帧检测法,写得很复杂,超过100多行,感兴趣的可以去读原代码,我采用的自己弄的双帧检测法,经过测试,也能很好的定位有错误帧的mp3,当然你也可以自行设计,基本过程就是定位数据流中的0xff,由于头部ID3V2标签中可能有jpeg图片数据,而jpeg头部数据也是0xff开头,也以一般要计算ID3V2标签长度,跳过这部分数据再进行定位首帧,可以加快定位,然后记录首帧的关键部分,再检测下一帧的关键数据是否和首帧相同,如果不是首帧,则检测当前帧是否是同步帧,下一帧的位置是当前帧位置+帧长(包括CRC的2个字节)。
四、最后谈一下Vbr可变帧mp3播放时长的计算公式:
2019年05月28日 02点05分
6
level 10
如何编程获取MP3播放时间,这个话题,其实在网上已经有人写了4篇相关的文章,以前由于忙着,只是收藏了该文章,没有仔细去深究,最近又花时间再次阅读了该文章,然后试想着写一段程序来验证该文章提到的四个公式算法。但结果很不幸,手头几个MP3用程序显示出来的时间,和windows里,以及常用的foorbar音乐播放软件相印证,结果是错误的。很明显,windows里显示时间和foorbar里显示的是相同的。
然后为了深究这个问题,俺又把以前修改过的可以编译的mpg123项目0.59s版,又翻了出来,通过调试,查找相关的函数调用关系,找到了问题的根源,原来是文章中提到的公式算法有误,不过该系列文章在MP3相关的知识介绍上,依然可以做为一个参考的资料。有兴趣的可以以“如何计算CBR/VBR播放时间”查找一下该资料,理解一下上面提到的各种术语。顺便说一下,大家最为熟悉的小小的mp3音乐格式文件里面包含的相关知识,复杂度超过你的想像,大端小端,LSF, 采样率, 比特率,各种各样的头部,尾部。。。mp3的专利权虽然在2016年失效了,但相关的软件依旧还在不断的发展中,ID3V2这个标签也被后起新秀MACTag取代了,这个特浪费空间,算法编码也复杂, ID3V1标签倒是有保留下来的趋势,不过个人觉得这个标签可以真的扔掉了,其设计真的有严重的缺陷,不过由于其读取算法最简单,俺倒是觉得可以扩展成512字节+UTF-8文字编码的话,一样可以比可变标签有优势,俺的想法是还可以加入一个可变字段,将歌词压缩后放在尾部,当然,这扯得有点远了。
在讲这个问题的时候,不得不提到另一个有名的开源项目:Lame,也就是MP3的编码器,以该项目为基础开发出来的一些开源或是闭源,免费的软件,在lame官方主页上也有很长的一个列表,或许是你曾经用过的,俺发现以前用过的Extract CD Creater也在上面,呵呵,把CD音频压缩成MP3。实际上这个项目去年2017依旧在更新, 虽然有一段时间3.9X版很长一段时间没有更新了。话说回来,俺用16进制查看软件看了一下手头的MP3,大部分VBR的mp3基本上都是lame生成的,ID关键字不是"Xing"就是"Info"四字,注意大小写,其实我们查看Lame源代码,就可以在libmp3lame目录下,有二个文件,VbrTag.c和VbrTag.h,里面有关于VbrTag的数据结构相关的定义,从定义上面来看,lame对这个数据结构在原Xing公司提供的结构上面进行了扩展, 加上了一些自己的数据项,比上面提到的那篇文章更详细。
2019年05月28日 14点05分
7
level 10
typedef struct {
int h_id; /* from MPEG header, 0=MPEG2, 1=MPEG1 */
int samprate; /* determined from MPEG header */
int flags; /* from Vbr header data */
int frames; /* total bit stream frames from Vbr header data */
int bytes; /* total bit stream bytes from Vbr header data */
int vbr_scale; /* encoded vbr scale from Vbr header data */
unsigned char toc[NUMTOCENTRIES]; /* may be NULL if toc not desired */
int headersize; /* size of VBR header, in bytes */
int enc_delay; /* encoder delay */
int enc_padding; /* encoder paddign added at end of stream */
} VBRTAGDATA;
其中第四项,第五项和本文提到的算法相关,第七项其实和快进,快退功能相关,如何定位从某处开始播放功能就是这个TOC项相关,关于如何计算,这二个文件在注释里,也很详细的提到了算法公式,感兴趣的可以读读,这个项目C语言写得给人的感觉很清爽,风格和另一个mp3解码器开源项目libmad很相似,值得我们去阅读和学习。由于lame进行了扩展,所以我们的程序要想获取这些消息,也要做一些相应的修改,你可以发现著名的播放器foorbar可以读出你的MP3是用lame什么版本压缩的,可以猜测,作者可能也是根据lame的原代码里这些消息,相应的编程进行显示。
2019年05月28日 15点05分
8
level 10
根据上面的结构体可以知道,如果是vbr可变帧的mp3,vbr头其实在mp3的第二帧处位置,首帧中只有前4个字节是有效帧头数据,其它部分基本是以0填充的无意义数据。定位到vbr头后,读取第8字节后的连续四个字节既为上面结构体第四项,mp3的总帧数,
非vbr的mp3的总帧数计算公式是total_frame = (int)((mpeg_end - mpeg_start) / get_
bp
f()); 也就是除去id3v2头部标签和尾部MACTAG或是id3v1标签后的字节数去除bpf值,是指byte per frame,平均每帧字节数。这个值和mp3属于哪个层,比特率以及采样频率和lsf值有关,见下面的公式:
double MPEGL3::get_bpf()
{
double v[]= {48000.0, 144000.0, 144000.0};
return v[frm.layer - 1] * getBitrate() / (getSampleFreq() << frm.lsf);
}
总时长=总帧数* tpf值,是指time per frame,每帧占用时间值,这个值由下面公式取得,层数对应值除采样率*2或是*0,这个值很小,好象是0.0026,一个固定的值,自己去打印看看吧,换句话说,要连续解码大概30-50帧,才会播放一秒。。
double MPEGL3::get_tpf()
{
double bs[4] = {0.0, 384.0, 1152.0, 1152.0};
return bs[frm.layer] / (getSampleFreq() << frm.lsf);
}
这就是最后的取得播放时间的函数。
void MPEGL3::getTime(int frame_no, struct MediaTime &mt)
{
double t = frame_no * get_tpf();
mt.hour = (int) t / 3600 % 24;
mt.minute = (int) t / 60 % 60;
mt.secnod = (int) t % 60;
mt.milli_seconds = (int) (t * 100) % 100;
}
经过验证,和windows以及foobar里显示的总时间,总算是对应上了。
2019年05月28日 15点05分
9
level 10
工程文件在上面11楼链结处。
文件夹mp3dec:mp3 layer3解码核心文件
bits.c bits.h: bit流操作函数,由layer3解码调用
Huffman.h :霍夫曼编码表
layer3.c:Layer第三层核心解码
synthe.c:单声道双声道合成和DCT离散余弦变换
build.cmd:gcc编译生成dll的批处理。
文件夹player1:第一份完成的播放器代码,编译时需要修改main.cpp中main主函数要播放mp3文件的路径后再编译
就是这几行:
string name1 = "d:\\mp3_sample\\琵琶语.mp3";
string name2 = "d:\\mp3_sample\\踏古 - 林海.mp3";
string name3 = "d:\\mp3_sample\\矶村由纪子 - 风居住的街道.mp3";
string name4 = "d:\\mp3_sample\\mich - lucky one.mp3";
改成你自己的mp3路径。
文件夹player2:使用内存文件映射的第二份代码,mingw gcc编译,里面有个build.cmd运行一下就可以生成player.exe了。运行方式 player "你的mp3文件"
外面还有一份mp3文件格式解析的PDF文件,感兴趣的可以打开来看看。
2019年05月29日 12点05分
12