【正点原子STM32连载】 第五十五章 录音机实验摘自【正点原子】STM32F103 战舰开发指南V1.2

news2024/11/26 0:46:35

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#

第五十五章 录音机实验

上一章,我们实现了一个简单的音乐播放器,本章我们将在上一章的基础上,继续用VS1053实现一个简单的录音机,录制WAV格式的录音。
55.1 WAV格式简介
55.2 硬件设计
55.3 软件设计
55.4 下载验证

55.1 WAV格式简介

WAV即WAVE文件,WAV是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(WaveformAudio),由于其扩展名为"*.wav"。它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITTA LAW等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!
要想实现WAV录音得先了解一下WAV文件的格式。WAVE文件的数据采用“Chunk”来存储。因此,如果想要在WAVE文件中补充一些新的信息,只需要在新Chunk中添加信息,而不需要改变整个文件。所以可以把WAVE文件看成是很多不同Chunk的集合。每个Chunk由块标识符、数据大小和数据三部分组成,如图55.1.1所示:
在这里插入图片描述

图55.1.1 Chunk结构示意图
其中块标识符由4个ASCII码构成,数据大小则标出紧跟其后的数据的长度(单位为字节),注意这个长度不包含块标识符和数据大小的长度,即不包含最前面的8个字节。所以实际Chunk的大小为数据大小加8。
对于一个基本的WAVE文件而言,以下三种Chunk是必不可少的:文件中第一个Chunk是RIFF Chunk,然后是FMT Chunk,最后是Data Chunk。对于其他的Chunk,顺序没有严格的限制。使用WAVE文件的应用程序必须具有读取以上三种chunk信息的能力,如果程序想要复制WAVE文件,必须拷贝文件中所有的chunk。本章,我们主要讨论PCM,因为这个最简单,它只包含3个Chunk,我们看一下它的文件构成,如图55.1.2。
在这里插入图片描述

图55.1.2 PCM格式的wav文件构成
可以看到,不同的Chunk有不同的长度,编码文件时,按照Chunk的字节和位序排列好之后写入文件头,加上wav的后缀,就可以生成一个能被正确解析的wav文件了,对于PCM结构,我们只需要把获取到的音频数据填充到Data Chunk中即可。我们将利用VS1053实现16位,8Khz采样率的单声道WAV录音(PCM格式)。
首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:

typedef __PACKED_STRUCT
{
    uint32_t ChunkID;         	/* chunk id;这里固定为"RIFF",即0X46464952 */
    uint32_t ChunkSize ;        	/* 集合大小;文件总大小-8 */
    uint32_t Format;            	/* 格式;WAVE,即0X45564157 */
} ChunkRIFF ;

接着,我们看看Format块(FormatChunk),该块以“fmt”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:

typedef __PACKED_STRUCT
{
    uint32_t ChunkID;          	/* chunk id;这里固定为"fmt ",即0X20746D66 */
    uint32_t ChunkSize ;     	/* 子集合大小(不包括ID和Size);这里为:20 */
    uint16_t AudioFormat;    	/* 音频格式;0X10,表示线性PCM;0X11表示IMA ADPCM */
    uint16_t NumOfChannels;    	/* 通道数量;1,表示单声道;2,表示双声道; */
    uint32_t SampleRate;        	/* 采样率;0X1F40,表示8Khz */
    uint32_t ByteRate;          	/* 字节速率 */
    uint16_t BlockAlign;        	/* 块对齐(字节 */
    uint16_t BitsPerSample;    	/* 单个采样数据大小;4位ADPCM,设置为4 */
    //uint16_t ByteExtraData; 	/* 附加的数据字节;2个; 线性PCM,没有这个参数 */
    //uint16_t ExtraData;        /* 附加的数据,单个采样数据块大小;0X1F9:505字节  线性PCM,没有这个参数 */
} ChunkFMT;

接下来,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个 WAV 文件都有,在非 PCM 格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:

typedef __PACKED_STRUCT
{
    uint32_t ChunkID;     		/* chunk id;这里固定为"fact",即0X74636166 */
    uint32_t ChunkSize ;        	/* 子集合大小(不包括ID和Size);这里为:4 */
    uint32_t NumOfSamples;      	/* 采样的数量 */
} ChunkFACT;

DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。
最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”'作为该Chunk的标示。然后是数据的大小。紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如表55.1.1所示的几种形式:
单声道 取样1 取样2 取样3 取样4
8位量化 声道0 声道0 声道0 声道0
双声道 取样1 取样2
8位量化 声道0(左) 声道1(右) 声道0(左) 声道1(右)
单声道 取样1 取样2
16位量化 声道0(低字节) 声道0(高字节) 声道0(低字节) 声道0(高字节)
双声道 取样1
16位量化 声道0
(左,低字节) 声道0
(左,高字节) 声道1
(右,低字节) 声道1
(右,高字节)
表55.1.1 WAVE文件数据采样格式
本实验,我们采用的是16位,单声道,所以每个取样为2个字节,低字节在前,高字节在后。数据块的Chunk结构如下:

typedef __PACKED_STRUCT
{
    uint32_t ChunkID;           	/* chunk id;这里固定为"data",即0X61746164 */
    uint32_t ChunkSize ;        	/* 子集合大小(不包括ID和Size);文件大小-60 */
} ChunkDATA;

通过以上学习,我们对WAVE文件结构有了个大概了解。如果对WAV的格式还存在疑问,请参考我们“A盘6,软件资料8,WAV文件格式说明”的内容。
接下来,我们看看如何使用VS1053实现WAV(PCM格式)录音。

  1. 激活PCM录音
    VS1053激活PCM录音需要设置的寄存器和相关位如表55.1.2所示:
    在这里插入图片描述

表55.1.2 VS1053激活PCM录音相关寄存器
通过设置SCI_MODE寄存器的2、12、14位,来激活PCM录音,SCI_MODE的各位描述见表48.1.4(也可以参考VS1053的数据手册)。SCI_AICTRL0寄存器用于设置采样率,我们本章用的是8K的采样率,所以设置这个值为8000即可。SCI_AICTRL1寄存器用于设置AGC,1024相当于数字增加1,这里建议大家设置AGC在4(4*1024)左右比较合适。SCI_AICTRL2用于设置自动AGC的时候的最大值,当设置为0的时候表示最大64(65536),这个大家按自己的需要设置即可。最后,SCI_AICTRL3,我们本章用到的是咪头线性PCM单声道录音,所以设置该寄存器值为6。
通过这几个寄存器的设置,我们就激活VS1053的PCM录音了。不过,VS1053的PCM录音有一个小BUG,必须通过加载patch才能解决,如果不加载patch,那么VS1053是不输出PCM数据的,VLSI提供了我们这个patch,只需要通过软件加载即可。
2. 读取PCM数据
在激活了PCM录音之后,SCI_HDAT0和SCI_HDAT1有了新的功能。VS1053的PCM采样缓冲区由1024个16位数据组成,如果SCI_HDAT1大于0,则说明可以从SCI_HDAT0读取至少SCI_HDAT1个16位数据,如果数据没有被及时读取,那么将溢出,并返回空的状态。
注意,如果SCI_HDAT1≥896,最好等待缓冲区溢出,以免数据混叠。所以,对我们来说,只需要判断SCI_HDAT1的值非零,然后从SCI_HDAT0读取对应长度的数据,即完成一次数据读取,以此循环,即可实现PCM数据的持续采集。
最后,我们看看本章实现WAV录音需要经过哪些步骤:

  1. 设置VS1053PCM采样参数
    这一步,我们要设置PCM的格式(线性PCM)、采样率(8K)、位数(16位)、通道数(单声道)等重要参数,同时还要选择采样通道(咪头),还包括AGC设置等。可以说这里的设置直接决定了我们wav文件的性质。
  2. 激活VS1053的PCM模式,加载patch
    通过激活VS1053的PCM格式,让其开始PCM数据采集,同时,由于VS1053的BUG,我们需要加载patch,以实现正常的PCM数据接收。
  3. 创建WAV文件,并保存wav头
    在前两部设置成功之后,我们即可正常的从SCI_HDAT0读取我们需要的PCM数据了,不过在这之前,我们需要先在创建一个新的文件,并写入wav头,然后才能开始写入我们的PCM数据。
  4. 读取PCM数据
    经过前面几步的处理,这一步就比较简单了,只需要不停的从SCI_HDAT0读取数据,然后存入wav文件即可,不过这里我们还需要做文件大小统计,在最后的时候写入wav头里面。
  5. 计算整个文件大小,重新保存wav头并关闭文件
    在结束录音的时候,我们必须知道本次录音的大小(数据大小和整个文件大小),然后更新wav头,重新写入文件,最后因为FATFS,在文件创建之后,必须调用f_close,文件才会真正写入到磁盘里面!所以最后还需要调用f_close,以保存文件。
    54.2 硬件设计
  1. 例程功能
    开机的时候先检测字库,然后初始化VS1053,进行RAM测试和正弦测试,之后,检测SD卡根目录是否存在RECORDER文件夹,如果不存在则创建,如果创建失败,则报错。在找到SD卡的RECORDER文件夹后,即设置VS1053进入录音模式,此时可以在耳机听到VS1053采集的音频。KEY0用于开始/暂停录音,KEY2用于保存并停止录音,KEY_UP用于AGC增加、KEY1用于AGC减小,TPAD用于播放最近一次的录音。当我们按下KEY0的时候,可以在屏幕上看到录音文件的名字,以及录音时间,然后通过KEY2可以保存该文件,同时停止录音(文件名和时间也都将清零),在完成一个录音后,我们可以通过按TPAD按键,来试听刚刚的录音。DS0用于提示程序正在运行,DS1用于指示当前是否处于录音暂停状态。
  2. 硬件资源
    本实验,大家需要准备1个microSD/SD卡(在里面新建一个MUSIC文件夹,并存放一些歌曲在MUSIC文件夹下)和一个耳机(非必备),分别插入SD卡接口和耳机接口,然后下载本实验就可以实现录音机的效果。实验用到的硬件资源如下:
    1)LED灯
    LED0 - PB5
    LED1 - PE5
    2)独立按键
    KEY0 - PE4
    KEY1 - PE3
    KEY2 - PE2
    KEY_UP - PA0 (程序中的宏名:WK_UP)
    3)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)
    4)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    5)SD卡:通过SDIO(SDIO_D0D4(PC8PC11),SDIO_SCK(PC12),SDIO_CMD(PD2))连接
    6)NOR FLASH(SPI FLASH芯片,连接在SPI2上)
    7)VS1053芯片,通过SPI1驱动
    8)功放芯片HT6872,用于放大1053的输出以支持扬声器
    9)开发板板载的咪头或自备麦克风输入
    10)TPAD:
    需要用跳线帽连接TPAD和ADC引脚
    录音机实验与上一章(音乐播放器实验)用到的硬件资源基本一样,我们这里就不重复介绍了,有差异的是这次我们用到板载的咪头用于信号输入,也可以通过3.5mm的音频接口通过LINE_IN接入麦克风输入录音音源。
    55.3 程序设计
    55.3.1 程序流程图
    程序的设计流程如下:
    在这里插入图片描述

