联合科技有限公司: 电邮: sales@unitedlink.hk, 电话: 香港 (852) 36940301, 深圳 (86) 755 82511234, QQ: 2330215579

实例 2: VS1053 音乐播放器实验
Read 338 times
* March 01, 2019, 12:13:53 PM
第四十九章 音乐播放器实验

前几年,MP3 曾经风行一时,几乎人手一个。如果能自己做一个 MP3,我想对于很多朋友来说,是十分骄傲的事情。ALIENTEK 战舰 STM32 开发板自带了一颗非常强劲的 MP3 解码芯片:VS1053,利用该芯片,我们可以实现 MP3/OGG/WMA/WAV/FLAC/AAC/MIDI 等各种音频文件的播放。本章,我们将利用战舰 STM32 开发板实现一个简单的MP3播放器。本章分为如下几个部:

49.1 MP3 简介
49.2 硬件设计
49.3 软件设计
49.4 下载验证

49.1 VS1053 简介
VS1053 是继 VS1003 后芬兰 VLSI 公司出品的又一款高性能解码芯片。该芯片可以实现对 MP3/OGG/WMA/FLAC/WAV/AAC/MIDI 等音频格式的解码,同时还可以支持 ADPCM/OGG 等格式的编码,性能相对以往的VS1003提升不少。VS1053 拥有一个高性能的 DSP 处理器核 VS_DSP,16K的指令 RAM,0.5K 的数据 RAM,通过 SPI 控制,具有8个可用的通用 IO 口和一个串口,芯片内部还带了一个可变采样率的立体声 ADC(支持咪头/咪头+线路/2线路)、一个高性能立体声 DAC 及音频耳机放大器。

VS1053 的特性如下:
● 支持众多音频格式解码,包括 OGG/MP3/WMA/WAV/FLAC(需要加载patch)/MIDI/AAC等。
● 对话筒输入或线路输入的音频信号进行 OGG(需要加载patch)/IMA ADPCM 编码
● 高低音控制
● 带有 EarSpeaker 空间效果(用耳机虚拟现场空间效果)
● 单时钟操作 12..13 MHz
● 内部 PLL 锁相环时钟倍频器
● 低功耗
● 内含高性能片上立体声 DAC,两声道间无相位差
● 过零交差侦测和平滑的音量调整
● 内含能驱动 30 欧负载的耳机驱动器
● 模拟,数字,I/O 单独供电
● 为用户代码和数据准备的 16KB 片上 RAM
● 可扩展外部 DAC 的 I2S 接口
● 用于控制和数据的串行接口(SPI)
● 可被用作微处理器的从机
● 特殊应用的 SPI Flash 引导
● 供调试用途的 UART 接口
● 新功能可以通过软件和 8 GPIO 添加

VS1053 相对于它的前辈 VS1003,增加了编解码格式的支持(比如支持 OGG/FLAC,还支持 OGG 编码,VS1003 不支持)、增加了GPIO 数量到 8 个(VS1003 只有 4 个)、增加了内部指令 RAM 容量到16KiB(VS1003只有5.5KiB)、增加了I2S接口(VS1003没有)、支持EarSpeaker空间效果(VS1003不支持)等。同时 VS1053 的 DAC 相对于 VS1003 有不少提高,同样的歌曲,用 VS1053 播放,听起来比 VS1003 效果好很多。

VS1053 的封装引脚和 VS1003 完全兼容,所以如果你以前用的是 VS1003,则只需要把 VS1003 换成 VS1053, 就可以实现硬件更新,电路板完全不用修改。不过需要注意的是 VS1003 的 CVDD 是 2.4-2.7V,AVDD 是 2.6-2.8V,IOVDD 是 2.1-3.6V;而 VS1053 的 CVDD 是1.8V,AVDD 是 2.5-3.6V,IOVDD 是 1.8-3.6V;所以你还需要把稳压芯片也变一下,其他都可以照旧了。



VS1053 通过 SPI 接口来接受输入的音频数据流,它可以是一个系统的从机,也可以作为独立的主机。这里我们只把它当成从机使用。我们通过 SPI 口向 VS1053 不停的输入音频数据,它就会自动帮我们解码了,然后从输出通道输出音乐,这时我们接上耳机就能听到所播放的歌曲了。

