老兵精讲:基于MM32实现音频播放系统的应用实例

嵌入式ARM 2022-08-16 12:01
一、基本介绍

I2S总线又称为Inter-IC Sound总线,它是集成在芯片内的音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准,该总线专门应用于音频设备之间的数据传输,广泛应用于各种多媒体系统。它采用了沿独立的导线传输时钟和数据信号的设计,通过将数据和时钟信号分离,避免了因时差诱发的失真问题。

MM32F3270系列MCU最多支持3个I2S接口,同一时刻只能工作在发送或者接收接收状态,支持16位、24位和32位三种数据格式、数据传输始终是MSB优先、每个I2S接口都支持DMA传输方式。

此外,MM32F3270系列MCU的I2S还支持4种I2S协议标准,分别是飞利浦标准、MSB对齐标准、LSB对齐标准、以及PCM标准,我们可以根据MM32F3270系列MCU连接的I2S外设所支持的协议标准进行灵活配置和选择。

二、实现功能

基于MM32-EVBoard(MB-039)开发板,实现音频播放系统。通过DMA的方式,将存储在TF卡中的音频文件(WAV格式/MP3格式)通过I2S总线,将音频数据发送给CS4344音频数模转换芯片,通过与其连接的音响设备播放出音乐;通过板载的4个按键来实现对音频文件的开始播放、停止播放、音频文件选择等操作,并在TFT液晶显示屏上显示系统运行信息、音频文件目录,以及当前的播放状态等内容。

根据如上功能要求,需要实现的技术点如下:

1、GPIO口控制LED闪烁和KEY按键检测;LED控制引脚与I2S引脚复用,所以在播放音乐的时候,LED是表象上不闪烁的;KEY在识**需要结合音频的当前状态做处理。

2、TFT LCD用于运行显示,通过FSMC接口与MCU进行连接,通过8080总线方式进行显示控制和内容显示;显示的字符包含ASCII码和汉字,其中ASCII码字库存放在MCU的FLASH程序空间,中文GBK编码字库存放在SPI FLASH中。

3、SPI FLASH用于存放中文GBK编码字库,通过板载的UART8接口,结合Xmodem串行文件传输协议,从电脑端把GBK_FONG.BIN文件传输并写入到板载的SPI FLASH芯片中;再通过回读取的方式,将SPI FLASH存储的字库数据通过Xmodem上传到电脑端,比较下载文件与读取到的文件是否相一致,确认字库的正确性。

4、通过MCU的SDIO接口来操作TF卡,移植FatFs文件系统对音频文件进行识别,对音频文件数据进行读取操作。

5、通过MCU的I2C接口来与CS4344音频数模转换芯片进行数据传输,实现播放音频文件,采用DMA传输方式,开启DMA传输完成中断和半传输完成中断,通过乒乓操作实现音频文件播放的流畅效果。

6、另外最重要的就是音频文件的识别和解码;对于WAV文件来说,它是未经编码/压缩的音频数据文件,在固定的文件头信息后面是音频数据,只需要在音频文件解析后将获取到的音频数据直接发送到I2S总线即可实现播放;而对于MP3文件来说,它是经过编码/压缩的音频数据文件,在没有外部解码芯片支持的时候,需要用软件解码的方式来实现音频数据的提取操作,本文中将会介绍Helix和libmad这两种开源的,且非常常用的软件解码方式。

三、软件环境

Windows操作系统下开发,使用的IDE和相应的软件如下:

1、Keil MDK开发软件;

2、MobaXterm终端软件,用于监控程序运行及代码中SHELL功能的操作;

3、SecureCRT终端软件,通过Xmodem传输协议结合MCU功能代码实现,用于将GBK汉字字库编码数据下载到板载的SPI FLASH芯片中;

4、Beyond Compare比较软件,用于比较GBK汉字字编码下载和上传数据的一致性;

5、酷狗音乐,下载音频文件。

