语音通信方案
系统级方案和自建协议
系统级
声卡 (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