你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

查看: 4887|回复: 1

【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第五十章 录音机实验

[复制链接]

24

主题

12

回帖

2

蝴蝶豆

初级会员

最后登录
2020-2-27
发表于 2013-4-13 23:09:10 | 显示全部楼层 |阅读模式
  
<a name="_Toc342394365">50.1 WAV简介 

WAV即WAVE文件,WAV是计算机领域最常用的数字化声音文件格式之一,它是微软专门为Windows系统定义的波形文件格式(Waveform Audio),由于其扩展名为"*.wav"它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!
ALIENTEK战舰STM32开发板板载的VS1053支持2种格式的WAV录音:PCM格式或者IMA ADPCM格式,其中PCM脉冲编码调制最基本的WAVE文件格式,这种文件直接存储采样的声音数据没有经过任何的压缩。而IAM ADPCM则是使用了压缩算法,压缩比率为4:1
本章,我们主要讨论PCM,因为这个最简单。我们将利用VS1053实现16位,8Khz采样率的单声道WAV录音(PCM格式)。要想实现WAV录音得先了解一下WAV文件的格式,WAVE文件是由若干个Chunk组成的。按照在文件中的出现位置包括:RIFF WAVE Chunk Format Chunk Fact Chunk(可选) Data Chunk。每个Chunk由块标识符、数据大小和数据三部分组成,如图50.1.1所示:

图50.1.1 Chunk结构示意图       其中标识符由4个ASCII码构成,数据大小则标出紧跟其后的数据的长度(单位为字节),注意这个长度不包含块标识符和数据大小的长度,即不包含最前面的8个字节。所以实际Chunk的大小为数据大小加8。
首先,我们来看看RIFF块(RIFF WAVE Chunk),该块以“RIFF”作为标示,紧跟wav文件大小(该大小是wav文件的总大小-8),然后数据段为“WAVE”,表示是wav文件。RIFF块的Chunk结构如下:
//RIFF块
typedef __packed struct
{
    u32 ChunkID;             //chunk id;这里固定为"RIFF",即0X46464952
    u32 ChunkSize ;            //集合大小;文件总大小-8
    u32 Format;                //格式;WAVE,即0X45564157
}ChunkRIFF ;
接着,我们看看Format块(Format Chunk),该块以“fmt ”作为标示(注意有个空格!),一般情况下,该段的大小为16个字节,但是有些软件生成的wav格式,该部分可能有18个字节,含有2个字节的附加信息。Format块的Chunk结构如下:
//fmt块
typedef __packed struct
{
    u32 ChunkID;             //chunk id;这里固定为"fmt ",即0X20746D66
    u32 ChunkSize ;            //子集合大小(不包括ID和Size);这里为:20.
    u16 AudioFormat;        //音频格式;0X10,表示线性PCM;0X11表示IMA ADPCM
       u16 NumOfChannels;    //通道数量;1,表示单声道;2,表示双声道;
       u32 SampleRate;           //采样率;0X1F40,表示8Khz
       u32 ByteRate;               //字节速率;
       u16 BlockAlign;            //块对齐(字节);
       u16 BitsPerSample;              //单个采样数据大小;4位ADPCM,设置为4
}ChunkFMT; 
接下来,我们再看看Fact块(Fact Chunk),该块为可选块,以“fact”作为标示,不是每个WAV文件都有,在非PCM格式的文件中,一般会在Format结构后面加入一个Fact块,该块Chunk结构如下:
//fact块
typedef __packed struct
{
    u32 ChunkID;                    //chunk id;这里固定为"fact",即0X74636166;
    u32 ChunkSize ;                 //子集合大小(不包括ID和Size);这里为:4.
    u32 DataFactSize;               //数据转换为PCM格式后的大小
}ChunkFACT;
DataFactSize是这个Chunk中最重要的数据,如果这是某种压缩格式的声音文件,那么从这里就可以知道他解压缩后的大小。对于解压时的计算会有很大的好处!不过本章我们使用的是PCM格式,所以不存在这个块。
最后,我们来看看数据块(Data Chunk),该块是真正保存wav数据的地方,以“data”'作为该Chunk的标示。然后是数据的大小。紧接着就是wav数据。根据Format Chunk中的声道数以及采样bit数,wav数据的bit位置可以分成如表50.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
            (右,高字节)

            
