DSP

通过dsp设备实现录音与放音编程实例

2019-07-13 09:37发布

今日诗词欣赏:                              柳永   《雨霖铃》     寒蝉凄切,对长亭晚,骤雨初歇。都门帐饮无绪,留恋处,兰舟催发。执手相看泪眼,竟无语凝噎。念去去,千里烟波,暮霭沉沉楚天阔。
    多情自古伤离别,更那堪,冷落清秋节!今宵酒醒何处?杨柳岸,晓风残月。此去经年,应是良辰好景虚设。便纵有千种风情,更与何人说?

==========================================================================

主机操作系统:Centos 6.7 
交叉编译器环境:arm-linux-gcc-4.5.4 (可通过命令/opt/buildroot-2012.08/arm920t/usr/bin/arm-linux-gcc -v查询)
开发板平台: fl2440 
Linux内核版本: linux-3.0 .54

声卡:uda1341(音频编程接口:ALSA),声卡不支持双工 ========================================================================== 一、关于dsp设备:          声卡驱动移植成功之后,在/dev目录下会出现一个dsp设备。           声卡驱动程序提供的/dev/dsp是用于数字采样(sampling)和数字录音(recording)的设备文件,它对于Linux下的音频编程来讲非常重要:向该设备写数据即意味着激活声卡上的D/A转换器进行放音,而向该设备读数据则意味着激活声卡上的A/D转换器进行录音。
                  DSP是数字信号处理器(Digital Signal Processor)的简称,它是用来进行数字信号处理的特殊芯片,声卡使用它来实现模拟信号和数字信号的转换。声卡中的DSP设备实际上包含两个组成部分:在以只读方式打开时,能够使用A/D转换器进行声音的输入;而在以只写方式打开时,则能够使用D/A转换器进行声音的输出。严格说来,Linux下的应用程序要么以只读方式打开/dev/dsp输入声音,要么以只写方式打开/dev/dsp输出声音,但事实上某些声卡驱动程序仍允许以读写的方式打开/dev/dsp,以便同时进行声音的输入和输出,这对于某些应用场合(如IP电话)来讲是非常关键的。                 在从DSP设备读取数据时,从声卡输入的模拟信号经过A/D转换器变成数字采样后的样本(sample),保存在声卡驱动程序的内核缓冲区中,当应用程序通过read系统调用从声卡读取数据时,保存在内核缓冲区中的数字采样结果将被复制到应用程序所指定的用户缓冲区中。需要指出的是,声卡采样频率是由内核中的驱动程序所决定的,而不取决于应用程序从声卡读取数据的速度。如果应用程序读取数据的速度过慢,以致低于声卡的采样频率,那么多余的数据将会被丢弃;如果读取数据的速度过快,以致高于声卡的采样频率,那么声卡驱动程序将会阻塞那些请求数据的应用程序,直到新的数据到来为止。                 在向DSP设备写入数据时,数字信号会经过D/A转换器变成模拟信号,然后产生出声音。应用程序写入数据的速度同样应该与声卡的采样频率相匹配,否则过慢的话会产生声音暂停或者停顿的现象,过快的话又会被内核中的声卡驱动程序阻塞,直到硬件有能力处理新的数据为止。与其它设备有所不同,声卡通常不会支持非阻塞(non-blocking)的I/O操作。
      无论是从声卡读取数据,或是向声卡写入数据,事实上都具有特定的格式(format),默认为8位无符号数据、单声道、8KHz采样率,如果默认值无法达到要求,可以通过ioctl系统调用来改变它们。通常说来,在应用程序中打开设备文件/dev/dsp之后,接下去就应该为其设置恰当的格式,然后才能从声卡读取或者写入数据。
二、dsp设备编程:         对声卡进行编程时首先要做的是打开与之对应的硬件设备,这是借助于open系统调用来完成的,并且一般情况下使用的是/dev/dsp文件。采用何种模式对声卡进行操作也必须在打开设备时指定,对于不支持全双工的声卡来说,应该使用只读或者只写的方式打开,只有那些支持全双工的声卡,才能以读写的方式打开,并且还要依赖于驱动程序的具体实现。Linux允许应用程序多次打开或者关闭与声卡对应的设备文件,从而能够很方便地在放音状态和录音状态之间进行切换,建议在进行音频编程时只要有可能就尽量使用只读或者只写的方式打开设备文件,因为这样不仅能够充分利用声卡的硬件资源,而且还有利于驱动程序的优化。下面的代码示范了如何以只写方式打开声卡进行放音(playback)操作:

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表示没有任何限制。

          接下来要做的是设置声卡工作时的声道(channel)数目,根据硬件设备和驱动程序的具体情况,可以将其设置为0(单声道,mono)或者1(立体声,stereo)。下面的代码示范了应该怎样设置声道数目:

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; }
        从下面的这两个命令,我们就能很直观的看出如何使用dsp实现放音录音,从dsp设备中读取数据就是录音过程,向dsp设备写入放音过程。该设备支持的是wav格式的音频文件,与而之前我们做的madplay能够实现对mp3文件解码,而该设备并不可以,所以不能播放mp3格式文件。并且,直接将音频写入的话,我们听到的也只是很多杂音,需要对他的采样频率,声道数目等进行设置,才能播放出正确的音频。 cat /dev/dsp >test.wav 将声音录为PCM数据
    cat test.wav >/dev/dsp 将PCM数据播放
       楼主亲测,将mp3文件转成wav文件就可以正常播放。   