图55.3.1.1 录音机实验程序流程图
我们通过板载的按键控制录音的开始和停止,检测到录音开始后在录音目录下随机生成一个wav后缀的文件名并写入文件头信息,通过VS1053的录音模式不断采集声音信息并定入文件,录音结束后,我们保存文件并修改对应的文件头信息以便文件能被解码。最后,我们设计了用TPAD触摸按键来播放上一次录音的文件,以便查看录音效果。
55.3.2 程序解析

  1. recorder驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,RECORDER的驱动主要包括两个文件:recorder.c和recorder.h。
    音乐播放器实验中我们已经学会配置VS1053的方法,我们编写函数配置1053工作在PCM录音模式,我们编写代码如下:
void recoder_enter_rec_mode(uint16_t agc)
{
    /* 如果是IMA ADPCM, 采样率计算公式如下:
     * 采样率 Fs = CLKI / 256 * d;
     * CLKI , 表示内部时钟频率(倍频后的频率)
     * d    , 表示SCI_AICTRL0的分频值, 注意: 如果 d = 0, 则表示12分频
     * 假设d = 0, 并2倍频, 外部晶振为12.288M. 那么Fs = (2 * 12288000)/256*12 = 8Khz
     * 如果是线性PCM, 采样率直接就写采样值 
     */
    vs10xx_write_cmd(SPI_BASS, 0x0000);
    vs10xx_write_cmd(SPI_AICTRL0, 8000);  /* 设置采样率, 设置为8Khz */
    vs10xx_write_cmd(SPI_AICTRL1, agc);   /* 设置增益 */
    vs10xx_write_cmd(SPI_AICTRL2, 0);      /* 设置增益最大值,0,代表最大值65536=64X */
    vs10xx_write_cmd(SPI_AICTRL3, 6);      /* 左通道(MIC单声道输入), 线性PCM */
/* 设置VS10XX的时钟,MULT:2倍频;ADD:不允许;CLK:12.288Mhz */
    vs10xx_write_cmd(SPI_CLOCKF, 0X2000); 
    vs10xx_write_cmd(SPI_MODE, 0x1804);    /* MIC, 录音激活 */
    delay_ms(5);                                /* 等待至少1.35ms */
    vs10xx_load_patch((uint16_t *)wav_plugin, 40);/* VS1053的WAV录音需要patch */
}