ALIENTEK 战舰 STM32 开发板,自带了一颗 VS1053 音频编解码芯片,所以,我们直接可以通过开发板来播放各种音频格式,实现一个音乐播放器。战舰 STM32 开发板自带的 VS1053 解码芯片电路原理图,如图49.1.1 ALIENTEK 音频解码模块原理图所示:

VS1053 通过 7 根线同 STM32 连接,他们是:VS_MISO、VS_MOSI、VS_SCK、VS_XCS、VS_XDCS、VS_DREQ 和 VS_RST。这 7 根线同 STM32 的连接关系如表 49.1.1 VS1053 各信号线与 STM32 连接关系所示:

其中 VS_RST 是 VS1053 的复位信号线,低电平有效。VS_DREQ 是一个数据 请求信号,用来通知主 机,VS1053 可以接收数据与否。VS_MISO、VS_MOSI 和 VS_SCK 则是 VS1053 的 SPI 接口他们在 VS_XCS 和 VS_XDCS 下面来执行不同的操作。从上表可以看出,VS1053 的 SPI 是接在 STM32 的 SPI1 上面的。

VS1053 的 SPI 支持两种模式:1,VS1002有效模式(即新模式)。2,VS1001兼容模式。这里我们仅介绍 VS1002 有效模式(此模式也是 VS1053 的默认模式)。表 49.1.2 是在新模式下 VS1053 的 SPI 信号线功能描述:

VS1053 的 SPI 数据传送,分为 SDI 和 SCI,分别用来传输数据/命令。SDI 和前面介绍的 SPI 协议一样的,不过 VS1053 的数据传输是通过 DREQ 控制的,主机在判断 DREQ 有效(高电平)之后,直接发送即可(一次可以发送 32 个字节)。

这里我们重点介绍一下 SCI。 SCI 串行总线命令接口包含了一个指令字节、一个地址字节和一个 16 位的数据字。读写操作可以读写单个寄存器,在 SCK 的上升沿读出数据位,所以主机必须在下降沿刷新数据。SCI 的字节数据总是高位在前低位在后的。第一个字节指令字节,只有 2 个指令,也就是读和写,读为 0X03,写为 0X02。

一个典型的SCI读时序如图49.1.2所示:

从图 49.1.2 可以看出,向 VS1053 读取数据,通过先拉低 XCS(VS_XCS),然后发送读指令(0X03),再发送一个地址,最后,我们在 SO 线(VS_MISO)上就可以读到输出的数据了。而同时 SI(VS_MOSI)上的数据将被忽略。

看完了 SCI 的读,我们再来看看 SCI 的写时序,如图 49.1.3所示:

图 49.1.3 中,其时序和图 49.1.2 基本类似,都是先发指令,再发地址。不过写时序中,我们的指令是写指令(0X02),并且数据是通过SI写入 VS1053 的, SO 则一直维持低电平。细心的读者可能发现了,在这两个图中,DREQ 信号上都产生了一个短暂的低脉冲,也就是执行时间。这个不难理解,我们在写入和读出 VS1053 的数据之后,它需要一些时间来处理内部的事情,这段时间,是不允许外部打断的,所以,我们在 SCI 操作之前,最好判断一下 DREQ 是否为高电平,如果不是,则等待 DREQ 变为高。

了解了 VS1053 的 SPI 读写,我们再来看看 VS1053 的 SCI 寄存器,VS1053 的所有 SCI 寄存器如表 49.1.3 所示:

VS1053 总共有 16 个 SCI寄存器,这里我们不介绍全部的寄存器,仅仅介绍几个我们在本章需要用到的寄存器。

首先是 MODE 寄存器,该寄存器用于控制 VS1053 的操作,是最关键的寄存器之一,该寄存器的复位值为 0x0800,其实就是默认设置为新模式。表 49.1.4 是 MODE 寄存器的各位描述:

这个寄存器,我们这里只介绍一下第 2 和第 11 位,也就是 SM_RESET 和 SM_SDINEW。其他位,我们用默认的即可。这里 SM_RESET,可以提供一次软复位,建议在每播放一首歌曲之后,软复位一次。SM_SDINEW 为模式设置位,这里我们选择的是 VS1002 新模式(本地模式),所以设置该位为 1(默认的设置)。其他位的详细介绍,请参考 VS1053 的数据手册。

接着我们看看BASS寄存器,该寄存器可以用于设置 VS1053 的高低音效。该寄存器的各位描述如表 49.1.5 所示:

通过这个寄存器以上位的一些设置,我们可以随意配置自己喜欢的音效(其实就是高低音的调节)。VS1053 的 EarSpeaker 效果则由 MODE 寄存器控制,请参考表 49.1.4。

接下来,我们介绍一下 CLOCKF 寄存器,这个寄存器用来设置时钟频率、倍频等相关信息,该寄存器的各位描述如表 49.1.6所示:
 
表 49.1.6 CLOCKF 寄存器各位描述

此寄存器,重点说明 SC_FREQ,SC_FREQ 是以 4Khz 为步进的一个时钟寄存器,当外部时钟不是 12.288M 的时候,其计算公式为:
SC_FREQ=(XTALI-8000000)/4000

式中为 XTALI 的单位为Hz。表 49.1.6 中 CLKI 是内部时钟频率,XTALI 是外部晶振的时钟频率。由于我们使用的是 12.288M 的晶振,在这里设置此寄存器的值为 0X9800,也就是设置内部时钟频率为输入时钟频率的 3倍,倍频增量为 1.0 倍。

接下来,我们看看 DECODE_TIME 这个寄存器。该寄存器是一个存放解码时间的寄存器,以秒钟为单位,我们通过读取该寄存器的值,就可以得到解码时间了。不过它是一个累计时间,所以我们需要在每首歌播放之前把它清空一下,以得到这首歌的准确解码时间。

HDAT0 和 HDTA1 是两个数据流头寄存器,不同的音频文件,读出来的值意义不一样,我们可以通过这两个寄存器来获取音频文件的码率,从而可以计算音频文件的总长度。这两个寄存器的详细介绍,请参考 VS1053 的数据手册。

最后我们介绍一下 VOL 这个寄存器,该寄存器用于控制 VS1053 的输出音量,该寄存器可以分别控制左右声道的音量,每个声道的控制范围为 0~254,每个增量代表 0.5db 的衰减,所以该值越小,代表音量越大。比如设置为 0X0000 则音量最大,而设置为 0XFEFE 则音量最小。注意:如果设置 VOL 的值为 0XFFFF,将使芯片进入掉电模式!

关于 VS1053 的介绍,我们就介绍到这里,更详细的介绍请看 VS1053 的数据手册。接下来我们说说如何控制通过最简单的步骤,来控制 VS1053 播放一首歌曲。

1) 复位 VS1053

这里包括了硬复位和软复位,是为了让 VS1053 的状态回到原始状态,准备解码下一首歌曲。这里建议大家在每首歌曲播放之前都执行一次硬件复位和软件复位,以便更好的播放音乐。

2) 配置 VS1053 的相关寄存器

这里我们配置的寄存器包括 VS1053 的模式寄存器(MODE)、时钟寄存器(CLOCKF)、音调寄存器(BASS)、音量寄存器(VOL)等。

3) 发送音频数据

当经过以上两步配置以后,我们剩下来要做的事情,就是往 VS1053 里面扔音频数据了,只要是 VS1053 支持的音频格式,直接往里面丢就可以了,VS1053 会自动识别,并进行播放。不过发送数据要在DREQ信号的控制下有序的进行,不能乱发。这个规则很简单:只要 DREQ 变高,就向 VS1053 发送32个字节。然后继续等待 DREQ 变高,直到音频数据发送完。

经过以上三步,我们就可以播放音乐了。这一部分就先介绍到这里。 

49.2 硬件设计

