语音通信方案

系统级方案和自建协议

系统级

声卡 (Sound Card)也叫音频卡(港台称之为声效卡),是计算机多媒体系统中最基本的组成部分,是实现声波/数字信号相互转换的一种硬件。声卡的基本功能是把来自话筒、磁带、光盘的原始声音信号加以转换,输出到耳机扬声器扩音机录音机等声响设备,或通过音乐设备数字接口(MIDI)发出合成乐器的声音

所有的电脑主板基本都有集成声卡的,如果有专业要求会再买个独立声卡,就像专业玩家一样买个独立显卡,手动狗头

声卡驱动

对于音频处理的技术,主要有如下几种:

  • 采集麦克风输入
  • 采集声卡输出
  • 将音频数据送入声卡进行播放
  • 对多路音频输入进行混音处理

Windows平台内核提供调用声卡API

一、MME(MultiMedia Extensions)

MME就是winmm.dll提供的接口,也是Windows平台下第一代API。优点是使用简单,一般场景下可以满足业务需求,缺点是延迟高,某些高级功能无法实现。

二、XAudio2

也是DirextX的一部分,为了取代DirectSound。DirextX套件中的音频组件,大多用于游戏中,支持硬件加速,所以比MME有更低的延迟。

三、Core Audio API

Vista系统开始引入的新架构,它是以COM的方式提供的接口,用户模式下处于最底层,上面提到的几种API最终都将使用它!功能最强,性能最好,但是接口繁杂,使用起来很麻烦。

四、Wasapi 就可以了(高性能,但更复杂)

而Wave系列的API函数主要是用来实现对麦克风输入的采集(使用WaveIn系列API函数)和控制声音的播放(使用后WaveOut系列函数)。

1.使用WaveIn系列API函数实现麦克风输入采集

2.使用Core Audio实现对声卡输出的捕捉

3.常用的混音算法

Linux平台内核提供调用声卡API

ALSA是目前linux的主流音频体系架构

是一个有社区维护的开源项目:http://www.alsa-project.org/

包括:

1.内核驱动包 alsa-driver

2.用户空间库 alsa-lib

3.附加库插件包 alsa-libplugins

4.音频处理工具集 alsa-utils

5.其他音频处理小工具包 alsa-tools

6.特殊音频固件支持包 alsa-firmware

7.alsa-lib的Python绑定包 pyalsa

8.OSS接口兼容包 alsa-oss

9.内核空间中,alsa-soc其实是对alsa-driver的进一步封装,他针对嵌入式设备提供了一些列增强的功能。

1.操作说明

2.架构图

硬件架构:

语音通信解决方案总结-LMLPHP

 软件架构:

语音通信解决方案总结-LMLPHP

 3.初识alsa设备

语音通信解决方案总结-LMLPHP

注:
controlC0:控制接口,用于控制声卡,如通道选择,混音,麦克风输入增益调节等。
midiC0D0:Raw迷笛接口,用于播放midi音频。
pcmC0D0c:pcm接口,用于录音的pcm设备。
pcmC0D0p:用于播放的pcm设备。
pcmC0D1p:
seq:音序器接口。
timer:定时器接口。
即该声卡下挂载了7个设备。根据声卡实际能力,驱动实际上可以挂载更多种类的设备
其中
C0D0表示声卡0中的设备0。
pcmC0D0c:最后的c表示capture。
pcmC0D0p:最后一个p表示playback。











设备种类 include/sound/core.h:

语音通信解决方案总结-LMLPHPinclude/sound/core.h

4.linux内核中音频驱动代码分布

语音通信解决方案总结-LMLPHP

其中:
core:包含 ALSA 驱动的核心层代码实现。
core/oss:包含模拟旧的OSS架构的PCM和Mixer模块。
core/seq:音序器相关的代码。
drivers:存放一些与CPU,bus架构无关的公用代码。
i2c:ALSA的i2c控制代码。
pci:PCI总线 声卡的顶层目录,其子目录包含各种PCI声卡代码。
isa:ISA总线 声卡的顶层目录,其子目录包含各种ISA声卡代码。
soc:ASoC(ALSA System on Chip)层实现代码,针对嵌入式音频设备。
soc/codecs:针对ASoC体系的各种音频编码器的驱动实现,与平台无关。








include/sound:ALSA驱动的公共头文件目录。

5.驱动分类

