北京信息科技大学 嵌入式 IP网络语音通信
- 1 -
光电学院电子信息
专业
“嵌入式信息系统课程
”任务书
目 嵌入式 IP网络语音通信
主要
内容
学习和掌握构建基于 OMAP3530硬件平台和嵌入式 Linux软件开发环境的方法,
掌握嵌入式应用程序设计与开发手段,在此基础上通过 socket 网络编程实现基于嵌入
式 IP网络的双向语音通信。
设计
要求
基本要求:
1、在嵌入式系统环境下,实现本地语音录放;
2、通过 Socket网络编程实现 IP网络双向语音传输。
扩展要求:选作 1个或多个功能,也可以自行设计其它扩展功能。
1、实现基于 G.711
的语音编码(A律 13折线);
2、网络性能及分组大小对通信质量的影响;
3、实现基于 G.721标准的语音编码(ADPCM);
4、实现基于 G.723.1标准的语音编码(码激励线性预测编码)。
主要
仪器
设备
1、OMAP3530嵌入式开发系统 1套
2、计算机 1台,安装 Linux操作系统及 arm-linux交叉编译工具
3、耳麦 1个
4、网线 2根,IP地址 2个
课程设计进度计划(起止时间、工作内容)
第 1周 掌握构建嵌入式 Linux开发环境及开发应用程序的基本方法;理解所选题目要求,
学习相关知识,搭建相应软件和硬件环境;
第 2周 完成基本要求,实现本地声音录放及嵌入式网络环境下的语音通信;
第 3周 进一步完善功能并进行扩展要求设计,现场检查、答辩;
课程设计期间的其它时间实验室也一直开放,需要实验的同学在遵守实验室管理规定的条件
下可以随时进行实验。课程设计报告要求在课程设计结束后一周内提交。
课程设计开始日期 2010-12-6 课程设计完成日期 2010.12.19
课程设计实验室名称 嵌入式网络系统实验室 地 点 实验楼 501-1
课程设计专题网站 http://59.64.74.111/ 资料和软件下载地址 ftp://59.64.74.111/
北京信息科技大学 嵌入式 IP网络语音通信
- 2 -
第一章 ALSA语音编程
一、数字音频
音频信号是一种连续变化的模拟信号,但计算机只能处理和记录二进制的数字信
号,由自然音源得到的音频信号必须经过一定的变换,成为数字音频信号之后,才能送
到计算机中作进一步的处理。
数字音频系统通过将声波的波型转换成一系列二进制数据,来实现对原始声音的重
现,实现这一步骤的设备常被称为模/数转换器(A/D)。A/D转换器以每秒钟上万次的
速率对声波进行采样,每个采样点都记录下了原始模拟声波在某一时刻的状态,通常称
之为样本(sample),而每一秒钟所采样的数目则称为采样频率,通过将一串连续的样
本连接起来,就可以在计算机中描述一段声音了。对于采样过程中的每一个样本来说,
数字音频系统会分配一定存储位来记录声波的振幅,一般称之为采样分辩率或者采样精
度,采样精度越高,声音还原时就会越细腻。
数字音频涉及到的概念非常多,对于在 Linux下进行音频编程的程序员来说,最重
要的是理解声音数字化的两个关键步骤:采样和量化。采样就是每隔一定时间就读一次
声音信号的幅度,而量化则是将采样得到的声音信号幅度转换为数字值,从本质上讲,
采样是时间上的数字化,而量化则是幅度上的数字化。下面介绍几个在进行音频编程时
经常需要用到的技术指标:
1、采样频率
采样频率是指将模拟声音波形进行数字化时,每秒钟抽取声波幅度样本的次数。采
样频率的选择应该遵循奈奎斯特(Harry Nyquist)采样理论:如果对某一模拟信号进行
采样,则采样后可还原的最高信号频率只有采样频率的一半,或者说只要采样频率高于
输入信号最高频率的两倍,就能从采样信号系列重构原始信号。正常人听觉的频率范围
大约在 20Hz~20kHz之间,根据奈奎斯特采样理论,为了保证声音不失真,采样频率应
该在 40kHz左右。常用的音频采样频率有 8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、
44.1kHz、48kHz等,如果采用更高的采样频率,还可以达到 DVD的音质。
2、量化位数
量化位数是对模拟音频信号的幅度进行数字化,它决定了模拟信号数字化以后的动
态范围,常用的有 8位、12位和 16位。量化位越高,信号的动态范围越大,数字化后
的音频信号就越可能接近原始信号,但所需要的存贮空间也越大。
3、声道数
声道数是反映音频数字化质量的另一个重要因素,它有单声道和双声道之分。双声
道又称为立体声,在硬件中有两条线路,音质和音色都要优于单声道,但数字化后占据
的存储空间的大小要比单声道多一倍。
二、声卡驱动
出于对安全性方面的考虑,Linux下的应用程序无法直接对声卡这类硬件设备进行
操作,而是必须通过内核提供的驱动程序才能完成。在 Linux上进行音频编程的本质就
是要借助于驱动程序,来完成对声卡的各种操作。对硬件的控制涉及到寄存器中各个比
特位的操作,通常这是与设备直接相关并且对时序的要求非常严格,如果这些工作都交
北京信息科技大学 嵌入式 IP网络语音通信
- 3 -
由应用程序员来负责,那么对声卡的编程将变得异常复杂而困难起来,驱动程序的作用
正是要屏蔽硬件的这些底层细节,从而简化应用程序的编写。
目前 Linux下常用的声卡驱动程序主要有两种:OSS和 ALSA。最早出现在 Linux
上的音频编程接口是 OSS(Open Sound System),它由一套完整的内核驱动程序模块
组成,可以为绝大多数声卡提供统一的编程接口。OSS出现的历史相对较长,这些内核
模块中的一部分(OSS/Free)是与 Linux内核源码共同免费发布的,另外一些则以二进
制的形式由 4Front Technologies公司提供。由于得到了商业公司的鼎力支持,OSS已经
成为在 Linux下进行音频编程的事实标准,支持 OSS的应用程序能够在绝大多数声卡
上工作良好。
虽然 OSS已经非常成熟,但它毕竟是一个没有完全开放源代码的商业产品,ALSA
(Advanced Linux Sound Architecture)恰好弥补了这一空白,它是在 Linux下进行音频
编程时另一个可供选择的声卡驱动程序。ALSA除了像 OSS那样提供了一组内核驱动
程序模块之外,还专门为简化应用程序的编写提供了相应的函数库,与 OSS提供的基
于 ioctl的原始编程接口相比,ALSA函数库使用起来要更加方便一些。ALSA的主要特
点有:
· 支持多种声卡设备
· 模块化的内核驱动程序
· 支持 SMP和多线程
· 提供应用开发函数库
· 兼容 OSS应用程序
ALSA和 OSS最大的不同之处在于 ALSA是由志愿者维护的自由项目,而 OSS则
是由公司提供的商业产品,因此在对硬件的适应程度上 OSS要优于 ALSA,它能够支
持的声卡种类更多。ALSA虽然不及 OSS运用得广泛,但却具有更加友好的编程接口,
并且完全兼容于 OSS,对应用程序员来讲无疑是一个更佳的选择。
三、 ALSA体系
ALSA 标准是一个先进的 linux 声音体系。它包含内核驱动集合,API 库和工具对
Linux 声音进行支持。ALSA 包含一系列内核驱动对不同的声卡进行支持,还提供了
libasound 的 API 库。用这些进行写程序不需要打开设备等操作,所以编程人员在写程
序的时候不会被底层的东西困扰。与此相反 OSS/Free 驱动在内核层次调用,需要指定
设备名和调用 ioctl。为提供向后兼容,ALSA 提供内核模块模仿 OSS/Free 驱动,所以
大多数的程序不需要改动。 ALSA 拥有调用插件的能力对新设备提供扩展,包括那些
用软件模拟出来的虚拟设备。 ALSA 还提供一组命令行工具包括 mixer, sound file
player 和工具控制一些特别的声卡的特别的作用。
ALSA API 被主要分为以下几种接口:
l 控制接口:提供灵活的方式管理注册的声卡和对存在的声卡进行查询。
l PCM接口:提供管理数字音频的捕捉和回放。
l 原始 MIDI 接口: 支持 MIDI (Musical Instrument Digital Interface), 一种标准
电子音乐指令集。 这些 API 提供访问声卡上的 MIDI 总线。这些原始借口直
接工作在 The MIDI 事件上,程序员只需要管理协议和时间。
北京信息科技大学 嵌入式 IP网络语音通信
- 4 -
l 记时接口: 为支持声音的同步事件提供访问声卡上的定时器。
l 音序器接口:一个比原始MIDI接口高级的MIDI编程和声音同步高层接口。它
可以处理很多的MIDI协议和定时器。
l 混音器接口:控制发送信号和控制声音大小的声卡上的设备。
声卡的缓存和数据的传输:
一块声卡有一个声卡内存用来存储记录的样本。当它被写满时就产生中断。内核驱
动就使用 DMA将数据传输到内存中。同样地,当在播放时就将内存中的声音样本使用
DMA传到声卡的内存中!
声卡的缓存是环状的,这里只讨论应用程序中的内存结构:ALSA将数据分成连续
的片段然后传到按单元片段传输。
四、典型的声音程序结构:
n open interface for capture or playback
n set hardware parameters
n (access mode, data format, channels, rate, etc.)
n while there is data to be processed:
n read PCM data (capture)
n or write PCM data (playback)
n close interface
五、ALSA交叉编译
1、下载源代码包
官方网址位于 http://www.alsa-project.org/main/index.php/Main_Page,可下载最新的源
代码。本地源文件位于/home/user/Project/audio目录下。
2、进入工作目录并解压缩
Host # cd /home/user/Project/audio
北京信息科技大学 嵌入式 IP网络语音通信
- 5 -
3、编译 alsa-lib
编译生成的程序和库位于/home/user/build目录下。
6、编译 alsa-utils
7、测试编译得到的 alsa工具包
拷贝编译得到的/home/user/build目录下各个文件夹里的内容到 SD卡的 EXT3文件
系统中/usr/local中。
8、启动开发板系统并测试
到开发板系统的/usr/local/build/bin目录下测试 alsa-utils工具程序。下载 wav文件到
开发板上,测试 wav文件可以在 Linux或者 windows下用录音机自行生成。
9、结果
可在开发板上听到声音,有兴趣的同学可以自己生成不同格式的 wav文件测试。程
序原理可以看 aplay的源代码。
六、编译采集和回放程序
测试程序位于 mini_app_pc和 mini_app_arm目录下,根据系统环境修改 Rules.make
和Makefile文件分别得到 PC端和 ARM端可执行程序,分别进行测试并分析程序。
Host # tar xvf alsa-lib-1.0.23.tar.bz2
Host # tar xvf alsa-utils-1.0.23.tar.bz2
Host # cd alsa-lib-1.0.23
Host# ./configure --prefix=/home/user/build --host=arm-none-linux-gnueabi
--disable-python
Host# make
Host# sudo make install
Host # cd alsa-utils-1.0.20
Host # ./configure --prefix=/home/user/build --host=arm-none-linux-gnueabi
--disable-xmlto --with-alsa-prefix=/home/user/build/lib
--with-alsa-inc-prefix=/home/user/build/include --disable-alsamixer --
disable-nls
Host# make
Host#sudo make install
Host #sudo mkdir /media/EXT3/usr/local
Host# cd /home/user/
Host#sudo cp -rf build /media/EXT3/usr/local
北京信息科技大学 嵌入式 IP网络语音通信
- 6 -
第二章 网络传输 socket编程指南
一、编程原理
1、套接口的功能
套接口本质上提供了进程间通信的端点。进程通信之前,双方必须首先各自创建一
个套接口。每个套接口拥有一个本地唯一的套接口描述符,由操作系统分配。套接口是
面向 C/S模型设计的。
2、套接口的两种类型
1)流套接口(SOCKET_STREAM):
流套接口提供了可靠的,面向连接的通信流。它使用 TCP协议来保证数据传输的质
量。有关 TCP 协议的内容请参考级网络讲义传输层部分。
2)数据报套接口(SOCK_DGRAM):
数据报套接口定义了一种面向无连接的服务,数据通过相互独立的报文进行传输,
是无序的,不保证可靠、无差错。数据报套接口使用 UDP协议。有关 UDP协议的内容
请参考网络讲义传输层部分。
其它套接口类型请查阅相关网络编程的资料。
3、面向连接和无连接的套接口的#工作
#
这两种类型的工作流程请参考网络讲义的 SOCKET 编程部分。服务器端程序的套
接口需要系统调用 bind 绑定,而对于客户端程序则不是必需的,一般不用绑定,系统
会自动选择一个 1024(不包括 1024)以外未被使用的端口号。因为我们不在乎客户端
程序会使用哪个端口,但我们在乎服务器端的哪个端口能被客户端程序连接。
二、编程接口
1、socket函数
调用 socket()分配一个套接口,该函数返回一个本地唯一的套接口描述符。Socket 声
明如下:
int socket (int domain, int type, int protocol);
其中:
北京信息科技大学 嵌入式 IP网络语音通信
- 7 -
第一个参数 domain 指定使用何种地址类型,一般是 AF_INET,其它值可在
/usr/include/sys/socket.h中找到。
第二个参数 type 的值一般使用如下两种:
SOCK_STREAM------提供了一个可靠的的顺序的双向连接
SOCK_DGRAM ------提供了无连接不可靠的连接
其它值可在/usr/include/sys/types.h查看。
第三个参数 protocol 设为 0 即可。
如果调用成功,返回一个套接口描述符,即一个整型值;如果发生错误,返回-1,
全局变量 errno将被设置为错误代码。(请参考 man 3 perror。)
更详细信息请 man 2 socket 一下。
2、bind函数
bind()函数将你刚刚分配的一个套接口与一个端口号绑定,当然,你可以指定在范
围 1025~65535(2^16-1)内的任意数值。1~1024 的端口号保留系统使用。那什么时候
需要绑定呢?当你需要进行端口监听 listen 操作, 等待接受一个连接请求的时候,就要
进行端口绑定。bind()的声明如下:
int bind (int sockfd, const struct sockaddr *my_addr,int addrlen);
第一个参数 sockfd是由 socket()函数返回的套接口描述符。
第二个参数 my_addr是一个指向 struct sockaddr 的指针。该指针指向的变量包含有关
的地址信息:名称、端口和 IP地址。struct sockddr请参考数据结构部分。
第三个参数 addrlen是数据结构 sockaddr的长度,可设置为 sizeof(struct sockaddr)。
如果发生错误,返回-1,全局变量 errno将被设置为错误代码。
更多请参考 man 2 bind。
3、listen函数
对于服务器程序,当把套接口与某一端口绑定后,可调用 listen()监听该端口是否有
连接请求。该函数的声明如下:
int listen (int sockfd,int backlog);
第一个参数 sockfd是前面调用 bind()绑定后的套接口描述符。
第二个参数 backlog是连接请求队列的最大请求连接数。
如果发生错误,返回-1,全局变量 errno将被设置为错误代码。
更多请参考 man 2 listen 。
4、accept函数
与 listen 一样,accept()也是用于服务器程序。accept() 用于接收远程连接请求,用
于建立基于 TCP的流套接口的连接。其声明如下:
北京信息科技大学 嵌入式 IP网络语音通信
- 8 -
int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen);
第一个参数 sockfd是被监听的套接口的描述符。
第二个参数 addr 是无类型指针,一般指向一个 struct sockaddr_in 结构的变量;该
变量存储有远程连接过来的计算机信息。
第三个参数 addrlen是一个指向整型变量的指针。该整型变量设置了 their_addr所能
容纳的最大字节数,这个变量的值一般是 sizeof(struct sockaddr_in)。
如果调用成功,accept 返回一个新的套接口描述符,用于与远程进程通信,原来的
套接口描述符仍然在原来的端口的 listen()上;如果调用失败,返回-1,全局变量 errno
被设为错误代码。
更多请看 man 2 accpet 。
5、connect函数
系统调用 connect 用于将本地的套接口与远程的套接口连接起来,完成建立基于
TCP 的流套接口的连接。其声明如下:
int connect (int sockfd, const struck sockaddr *server_addr, int addrlen);
第一个参数 sockfd 是套接口描述符,由 socket()函数返回。
第二个参数 server_adddr是指针,指向存储有远程计算机的 IP地址和端口号信息的
结构。
第三个参数 addrlen是数据结构的大小,应该是 sizeof(struct sockaddr)。
如果调用发生错误,返回-1,全局变量 errno将会存储错误代码。
更多请看 man 2 connect 。
6、send和 recv函数
这两个函数是通过连接的流套接口进行通信的函数。
send 是用于通过套接口向其它进程发送数据的函数,其声明如下:
ssize_t send(int s, const void *buf, size_t len, int flags);
第一个参数 s是必须是已经连接到远程计算机的一个端口上的套接口描述符。
第二个参数 buf是指向内存块的指针,此内存块用来存储要发送的信息。
第三个参数 len表示发送信息的长度。
第四个参数 flag是操作标志,一般设为 0。
如果调用成功,返回发送出去的数据的真正长度;如果失败,返回-1,全局变量 errno
存储错误代码。
更多请看 man 2 send 。
Recv是用于接收从已经连接的套接口传来的数据,其声明如下:
北京信息科技大学 嵌入式 IP网络语音通信
- 9 -
ssize_t recv(int s, void *buf, size_t len, int flags);
第一个参数 s必须是已经连接到远程计算机的端口上的套接口描述符。
第二个参数 buf是指向内存块的指针,此内存块用于存储接收到的数据。
第三个参数 len是 buf指向的内存块的大小。
第四个参数 flags是操作标志,一般设为 0。
如果调用成功,返回接受到的实际数据长度;如果调用失败,返回-1,全局变量 errno
存储错误代码。
更多请看 man 2 recv 。
7、sendto函数和 recvfrom函数
这两个函数是进行无连接的 UDP 通信时使用的。因为传输之前没有建立连接,所
以发送数据时,要知道远程主机的 IP地址和端口号。
1)sendto函数
用于接收数据。其声明如下:
ssize_tsendto(ints,constvoid*buf,size_t len, int flags, const struct sockaddr *to, socklen_t
tolen);
sendto与 send函数基本上一致,只是多加了两个存储有远程主机地址信息的参数。
第一个参数 s是 socket()调用返回的套接口描述符。
第二个参数 buf是个指针,指向要发送的数据的地址。
第三个参数 len是要发送数据的长度。
第四个参数 flags是操作标志,一般设为 0。
第五个参数 to是个指针,指向存储有远程主机地址信息的数据结构。
第六个参数 tolen是数据结构 sockaddr 的大小,为 sizeof(struct sockaddr)。
如果调用成功,返回发送的数据的实际长度;如果失败,返回-1,全局变量 errno
存储错误代码。
更多请看 man 2 sendto.
2)recvfrom函数
同样,recvfrom与 recv 也基本一致。其声明如下:
ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t
*fromlen);
第一个参数 s是 socket()调用返回的套接口描述符。
第二个参数 buf是个指针,指向能存储数据的内存缓冲区。
第三个参数 len是缓冲区的最大长度。
第四个参数 flags是操作标志,一般设为 0。
第五个参数 from是个指针,指向存储源主机地址信息的数据结构 struct sockaddr。
第六个参数 fromlen是个整型指针,该整型变量的值为 sizeof(struct sockaddr)。
如果调用成功,返回接收到的数据字节数;如果失败,返回-1,全局变量 errno 存储错
误代码。
更多请看 man 2 recvfrom 。
北京信息科技大学 嵌入式 IP网络语音通信
- 10 -
8、close函数
参考语音编程部分。
9、gethostbyname函数和 herror函数
1)gethostbyname函数
gethostbyname() 接受点分形式的 IP 地址或实际的主机名,执行域名服务器查
询并返回一个结构指针,这个结构包含了 sockaddr_in 结构所期望的那种形式的 IP
地址数据。其声明如下:
struct hostent *gethostbyname(const char *name);
如果调用成功,返回一个结构指针;如果失败,返回一个 NULL指针,全局变
量 h_errno存储错误代码。
2)herror函数
void herror(const char *s);
herror()函数是在标准错误输出上打印与变量 h_error的当前值有关的错误信息。
更多请看 man 3 gethostbyname 。
10、bzero函数
bzero()将一段内存的内容清零。其声明如下:
void bzero(void *s, size_t n);
第一个参数 s是指向一个内存块的指针。
第二个参数 n是要清零的内存块大小。
该函数无返回值。
更多请看 man 3 bsero 。
11、fgets函数
fgets()从文件中读取一个字符串并存到指定的内存空间,直到遇到换行符、读到文
件尾或者已读入了 size-1个字符为止,最后加上 NULL作为字符串结束。其声明如下:
char *fgets(char *s, int size, FILE *stream);
第一个参数 s是一个指向一个内存块的指针,该内存块用于存储从 stream读出的数
据。
如果调用成功,返回指针 s;如果失败,返回 NULL。
更多请看 man 3 fgets 。
12、转换函数:htonl(), htons(), inet_ntoa()
1)htonl
北京信息科技大学 嵌入式 IP网络语音通信
- 11 -
htonl()将参数指定的 32位主机字节顺序数据转换成网络字节顺序。其声明如下:
uint32_t htonl(uint32_t hostlong);
参数是一个主机字节顺序的 32位数据。该函数返回对应的网络字节顺序。
更多请看 man 3 htonl 。
2)htons
htons()与 htonl()相似,用来将 16位的主机字节顺序的数据转换成网络字节顺序。
其声明如下:
uint16_t htons(uint16_t hostshort);
参数指定要转换的 16位数据。该函数返回对应的网络字节顺序的数据。
更多请看 man 3 htons 。
3)inet_ntoa
其声明如下:
char *inet_ntoa (srtuct in_addr in);
函数 inet_ntoa()将网络字节顺序的 Internet主机地址 in转换成标准的点分形式的
地址字符串。
如果调用成功,返回一个字符串指针:如果失败,返回 NULL。
更多请看 man 3 inet_ntoa 。
13、以上编程接口函数用到的数据结构
struct sockaddr{
unsigned short sa_family;
char sa_data[14];
};
sa_family是地址族,一般是 AF_INET.
sa_data包含了远程主机的地址、端口号和套接口的数目,它里面的数据是杂溶在一
起的。
该数据结构必须被分配并作为 bind()的第二个参数,但在程序中不能直接访问。
为了访问 struct sockaddr,建立了另外一个相似的数据结构
struct sockaddr_in{
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
sin_family与 sa_family相同,设为 AF_INET。
sin_port是端口号。
sin_addr是 IP地址。
sin_zero[8]是为了使 struct sockaddr_in和 struct sockaddr在内存中具有相同的大小,
使用 struct sockaddr_in 时要把 sin_zero[8]全部设成 0(使用 bzero()函数或 memset())。
需要注意的是 sin_port 和 sin_addr一定要是网络字节顺序。
北京信息科技大学 嵌入式 IP网络语音通信
- 12 -
14、对字节顺序的简单解释
因为每一个机器内部对变量的字节顺序不同(有的高位在前,地位在后,而有的地
位在前,高位在后。),而网络传输的数据一定要有一个统一的顺序。所以一定要对内部
机器字节顺序和网络字节顺序不同的机器进行数据的转换。对与内部机器字节顺序和网
络字节顺序相同的,也要调用转换函数,但要不要转换由转换函数自己决定。
struct in_addr{
unsigned long s_addr;
};
struct hostent{
char *h_name;
char * *h_aliases;
int h_addrtype;
in h_length;
char **h_addr_list; /* for backward compatibility */
};
#define h_addr h_addr_list[0]
h_name是这个主机的正式名字。
h_aliases是一个以 NULL(空字符)结尾的数组,存储了主机的备用名字。
h_addrtype是返回地址的类型,一般是 AF_INET.
h_length是地址的字节长度。
h_addr_list是一个以 0结尾的数组,存储了主机的网络地址。
三、示例程序
Socket示例程序位于/home/user/Project/audio/目录下,或者可以到
ftp://59.64.74.111/10embedded/ 目录下载。