本章实验功能简介:开机先检测字库是否存在,如果检测无问题,则对 VS1053 进行 RAM 测试和正弦测试,测试完后开始循环播放 SD 卡 MUSIC 文件夹里面的歌曲(必须在 SD 卡根目录建立一个 MUSIC 文件夹,并存放歌曲在里面),在 TFTLCD 上显示歌曲名字、播放时间、歌曲总时间、歌曲总数目、当前歌曲的编号等信息。KEY0 用于选择下一曲,KEY2用于选择上一曲,WK_UP 和 KEY1 用来调节音量。DS0 还是用于指示程序运行状态,DS1 用于指示 VS1053 正在初始化。

本实验用到的资源如下:

1) 指示灯 DS0 和 DS1
2) 四个按键(WK_UP/KEY0/KEY1/KEY2) 
3) 串口
4) TFT LCD 模块
5) SD 卡
6) SPI  FLASH 
7) VS1053
8) 74HC4052
9) TDA1308 

这些硬件我们都已经介绍过了,其中 74HC4052 和 TDA1308 分别是用作音频选择和耳机驱动,在第四十章,我们已经介绍过。本章,我们使用的 VS1053 同 STM32 的连接关系详见表 49.1.1。

本实验,大家需要准备1个 SD 卡(在里面新建一个 MUSIC 文件夹,并存放一些歌曲在 MUSIC 文件夹下)和一个耳机,分别插入 SD 卡接口和耳机接口,然后下载本实验就可以通过耳机来听歌了。

49.3 软件设计

打开上一章的工程,首先在 HARDWARE 文件夹所在的文件夹下新建一个 APP 的文件夹。在该文件夹里面新建 mp3player.c 和 mp3player.h 两个文件。并将 APP 文件夹加入头文件包含路径。

然后在 HARDWARE 文件夹下新建一个 VS10XX 的文件夹,在该文件夹里面新建 VS10XX.c、VS10XX.h 和 flac.h 等三个文件。
首先打开 VS10XX.c,里面的代码我们不一一贴出了,这里挑几个重要的函数给大家介绍一下,首先要介绍的是 VS_Soft_Reset,该函数用于软复位 VS1003,其代码如下:

//软复位VS10XX
void VS_Soft_Reset(void)
{   
 u8 retry=0;         
 while(VS_DQ==0); //等待软件复位结束     
 VS_SPI_ReadWriteByte(0Xff);//启动传输
 retry=0;
 while(VS_RD_Reg(SPI_MODE)!=0x0800)// 软件复位,新模式   
 {
  VS_WR_Cmd(SPI_MODE,0x0804);// 软件复位,新模式     
  delay_ms(2);//等待至少1.35ms 
  if(retry++>100)break;       
 }     
 while(VS_DQ==0);//等待软件复位结束   
 retry=0;
 while(VS_RD_Reg(SPI_CLOCKF)!=0X9800)//设置VS10XX的时钟,3倍频 ,1.5xADD 
 {
  VS_WR_Cmd(SPI_CLOCKF,0X9800);//设置VS10XX的时钟,3倍频 ,1.5xADD
  if(retry++>100)break;       
 }                     
 delay_ms(20);
}

该函数比较简单,先配置一下 VS1053 的模式顺便执行软复位操作,在软复位结束之后,再设置好时钟,完成一次软复位。接下来,我们介绍一下 VS_WR_Cmd 函数,该函数用于向 VS1003 写命令,代码如下:

//向VS10XX 写命令
//address: 命令地址
//data:命令数据
void VS_WR_Cmd(u8 address,u16 data)
{   
 while(VS_DQ==0);//等待空闲     
 VS_SPI_SpeedLow();//低速 
 VS_XDCS=1;   ;
 VS_XCS=0;     
 VS_SPI_ReadWriteByte(VS_WRITE_COMMAND);//发送VS10XX的写命令
 VS_SPI_ReadWriteByte(address); //地址
 VS_SPI_ReadWriteByte(data>>8); //发送高八位
 VS_SPI_ReadWriteByte(data);  //第八位
 VS_XCS=1;         
 VS_SPI_SpeedHigh(); //高速     
}