该函数就是用我们前面介绍的方法,激活VS1053的PCM模式,本章,我们使用的是8Khz采样率,16位单声道线性PCM模式,AGC通过函数参数设置。最后加载patch(用于修复VS1053录音BUG)。
由于最后要把录音写入到文件,这里需要准备wav的文件头,为方便,我们定义了一个__WaveHeader结构体来定义文件头的数据字节,这个结构体包含了前面提到的wav文件的数据结构块:

typedef __PACKED_STRUCT
{
    ChunkRIFF riff;           	/* riff块 */
    ChunkFMT fmt;               	/* fmt块 */
    //ChunkFACT fact;           	/* fact块 线性PCM,没有这个结构体 */
    ChunkDATA data;             	/* data块 */
} __WaveHeader;
我们定义一个recoder_wav_init()函数方便初始化文件信息,代码如下:
void recoder_wav_init(__WaveHeader *wavhead)
{
    wavhead->riff.ChunkID = 0X46464952;  	/* "RIFF" */
    wavhead->riff.Format = 0X45564157;      	/* "WAVE" */
    wavhead->fmt.ChunkID = 0X20746D66;      	/* "fmt " */
    wavhead->fmt.ChunkSize = 16;            	/* 大小为16个字节 */
    wavhead->fmt.AudioFormat = 1;           	/* 1, 表示PCM; 0, 表示IMA ADPCM; */
    wavhead->fmt.NumOfChannels = 1;         	/* 单声道 */
    wavhead->fmt.SampleRate = 8000;         	/* 8Khz采样率 采样速率 */
/* 字节速率, 等于采样率*2(单声道, 16位) */
wavhead->fmt.ByteRate = wavhead->fmt.SampleRate * 2;
    wavhead->fmt.BlockAlign = 2;            	/* 块大小,2个字节为一个块 */
    wavhead->fmt.BitsPerSample = 16;       	/* 16位PCM */
    wavhead->data.ChunkID = 0X61746164;   	/* "data" */
    wavhead->data.ChunkSize = 0;           	/* 数据大小, 还需要计算 */
}
录音完成我们还要重新计算录音文件的大小写入文件头,以保证音频文件能正常被解析。录音时通过读取SCI_HDAT0寄存器中的16位数据获取到录音的ADC值,我们把这些数据直接按顺序写入文件即可完成录音操作,结合文件操作和按键功能定义,我们用recoder_play()函数实现录音过程,代码如下:
/**
 * @brief       录音机
 *   @note      所有录音文件, 均保存在 SD卡 RECORDER 文件夹内
 * @param       无
 * @retval      0, 成功; 0XFF, 播放出错;
 */