表50.1.1 WAVE文件数据采样格式

       本章,我们采用的是16位,单声道,所以每个取样为2个字节,低字节在前,高字节在后。数据块的Chunk结构如下:
//data块
typedef __packed struct
{
    u32 ChunkID;             //chunk id;这里固定为"data",即0X61746164
    u32 ChunkSize ;            //子集合大小(不包括ID和Size);文件大小-60.
}ChunkDATA;
       通过以上学习,我们对WAVE文件有了个大概了解。接下来,我们看看如何使用VS1053实现WAV(PCM格式)录音。
       激活PCM录音
VS1053激活PCM录音需要设置的寄存器和相关位如表50.1.2所示:

图50.1.2 VS1053激活PCM录音相关寄存器       通过设置SCI_MODE寄存器的2、12、14位,来激活PCM录音,SCI_MODE的各位描述见表49.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,只需要通过软件加载即可。
       读取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)设置VS1053 PCM采样参数
这一步,我们要设置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,以保存文件。
<a name="_Toc342394367">50.3 软件设计
打开上一章的工程,首先在APP文件夹下面新建recorder.c和recorder.h两个文件,然后将recorder.c加入到工程的APP组下。
因为recorder.c代码比较多,我们这里仅介绍其中的三个函数,首先是设置VS1053进入PCM模式的函数:recoder_enter_rec_mode,该函数代码如下:
//进入PCM 录音模式
//agc:0,自动增益.1024相当于1倍,512相当于0.5倍,最大值65535=64倍            
void recoder_enter_rec_mode(u16 agc)
{
       //如果是IMA ADPCM,采样率计算公式如下:
      //采样率=CLKI/256*d; 
       //假设d=0,并2倍频,外部晶振为12.288M.那么Fc=(2*12288000)/256*6=16Khz
       //如果是线性PCM,采样率直接就写采样值
     VS_WR_Cmd(SPI_BASS,0x0000);   
      VS_WR_Cmd(SPI_AICTRL0,8000);   //设置采样率,设置为8Khz
      VS_WR_Cmd(SPI_AICTRL1,agc);            
//设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍,最大值65535=64倍 
      VS_WR_Cmd(SPI_AICTRL2,0);         //设置增益最大值,0,代表最大值65536=64X
      VS_WR_Cmd(SPI_AICTRL3,6);         //左通道(MIC单声道输入)
       VS_WR_Cmd(SPI_CLOCKF,0X2000);      
//设置VS10XX的时钟,MULT:2倍频;ADD:不允许;CLK:12.288Mhz
       VS_WR_Cmd(SPI_MODE,0x1804);    //MIC,录音激活   
      delay_ms(5);                                      //等待至少1.35ms
      VS_Load_Patch((u16*)wav_plugin,40);//VS1053的WAV录音需要patch
}
该函数就是用我们前面介绍的方法,激活VS1053的PCM模式,本章,我们使用的是8Khz采样率,16位单声道线性PCM模式,AGC通过函数参数设置。最后加载patch(用于修复VS1053录音BUG)。
第二个函数是初始化wav头的函数:recoder_wav_init,该函数代码如下:
//初始化WAV头.
void recoder_wav_init(__WaveHeader* wavhead) //初始化WAV头                  
{
       wavhead->riff.ChunkID=0X46464952;        //"RIFF"
       wavhead->riff.ChunkSize=0;                      //还未确定,最后需要计算
       wavhead->riff.Format=0X45564157;          //"WAVE"
       wavhead->fmt.ChunkID=0X20746D66;      //"fmt "
       wavhead->fmt.ChunkSize=16;                   //大小为16个字节
       wavhead->fmt.AudioFormat=0X01;           //0X01,表示PCM;0X01,表示IMA ADPCM
      wavhead->fmt.NumOfChannels=1;             //单声道
      wavhead->fmt.SampleRate=8000;               //8Khz采样率 采样速率
      wavhead->fmt.ByteRate=wavhead->fmt.SampleRate*2;//16位,即2个字节
      wavhead->fmt.BlockAlign=2;                     //块大小,2个字节为一个块
      wavhead->fmt.BitsPerSample=16;                     //16位PCM
     wavhead->data.ChunkID=0X61746164;       //"data"
      wavhead->data.ChunkSize=0;                     //数据大小,还需要计算 
}
该函数初始化wav头的绝大部分数据,这里我们设置了该wav文件为8Khz采样率,16位线性PCM格式,另外由于录音还未真正开始,所以文件大小和数据大小都还是未知的,要等录音结束才能知道。该函数__WaveHeader结构体就是由前面介绍的三个Chunk组成,结构为:
//wav头
typedef __packed struct
{
       ChunkRIFF riff;     //riff块
       ChunkFMT fmt;  //fmt块
       //ChunkFACT fact; //fact块 线性PCM,没有这个结构体 
       ChunkDATA data;   //data块        
}__WaveHeader;
最后,我们介绍recoder_play函数,是录音机实现的主循环函数,该函数代码如下:
//录音机
//所有录音文件,均保存在SD卡RECORDER文件夹内.
u8 recoder_play(void)
{
       u8 res, key, rval=0;
       __WaveHeader *wavhead=0;
       u32 sectorsize=0;u16 w; u16 idx=0;    
       FIL* f_rec=0;                             //文件               
      DIR recdir;                               //目录
       u8 *recbuf;                                 //数据内存    
       u8 rec_sta=0;                              //录音状态
                                                        //[7]:0,没有录音;1,有录音;
                                                        //[6:1]:保留
                                                        //[0]:0,正在录音;1,暂停录音;
      u8 *pname=0;
       u8 timecnt=0;                             //计时器  
       u32 recsec=0;                              //录音时间
      u8 recagc=4;                               //默认增益为4
      while(f_opendir(&recdir,"0:/RECORDER"))//打开录音文件夹
      {    
              Show_Str(60,230,240,16,"RECORDER文件夹错误!",16,0); delay_ms(200);          
              LCD_Fill(60,230,240,246,WHITE); delay_ms(200);          //清除显示    
              f_mkdir("0:/RECORDER");//创建该目录  
       }
      f_rec=(FIL *)mymalloc(SRAMIN,sizeof(FIL));   //开辟FIL字节的内存区域
       if(f_rec==NULL)rval=1;       //申请失败
      wavhead=(__WaveHeader*)mymalloc(SRAMIN,sizeof(__WaveHeader));
//开辟__WaveHeader字节的内存区域
       if(wavhead==NULL)rval=1;
       recbuf=mymalloc(SRAMIN,512);      
       if(recbuf==NULL)rval=1;                     
       pname=mymalloc(SRAMIN,30); 
//申请30个字节内存,存放路径+名字,类似"0:RECORDER/REC00001.wav"
       if(pname==NULL)rval=1;
      if(rval==0)                                                      //内存申请OK
       {     
             recoder_enter_rec_mode(1024*recagc);                           
            while(VS_RD_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 KEY_LEFT:   //STOP&SAVE
                                   if(rec_sta&0X80)//有录音
                                   {
                                          wavhead->riff.ChunkSize=sectorsize*512+36;     //文件大小-8;
                                        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(60,230,240,246,WHITE);    
//清除显示,清除之前显示的录音文件名        
                                   recoder_show_time(recsec);          //显示时间
                                   break;    
                            case KEY_RIGHT: //REC/PAUSE
                                   if(rec_sta&0X01)//原来是暂停,继续录音
                                   {
                                          rec_sta&=0XFE;//取消暂停
                                   }else if(rec_sta&0X80)//已经在录音了,暂停
                                   {
                                          rec_sta|=0X01;       //暂停
                                   }else                            //还没开始录音
                                   {
                                         rec_sta|=0X80;       //开始录音           
                                          recoder_new_pathname(pname);                 //得到新的名字
                                          Show_Str(60,230,240,16,pname+11,16,0);  //显示录音文件名字
                                         recoder_wav_init(wavhead);                       //初始化wav数据
                                         res=f_open(f_rec,(const TCHAR*)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);//写入头数据
                                  }
                                   if(rec_sta&0X01)LED1=0;    //提示正在暂停
                                   else LED1=1;
                                   break;
                            case KEY_UP:       //AGC+  
                            case KEY_DOWN: //AGC-
                                   if(key==KEY_UP)recagc++;
                                   else if(recagc)recagc--;
                                   if(recagc>15)recagc=15;       //范围限定为0~15,自动AGC.其他AGC倍数
                                   recoder_show_agc(recagc);
                                   VS_WR_Cmd(SPI_AICTRL1,1024*recagc);      
//设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍
                                   break;
                     }
                     if(rec_sta==0X80)//已经在录音了
                     {
                           w=VS_RD_Reg(SPI_HDAT1);    
                            if((w>=256)&&(w8;                     
                                   }                   
                                  res=f_write(f_rec,recbuf,512,&bw);//写入文件
                                   if(res) break;//写入出错.      
                                   sectorsize++;//扇区数增加1,约为32ms     
                            }                  
                     }else//没有开始录音,则检测TPAD按键
                     {                                                       
                            if(TPAD_Scan(0)&&pname[0])//如果触摸按键被按下,且pname不为空
                            {                         
                                   Show_Str(60,230,240,16,"播放:",16,0);               
                                   Show_Str(60+40,230,240,16,pname+11,16,0);//显示播放的文件名字  
                                   rec_play_wav(pname);                        播放pname
                                   LCD_Fill(60,230,240,246,WHITE);     /清除之前显示的录音文件名 
                                   recoder_enter_rec_mode(1024*recagc);       //重新进入录音模式    
                                 while(VS_RD_Reg(SPI_HDAT1)>>8); //等到buf 较为空闲再开始 
                                  recoder_show_time(recsec);                 //显示时间
                                   recoder_show_agc(recagc);                  //显示agc
                           }
                            delay_ms(5); timecnt++;
                            if((timecnt%20)==0)LED0=!LED0;//DS0闪烁
                     }
                    if(recsec!=(sectorsize*4/125))//录音时间显示
                     {       
                            LED0=!LED0;//DS0闪烁
                            recsec=sectorsize*4/125;
                            recoder_show_time(recsec);//显示时间
                     }
              }                                   
       }                                               
       myfree(SRAMIN,wavhead); myfree(SRAMIN,recbuf);              
      myfree(SRAMIN,f_rec);       myfree(SRAMIN,pname);
       return rval;
}
该函数实现了我们在硬件设计时介绍的功能,我们就不详细介绍了。recorder.c的其他代码和recorder.h的代码我们这里就不再贴出了,请大家参考光盘本实验的源码。保存recorder.c,最后,我们在test.c里面修改main函数如下:
int main(void)
{           
     Stm32_Clock_Init(9);    //系统时钟设置
       delay_init(72);                     //延时初始化
       uart_init(72,9600);       //串口1初始化      
       LCD_Init();                  //初始化液晶
       LED_Init();           //LED初始化
       KEY_Init();                  //按键初始化
       TPAD_Init(72);             //初始化触摸按键   
       Audiosel_Init();            //初始化音源选择
       usmart_dev.init(72);      //usmart初始化    
      mem_init(SRAMIN);     //初始化内部内存池    
      VS_Init();       
      exfuns_init();                //为fatfs相关变量申请内存 
      f_mount(0,fs[0]);         //挂载SD卡
      f_mount(1,fs[1]);         //挂载FLASH.
       POINT_COLOR=RED;     
      while(font_init())         //检查字库
       {        
              LCD_ShowString(60,50,200,16,16,"Font Error!"); delay_ms(200);                         
              LCD_Fill(60,50,240,66,WHITE);//清除显示          
       }
      Show_Str(60,50,200,16,"战舰 STM32开发板",16,0);                                       
       Show_Str(60,70,200,16,"WAV录音机实验",16,0);                                     
       Show_Str(60,90,200,16,"广州星翼电子",16,0);                                   
       Show_Str(60,110,200,16,"2012年9月20日",16,0);
       Show_Str(60,130,200,16,"KEY0:REC/PAUSE",16,0);
       Show_Str(60,150,200,16,"KEY2:STOP&SAVE",16,0);
       Show_Str(60,170,200,16,"KEY_UP:AGC+ KEY1:AGC-",16,0);
       Show_Str(60,190,200,16,"TPADlay The File",16,0);
       while(1)
       {
              Audiosel_Set(0);   //MP3通道
              Show_Str(60,210,200,16,"存储器测试...",16,0);
              VS_Ram_Test();        
              Show_Str(60,210,200,16,"正弦波测试...",16,0);              
             VS_Sine_Test();       
              Show_Str(60,210,200,16,"",16,0);
              recoder_play();
       }                                                                                                     
}
    该函数代码同上一章的main函数代码几乎一样,只是我们这里增加了TPAD初始化,然后修改了一些显示内容,其他两者就都差不多了,我们就不再细说了。