该函数用于向 VS1053 发送命令,这里要注意 VS1053 的写操作比读操作快(写1/4 CLKI,读1/7 CLKI),虽然说写寄存器最快可以到 1/4CLKI,但是经实测在 1/4CLKI 的时候会出错,所以在写寄存器的时候最好把 SPI 速度调慢点,然后在发送音频数据的时候,就可以1/4CLKI 的速度了。有写命令的函数,当然也有读命令的函数了。VS_RD_Reg 用于读取 VS1053 的寄存器的内容。该函数代码如下:     

//读VS10XX的寄存器           
//address:寄存器地址
//返回值:读到的值
//注意不要用倍速读取,会出错
u16 VS_RD_Reg(u8 address)

 u16 temp=0;       
    while(VS_DQ==0);//非等待空闲状态     
 VS_SPI_SpeedLow();//低速 
 VS_XDCS=1;       
 VS_XCS=0;         
 VS_SPI_ReadWriteByte(VS_READ_COMMAND); //发送VS10XX的读命令
 VS_SPI_ReadWriteByte(address);          //地址
 temp=VS_SPI_ReadWriteByte(0xff);     //读取高字节
 temp=temp<<8;
 temp+=VS_SPI_ReadWriteByte(0xff);    //读取低字节
 VS_XCS=1;     
 VS_SPI_SpeedHigh();//高速   
   return temp; 
}   

该函数的作用和 VS_WR_Cmd 的作用基本相反,用于读取寄存器的值。VS10XX.c 的剩余代码、VS10XX.h 以及 flac.h 的代码,这里就不贴出来了,其中 flac.h 仅仅用来存储播放 flac 格式所需要的 patch 文件,以支持flac 解码。大家可以去光盘查看他们的详细源码。保存 VS10XX.c、VS10XX.h 和 flac.h 三个文件。把 VS10XX.c 加入到 HARDWARE 组下。然后我们打开 mp3player.c,该文件我们仅介绍一个函数,其他代码请看光盘的源码。这里要介绍的是 mp3_play_song 函数,该函数代码如下:

//播放一曲指定的歌曲                           
//返回值:0,正常播放完成;1,下一曲;2,上一曲;0XFF,出现错误了; 
u8 mp3_play_song(u8 *pname)
{   
  FIL* fmp3;
    u16 br;
 u8 res,rval;   
 u8 *databuf;         
 u16 i=0; 
 u8 key;       
 rval=0;     
 fmp3=(FIL*)mymalloc(SRAMIN,sizeof(FIL)); //申请内存
 databuf=(u8*)mymalloc(SRAMIN,4096);  //开辟4096字节的内存区域
 if(databuf==NULL||fmp3==NULL)rval=0XFF ; //内存申请失败.
 if(rval==0)
 {   
    VS_Restart_Play();       //重启播放 
  VS_Set_All();             //设置音量等信息     
  VS_Reset_DecodeTime();    //复位解码时间     
  res=f_typetell(pname);       //得到文件后缀
  if(res==0x4c)//如果是flac,加载patch
  { 
   VS_Load_Patch((u16*)vs1053b_patch,VS1053B_PATCHLEN);
  }                       
  res=f_open(fmp3,(const TCHAR*)pname,FA_READ);//打开文件   
   if(res==0)//打开成功.
  { 
   VS_SPI_SpeedHigh(); //高速         
   while(rval==0)
   {
    res=f_read(fmp3,databuf,4096,(UINT*)&br);//读出4096个字节   
    i=0;
    do//主播放循环
       {   
     if(VS_Send_MusicData(databuf+i)==0)//给VS10XX发送音频数据
     {
      i+=32;
     }else   
     {
      key=KEY_Scan(0);
      switch(key)
      {
       case KEY_RIGHT: rval=1; break; //下一曲   
       case KEY_LEFT: rval=2; break; //上一曲
       case KEY_UP: //音量增加
        if(vsset.mvol<250)
        {
         vsset.mvol+=5;
          VS_Set_Vol(vsset.mvol); 
        }else vsset.mvol=250;
        mp3_vol_show((vsset.mvol-100)/5);//音量限制在: 100~
//250,显示时,按照公式(vol-100)/5,显示,也就是0~30   
        break;
       case KEY_DOWN: //音量减
        if(vsset.mvol>100)
        {
         vsset.mvol-=5;
          VS_Set_Vol(vsset.mvol); 
        }else vsset.mvol=100;
        mp3_vol_show((vsset.mvol-100)/5);
break;
      }
      mp3_msg_show(fmp3->fsize);//显示信息     
     }           
    }while(i<4096);//循环发送4096个字节 
    if(br!=4096||res!=0) {rval=0; break;}//读完了.         
   }
   f_close(fmp3);
  }else rval=0XFF;//出现错误       
 }             
 myfree(SRAMIN,databuf);               
 myfree(SRAMIN,fmp3);
 return rval;               
}