uint8_t recoder_play(void)
{
    uint8_t res;
    uint8_t key;
    uint8_t rval = 0;
    __WaveHeader *wavhead = 0;
    uint32_t sectorsize = 0;
    FIL *f_rec = 0;        	/* 文件 */
    DIR recdir;              	/* 目录 */
    UINT bw;                 	/* 写入长度 */
    uint8_t *recbuf;        	/* 数据内存 */
    uint16_t w;
    uint16_t idx = 0;
    char *pname = 0;
    uint8_t timecnt = 0;    	/* 计时器 */
    uint32_t recsec = 0;    	/* 录音时间 */
    uint8_t recagc = 4;     	/* 默认增益为4 */
    uint8_t rec_sta = 0;    	/* 录音状态
                             * [7]  : 0, 没有录音; 1, 有录音;
                             * [6:1]: 保留
                             * [0]  : 0, 正在录音; 1, 暂停录音
                             */
    while (f_opendir(&recdir, "0:/RECORDER"))   /* 打开录音文件夹 */
    {
        text_show_string(30, 230, 240, 16, "RECORDER文件夹错误!", 16, 0, RED);
        delay_ms(200);
        lcd_fill(30, 230, 240, 246, WHITE); /* 清除显示 */
        delay_ms(200);
        f_mkdir("0:/RECORDER"); /* 创建该目录 */
    }

   pname = mymalloc(SRAMIN, 30);/*申请30个字节内存,类似"0:RECORDER/REC00001.wav"*/
    f_rec = (FIL *)mymalloc(SRAMIN, sizeof(FIL));   /* 开辟FIL字节的内存区域 */
/* 开辟__WaveHeader字节的内存区域 */
wavhead = (__WaveHeader *)mymalloc(SRAMIN, sizeof(__WaveHeader)); 
    recbuf = mymalloc(SRAMIN, 512);
    if (pname == NULL   || f_rec == NULL ||wavhead == NULL || recbuf == NULL)
    {
        rval = 1;   	/* 申请失败 */
    }
    if (rval == 0)  	/* 内存申请OK */
    {
        recoder_enter_rec_mode(1024 * recagc);
        while (vs10xx_read_reg(SPI_HDAT1) >> 8);    /* 等到buf 较为空闲再开始 */
        recoder_show_time(recsec); 	/* 显示时间 */
        recoder_show_agc(recagc);   	/* 显示agc */
        pname[0] = 0;               	/* pname没有任何文件名 */
        while (rval == 0)
        {
            key = key_scan(0);
            switch (key)
            {
                case KEY2_PRES:     		/* STOP&SAVE */
                  if (rec_sta & 0X80) 		/* 有录音 */
                    {
/* 整个文件的大小-8 */
                      wavhead->riff.ChunkSize = sectorsize * 512 + 36; 
                      wavhead->data.ChunkSize = sectorsize * 512;   /* 数据大小 */
                      f_lseek(f_rec, 0);  /* 偏移到文件头 */
                      f_write(f_rec, (const void *)wavhead, 
sizeof(__WaveHeader), &bw);   /* 写入头数据 */
                        f_close(f_rec);
                        sectorsize = 0;
                    }
                    rec_sta = 0;
                    recsec = 0;
                    LED1(1);        /* 关闭DS1 */
/* 清除显示,清除之前显示的录音文件名 */
lcd_fill(30, 230, 240, 246, WHITE); 
                    recoder_show_time(recsec);  	/* 显示时间 */
                    break;
                case KEY0_PRES:     				/* REC/PAUSE */
                    if (rec_sta & 0X01)    	/* 原来是暂停,继续录音 */
                    {
                        rec_sta &= 0XFE;    	/* 取消暂停 */
                    }
                    else if (rec_sta & 0X80)	/* 已经在录音了,暂停 */
                    {
                        rec_sta |= 0X01;    	/* 暂停 */
                    }
                    else    					/* 还没开始录音 */
                    {
                        rec_sta |= 0X80;    	/* 开始录音 */
                        recoder_new_pathname((uint8_t *)pname);    /* 得到新的名字 */
/* 显示当前录音文件名字 */
                        text_show_string(30, 230, 240, 16, pname + 11, 16, 0, RED);
                        recoder_wav_init(wavhead);      /* 初始化wav数据 */
                        res = f_open(f_rec, pname, FA_CREATE_ALWAYS | FA_WRITE);
                        if (res)            		/* 文件创建失败 */
                        {
                            rec_sta = 0;    		/* 创建文件失败,不能录音 */
                            rval = 0XFE;    		/* 提示是否存在SD卡 */
                        }
                        else
                        {
res = f_write(f_rec, (const void *)wavhead,
 sizeof(__WaveHeader), &bw); /* 写入头数据 */
                        }
                    }
                    LED1(!(rec_sta & 0X01)); 	/* 提示录音状态 */
                    break;

                case WKUP_PRES:     			/* AGC+ */
                case KEY1_PRES:     			/* AGC- */
                    if (key == WKUP_PRES)
                    {
                        recagc++;
                    }
                    else if (recagc)
                    {
                        recagc--;
                    }
/* 范围限定为0~15.0, 自动AGC; 其他,AGC倍数; */
                    if (recagc > 15) recagc = 15;
                    recoder_show_agc(recagc);
/* 设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍 */
 vs10xx_write_cmd(SPI_AICTRL1, 1024 * recagc);   
                    break;
            }
            /* 读取数据 */
            if (rec_sta == 0X80)    /* 已经在录音了 */
            {
                w = vs10xx_read_reg(SPI_HDAT1);
                if ((w >= 256) && (w < 896))
                {
                    idx = 0;
                     while (idx < 512)   /* 一次读取512字节 */
                    {
                        w = vs10xx_read_reg(SPI_HDAT0);
                        recbuf[idx++] = w & 0XFF;
                        recbuf[idx++] = w >> 8;
                    }
                    res = f_write(f_rec, recbuf, 512, &bw); /* 写入文件 */
                    if (res)
                    {
                        printf("err:%d\r\n", res);
                        printf("bw:%d\r\n", bw);
                        break;      /* 写入出错 */
                    }
                    sectorsize++;   /* 扇区数增加1,约为32ms */
                }
            }
            else    /* 没有开始录音,则检测TPAD按键 */
            {
                if (tpad_scan(0) && pname[0])   /* 如果触摸按键被按下,且pname不为空 */
                {
                  text_show_string(30, 230, 240, 16, "播放:", 16, 0, RED);
/* 显示当播放的文件名字 */
text_show_string(30 + 40, 230, 240, 16, pname + 11, 16, 0, RED); 
                  rec_play_wav((uint8_t *)pname);         /* 播放pname */
lcd_fill(30,230,240,246, WHITE);/*清除显示,清除之前显示的录音文件名 */
                  recoder_enter_rec_mode(1024 * recagc);  /* 重新进入录音模式 */
                  while (vs10xx_read_reg(SPI_HDAT1) >> 8);/*等到buf较为空闲再开始*/
                  recoder_show_time(recsec);  /* 显示时间 */
                  recoder_show_agc(recagc);   /* 显示agc */
                }
                delay_ms(5);
                timecnt++;
                if ((timecnt % 20) == 0)LED0_TOGGLE();  /* DS0闪烁 */
            }
            if (recsec != (sectorsize * 4 / 125))   /* 录音时间显示 */
            {
                LED0_TOGGLE();              /* DS0闪烁 */
                recsec = sectorsize * 4 / 125;
                recoder_show_time(recsec);  /* 显示时间 */
            }
        }
    }

    myfree(SRAMIN, wavhead);
    myfree(SRAMIN, recbuf);
    myfree(SRAMIN, f_rec);
    myfree(SRAMIN, pname);
    return rval;
}
  1. main.c代码
    由于我们把大部分功能已经在recoder_play ()中实现了,main函数进行必要的外设初始化,显示相关的数据信息后,调用该接口即可实现我们需要的录音机功能了,最后我们在main.c中实现代码如下:
int main(void)
{
    HAL_Init();                                	/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);    	/* 设置时钟, 72Mhz */
    delay_init(72);                             	/* 延时初始化 */
    usart_init(115200);                        	/* 串口初始化为115200 */
    usmart_dev.init(72);                     	/* 初始化USMART */
    led_init();                                  /* 初始化LED */
    lcd_init();                                  /* 初始化LCD */
    key_init();                                  /* 初始化按键 */
    sram_init();                                 /* SRAM初始化 */
    norflash_init();                            /* 初始化NORFLASH */
    tpad_init(6);                               	/* 初始化TPAD */
    vs10xx_init();                            	/* VS10XX初始化 */
    my_mem_init(SRAMIN);                      	/* 初始化内部SRAM内存池 */
    my_mem_init(SRAMEX);                      	/* 初始化外部SRAM内存池 */

    exfuns_init();                            	/* 为fatfs相关变量申请内存 */
    f_mount(fs[0], "0:", 1);                	/* 挂载SD卡 */
    f_mount(fs[1], "1:", 1);                	/* 挂载FLASH */
    while (fonts_init())                     	/* 检查字库 */
    {
        lcd_show_string(30, 50, 200, 16, 16, "Font Error!", RED);
        delay_ms(200);
        lcd_fill(30, 50, 240, 66, WHITE);	/* 清除显示 */
        delay_ms(200);
    }
    text_show_string(30, 50, 200, 16, "正点原子STM32开发板", 16, 0, RED);
    text_show_string(30, 70, 200, 16, "WAV录音机 实验", 16, 0, RED);
    text_show_string(30, 110, 200, 16, "KEY0:REC/PAUSE", 16, 0, RED);
    text_show_string(30, 130, 200, 16, "KEY2:STOP&SAVE", 16, 0, RED);
    text_show_string(30, 150, 200, 16, "KEY_UP:AGC+ KEY1:AGC-", 16, 0, RED);
    text_show_string(30, 170, 200, 16, "TPAD:Play The File", 16, 0, RED);

    while (1)
    {
        LED1(0);
        text_show_string(30, 190, 200, 16, "存储器测试...", 16, 0, RED);
        printf("Ram Test:0X%04X\r\n", vs10xx_ram_test());   /* 打印RAM测试结果 */

        text_show_string(30, 190, 200, 16, "正弦波测试...", 16, 0, RED);
        vs10xx_sine_test();

        text_show_string(30, 190, 200, 16, "<<WAV录音机>>", 16, 0, RED);
        LED1(1);
        recoder_play();
    }
}