四、硬件环境

1、MM32-EVBoard(MB-039)开发板;

2、8GB容量的TF卡及读卡器,通过读卡器连接电脑,将音频文件存放在TF卡中;

3、音箱设备;

4、Micro USB数据线、音频接口连接线;

5、USB转TTL调试工具。

五、WAV音频文件

WAV文件Waveform的简写,也称为波形文件,是一种可以存储声音波形的数字音频格式。WAV支持多种音频数字、采样频率和声道,标准格式化的WAV文件和CD的格式一样,也是44.1kHz的采样频率、16位量化数据,具有真实记录声音波形的特点,基本无数据压缩,所以数据体量也变得相对大些。

WAV文件的编码包括两方面的内容,一个是按一定格式存储数据,另外一个就是采用一定的算法压缩数据。WAV格式对音频流的编码没有硬性规定,支持非压缩算法的PCM(Plus Code Modulation)脉冲编码调制格式,也支持其它一些压缩算法。

典型的WAV文件格式,如下图所示:

1、WAV文件结构

在Windows环境下,大部分多媒体文件都是按照资源互换文件格式(Resources Interchange File Format)存放信息的,简称RIFF格式。构成RIFF文件的基本单位称之为块(Chunk),每个RIFF文件都是由若干个块组成的,而每个块都是由块标识、块长度以及数据这三部分组成的。块标识是由4个ASCII码字符组成的,如果不满4个字符则在右边以空格来补齐;块长度也占4个字节存储空间,保存的是当前块数据的长度,但不包含块标识和块长度字段;所以一个块的实际长度就是块长度字段的数值再加上8字节。

RIFF格式规定,只有RIFF块和LIST块可以包含子块,其它的块都不允许包含子块;而一个RIFF格式文档本身就是一个块。分析一个RIFF文档的组成部分,第一部分的4个字节为文档的标识符“RIFF”,同时也是RIFF的块标识,指示该文档是一个有效的RIFF格式文档;第二部分的4个字节为块长度,指示文件的数据长度,其数据为文件总长度减8字节;第三部分为块数据,其中,前4个字节为文件格式类型标识,如WAVE、AVI等;面后面其它部分就是RIFF块的子块了。

WAV文件采用的是RIFF格式结构,其至少由RIFF块、fmt块和data块组成,若是基于压缩编码的WAV文件还必须包含fact块;其中fmt块、data块和fact块都是RIFF块的子块。WAV文件的文件格式类型标识符为“WAVE”,其基本结构如下所示:


2、WAV文件头格式


3、WAV文件格式实例解析


4、扩展子块格式


5、fact块格式


6、WAV文件格式实例解析


  • 5249 4646这个是ASCII字符”RIFF”,这部分是固定格式,表明了这是一个有效的RIFF格式文件;
  • AC96 5E03这个是WAV文件的数据大小,对应0x035E96AC,十进制值为56530604;这个值再加上8个字节就是WAV文件总长度了,如下图所示:


  • 5741 5645这个是ASCII字符”WAVE”
  • 666D 7420这个是ASCII字符”fmt ”,即fmt块的块标识
  • 1000 0000这个是fmt格式块长度,16字节
  • 0100这个是编码格式,0x0001对应PCM/非压缩格式
  • 0200这个是声道个数,0x0002表示两声道或者立体声道
  • 44AC 0000这个是采样频率,对应16进制数为0x0000AC44,对应十进制为44100

其它的字段数据可以对照上述表格一一列举出来,但需要注意的是数据的大小端不同的存储方式哈!!!

7、WAV文件格式在代码中的结构体定义