OSS音频设备驱动:
OSS 标准中有两个最基本的音频设备: mixer(混音器)和 dsp(数字信号处理器)。

ALSA音频设备驱动:

虽然 OSS 已经非常成熟,但它毕竟是一个没有完全开放源代码的商业产品,而且目前基本上在 Linux mainline 中失去了更新。而 ALSA (Advanced Linux Sound Architecture)恰好弥补了这一空白,它符合 GPL,是在 Linux 下进行音频编程时另一种可供选择的声卡驱动体系结构。 ALSA 除了像 OSS 那样提供了一组内核驱动程序模块之外,还专门为简化应用程序的编写提供了相应的函数库,与 OSS 提供的基于 ioctl 的原始编程接口相比, ALSA 函数库使用起来要更加方便一些。 ALSA 的主要特点如下。支持多种声卡设备。

模块化的内核驱动程序。
支持 SMP 和多线程。
提供应用开发函数库(alsa-lib)以简化应用程序开发。
支持 OSS API,兼容 OSS 应用程序。


ASoC音频设备驱动:
ASoC(ALSA System on Chip)是 ALSA 在 SoC 方面的发展和演变,它在本质上仍然属于
ALSA,但是在 ALSA 架构基础上对 CPU 相关的代码和 Codec 相关的代码进行了分离。其原因是,采用传统 ALSA 架构的情况下,同一型号的 Codec 工作于不同的 CPU 时,需要不同的驱动,这不符合代码重用的要求。对于目前嵌入式系统上的声卡驱动开发,我们建议读者尽量采用 ASoC 框架, ASoC 主要由 3 部分组成。

Codec 驱动。这一部分只关心 Codec 本身,与 CPU 平台相关的特性不由此部分操作。

平台驱动。这一部分只关心 CPU 本身,不关心 Codec。它主要处理两个问题: DMA 引擎和 SoC 集成的 PCM、 I2S 或 AC ‘97 数字接口控制。

板驱动(也称为 machine 驱动)。这一部分将平台驱动和 Codec 驱动绑定在一起,描述了板一级的硬件特征。

在以上 3 部分中, 1 和 2 基本都可以仍然是通用的驱动了,也就是说, Codec 驱动认为自己可以连接任意 CPU,而 CPU 的 I2S、 PCM 或 AC ‘97 接口对应的平台驱动则认为自己可以连接任意符合其接口类型的 Codec,只有 3 是不通用的,由特定的电路板上具体的 CPU 和 Codec 确定,因此它很像一个插座,上面插上了 Codec 和平台这两个插头。在以上三部分之上的是 ASoC 核心层,由内核源代码中的 sound/soc/soc-core.c 实现,查看其源代码发现它完全是一个传统的 ALSA 驱动。因此,对于基于 ASoC 架构的声卡驱动而言, alsa-lib以及 ALSA 的一系列 utility 仍然是可用的,如 amixer、 aplay 均无需针对 ASoC 进行任何改动。而ASoC 的用户编程方法也与 ALSA 完全一致。内核源代码的 Documentation/sound/alsa/soc/目录包含了 ASoC 相关的文档。

Android平台内核提供调用声卡API

目前linux中主流的音频体系结构是ALSA(Advanced Linux Sound Architecture),ALSA在内核驱动层提供了alsa-driver,在应用层提供了alsa-lib,应用程序只需要调用alsa-lib(libtinyalsa.so)提供的API就可以完

成对底层硬件的操作。说的这么好,但是Android中没有使用标准的ALSA,而是一个ALSA的简化版叫做tinyalsa。Android中使用tinyalsa控制管理所有模式的音频通路,我们也可以使用tinyalsa提供的工具进行查看、

调试。

TINYALSA子系统
音频帧(frame)
 
这个概念在应用开发中非常重要,网上很多文章都没有专门介绍这个概念。

音频跟视频很不一样,视频每一帧就是一张图像,而从上面的正玄波可以看出,音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取2.5ms~60ms为单位的数据量为一帧音频。

语音通信解决方案总结-LMLPHP

这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的,我们可以计算一下一帧音频帧的大小:

假设某音频信号是采样率为8kHz、双通道、位宽为16bit,20ms一帧,则一帧音频数据的大小为:

int size = 8000 x 2 x 16bit x 0.02s = 5120bit = 640 byte

音频帧总结

嵌入式硬件级

电路组成

代码示例

