在Linux下,音频设备程序的实现与文件系统的操作密切相关。Linux将各种设备以文件的形式给出统一的接口,这样的设计使得对设备的编程与对文件的操作基本相同,对Linux内核的系统调用也基本一致,从而简化了设备编程。
如何对各种音频设备进行操作是在Linux上进行音频编程的关键,通过内核提供的一组系统调用,应用程序能够访问声卡驱动程序提供的各种音频设备接口,这是在Linux下进行音频编程最简单也是最直接的方法。
声卡不是Linux控制台的一部分,它是一个特殊的设备。声卡主要提供3个重要的特征:
- 数字取样输入/输出
- 频率调制输出
- MIDI接口
这3个特征都有它们自己的设备驱动程序接口
- 数字取样的接口是/dev/dsp
- 频率调制的接口/dev/sequencer
- MIDI接口是/dev/midi
混音设备(如音量、平衡或者贝斯)可以通过/dev/mixer接口来控制。
为了满足兼容性的需要,还提供了一个/dev/audio设备,该设备可用于读SUN_law的声音数据,但它是映射到数字取样设备的。
1、音频编程接口
程序员可以使用ioctl()来操作这些设备,ioctl()请求是在linux/soundcard.h中定义的,它们以SNDCTL_开头。
- 首先使用open系统调用建立起与硬件间的联系,此时返回的文件描述符将作为随后操作的标识;
- 接着使用read系统调用从设备接收数据,或者使用write系统调用向设备写入数据,而其他所有不符合读/写这一基本模式的操作都可以由ioctl系统调用来完成;
- 最后,使用close系统调用告诉Linux内核不会再对该设备做进一步的处理。
1.1.open系统调用
系统调用open可以获得对声卡的访问权,同时还能为随后的系统调用做好准备,其函数原型如下所示:
int open(const char *pathname, int flags, int mode);
- 参数pathname是将要被打开的设备文件的名称,对于声卡来讲一般是/dev/dsp。
- 参数flags用来指明应该以什么方式打开设备文件,它可以是O_RDONLY、O_WRONLY或者O_RDWR,分别表示以只读、只写或者读写的方式打开设备文件;
- 参数mode通常是可选的,它只有在指定的设备文件不存在时才会用到,指明新创建的文件应该具有怎样的权限。如果open系统调用能够成功完成,它将返回一个正整数作为文件标志符,在随后的系统调用中需要用到该标志符。
如果open系统调用失败,它将返回-1,同时还会设置全局变量errno,指明是什么原因导致了错误的发生。
1.2.read系统调用
read用来从声卡读取数据,其函数原型如下所示:
int read(int fd, char *buf, size_t count);
- 参数fd是设备文件的标志符,它是通过之前的open系统调用获得的;
- 参数buf是指向缓冲区的字符指针,它用来保存从声卡获得的数据;
- 参数count则用来限定从声卡获得的最大字节数。
如果read系统调用成功完成,它将返回从声卡实际读取的字节数,通常情况会比count的值小一些;如果read系统调用失败,它将返回-1,同时还会设置全局变量errno,来指明是什么原因导致了错误的发生。
1.3.write系统调用
write用来向声卡写入数据,其函数原型如下所示:- 参数fd同样是设备文件的标志符,它也是通过之前的open系统调用获得的;
- 参数buf是指向缓冲区的字符指针,它保存着即将向声卡写入的数据;
- 参数count则用来限定向声卡写入的最大字节数。
1.4.ioctl系统调用
系统调用ioctl可以对声卡进行控制,凡是对设备文件的操作不符合读/写基本模式的,都是通过ioctl来完成的,它可以影响设备的行为,或者返回设备的状态,其函数原型如下所示:- 参数fd是设备文件的标志符,它是在设备打开时获得的,如果设备比较复杂,那么对它的控制请求相应地也会有很多种,
- 参数request的目的就是用来区分不同的控制请求;
1.5.close系统调用
当应用程序使用完声卡之后,需要用close系统调用将其关闭,以便及时释放占用的硬件资源,其函数原型如下所示:- 参数fd是设备文件的标志符,它是在设备打开时获得的。
2、音频设备文件
- /dev/sndstat
- /dev/dsp
- /dev/audio
- /dev/mixer
- /dev/sequencer
3、音频设备编程设计
3.1.DSP编程
int handle = open("/dev/dsp", O_WRONLY); if (handle == -1) { perror("open /dev/dsp"); return -1; }
运行在Linux内核中的声卡驱动程序专门维护了一个缓冲区,其大小会影响到放音和录音时的效果,使用ioctl系统调用可以对它的尺寸进行恰当的设置。调节驱动程序中缓冲区大小的操作不是必需的,如果没有特殊的要求,一般采用默认的缓冲区大小就可以了。但需要注意的是, 缓冲区大小的设置通常应紧跟在设备文件打开之后,这是因为对声卡的其他操作有可能会导致驱动程序无法再修改其缓冲区的大小。下面的代码示范了怎样设置声卡驱动程序中的内核缓冲区的大小:
int setting = 0xnnnnssss; int result = ioctl(handle, SNDCTL_DSP_SETFRAGMENT, &setting); if (result == -1) { perror("ioctl buffer size"); return -1; }在设置缓冲区大小时,参数setting实际上由两部分组成,其低16位标明缓冲区的尺寸,相应的计算公式为buffer_size = 2^ssss,即若参数setting低16位的值为16,那么相应的缓冲区的大小会被设置为65536字节。参数setting的高16位则用来标明分片(fragment)的最大序号,它的取值范围从2到0x7FFF,其中0x7FFF表示没有任何限制。
int channels = 0; // 0=mono 1=stereo int result = ioctl(handle, SNDCTL_DSP_STEREO, &channels); if ( result == -1 ) { perror("ioctl channel number"); return -1; } if (channels != 0) { // 只支持立体声 }
采样格式和采样频率是在进行音频编程时需要考虑的另一个问题,声卡支持的所有采样格式可以在头文件soundcard.h中找到,而通过ioctl系统调用则可以很方便地更改当前所使用的采样格式。下面的代码示范了如何设置声卡的采样格式。
int format = AFMT_U8; int result = ioctl(handle, SNDCTL_DSP_SETFMT, &format); if ( result == -1 ) { perror("ioctl sample format"); return -1; }
声卡采样频率的设置也非常容易,只需在调用ioctl时将第二个参数的值设置为SNDCTL_ DSP_SPEED,同时在第三个参数中指定采样频率的数值就行了。对于大多数声卡来说,其支持的采样频率范围一般为5kHz到44.1kHz或者48kHz,但并不意味着该范围内的所有频率都会被硬件支持,在Linux下进行音频编程时最常用到的几种采样频率是11025Hz、16000Hz、22050Hz、32000Hz和44100Hz。下面的代码示范了如何设置声卡的采样频率。
int rate = 22050; int result = ioctl(handle, SNDCTL_DSP_SPEED, &rate); if ( result == -1 ) { perror("ioctl sample format"); return -1; }
3.2.录音与回放
#include<unistd.h> #include<fcntl.h> #include<sys/types.h> #include<sys/ioctl.h> #include<linux/soundcard.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #define DEV "/dev/dsp" /*device name*/ #define RATE 48000 /*sample rate*/ #define SIZE 16 /*data size*/ #define CHANNELS 1 /*channels*/ #define TIME 3 /*times to be recorded*/ #define LENGTH RATE*SIZE*CHANNELS*TIME/8 /*lenght of datas*/ #define ACT_RECORD 0 /*action record*/ #define ACT_PLAY 1 /*action playback*/ #define ACT_RECORD_PLAY 2 /*action record and playback*/ int open_device(const char*, unsigned int); int close_device(unsigned int); int set_format(unsigned int, unsigned int, unsigned int, unsigned int); int main() { int i; int fd; /*file descriptor*/ unsigned char* buf; /*buffer for sound datas*/ buf = malloc(LENGTH); printf("open...... "); /*open the sound device*/ fd = open_device(DEV,ACT_RECORD_PLAY); if(fd == -1){ fprintf(stderr,"Open Audio Device %s failed:%s ",DEV,strerror(errno)); return -1; } printf("open successed "); printf("set...... "); /*set the format of device*/ if(set_format(fd, SIZE, CHANNELS, RATE) < 0){ fprintf(stderr,"Cann't set %s in bit %d, channel %d, sample rate %d ",DEV,SIZE,CHANNELS,RATE); return -1; } printf("set successed "); /*write data to device*/ for(i = 0; i < 10; i++){ printf("Say... "); if(read(fd, buf, LENGTH) < 0){ fprintf(stderr,"Read sound failed:%s ",strerror(errno)); printf("read "); return -1; } printf("Listen... "); if(write(fd, buf, LENGTH) < 0){ fprintf(stderr,"Write sound failed:%s ",strerror(errno)); printf("write "); return -1; } if(ioctl(fd, SOUND_PCM_SYNC, 0) < 0){ fprintf(stderr,"Sound sync failed:%s ",strerror(errno)); printf("sync "); return -1; } } /*delete the ram and close the file*/ free(buf); if(close_device(fd) < 0){ fprintf(stderr,"Cann't close device %s:%s ",DEV,strerror(errno)); return -1; } return 0; } /********************************************** * open_device():open sound device ***** params: ***** * dev_name --> device name, such as '/dev/dsp' * flag --> distinguish the action of open device(ACT_RECORD or ACT_PLAY) ***** return: ***** * fild descriptor of sound device if success, -1 if failed ***********************************************/ int open_device(const char* dev_name, unsigned int flag){ int dev_fd; /*open device*/ if(flag == ACT_RECORD){ if((dev_fd = open(dev_name, O_RDONLY)) < 0){ return -1; } } else if(flag == ACT_PLAY){ if((dev_fd = open(dev_name, O_WRONLY)) < 0){ return -1; } } else if(flag == ACT_RECORD_PLAY){ if((dev_fd = open(dev_name, O_RDWR)) < 0){ return -1; } } return dev_fd; } /********************************************** * close_device():close sound device ***** params: ***** * dev_fd --> the sound device's file descriptor ***** return: ***** * 0 if success, -1 if failed ***********************************************/ int close_device(unsigned int dev_fd){ return (close(dev_fd)); } /********************************************** * set_format():set record and playback format ***** params: ***** * fd --> device file descriptor * chn --> channel(MONO or STEREO) * bits --> data size(8bits or 16bits) * sr --> sample rate(8KHz or 16KHz or ....) ***** return: ***** * 0 if success, -1 if failed ***** notes: ***** * parameter setting order should be like as below: * 1. data size(number of bits) * 2. number of channel(mono or stereo) * 3. sample rate ***********************************************/ int set_format(unsigned int fd, unsigned int bits, unsigned int chn, unsigned int sr){ int ioctl_val; /*set data size*/ ioctl_val = bits; if(ioctl(fd, SOUND_PCM_WRITE_BITS, &ioctl_val) == -1){ fprintf(stderr,"Set SOUND_PCM_WRITE_BITS %d bits failed:%s ",bits,strerror(errno)); return -1; } if(ioctl_val != bits){ fprintf(stderr,"Don't support %d bits,supported %d ",bits,ioctl_val); return -1; } /*set number of channel*/ ioctl_val = chn; if(ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &ioctl_val) == -1){ fprintf(stderr,"Set SOUND_PCM_WRITE_CHANNELS %d failed:%s ",chn,strerror(errno)); return -1; } if(ioctl_val != chn){ fprintf(stderr,"Don't support %d channel,supported %d ",chn,ioctl_val); return -1; } /*set sample rate*/ ioctl_val = sr; if(ioctl(fd, SOUND_PCM_WRITE_RATE, &ioctl_val) == -1){ fprintf(stderr,"Set SOUND_PCM_WRITE_RATE %d failed:%s ",sr,strerror(errno)); return -1; } if(ioctl_val != sr){ fprintf(stderr,"Don't support %d sample rate,supported %d ",sr,ioctl_val); return -1; } return 0; }
关闭/dev/dsp设备可以通过命令:fuser -k /dev/dsp