WAV.h /* 由于字数限制,请参考源代码 */
8、WAV文件格式在MM32中的结构解析
static uint8_t WAV_DecodeFile(WAV_TypeDef *pWav, char *Path, char *Name) /* 由于字数限制,请参考源代码 */
9、WAV文件格式在MM32中的播放实现
void WAV_PlaySong(char *Path, char *Name){
    WAV_TypeDef WaveFile;
    char FilePath[100];


    /* 获取WAV文件的信息 */
    if(WAV_DecodeFile(&WaveFile, Path, Name) == 0)
    {
        if((WaveFile.BitsPerSample == 16) && (WaveFile.nChannels == 2) &&
           (WaveFile.SampleRate  > 44000) && (WaveFile.SampleRate < 48100))
        {
            I2S_InitGPIO();


            I2S_PowerON(1);


            I2S_Configure(I2S_Standard_Phillips, I2S_DataFormat_16b,
                          I2S_AudioFreq_44k,     I2S_Mode_MasterTx);
        }
        else
        {
            printf("\r\nWAV File Error!\r\n");  return;
        }
    }
    else
    {
        printf("\r\nNot WAV File!\r\n");    return;
    }


    memset( FilePath, 0x00, sizeof(FilePath));
    sprintf(FilePath, "%s%s",   Path,   Name);


    WAV_RES = f_open(&WAV_File, FilePath, FA_READ);


    if(WAV_RES == FR_OK)
    {
        WAV_NextIndex = 0;
        WAV_PlayEnded = 0;


        WAV_PlaybackProgress = 0;


        WAV_PrepareData();
        WAV_PlayHandler();
    }
    else
    {
        printf("\r\nWAV File Open Error : %d", WAV_RES);
    }
}

六、MP3音频文件

MP3格式音乐文件普遍存在我们生活中,实际上MP3本身是一种音频编码方式,全称为 Moving Picture Experts Group Audio Layer III(MPEG Audio Layer 3)。MPEG音频文件是MPEG标准中的声音部分,根据压缩质量和编码复杂程度划分为三层,即 Layer-1、Layer2、Layer3,且分别对应MP1、MP2、MP3 这三种声音文件。其中,MP3压缩率可达到10:1至12:1,可以大大减少文件占用存储空间大小。

MPEG 音频编码的层次越高,编码器越复杂,压缩率也越高。MP3是利用人耳对高频声音信号不敏感的特性,将时域波形信号转换成频域信号,并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)对低频信号使用小压缩比,保证信号不失真。这样一来,就相当于抛弃人耳基本听不到的高频声音,只保留能听到的低频部分,这样可得到很高的压缩率。

1、MP3文件结构

MP3文件大致分为3个部分:TAG_V2(ID3V2)、音频数据、TAG_V1(ID3V1)。ID3是MP3文件中附加关于该MP3文件的歌手、标题、专辑名称、年代、风格等等信息,有两个版本ID3V1和ID3V2。ID3V1固定存放在MP3文件末尾,固定长度为128字节,以TAG三个字符开头,后面跟上歌曲信息。因为ID3V1可存储信息量有限,有些MP3文件添加了ID3V2,ID3V2是可选的,如果存在ID3V2那它必然存在在MP3文件起始位置,它实际是ID3V1的补充。

2、MP3数据帧

经过压缩后的MP3文件数据是由多个帧组成的,帧是MP3文件的最小组成单位。每个帧又由帧头 、附加信息和声音数据组成,每个帧的长度会随着位率的不同而变化,有些MP3文件末尾还有些额外的字节数据存放非声音数据的说明信息等。每个帧包含一段音频的压缩数据,通过解码库解码即可得到对应PCM音频数据,就可以通过I2S发送到CS4344芯片播放音乐,按顺序解码所有帧就可以得到整个MP3文件的音轨。

3、MP3解码库

MP3文件是经过压缩算法压缩而存在的,为得到PCM信号,需要对MP3文件进行解码,解码 过程大致为:比特流分析、霍夫曼编码、逆量化处理、立体声处理、频谱重排列、抗锯齿处理、 IMDCT 变换、子带合成、PCM输出。整个过程涉及很多算法计算,要自己编程实现不是一件现 实的事情,还好有很多公司经过长期努力实现了解码库编程。具体的MP3文件格式内容解析可以参考附件中的《MP3文件格式解析》一文,文本不需要对MP3数据帧的细节做深入的研究,我只需要掌握如何使用当前比较常用的开源软件来播放MP3音频文件即可。

