音频接口程序设计.docx
《音频接口程序设计.docx》由会员分享,可在线阅读,更多相关《音频接口程序设计.docx(16页珍藏版)》请在冰豆网上搜索。
音频接口程序设计
音频接口程序设计
南京大学
黄开成101180046
2013.1.2
摘要
本实验通过对两个音频设备dsp和mixer的编程,学习了与数字音频相关的几个重要技术指标(量化位数、采样频率、声道数),完成了对音频文件的播放和录放音并且通过线程实现了对音量的实时控制。
此外通过输入正弦波观察输出到示波器和framebuffer的波形,验证了奈奎斯特采样理论。
巩固了嵌入式开发中驱动程序、系统调用和应用程序的关系。
一、实验目的
1、了解音频编、解码的作用和工作原理。
2、学习Linux系统的音频接口编程方法。
二、原理概述
1、数字音频
音频信号是一种连续变化的模拟信号,但计算机只能处理和记录二进制的数字信号,因而由自然音源得到的音频信号必须经过一定的变换,成为数字音频信号之后才能送到计算机中做进一步的处理。
模拟音频信号数字化的典型方法是对时间坐标按相等的时间间隔做采样,对振幅做量化。
从本质上讲,采样是时间上的数字化,而量化则是幅度上的数字化。
数字化的过程中涉及到在编程过程中需要设置的三个重要的技术指标:
(1)采样频率
单位时间内的采样次数称为采样频率。
依据奈奎斯特采样理论:
只有采样频率高于输入信号最高频率的两倍才能从采样信号序列中重构原始信号。
正常人听觉的频率范围大约在20Hz~20kHz之间,因此为保证声音不失真,采样频率应该在40kHz左右。
常用的音频采样频率有8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等。
如果采用更高的采样频率,还可以达到DVD的音质。
(2)量化位数
数字化模拟音频信号的幅度时决定模拟信号数字化以后的动态范围,常用的有8位、12位和16位。
量化位越高,信号的动态范围越大,数字化后的音频信号就越可能接近原始信号,但所需要的存储空间也越大。
(3)声道数
声道数是反映音频数字化质量的另一个重要因素,有单声道、双声道和多声道之分。
双声道又称为立体声,在硬件中有两条线路,音质和音色都要优于单声道,但数字化后占据的存储空间的大小要比单声道多一倍。
多声道能提供更好的听觉感受,不过占用的存储空间更大。
2、音频设备文件
在8253驱动编程和IO接口实验中我们需要自己编写驱动程序。
而本次实验中,只要在内核编译时选中Sound-->Soundsupport和IntelPXA27xAC97audio就可以将驱动编译进内核,我们只要通过阅读内核目录/usr/include/linux下的soundcard.h头文件就可以完成应用程序的编写。
应用程序可以通过驱动程序提供的系统调用来对以下的音频设备文件进行操作:
/dev/dsp(主设备号14,次设备号3):
负责音频数据的输入和输出(即A/D、D/A),工作方式设置(采样/输出频率、通道数、数据格式等)。
简单而言就是向该设备写数据(write)即意味着激活声卡上的D/A转换器进行放音,而从该设备读数据(read)则意味着激活声卡上的A/D转换器进行录音。
/dev/mixer(主设备号14,次设备号0),将多个信号组合或者叠加在一起,对于不同的声卡来说,其混音器的作用可能各不相同。
通常负责混音、音量设置、音源、高低音等等。
三、实验过程
1.Dsp编程
在从dsp设备读取数据时,从声卡输入的模拟信号经过A/D转换器变成数字采样后的样本,保存在声卡驱动程序的内核缓冲区中,当应用程序通过read系统调用从声卡读取数据时,保存在内核缓冲区中的数字采样结果将被复制到应用程序所指定的用户缓冲区中。
需要指出的是,声卡采样频率是由内核中的驱动程序所决定的,而不取决于应用程序从声卡读取数据的速度。
如果应用程序读取数据的速度过慢,以致低于声卡的采样频率,那么多余的数据将会被丢弃;如果读取数据的速度过快,以致高于声卡的采样频率,那么声卡驱动程序将会阻塞那些请求数据的应用程序,直到新的数据到来为止。
在向dsp设备写入数据时,数字信号会经过D/A转换器变成模拟信号,然后产生出声音。
应用程序写入数据的速度同样应该与声卡的采样频率相匹配,过慢的话会产生声音暂停或者停顿的现象,而过快的话又会被内核中的声卡驱动程序阻塞,直到硬件有能力处理新的数据为止。
(1)打开dsp设备
fd_dsp=open("/dev/dsp",O_RDWR);
//以读写的方式打开/dev/dsp,以便同时进行声音的输入和输出
if(fd_dsp<0){printf("openof/dev/dspfailed!
\n");exit
(1);}
(2)量化位数、声道数、采样频率的设置
查看soundcard.h可知,声卡默认为8位无符号数据、单声道、8KHz采样率。
有关三个参数设置的宏有:
SNDCTL_DSP_SETFMT声卡采样格式
SNDCTL_DSP_STEREO声卡工作时的声道数目
SNDCTL_DSP_SPEED声卡采样频率
还有一些别名的定义:
/*Somealiasnames*/
#defineSOUND_PCM_WRITE_BITSSNDCTL_DSP_SETFMT
#defineSOUND_PCM_WRITE_CHANNELSSNDCTL_DSP_CHANNELS
#defineSOUND_PCM_WRITE_RATESNDCTL_DSP_SPEED
所以程序中使用SOUND_PCM_WRITE_RATE或者SNDCTL_DSP_SPEED来设置采样频率是一致的。
实际编程如下:
#defineRate44100
#defineSize16
#defineChannel2
arg=Size;//量化位数
status=ioctl(fd_dsp,SOUND_PCM_WRITE_BITS,&arg);
if(status==-1||arg!
=Size)printf("Unabletosetsamplesize\n");
arg=Channel;//设置声道数
status=ioctl(fd_dsp,SOUND_PCM_WRITE_CHANNELS,&arg);
if(status==-1||arg!
=Channel)printf("Unabletosetchannel\n");
arg=Rate;//设置采样频率
status=ioctl(fd_dsp,SOUND_PCM_WRITE_RATE,&arg);
if(status==-1||arg!
=Rate)printf("Unabletosetrate\n");
即通过ioctl()系统调用来设置。
2.Mixer编程
混音器电路通常由两个部分组成:
输入混音器和输出混音器。
输入混音器负责从多个不同的信号源接收模拟信号,这些信号源有时也被称为混音通道或者混音设备。
模拟信号通过增益控制器和由软件控制的音量调节器后,在不同的混音通道中分别进行调制,然后被送到输入混音器中进行声音的合成。
混音器上的电子开关可以控制不同通道中的信号与混音器相连,有些声卡只允许连接一个混音通道作为录音的音源,而有些声卡则允许对混音通道做任意的连接。
经过输入混音器处理后的信号仍然为模拟信号,它们将被送到A/D转换器进行数字化处理。
输出混音器的工作原理与输入混音器类似,同样也有多个信号源与混音器相连,并且事先都经过了增益调节。
当输出混音器对所有的模拟信号进行混合之后,通常还会有一个总控增益调节器来控制输出声音的大小,此外,还有一些音调控制器来调节输出声音的音调。
经过输出混音器处理后的信号也是模拟信号,它们最终会被送给喇叭或者其他的模拟输出设备。
查看soundcard.h文件,可以看到几个主要的用于混音器控制命令的宏:
#defineSOUND_MIXER_NRDEVICES25//获取设备数量
#defineSOUND_MIXER_VOLUME0//总音量设置,其取值范围为0~100
#defineSOUND_MIXER_BASS1//低音设置
#defineSOUND_MIXER_TREBLE2//高音设置
#defineSOUND_MIXER_PCM4//主D/A转换器
#defineSOUND_MIXER_SPEAKER5//PC喇叭
#defineSOUND_MIXER_LINE6//音频线输入
#defineSOUND_MIXER_MIC7//麦克风输入
#defineSOUND_MIXER_CD8//CD输入
#defineSOUND_MIXER_ALTPCM10//从D/A转换器
#defineSOUND_MIXER_RECLEV11//录音音量
#defineSOUND_MIXER_IGAIN12//输入增益
#defineSOUND_MIXER_OGAIN13//输出增益
#defineSOUND_MIXER_LINE114//声卡的第1输入
#defineSOUND_MIXER_LINE215//声卡的第2输入
#defineSOUND_MIXER_LINE316//声卡的第3输入
在进行混音器编程时,可以使用如下方式来读取混音通道的增益大小:
MIXER_READ(dev)——读取混音通道的增益大小
MIXER_WRITE(dev)——设置混音通道的增益大小
其中dev可以是具体的通道名称或是soundcard.h所提供的名称后面对应的数字。
例如在获取麦克风的输入增益时,可以使用如下的代码:
fd_mixer=open("/dev/mixer",O_RDONLY);
if(fd_mixer<0){printf("openof/dev/mixerfailed!
\n");exit
(2);}
intvol;
ioctl(fd_mixer,MIXER_READ(SOUND_MIXER_MIC),&vol);
printf("Micgainis%d%%\n",vol);
对于只有一个混音通道的单声道设备来说,返回的增益大小保存在低字节中。
而对于支持多个混音通道的双声道设备来说,返回的增益大小实际上包括两个部分,分别代表左、右两个声道的值,其中低位字节保存左声道的音量,而高位字节则保存右声道的音量。
类似的,设置麦克风的输入增益时,可以使用如下代码:
vol=(right<<8&0xff00)|(left&0x00ff);
ioctl(fd_mixer,MIXER_WRITE(SOUND_MIXER_MIC),&vol);
此外,因为不同的声卡所提供的混音器资源是有所区别的,所以需要检查混音设备的有效性,声卡驱动程序提供了多个ioctl系统调用来获得混音器的信息,它们通常返回一个整型的位掩码,其中每一位分别代表一个特定的混音通道,如果相应的位为1,则说明与之对应的混音通道是可用的。
soundcard.h文件提供了如下的宏:
SOUND_MIXER_READ_DEVMASK获取的位掩码指示可用混音通道
SOUND_MIXER_READ_RECMASK获取的位掩码指示可作为录音源的通道
例如,检查麦克风输入是否是一个有效的混音通道:
ioctl(fd_mixer,MIXER_READ(SOUND_MIXER_DEVMASK),&devmask);
if(devmask&SOUND_MIXER_MIC)printf("TheMICinputissupported");
在上文中已经提到,每个混音通道都有一个相应的数字与之对应,代码中将位掩码与SOUND_MIXER_MIC相与(即和7相与),返回值非零的话就说明该混音通道是有效的。
检查麦克风输入是否一个有效的录音源:
ioctl(fd_mixer,MIXER_READ(SOUND_MIXER_RECMASK),&recmask);
if(recmask&SOUND_MIXER_MIC)printf("TheMICinputcanbearecordingsource");
实际编程如下:
intleft=80,right=80;
vol=(right<<8&0xff00)|(left&0x00ff);//设置混音器mixer音量
ioctl(fd_mixer,MIXER_WRITE(SOUND_MIXER_VOLUME),&vol);
vol=88;//主D/A转换器
ioctl(fd_mixer,MIXER_WRITE(SOUND_MIXER_PCM),&vol);
vol=100;//麦克风输入
ioctl(fd_mixer,MIXER_WRITE(SOUND_MIXER_MIC),&vol);
vol=100;//输入增益
ioctl(fd_mixer,MIXER_WRITE(SOUND_MIXER_IGAIN),&vol);
3.终端重定向实现录放音功能
对声卡的操作可以简单的通过设备文件节点/dev/dsp来进行。
#cat/dev/dsp>voice(录音)
^c
#catvoice>/dev/dsp(放音)
能够不失真的将录入的声音放出来。
将wav文件重定向到/dev/dsp如下:
#cathello.wav>/dev/dsp能够播放hello.wav文件。
关于重定向:
利用shell重定向操作符,用户可以更改从何处得到输入以及把输出发送到哪里的命令。
输出重定向允许用户把进程的输出保存在文件中,然后用户就可以对此进行编辑等操作。
4.编写程序实现录放音功能
(1)录音放音
前面已经说过向/dev/dsp写数据(write)即意味着激活声卡上的D/A转换器进行放音,而从/dev/dsp读数据(read)则意味着激活声卡上的A/D转换器进行录音。
因而实际编程如下:
unsignedcharbuf[Lenth*Rate*Size*Channel/8];//用于保存录音数据的内存缓冲区
printf("\nSaysomething:
\n");
status=read(fd_dsp,buf,sizeof(buf));//read,即录音
if(status!
=sizeof(buf))
perror("\nreadwrongnumberofbytes");
printf("Yousaid:
\n");
status=write(fd_dsp,buf,sizeof(buf));//write,即放音
//在继续录音前等待录音结束
if(status!
=sizeof(buf))
perror("\nwrotewrongnumberofbytes");
status=ioctl(fd_dsp,SOUND_PCM_SYNC,0);//等待声卡把缓冲区的内容播放完
if(status==-1)
perror("\nSOUND_PCM_SYNCioctlfailed");
程序实现了正常的录音和放音。
(2)播放wav文件
Wav是一种古老的音频文件格式,由微软开发,应用非常广泛。
它使用三个参数来表示声音:
采样位数、采样频率和声道数。
Wav都有一个文件头,这个文件头音频流的编码参数。
Wav格式文件所占容量(KB)=(取样频率*量化位数*声道)*时间/8,其大小不随音量大小及清晰度的变化而变化。
实现代码如下:
unsignedcharbuffer[100];FILE*wav;
wav=fopen("hello.wav","r");//把音频文件打开为文件流
puts("opensuccessed\n");
while(!
feof(wav))
{fread(buffer,sizeof(buffer),1,wav);//将文件流读入缓冲区
write(fd_dsp,buffer,sizeof(buffer));}//write,即播放音频文件
其中feof(FILE*stream)为测试文件尾函数,当文件读完时返回非零值。
程序实现了歌曲的播放。
此时联想到播放器应该能够控制音量,于是利用ioctl对/dev/dsp进行设置,用按键'4'、'5'代表+-音量实现调节音量功能。
起初我把按键识别放在主函数的两个case中,程序实现的现象是在放音的过程中按下‘4’,音量并没有马上增加而是直到下一次放音的时候音量才增加。
这与设计初衷在任何时刻按下键音量就能够得到改变不同。
检查程序逻辑确实不能使音量马上改变,于是决定用多线程来实现这一功能。
实现程序如下:
intvol_set(intleft,intright)//调节音量子函数
{intarg;intret_val;intfd_mixer;
fd_mixer=open("/dev/mixer",O_RDWR);
arg=(right<<8&0xff00)|(left&0x00ff);
ret_val=ioctl(fd_mixer,MIXER_WRITE(SOUND_MIXER_VOLUME),&arg);
returnret_val;}
voidkey()
{charc;while
(1)
{c=getchar();switch(c)
{case'4':
{printf("theleftvolumeis%d%%,therightis%d%%\n",left,right);
if(left<100)left+=10;
elseprintf("warn:
highestvolume");
if(right<100)right+=10;
elseprintf("warn:
highestvolume");
vol_set(left,right);
printf("theleftvolumeis%d%%,therightis%d%%\n",left,right);}break;
case'5':
{printf("theleftvolumeis%d%%,therightis%d%%\n",left,right);
if(left>0)left-=10;
elseprintf("warn:
lowestvolume");
if(right>0)right-=10;
elseprintf("warn:
lowestvolume");
vol_set(left,right);
printf("theleftvolumeis%d%%,therightis%d%%\n",left,right);}break;}}}
intmain()
{pthread_tkeyscanf;//保存进程号
pthread_create(&keyscanf,NULL,(void*)key,NULL);//创建一个进程
}
查看pthread_create的声明:
intpthread_create(pthread_t*thread,constpthread_attr_t*attr,
void*(*start_routine)(void*),void*arg);
其中thread用于返回创建的线程的ID;attr用于指定的被创建的线程的属性,上面的函数中使用NULL,表示使用默认的属性;start_routine是一个函数指针,指向线程被创建后要调用的函数;arg用于给线程传递参数,在上面的函数中没有传递参数,所以使用了NULL。
注意:
在头文件要加上#include
在编译时要加上-lpthread
在目标机终端#ln-s/test/libpthread-o.10.so/lib(即动态链接libpthread-o.10.so)最终实现的结果是在任意时刻按下‘4’或者‘5’都能够立即改变音量,并且在屏幕上显示改变前和改变后两个声道的百分比音量。
把录放音、播放wav文件以及实时控制音量和退出写在同一个程序中,见附程序。
4.用信号源输出正弦波接到mic端并用示波器观察mic端和输出端波形
在设置dsp时采样频率为44100Hz,双通道。
测得当输入峰峰值超过250mVpp时输出波形出现饱和失真。
思考原因如下:
声卡输入端有放大器将输入信号放大,当输入信号过大可能导致声卡输入前级放大器饱和也可能导致A/D转换时超过了转换的电压范围而最终使波形失真。
调节输入频率,当输入频率超过22kHz时输出波形有严重失真。
这也验证了奈奎斯特采样理论。
5.利用framebuffer显示输入波形
由于前阶段做了framebuffer图形用户界面,所以在这里把输入波形画出来并不困难,只需将buf的数据传递给画点函数即可。
在录音中加上如下程序:
printf("\nSaysomething:
\n");
status=read(fd,buf,sizeof(buf));//录音
memset(fbp,0,screensize);//清屏
for(x=0;x<640;x+=2)
{draw_point(x,240+buf[i]/100,0xffff);//画点函数
i=i+2;}//双声道,只取一个声道
注:
此时framebuffer设备打开,地址映射,画点函数的编写都已写在程序中,不再赘述。
成功画出波形如下图:
注意的是这里只是画出了波形,但并没有幅值上的对应关系。
附完整程序
#include
#include
#include
#include
#include
#include
#include
#include
#include
#defineLenth10
#defineRate44100
#defineSize16
#defineChannel2
unsignedcharbuf[Lenth*Rate*Size*Channel/8];//用于保存录音数据的内存缓冲区
unsignedcharbuffer[100];
charchoice=0;
intflag_stop=1;
intflag=1;
intleft=80,right=80;
intvol_set(intleft,intright)//音量设置函数
{
intarg;
intret_val;
intfd_mixer;
fd_mixer=open("/dev/mixer",O_RDWR);
arg=(right<<8&0