那么接下来我们就来看看怎么样通过程序来实现录音,与放音:
三、录音与放音编程:  1、录音程序: /********************************************************************************* * Copyright: (C) 2017 qicheng * All rights reserved. * * Filename: record.c * Description: This file * * Version: 1.0.0(05/07/2017) * Author: yangni <497049229@qq.com> * ChangeLog: 1, Release initial version on "05/07/2017 08:26:29 AM" * ********************************************************************************/ #include #include #include #include #include #include #include #include #define RATE 48000 //设置频率 #define CHANNELS 2 //通道数为2(立体声) #define BITS 16 // 量化位数 #define TOTAL (BITS * RATE * CHANNELS / 8 * 15) //录制时间 15 seconds #define BYTES_4K 4096 #define RECORD_FILE "/data/media/record.pcm" //生成文件record.pcm到指定目录下 int main() { int pcm_fd; int dsp_fd_rd; char buf[4 * 1024] = {0}; int ret = 0; int total = 0; int rate = RATE; int channels = CHANNELS; int bits = BITS; dsp_fd_rd = open("/dev/dsp", O_RDONLY); //打开设备(单工) if (dsp_fd_rd < 0) { perror("Failed to open! "); return -1; } pcm_fd = open(RECORD_FILE, O_WRONLY | O_CREAT, 0666); //打开一个文件(只写)来存储音频 if (pcm_fd < 0) { perror("Failed to open/create pcm file"); return -1; } ioctl(dsp_fd_rd, SNDCTL_DSP_SPEED, &rate); //设置频率 ioctl(dsp_fd_rd, SNDCTL_DSP_STEREO, &channels); //设置通道数 ioctl(dsp_fd_rd, SNDCTL_DSP_SETFMT, &bits); //设置量化位数 while (1) { ret = read(dsp_fd_rd, buf, BYTES_4K); //从dsp中读数据到buf(录音过程),每次读4K if (ret < 0) { perror("Failed to read!"); return -1; } if (write(pcm_fd, buf, ret) < 0) //把读到的数据写入我们创建的文件中 { perror("Failed to write!"); return -1; } total += ret; //15s以后跳出循环 if (total >= TOTAL) break; } close(dsp_fd_rd); close(pcm_fd); return 0; }
2、放音程序:        /********************************************************************************* * Copyright: (C) 2017 qicheng * All rights reserved. * * Filename: record.c * Description: This file * * Version: 1.0.0(05/07/2017) * Author: yangni <497049229@qq.com> * ChangeLog: 1, Release initial version on "05/07/2017 08:26:29 AM" * ********************************************************************************/ #include #include #include #include #include #include #include #include #define RATE 48000 #define CHANNELS 2 #define BITS 16 #define TOTAL (BITS * RATE * CHANNELS / 8 * 15) //15 seconds #define BYTES_4K 4096 #define RECORD_FILE "/data/media/record.pcm" int main() { int pcm_fd; int dsp_fd_rd; char buf[4 * 1024] = {0}; int ret = 0; int total = 0; int rate = RATE; int channels = CHANNELS; int bits = BITS; dsp_fd_rd = open("/dev/dsp", O_WRONLY); //只写方式打开 if (dsp_fd_rd < 0) { perror("Failed to open! "); return -1; } pcm_fd = open(RECORD_FILE, O_RDONLY | O_CREAT, 0666); //只读方式打开 if (pcm_fd < 0) { perror("Failed to open/create pcm file"); return -1; } ioctl(dsp_fd_rd, SNDCTL_DSP_SPEED, &rate); ioctl(dsp_fd_rd, SNDCTL_DSP_STEREO, &channels); ioctl(dsp_fd_rd, SNDCTL_DSP_SETFMT, &bits); while (1) { ret = read(pcm_fd, buf, BYTES_4K); //从record.c中读数据到buf if (ret < 0) { perror("Failed to read!"); return -1; } if (write(dsp_fd_rd, buf, ret) < 0) //将buf中的数据写入dsp设备 { perror("Failed to write!"); return -1; } total += ret; if (total >= TOTAL) break; } close(dsp_fd_rd); close(pcm_fd); return 0; }
         总而言之,放音程序与录音程序基本相似,不过是把过程反了过来。录音是把dsp设备数据写入record.c,而放音则是是把record.c的数据写入dsp设备中,还是要设置相应的频率灯。                因为我们声卡是单工的,所以每次打开只能以只写或者只读打开。而且,我们开发板的两个耳机孔,红 {MOD}的是麦克风接口,绿 {MOD}的是耳机接口,要想实现录音,必须用有两个耳塞头的耳机,也就是麦克风与听筒是分开的街头,因为是声卡是单工的,不支持双向传输。
本文参考:http://blog.csdn.net/kevinx_xu/article/details/8501231