现在合适在小型嵌入式控制器移植运行的有两个版本的开源MP3解码库,分别为libmad解码库和Helix解码库。

其中,libmad是一个开源的高精度MPEG音频解码库,是专门面向嵌入式应用的MP3解码程序,可以简单地实现MP3数据解码工作,支持MPEG-1、MPEG-2,以及MPEG-2.5标准,它可以提供24位PCM输出,用定点运算模拟浮点运算,因此不需要处理器有浮点运算功能,非常适合没有浮点支持的平台上使用;libmad对MP3解码中关键部分采用了优化的算法,这些优化算法能够大幅度地减少计算量,而且大多应用于MP3解码的VLSI实现中。libmad的源代码文件目录下的mad.h文件中,可以看到绝大部分该库的数据结构和API等,软件库结构清晰,易于使用和开发。

而Helix解码库则支持浮点和定点这两种的计算实现,它同样支持MPEG-1、MPEG-2以及MPEG-2.5标准的Layer3解码,此外Helix解码库还支持可变位速率、恒定位速率,以及立体声和单声道音频格式。这两个解码库都是以一帧为解码单位的,一次解码一帧。

各有优点,至于哪个更好,我还没有深入研究,但从使用下来最明显的体验来说,就是libmad库对堆栈空间大小的要求肯定是比Helix要大得多的,所以如果为了节省些RAM空间,可以优先考虑选择Helix库。

4、Helix MP3解码库在MM32中的播放实现