该函数,就是我们解码 MP3 的核心函数了,该函数在初始化 VS1053 后,根据文件格式选择是否加载 patch(如果是 flac 格式,则需要加载 patch),最后在死循环里面等待 DREQ 信号的到来,每次 VS_DQ 变高,就通过 VS_Send_MusicData 函数向 VS1053 发送 32 个字节,直到整个文件读完。此段代码还包含了对按键的处理(音量调节、上一首、下一首)及当前播放的歌曲的一些状态(码率、播放时间、总时间)显示。

mp3player.c 的其他代码和 mp3player.h 在这里就不详细介绍了,请大家直接参考光盘源码。最后,我们在 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();   //按键初始化                 
 Audiosel_Init();  //初始化音源选择
 usmart_dev.init(72); //usmart初始化 
  mem_init(SRAMIN); //初始化内部内存池 
  VS_Init();     //初始化VS1053 
  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,"音乐播放器实验",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:NEXT   KEY2:PREV",16,0);
 Show_Str(60,150,200,16,"KEY_UP:VOL+ KEY1:VOL-",16,0);
 while(1)
 {
  Audiosel_Set(0); //音频通道选择MP3音源
   LED1=0;     
  Show_Str(60,170,200,16,"存储器测试...",16,0);
  printf("Ram Test:0X%04X\r\n",VS_Ram_Test());//打印RAM测试结果     
  Show_Str(60,170,200,16,"正弦波测试...",16,0);     
   VS_Sine_Test();     
  Show_Str(60,170,200,16,"<<音乐播放器>>",16,0);     
  LED1=1;
  mp3_play();
 }                           
}

该函数先检测外部flash是否存在字库,然后选择音频通道为MP3音源,之后执行 VS1053 的 RAM 测试和正弦测试,这两个测试结束后,调用 mp3_play 函数开始播放 SD 卡 MUSIC 文件夹里面的音乐。软件部分就介绍到这里。

49.4 下载验证

在代码编译成功之后,我们下载代码到 ALIENTEK 战舰 STM32 开发板上,程序先执行字库监测,然后对 VS1053 进行 RAM 测试和正弦测试。

当检测到 SD 卡根目录的MUSIC文件夹有有效音频文件(VS1053 所支持的格式)的时候,就开始自动播放歌曲了,如图 49.4.1 所示:

从上图可以看出,当前正在播放第 4 首歌曲,总共 4 首歌曲,歌曲名、播放时间、总时长、码率、音量等信息等也都有显示。此时 DS0 会随着音乐的播放而闪烁,2 秒闪烁一次。 

只要我们在开发板的 PHONE 端子插入耳机,就能听到歌曲的声音了。同时,我们可以通过按 KEY0 和 KEY2 来切换下一曲和上一曲,通过 WK_UP 按键来控制音量增加,通过 KEY1 控制音量减小。

本实验,我们还可以通过 USMART 来测试 VS1053 的其他功能,通过将 VS10XX.c 里面的部分函数加入 USMART 管理,我们可以很方便的设置/获取 VS1053 各种参数,达到验证测试的目的。有兴趣的朋友,可以实验测试一下。

至此,我们就完成了一个简单的 MP3 播放器了,在此基础上进一步完善,就可以做出一个比较实用的MP3了。大家可以自己发挥想象,做出一个你心仪的 MP3。

* 原文出处: ALIENTEK 正点原子战舰 STM32F103 开发板特点和资源介绍
« Last Edit: May 02, 2019, 03:21:17 PM by Mark »

Logged