可以看到main函数与音乐播放器实验十分类似,封装好了APP,main函数会精简很多。
55.4 下载验证
在代码编译成功之后,我们下载代码到正点原子战舰STM32开发板上,程序先检测字库,然后对VS1053进行RAM测试和正弦测试,之后检测SD卡的RECORDER文件夹,一切顺利通过之后,激活VS1053的PCM录音模式,得到,如图55.4.1所示
在这里插入图片描述

图55.4.1 录音机实验界面
此时,我们按下KEY0就开始录音了,此时看到屏幕显示录音文件的名字以及录音时长,如图55.4.2所示:
在这里插入图片描述

图55.4.2 录音进行中
在录音的时候按下KEY0则执行暂停/继续录音的切换,通过DS1指示录音暂停,按KEY_UP和KEY1可以调节AGC,AGC越大,越灵敏,不过不建议设置太大,因为这可能导致失真。通过按下KEY2,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按TPAD按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。
我们可以把录音完成的wav文件放到电脑上,可以通过一些播放软件播放并查看详细的音频编码信息,本例程使用的是KMPlayer播放,查看到的信息如图55.4.3所示:
在这里插入图片描述

图55.4.3 录音文件属性
这和我们程序设计时的效果一样,通过电脑端的播放器可以直接播放我们所录的音频。经实测,效果还是非常不错的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/753803.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大学生用一周时间给麦当劳做了个App(Flutter版)

背景 有个大学生粉丝最近私信联系我&#xff0c;说基于我之前开源的多语言项目做了个仿麦当劳的项目&#xff0c;虽然只是个样子货&#xff0c;但是收获颇多&#xff0c;希望把自己写的代码开源出来供大家一起学习进度。这个小伙伴确实是非常积极上进&#xff0c;很多大学生&a…

Django admin管理工具TabularInline表格内联

详解 TabularInline 是 Django Admin 中的一个内联模型选项&#xff0c;用于在父模型的编辑页面中以表格形式显示关联的子模型对象。下面是对 TabularInline 的一些详解&#xff1a; 显示方式&#xff1a;TabularInline 以表格的形式显示子模型对象。每个子模型对象将以一行的…

12.0、Java_IO流 - 字节数组输入输出流

12.0、Java_IO流 - 字节数组输入输出流 字节数组流&#xff1a; ByteArrayInputStream 和 byteArrayOutputStream 经常用在需要流和数组之间转化的情况&#xff1b; 字节数组输入流&#xff1a; 说白了&#xff0c;FileInputStream 是把文件当做数据源&#xff1b;ByteArrayInp…

变动率ROC指标详解及改进版选股公式

ROC指标&#xff08;变动率指标&#xff09;是一种基于动量的技术指标&#xff0c;衡量当前价格与一定天数前价格之间变化的百分比。ROC指标围绕零轴上下波动&#xff0c;如果价格变化向上&#xff0c;指标会移动到零轴之上&#xff1b;如果价格变动向下&#xff0c;则指标会移…

NestJS 编写 SSE 接口推送数据

做项目的时候遇到了顺便就记一下相关的内容。 SSE Server-Sent Events&#xff08;SSE&#xff09;技术&#xff0c;它是一种用于实现服务器向客户端实时推送数据的Web技术。SSE基于HTTP协议&#xff0c;允许服务器将数据以事件流&#xff08;Event Stream&#xff09;的形式…

深度学习(29)—— DETR

深度学习&#xff08;29&#xff09;—— DETR DETR代码欢迎光临Jane的GitHub&#xff1a;在这里等你 看完YOLO 之后&#xff0c;紧接着看了DETR。作为Transformer在物体检测上的开山之作&#xff0c;虽然他的性能或许不及其他的模型&#xff0c;但是想法是OK的。里面还有一些…

数据结构day1(2023.7.13)

一、Xmind整理&#xff1a; 二、课上练习&#xff1a; 练习1&#xff1a;static&#xff08;全局变量、局部变量作用域&#xff09; int a0;//全局变量 生命周期和作用于都是从定义开始到整个文件结束 void fun() { int b0;//局部变量 static int c0;//局部变量 作用于&#x…

智头条|第25届中国建博会(广州)成功举行,马斯克组建xAI公司

行业动态&#xff1a; 第25届中国建博会&#xff08;广州&#xff09;成功举行 7月8日至11日期间&#xff0c;2023中国建博会(广州)暨首届广州卫博会在广州如火如荼地进行。本届展会以“冠军企业首秀平台”为定位&#xff0c;以“建装理想家&#xff0c;服务新格局”为主题&a…