MCU裸板开发

  1 #include <reg52.h>
  2 #include <intrins.h>
  3 #define uchar unsigned char
  4 #define uint  unsigned int
  5 //录音和放音键IO口定义:  
  6 sbit   AN=P2^6;//放音键控制接口  
  7 sbit    set_key=P2^7;//录音键控制口
  8 // ISD4004控制口定义:  
  9 sbit SS  =P1^0;     //4004片选  
 10 sbit MOSI=P1^1;     //4004数据输入  
 11 sbit MISO=P1^2;     //4004数据输出  
 12 sbit SCLK=P1^3;     //ISD4004时钟  
 13 sbit INT =P1^4;     //4004中断  
 14 sbit STOP=P3^4;     //4004复位  
 15 sbit LED1 =P1^6;    //录音指示灯
 16 //===============================LCD1602接口定义=====================  
 17 /*-----------------------------------------------------
 18        |DB0-----P2.0 | DB4-----P2.4 | RW-------P0.1    |
 19        |DB1-----P2.1 | DB5-----P2.5 | RS-------P0.2    |
 20        |DB2-----P2.2 | DB6-----P2.6 | E--------P0.0    |
 21        |DB3-----P2.3 | DB7-----P2.7 | 注意,P0.0到P0.2需要接上拉电阻
 22     ---------------------------------------------------
 23 =============================================================*/
 24 #define LCM_Data     P0    //LCD1602数据接口  
 25 sbit    LCM_RW     = P2^3;  //读写控制输入端,LCD1602的第五脚  
 26 sbit    LCM_RS     = P2^4;  //寄存器选择输入端,LCD1602的第四脚  
 27 sbit    LCM_E      = P2^2;  //使能信号输入端,LCD1602的第6脚
 28 //***************函数声明************************************************  
 29 void    WriteDataLCM(uchar WDLCM);//LCD模块写数据  
 30 void    WriteCommandLCM(uchar WCLCM,BuysC); //LCD模块写指令  
 31 uchar   ReadStatusLCM(void);//读LCD模块的忙标  
 32 void    DisplayOneChar(uchar X,uchar Y,uchar ASCII);//在第X+1行的第Y+1位置显示一个字符  
 33 void    LCMInit(void);
 34 void    DelayUs(uint us); //微妙延时程序  
 35 void    DelayMs(uint Ms);//毫秒延时程序  
 36 void    init_t0();//定时器0初始化函数  
 37 void    setkey_treat(void);//录音键处理程序  
 38 void    upkey_treat(void);//播放键处理程序  
 39 void    display();//显示处理程序  
 40 void    isd_setrec(uchar adl,uchar adh);//发送setrec指令  
 41 void    isd_rec();//发送rec指令  
 42 void    isd_stop();//stop指令(停止当前操作)  
 43 void    isd_powerup();//发送上电指令  
 44 void    isd_stopwrdn();//发送掉电指令  
 45 void    isd_send(uchar isdx);//spi串行发送子程序,8位数据  
 46 void    isd_setplay(uchar adl,uchar adh);
 47 void    isd_play();
 48 //程序中的一些常量定义  
 49 uint    time_total,st_add,end_add=0;
 50 uint    adds[25];//25段语音的起始地址暂存  
 51 uint    adde[25];//25段语音的结束地址暂时  
 52 uchar   t0_crycle,count,count_flag,flag2,flag3,flag4;
 53 uchar   second_count=170,msecond_count=0;
 54 //second_count为芯片录音的起始地址,起始地址本来是A0,也就是160,
 55 //我们从170开始录音吧。  
 56 #define Busy         0x80   //用于检测LCM状态字中的Busy标识  
 57
 58 /*===========================================================================
 59  主程序
 60 =============================================================================*/
 61 void main(void)
 62 {
 63    LED1=0;//灭录音指示灯  
 64    flag3=0;
 65    flag4=0;
 66    time_total=340;//录音地址从170开始,对应的单片机开始计时的时间就是340*0.1秒  
 67    adds[0]=170;
 68    count=0;
 69    LCMInit();        //1602初始化  
 70    init_t0();//定时器初始化  
 71    DisplayOneChar( 0,5,'I'); //开机时显示000  ISD4004-X  
 72    DisplayOneChar( 0,6,'S');
 73    DisplayOneChar( 0,7,'D');
 74    DisplayOneChar( 0,8,'4');
 75    DisplayOneChar( 0,9,'0');
 76    DisplayOneChar( 0,10,'0');
 77    DisplayOneChar( 0,11,'4');
 78    DisplayOneChar( 0,12,'-');
 79    DisplayOneChar( 0,13,'X');
 80    while(1)
 81    {
 82       display();//显示处理  
 83       upkey_treat();//放音键处理  
 84       setkey_treat();//录音键处理  
 85    }
 86 }
 87 //*******************************************
 88 //录音键处理程序
 89 //从指定地址开始录音的程序就是在这段里面  
 90 void setkey_treat(void)
 91 {
 92    set_key=1;//置IO口为1,准备读入数据  
 93    DelayUs(1);
 94    if(set_key==0)
 95    {
 96       if(flag3==0)//录音键和放音键互锁,录音好后,禁止再次录音。如果要再次录音,那就要复位单片机,重新开始录音  
 97       {
 98         if(count==0)//判断是否为上电或复位以来第一次按录音键  
 99         {
100            st_add=170;
101         }
102         else
103         {
104           st_add=end_add+3;
105         }//每段语言间隔3个地址  
106         adds[count]=st_add;//每段语音的起始地址暂时  
107         if(count>=25)//判断语音段数时候超过25段,因为单片机内存的关系?
108        //本程序只录音25段,如果要录更多的语音,改为不可查询的即可  
109         {//如果超过25段,则覆盖之前的语音,从新开始录音  
110            count=0;
111            st_add=170;
112            time_total=340;
113         }
114         isd_powerup(); //AN键按下,ISD上电并延迟50ms  
115         isd_stopwrdn();
116         isd_powerup();
117         LED1=1;//录音指示灯亮,表示录音模式  
118         isd_setrec(st_add&0x00ff,st_add>>8); //从指定的地址  
119         if(INT==1)// 判定芯片有没有溢出  
120         {
121             isd_rec(); //发送录音指令  
122         }
123         time_total=st_add*2;//计时初始值计算  
124         TR0=1;//开计时器  
125         while(set_key==0);//等待本次录音结束  
126         TR0=0;//录音结束后停止计时  
127         isd_stop(); //发送4004停止命令  
128         end_add=time_total/2+2;//计算语音的结束地址  
129         adde[count]=end_add;//本段语音结束地址暂存  
130         LED1=0; //录音完毕,LED熄灭  
131         count++;//录音段数自加  
132         count_flag=count;//录音段数寄存  
133         flag2=1;
134         flag4=1;//解锁放音键  
135       }
136   }
137 }
138 //=================================================
139 //放音机处理程序
140 //从指定地址开始放本段语音就是这段程序  
141 void upkey_treat(void)
142 {
143    uchar ovflog;
144    AN=1;//准备读入数据  
145    DelayUs(1);
146    if(AN==0)//判断放音键是否动作  
147    {
148  //    if(flag4==1)//互锁录音键
149  //    {  
150         if(flag2==1)//判断是否为录音好后的第一次放音  
151         {
152            count=0;//从第0段开始播放  
153         }
154         isd_powerup(); //AN键按下,ISD上电并延迟50ms  
155         isd_stopwrdn();
156         isd_powerup();
157         //170 184 196 211
158    //     st_add=adds[count];//送当前语音的起始地址  
159         st_add=211;//送当前语音的起始地址  
160         isd_setplay(st_add&0x00ff,st_add>>8); //发送setplay指令,从指定地址开始放音  
161         isd_play(); //发送放音指令  
162         DelayUs(20);
163         while(INT==1); //等待放音完毕的EOM中断信号  
164         isd_stop(); //放音完毕,发送stop指令  
165         while(AN==0); //
166         isd_stop();
167         count++;//语音段数自加  
168         flag2=0;
169         flag3=1;
170         if(count>=count_flag)//如果播放到最后一段后还按加键,则从第一段重新播放  
171         {
172              count=0;
173         }
174
175  //     }  
176    }
177 }
178 //************************************************?
179 //发送rec指令  
180 void isd_rec()
181 {
182     isd_send(0xb0);
183     SS=1;
184 }
185 //****************************************
186 //发送setrec指令  
187 void isd_setrec(unsigned char adl,unsigned char adh)
188 {
189     DelayMs(1);
190     isd_send(adl); //发送放音起始地址低位  
191     DelayUs(2);
192     isd_send(adh); //发送放音起始地址高位  
193     DelayUs(2);
194     isd_send(0xa0); //发送setplay指令字节  
195     SS=1;
196 }
197 //=============================================================================
198 //**********************************************
199 //定时器0中断程序  
200 void timer0() interrupt 1
201 {
202     TH0=(65536-50000)/256;
203     TL0=(65536-50000)%256;
204     t0_crycle++;
205     if(t0_crycle==2)// 0.1秒  
206     {
207       t0_crycle=0;
208       time_total++;
209       msecond_count++;
210       if(msecond_count==10)//1秒  
211       {
212         msecond_count=0;
213         second_count++;
214         if(second_count==60)
215         {
216           second_count=0;
217         }
218       }
219       if(time_total==4800)time_total=0;
220     }
221 }
222 //********************************************************************************************
223 //定时器0初始化函数  
224 void init_t0()
225 {
226     TMOD=0x01;//设定定时器工作方式1,定时器定时50毫秒  
227     TH0=(65536-50000)/256;
228     TL0=(65536-50000)%256;
229     EA=1;//开总中断  
230     ET0=1;//允许定时器0中断  
231     t0_crycle=0;//定时器中断次数计数单元  
232 }
233 //******************************************
234 //显示处理程序  
235 void display()
236 {
237         uchar x;
238         if(flag3==1||flag4==1)//判断是否有录音过或者放音过  
239         {
240           x=count-1;
241           if(x==255){x=count_flag-1;}
242         }
243         DisplayOneChar( 0,0,x/100+0x30);    //显示当前语音是第几段  
244         DisplayOneChar( 0,1,x/10%10+0x30);
245         DisplayOneChar( 0,2,x%10+0x30);
246         if(flag3==0)//录音时显示本段语音的起始和结束地址  
247         {
248            DisplayOneChar( 1,0,st_add/1000+0x30);//计算并显示千位     
249            DisplayOneChar( 1,1,st_add/100%10+0x30);
250            DisplayOneChar( 1,2,st_add/10%10+0x30);
251            DisplayOneChar( 1,3,st_add%10+0x30);
252            DisplayOneChar( 1,4,'-');
253            DisplayOneChar( 1,5,'-');
254            DisplayOneChar( 1,6,end_add/1000+0x30);
255            DisplayOneChar( 1,7,end_add/100%10+0x30);
256            DisplayOneChar( 1,8,end_add/10%10+0x30);
257            DisplayOneChar( 1,9,end_add%10+0x30);
258         }
259         if(flag4==1)//放音时显示本段语音的起始和结束地址  
260         {
261            DisplayOneChar( 1,0,adds[x]/1000+0x30);
262            DisplayOneChar( 1,1,adds[x]/100%10+0x30);
263            DisplayOneChar( 1,2,adds[x]/10%10+0x30);
264            DisplayOneChar( 1,3,adds[x]%10+0x30);
265            DisplayOneChar( 1,4,'-');
266            DisplayOneChar( 1,5,'-');
267            DisplayOneChar( 1,6,adde[x]/1000+0x30);
268            DisplayOneChar( 1,7,adde[x]/100%10+0x30);
269            DisplayOneChar( 1,8,adde[x]/10%10+0x30);
270            DisplayOneChar( 1,9,adde[x]%10+0x30);
271         }
272 }
273 //======================================================================
274 // LCM初始化
275 //======================================================================  
276 void LCMInit(void)
277 {
278  LCM_Data = 0;
279  WriteCommandLCM(0x38,0); //三次显示模式设置,不检测忙信号  
280  DelayMs(5);
281  WriteCommandLCM(0x38,0);
282  DelayMs(5);
283  WriteCommandLCM(0x38,0);
284  DelayMs(5);
285  WriteCommandLCM(0x38,1); //显示模式设置,开始要求每次检测忙信号  
286  WriteCommandLCM(0x08,1); //关闭显示  
287  WriteCommandLCM(0x01,1); //显示清屏  
288  WriteCommandLCM(0x06,1); // 显示光标移动设置  
289  WriteCommandLCM(0x0C,1); // 显示开及光标设置  
290  DelayMs(100);
291 }
292 //*=====================================================================
293 // 写数据函数: E =高脉冲 RS=1 RW=0
294 //======================================================================  
295 void WriteDataLCM(uchar WDLCM)
296 {
297  ReadStatusLCM(); //检测忙  
298  LCM_Data = WDLCM;
299  LCM_RS = 1;
300  LCM_RW = 0;
301  LCM_E = 0; //若晶振速度太高可以在这后加小的延时  
302  LCM_E = 0; //延时  
303  LCM_E = 1;
304 }
305 //*====================================================================
306  // 写指令函数: E=高脉冲 RS=0 RW=0
307 //======================================================================  
308 void WriteCommandLCM(unsigned char WCLCM,BuysC) //BuysC为0时忽略忙检测  
309 {
310  if (BuysC) ReadStatusLCM(); //根据需要检测忙  
311  LCM_Data = WCLCM;
312  LCM_RS = 0;
313  LCM_RW = 0;
314  LCM_E = 0;
315  LCM_E = 0;
316  LCM_E = 1;
317 }
318 //*====================================================================
319 //  正常读写操作之前必须检测LCD控制器状态:E=1 RS=0 RW=1;
320 //  DB7: 0 LCD控制器空闲,1 LCD控制器忙。
321  // 读状态
322 //======================================================================  
323 unsigned char ReadStatusLCM(void)
324 {
325  LCM_Data = 0xFF;
326  LCM_RS = 0;
327  LCM_RW = 1;
328  LCM_E = 0;
329  LCM_E = 0;
330  LCM_E = 1;
331  while (LCM_Data & Busy); //检测忙信号    
332  return(LCM_Data);
333 }
334 //======================================================================
335 //功 能:     在1602 指定位置显示一个字符:第一行位置0~15,第二行16~31
336 //说 明:     第 X 行,第 y 列  注意:字符串不能长于16个字符
337 //======================================================================  
338 void DisplayOneChar( unsigned char X, unsigned char Y, unsigned char ASCII)
339 {
340  X &= 0x1;
341  Y &= 0xF; //限制Y不能大于15,X不能大于1  
342  if (X) Y |= 0x40; //当要显示第二行时地址码+0x40;  
343  Y |= 0x80; // 算出指令码  
344  WriteCommandLCM(Y, 0); //这里不检测忙信号,发送地址码  
345  WriteDataLCM(ASCII);
346 }
347 //======================================================================
348 //spi串行发送子程序,8位数据  
349 void isd_send(uchar isdx)
350 {
351     uchar isx_counter;
352     SS=0;//ss=0,打开spi通信端  
353     SCLK=0;
354     for(isx_counter=0;isx_counter<8;isx_counter++)//先发低位再发高位,依次发送。  
355     {
356         if((isdx&0x01)==1)
357             MOSI=1;
358         else
359             MOSI=0;
360             isdx=isdx>>1;
361             SCLK=1;
362             DelayUs(2);
363             SCLK=0;
364             DelayUs(2);
365     }
366 }
367 //======================================================================
368 //stop指令(停止当前操作)  
369 void isd_stop()//
370 {
371     DelayUs(10);
372     isd_send(0x30);
373     SS=1;
374     DelayMs(50);
375 }
376 //======================================================================
377 //发送上电指令  
378 void isd_powerup()//
379 {
380     DelayUs(10);
381     SS=0;
382     isd_send(0x20);
383     SS=1;
384     DelayMs(50);
385 }
386 //======================================================================
387 //发送掉电指令  
388 void isd_stopwrdn()//
389 {
390     DelayUs(10);
391     isd_send(0x10);
392     SS=1;
393     DelayMs(50);
394 }
395
396 void isd_play()//发送play指令  
397 {
398     isd_send(0xf0);
399     SS=1;
400 }
401 void isd_setplay(uchar adl,uchar adh)//发送setplay指令  
402 {
403     DelayMs(1);
404     isd_send(adl); //发送放音起始地址低位  
405     DelayUs(2);
406     isd_send(adh); //发送放音起始地址高位  
407     DelayUs(2);
408     isd_send(0xe0); //发送setplay指令字节  
409     SS=1;
410 }
411 void DelayUs(uint us)
412 {
413     while(us--);
414 }
415 //====================================================================
416 // 设定延时时间:x*1ms
417 //====================================================================  
418 void DelayMs(uint Ms)
419 {
420   uint i,TempCyc;
421   for(i=0;i<Ms;i++)
422   {
423     TempCyc = 250;
424     while(TempCyc--);
425   }
426 }
427
04-22 20:27