void MP3_Helix_PlaySong(char *Path, char *Name)
{
    char FilePath[100];
    memset( FilePath, 0x00, sizeof(FilePath));
    sprintf(FilePath, "%s%s",   Path,   Name);


    if(f_open(&MP3_Helix_File, FilePath, FA_READ) == FR_OK)
    {
        hMP3Decoder = MP3InitDecoder();                 /* 初始化MP3解码器 */


        if(hMP3Decoder == 0)
        {
            f_close(&MP3_Helix_File); return;
        }


        MP3_Helix_RES = f_read(&MP3_Helix_File, MP3_Helix_iBuffer, MP3_HELIX_I_BUFFER_SIZE, &MP3_Helix_BR);


        if((MP3_Helix_RES == FR_OK) && (MP3_Helix_BR != 0))
        {
            printf("\r\nMP3 Play\r\n");


            I2S_InitGPIO();
            I2S_PowerON(1);


            MP3_Helix_InPointer  = MP3_Helix_iBuffer;   /* 数据读取缓存指针 */
            MP3_Helix_BytesLeft  = MP3_Helix_BR;


            MP3_Helix_PlayEnded  = 0;
            MP3_Helix_SampleRate = 0;


            I2S_DMA_Finish = 1;


            while(!MP3_Helix_PlayEnded)
            {
                MP3_Helix_PrepareData();


                EVENT_Scanning();


                MP3_Helix_PlayHandler();


                TASK_Scheduling();
            }
        }
    }
    else
    {
        printf("\r\nMP3 File Open Fail!\r\n");
    }
}
5、libmad MP3解码库在MM32中的播放实现
void MP3_libmad_PlaySong(char *Path, char *Name)
{
    char     FilePath[100];
    int      TagSize   = 0;
    uint32_t FrameCount  = 0;


    /* First the structures used by libmad must be initialized. */
    mad_stream_init(&MP3_libmad_Stream);
    mad_frame_init( &MP3_libmad_Frame );
    mad_synth_init( &MP3_libmad_Synth );
    mad_timer_reset(&MP3_libmad_Timer );


    memset( FilePath, 0x00, sizeof(FilePath));
    sprintf(FilePath, "%s%s",   Path,   Name);


    if(f_open(&MP3_libmad_File, FilePath, FA_READ) == FR_OK)
    {
        I2S_InitGPIO();
        I2S_PowerON(1);


        MP3_libmad_PlayEnded = 0;
        I2S_DMA_Finish       = 1;




        MP3_libmad_RES = f_read(&MP3_libmad_File, MP3_libmad_iBuffer, MP3_LIBMAD_I_BUFFER_SIZE, &MP3_libmad_BR);


        if(strncmp("ID3", (char *)MP3_libmad_iBuffer, 3) == 0)
        {
            /*计算标签信息总大小  不包括标签头的10个字节*/
            TagSize =  ((unsigned int)MP3_libmad_iBuffer[6] << 21) |
                       ((unsigned int)MP3_libmad_iBuffer[7] << 14) |
                       ((unsigned int)MP3_libmad_iBuffer[8] << 7)  |
                       ((unsigned int)MP3_libmad_iBuffer[9] << 0);


            TagSize += 10;


            printf("\r\nMP3 TAG Size : %d\r\n", TagSize);
        }


        f_lseek(&MP3_libmad_File, TagSize);   /* 跳过TAG信息 */




        while(1)
        {
            if((MP3_libmad_Stream.buffer == NULL) || (MP3_libmad_Stream.error == MAD_ERROR_BUFLEN))
            {
                size_t         ReadSize, Remaining;               
                unsigned char *ReadStart = NULL;


                if(MP3_libmad_Stream.next_frame != NULL)
                {
                    Remaining = MP3_libmad_Stream.bufend - MP3_libmad_Stream.next_frame;
                    memmove(MP3_libmad_iBuffer, MP3_libmad_Stream.next_frame, Remaining);


                    ReadStart = MP3_libmad_iBuffer       + Remaining;
                    ReadSize  = MP3_LIBMAD_I_BUFFER_SIZE - Remaining;
                }
                else
                {
                    ReadSize  = MP3_LIBMAD_I_BUFFER_SIZE,
                    ReadStart = MP3_libmad_iBuffer,
                    Remaining = 0;
                }


                MP3_libmad_RES = f_read(&MP3_libmad_File, (char *)ReadStart, ReadSize, &MP3_libmad_BR);


                if((MP3_libmad_BR <= 0) || (MP3_libmad_BR < ReadSize))
                {
                    printf("\r\nEnd Of File\r\n");  break;
                }


                mad_stream_buffer(&MP3_libmad_Stream, MP3_libmad_iBuffer, MP3_libmad_BR + Remaining);
                MP3_libmad_Stream.error = MAD_ERROR_NONE;
            }


            if(mad_frame_decode(&MP3_libmad_Frame, &MP3_libmad_Stream))
            {
                if(MAD_RECOVERABLE(MP3_libmad_Stream.error))
                {
                    if((MP3_libmad_Stream.error != MAD_ERROR_LOSTSYNC) || (MP3_libmad_Stream.this_frame != NULL))
                    {
                        printf("\r\nRecoverable   Frame Level Error (%s)\r\n", MP3_libmad_MadErrorString(&MP3_libmad_Stream));
                    }


                    continue;
                }
                else
                {
                    if(MP3_libmad_Stream.error == MAD_ERROR_BUFLEN)
                    {
                        continue;
                    }
                    else
                    {
                        printf("\r\nUnrecoverable Frame Level Error (%s)\r\n", MP3_libmad_MadErrorString(&MP3_libmad_Stream));
                        break;
                    }
                }
             }


            if(FrameCount == 0)
            {
                MP3_libmad_PrintFrameInfo(&MP3_libmad_Frame.header);
            }


            FrameCount++;
            mad_timer_add(&MP3_libmad_Timer, MP3_libmad_Frame.header.duration);


            mad_synth_frame(&MP3_libmad_Synth, &MP3_libmad_Frame);


            for(uint32_t i = 0; i < MP3_libmad_Synth.pcm.length; i++)
            {
                short Sample = MP3_libmad_MadFixedToSshort(MP3_libmad_Synth.pcm.samples[0][i]);


                MP3_libmad_oBuffer[MP3_libmad_NextIndex][MP3_libmad_BufferSize++] = Sample;


                if(MAD_NCHANNELS(&MP3_libmad_Frame.header) == 2)        
                {
                    Sample = MP3_libmad_MadFixedToSshort(MP3_libmad_Synth.pcm.samples[1][i]);
                }


                MP3_libmad_oBuffer[MP3_libmad_NextIndex][MP3_libmad_BufferSize++] = Sample;


                MP3_libmad_PlayHandler(MP3_libmad_Synth.pcm.samplerate);
            }


            if(MP3_libmad_PlayEnded == 1)
            {
                break;
            }
        }


        DMA_Cmd(DMA2_Channel2, DISABLE);


        f_close(&MP3_libmad_File);


        I2S_PowerON(0);
    }


    mad_synth_finish( &MP3_libmad_Synth );
    mad_frame_finish( &MP3_libmad_Frame );
    mad_stream_finish(&MP3_libmad_Stream);


    char Buffer[80];
    mad_timer_string(MP3_libmad_Timer, Buffer, "%lu:%02lu.%03u", MAD_UNITS_MINUTES, MAD_UNITS_MILLISECONDS, 0);
    printf("\r\n%d Frames Decoded (%s).\r\n", FrameCount, Buffer);
}
七、代码实现