我的创作纪念日——创作的第2048天

创作机缘 今天收到私信&#xff0c;在CSDN已经7年码龄&#xff0c;创作2048天了&#xff0c;刚开始写作的时候似乎还是在大二&#xff0c;那个懵懂无知的年纪&#xff0c;也是在那个时候开始接触开发&#xff0c;接触编程。 之后便是无尽的探索与尝试&#xff0c;没有明确的发…

DuiLib的消息传递机制

前言 学会了怎么写XML文件&#xff0c;但是我还是不知道怎么实现各个控件之间的消息传递。于是我对源代码好好研究了一下&#xff0c;发现duilib作为一个界面库有自己独立的封装的窗口类,也就是WindowsImplBase。 在这个类中&#xff0c;实现对windows窗口传过来的消息的处理…

【每日一题】2673. 使二叉树所有路径值相等的最小代价

【每日一题】2673. 使二叉树所有路径值相等的最小代价 2673. 使二叉树所有路径值相等的最小代价题目描述解题思路 2673. 使二叉树所有路径值相等的最小代价 题目描述 给你一个整数 n 表示一棵 满二叉树 里面节点的数目&#xff0c;节点编号从 1 到 n 。根节点编号为 1 &#…

前端day06笔记

数组遍历 for let i in 数组名 函数 没有return返回的是underfined var是全局作用域 匿名函数 具名函数 值传递 引用传递 arguments:接实参 箭头函数 递归 闭包 对象的增删改查 对象的遍历 数组对象 获取数组对象 内置对象 从2-10

保险企业如何做好数据安全合规与敏感数据保护

监管部门多次“重拳出击”&#xff0c;保险企业如何做好敏感数据保护工作&#xff1f; 继《个人信息保护法》、《中华人民共和国消费者权益保护法》、《中国银保监会办公厅关于印发银行保险机构信息科技外包风险监管办法的通知》、《互联网保险业务监管办法》等相关法规之后&am…

CNN从搭建到部署实战(pytorch+libtorch)

模型搭建 下面的代码搭建了CNN的开山之作LeNet的网络结构。 import torchclass LeNet(torch.nn.Module):def __init__(self):super(LeNet, self).__init__()self.conv torch.nn.Sequential(torch.nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_sizetorch.nn.Sig…

mysql 第三章

目录 1.索引 2.事务 3.总结 1.索引 2.事务 3.总结 事务是一种机制&#xff0c;一个操作序列。包含了一组数据库操作命令。

静态数码管——FPGA

文章目录 前言一、数码管1、数码管简介2、共阴极数码管or共阳极数码管3、共阴极与共阳极的真值表 二、系统设计1、模块框图2、RTL视图 三、源码1、seg_led_static模块2、time_count模块3、top_seg_led_static(顶层文件) 四、效果五、总结六、参考资料 前言 环境&#xff1a; 1、…

大学生用一周时间给麦当劳做了个App(微信小程序版)

背景 有个大学生粉丝最近私信联系我&#xff0c;说基于我之前开源的多语言项目做了个仿麦当劳的项目&#xff0c;虽然只是个样子货&#xff0c;但是收获颇多&#xff0c;希望把自己写的代码开源出来供大家一起学习进度。这个小伙伴确实是非常积极上进&#xff0c;很多大学生&a…

MySQL数据库(三)

前言 聚合查询、分组查询、联合查询是数据库知识中最重要的一部分&#xff0c;是将表的行与行之间进行运算。 目录 前言 一、聚合查询 &#xff08;一&#xff09;聚合函数 1、count 2、sum 3、avg 4、max 5、min 二、分组查询 &#xff08;一&#xff09;group by …

Docker架构

目录 Docker总架构图Docker ClientDocker DaemonDocker ServerDocker EngineJob Docker RegistryGraphDriverGraphDriverNetworkDriverExecDriver LibcontainerDocker Container Docker可以帮助用户在容器内部快速自动化部署应用&#xff0c;并利用Linux内核特性命名空间&#…

微软将推出更多Edge特有功能,与Chrome展开竞争

微软在 2018 年宣布将推出基于 Chromium 构建的 Edge 浏览器&#xff0c;并于 2020 年 1 月推出了新版 Edge。如今时隔三年&#xff0c;根据统计 Edge 全平台的市场占有率仅为 4.23%&#xff0c;如果只考虑桌面端的话&#xff0c;Edge 的市场占有率则是 10.98%&#xff0c;这两…