mingw吧 关注:1,423贴子:4,660
  • 23回复贴,共1

【分享2】mp3解码核心代码与文件映射函数在播放中的应用

只看楼主收藏回复

一楼祭度娘。


1楼2019-05-26 22:52回复
    二、这只是一个核心,对于解码调用很简单,二个初始化
    _mp3dec.make_decode_tables(32768);//这个数值可以用来减少音量
    _mp3dec.init_layer3(SBLIMIT);
    解码时,只要读取正确的一帧数据,拷贝到set_decode_buf()返回的解码缓冲区里,然后调用do_layer3解码既可返回wav波型数据,当解码到超过一定的大小AUDIOBUFSIZE时,就送到声卡处进行播放。详情见
    包内播放代码中的readFrame和decode两个函数处的代码,虽然是C++的代码,其实是可以很简单的改写成C代码的。主要过程就这些,解码帧信息的过程和文件读写过程,包括同步帧的代码(它的作用就是查找帧,跳过可能有坏帧的部分继续解码),可能各人在编程上的不一样,这些代码完全可以自己尝试着查找帧信息相关文章写出来,有兴趣的可以完全可以根据这些原理,自己动手写C程序来实现调用这个解码器,或许当你写出来以后,听着从音箱里传出来的音乐的那个时间,会发现音乐播放的底层原来是这样的。


    3楼2019-05-26 23:40
    回复
      后面呢?太监木有小JJ滴


      IP属地:江苏来自Android客户端4楼2019-05-28 08:05
      收起回复
        第三,包里是两份代码,一份是使用老式的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++写成类以方便以后的使用,代码如下:


        5楼2019-05-28 09:51
        回复
          本类写文件部分没有测试,读取部分通过测试,也可以用于上一篇的图片解码的文件操作,经测试,解码速度还会加快。
          #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播放时长的计算公式:


          6楼2019-05-28 10:44
          回复
            如何编程获取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公司提供的结构上面进行了扩展, 加上了一些自己的数据项,比上面提到的那篇文章更详细。


            7楼2019-05-28 22:57
            回复
              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的原代码里这些消息,相应的编程进行显示。


              8楼2019-05-28 23:00
              回复
                根据上面的结构体可以知道,如果是vbr可变帧的mp3,vbr头其实在mp3的第二帧处位置,首帧中只有前4个字节是有效帧头数据,其它部分基本是以0填充的无意义数据。定位到vbr头后,读取第8字节后的连续四个字节既为上面结构体第四项,mp3的总帧数,
                非vbr的mp3的总帧数计算公式是total_frame = (int)((mpeg_end - mpeg_start) / get_bpf()); 也就是除去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里显示的总时间,总算是对应上了。


                9楼2019-05-28 23:40
                回复
                  播放结果如下图所示:其中两个mp3就是可变vbr的文件


                  10楼2019-05-28 23:45
                  回复
                    下图是使用文件内存映射时,播放所占用的内存空间,1.4MB而矣,爽吧?


                    11楼2019-05-28 23:50
                    回复
                      工程文件在上面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文件,感兴趣的可以打开来看看。


                      12楼2019-05-29 20:22
                      收起回复