至此,本实验的软件设计部分结束。
50.4下载验证
在代码编译成功之后,我们下载代码到ALIENTEK战舰STM32开发板上,程序先检测字库,然后对VS1053进行RAM测试和正弦测试,之后检测SD卡的RECORDER文件夹,一切顺利通过之后,激活VS1053的PCM录音模式,得到,如图50.4.1所示:

图50.4.1 录音机界面此时,我们按下KEY0就开始录音了,此时看到屏幕显示录音文件的名字以及录音时长,如图50.4.2所示:

图50.4.2 录音进行中       在录音的时候按下KEY0则执行暂停/继续录音的切换,通过DS1指示录音暂停,按WK_UP和KEY1可以调节AGC,AGC越大,越灵敏,不过不建议设置太大,因为这可能导致失真。通过按下KEY2,可以停止当前录音,并保存录音文件。在完成一次录音文件保存之后,我们可以通过按TPAD按键,来实现播放这个录音文件(即播放最近一次的录音文件),实现试听。
       我们将开发板的录音文件放到电脑上面,可以通过属性查看录音文件的属性,如图50.4.3所示:

图50.4.3 录音文件属性       这和我们预期的效果一样,通过电脑端的播放器(winamp/千千静听等)可以直接播放我们所录的音频。经实测,效果还是非常不错的。
 

《STM32开发指南》第五十章 录音机实验.rar

下载

646.07 KB, 下载次数: 77, 下载积分: ST金币 -1

实验45 录音机实验.rar

下载

848.98 KB, 下载次数: 84, 下载积分: ST金币 -1

回复

使用道具 举报

0

主题

3

回帖

0

蝴蝶豆

新手上路

最后登录
1970-1-1
发表于 2013-5-16 09:36:41 | 显示全部楼层

RE:【连载】【ALIENTEK 战舰STM32开发板】STM32开发指南--第五十章 录音机实验

原子实在是太厉害了,什么时候我能上升到原子这样的高度啊!!!
回复 支持 反对

使用道具 举报

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版