代码实现部分,我们只展示出功能实现的主体部分,对于像FatFs文件系统的移植、LED控制、KEY按键处理、Xmodem文件传输协议的实现等部分,这些在之前的分享帖中有详细的描述,可以参考之前的分享**,当然也可以直接下载附件中的软件工程源代码,直接查看源代码。

  • 音频播放整体功能控制逻辑
void AUDIO_Handler(void/* 由于字数限制,请参考源代码 */
  • 根据识别按键处理对应功能
void KEY_Handler(eKEY_VALUE value, eKEY_TYPE type/* 由于字数限制,请参考源代码 */
  • 加载中文字库编码点阵数据,根据LCD显示方向进行旋转

void LCD_ShowCN(uint16_t StartX, uint16_t StartY, const char *str/* 由于字数限制,请参考源代码 */
  • 实现自动判断中英文字符并显示在显示屏上
uint16_t LCD_ShowLOG(uint16_t StartX, uint16_t StartY, const char *str) /* 由于字数限制,请参考源代码 */
  • 通过FatFs文件系统列举当前目录文件,并显示在LCD屏上

FRESULT AUDIO_ScanFiles(char *path) /* 由于字数限制,请参考源代码 */
八、测试运行

系统初始化加载:

歌曲目录文件显示:

播放WAV歌曲:

通过Helix解码库播放MP3歌曲:


通过labmad解码库播放MP3歌曲:


通过Xmodem串行传输协议下载中文字库到SPI FLASH:

(1)通过Xmodem串行传输协议下载字库

(2)通过Xmodem串行传输协议上传字库

(3)比较下载的字库文件和上传的字库文件是否一致

以上就是基于MM32实现音频播放系统的应用实例了,如果有需要查看原图、代码、视频演示的小伙伴,请点击底部“阅读原文”进行下载。

END

作者:xld0932
来源:21ic论坛

版权归原作者所有,如有侵权,请联系删除。

推荐阅读
让人眼前一亮的Linux终端工具!
CPU明明8个核,网卡为什么拼命折腾一号核?
为什么中国的数字是四位一进,西方的是三位一进?

→点关注,不迷路←
嵌入式ARM 关注这个时代最火的嵌入式ARM,你想知道的都在这里。
评论 (0)
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