语音通信方案
系统级方案和自建协议
系统级
声卡 (Sound Card)也叫音频卡(港台称之为声效卡),是计算机多媒体系统中最基本的组成部分,是实现声波/数字信号相互转换的一种硬件。声卡的基本功能是把来自话筒、磁带、光盘的原始声音信号加以转换,输出到耳机、扬声器、扩音机、录音机等声响设备,或通过音乐设备数字接口(MIDI)发出合成乐器的声音
所有的电脑主板基本都有集成声卡的,如果有专业要求会再买个独立声卡,就像专业玩家一样买个独立显卡,手动狗头
声卡驱动
对于音频处理的技术,主要有如下几种:
- 采集麦克风输入
- 采集声卡输出
- 将音频数据送入声卡进行播放
- 对多路音频输入进行混音处理
Windows平台内核提供调用声卡API
一、MME(MultiMedia Extensions)
MME就是winmm.dll提供的接口,也是Windows平台下第一代API。优点是使用简单,一般场景下可以满足业务需求,缺点是延迟高,某些高级功能无法实现。
二、XAudio2
也是DirextX的一部分,为了取代DirectSound。DirextX套件中的音频组件,大多用于游戏中,支持硬件加速,所以比MME有更低的延迟。
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.架构图
硬件架构:
软件架构:
3.初识alsa设备
注:
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:
4.linux内核中音频驱动代码分布
其中:
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提供的工具进行查看、
调试。
音频跟视频很不一样,视频每一帧就是一张图像,而从上面的正玄波可以看出,音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取2.5ms~60ms为单位的数据量为一帧音频。
这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的,我们可以计算一下一帧音频帧的大小:
假设某音频信号是采样率为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