DSP

第17章Linux OSS 音频设备驱动之一

2019-07-13 19:22发布

17.3 Linux OSS 音频设备驱动17.3.1 OSS 驱动的组成    OSS(Open Sound System) 标准中有两个最基本的音频设备:mixer(混音器)dsp(数字信号处理器)
    在声卡的硬件电路中,mixer 是一个很重要的组成部分,其作用是将多个信号组合或者叠加在一起,对于不同的声卡,其混音器的作用可能各不相同。OSS 驱动中,/dev/mixer 设备文件是应用程序对 mixer (混音器)进行操作的软件接口。
    混音器电路通常由两部分组成:输入混音器(input mixer)和输出混音器(output mixer)。输入混音器负责从多个不同的信号源接收模拟信号,这些信号源有时也被称为混音通道或者混音设备。模拟信号通过增益控制器和由软件控制的音量调节器,在不同的混音通道中进行级别(level)调制,然后被送到输入混音器中进行声音的合成。混音器上的电子开关可以控制哪些通道中有信号与混音器相连,有些声卡只允许连接一个混音通道作为录音的音源,有些声卡则允许对混音通道做任意的连接。经过输入混音器处理后的信号仍为模拟信号,再被送到 A/D (模拟/数字)转换器进行数字化处理。
    输出混音器的工作原理与输入混音器类似,也有多个信号源与混音器相连,并且事先都经过了增益调节。当输出混音器对所有的模拟信号进行了混合之后,通常还会有一个总控增益调节器来控制输出声音的大小,此外还有一些音调控制器来调节输出声音的音调。经过输出混音器处理后的信号也是模拟信号,它们最终会被送给喇叭或者其他的模拟输出设备。
    对混音器的编程包括如何设置增益控制器的级别,以及怎样在不同的音源间进行切换,这些操作通常来讲是不连续的,而且不会像录音或者播放那样需要占用大量的计算机资源。由于混音器的操作不符合典型的读/写操作模式,因此除了 open()和 close()这两个系统调用之外,大部分的操作都是通过 ioctl()系统调用来完成的。/dev/mixer 允许多个应用程序同时访问,并且混音器的设置值会一直保持到对应的设备文件被关闭为止。
    DSP 也称为编解码器,实现录音和放音,对应的设备文件是/dev/dsp 或/dev/sound/dsp。OSS声卡驱动程序提供的/dev/dsp 是用于数字采样和数字录音的设备文件,向该设备写数据意味着激活声卡上的 D/A 转换器进行播放,向该设备读数据意味着激活声卡上的 A/D 转换器进行录音。
    从 DSP 设备读取数据时,从声卡输入的模拟信号经过 A/D 转换器变成数字采样后的样本,保存在声卡驱动程序的内核缓冲区中,当应用程序通过 read()系统调用从声卡读取数据时,保存在内核缓冲区中的数字采样结果将被复制到应用程序所指定的用户缓冲区中。如果应用程序读取数据的速度过慢,以致低于声卡的采样频率,那么多余的数据将会被丢弃(overflow);如果读取数据的速度过快,以致高于声卡的采样频率,那么声卡驱动程序将会阻塞那些请求数据的应用程序,直到新的数据到来为止。
    向 DSP 设备写入数据时,数字信号经过 D/A 转换器变成模拟信号,产生声音。应用程序写入数据的速度应该至少等于声卡的采样频率,过慢产生声音暂停或者停顿的现象(underflow)。写入过快,会被内核中的声卡驱动程序阻塞,直到硬件有能力处理新的数据为止。
    与其他设备有所不同,声卡通常不需要支持非阻塞(non-blocking)的 I/O 操作。即便内核OSS 驱动提供非阻塞的 I/O 支持,用户空间也很少采用。
    无论是从声卡读取数据,或是向声卡写入数据,都具有特定的格式(format),如无符号 8 位、单声道、8kHz 采样率,如果默认值无法达到要求,可以通过 ioctl()系统调用来改变。通常来说,在应用程序中打开设备文件/dev/dsp 之后,接下去就应该为其设置恰当的格式,然后才能从声卡读取或者写入数据。
17.3.2 mixer 接口include/linux/sound.h
int register_sound_mixer(const struct file_operations *fops, int dev);
    该函数用于注册一个混音器,第一个参数 fops 即是文件操作接口,第二个参数 dev 是设备编号,如果填入-1,则系统自动分配一个设备编号。mixer 是一个典型的字符设备,因此编码的主要工作是实现 file_operations 中的 open()、ioctl()等函数。
void unregister_sound_mixer(int unit);
sound/sound_core.c
/**
 *      register_sound_mixer - register a mixer device
 *      @fops: File operations for the driver
 *      @dev: Unit number to allocate
 *
 *      Allocate a mixer device. Unit is the number of the mixer requested.
 *      Pass -1 to request the next free mixer unit. On success the allocated
 *      number is returned, on failure a negative error code is returned.
 */
int register_sound_mixer(const struct file_operations *fops, int dev)
{
        return sound_insert_unit(&chains[0], fops, dev, 0, 128,
                                 "mixer", S_IRUSR | S_IWUSR, NULL);
}
EXPORT_SYMBOL(register_sound_mixer);/**
 *      unregister_sound_mixer - unregister a mixer
 *      @unit: unit number to allocate
 *
 *      Release a sound device that was allocated with register_sound_mixer().
 *      The unit passed is the return value from the register function.
 */
void unregister_sound_mixer(int unit)
{
        sound_remove_unit(&chains[0], unit);
}

EXPORT_SYMBOL(unregister_sound_mixer);
    mixer 接口 file_operations 中的最重要函数是 ioctl()实现混音器的不同 I/O 控制命令,代码清单 17.1 所示为一个 ioctl()的范例。
代码清单 17.1 mixer()接口的 ioctl()函数范例
 static int mixdev_ioctl(struct inode *inode, struct file *file, unsigned int cmd,
unsigned long arg)
 {
         ...
         switch (cmd) {
         case SOUND_MIXER_READ_MIC:
          ...
         case SOUND_MIXER_WRITE_MIC:
          ...
         case SOUND_MIXER_WRITE_RECSRC:
         ...
         case SOUND_MIXER_WRITE_MUTE:
         ...
         }
            /* 其他命令 */
         return mixer_ioctl(codec, cmd, arg); }17.3.3 dsp 接口
 include/linux/sound.h
int register_sound_dsp(const struct file_operations *fops, int dev);
    该函数用于注册一个 dsp 设备,第一个参数 fops是文件操作接口,第二个参数 dev 是设备编号,如果填入-1,则系统自动分配一个设备编号。void unregister_sound_dsp(int unit);
sound/sound_core.c
/**
 *      register_sound_dsp - register a DSP device
 *      @fops: File operations for the driver
 *      @dev: Unit number to allocate
 *
 *      Allocate a DSP device. Unit is the number of the DSP requested.
 *      Pass -1 to request the next free DSP unit. On success the allocated
 *      number is returned, on failure a negative error code is returned.
 *
 *      This function allocates both the audio and dsp device entries together
 *      and will always allocate them as a matching pair - eg dsp3/audio3
 */

int register_sound_dsp(const struct file_operations *fops, int dev)
{
        return sound_insert_unit(&chains[3], fops, dev, 3, 131,
                                 "dsp", S_IWUSR | S_IRUSR, NULL);
}

EXPORT_SYMBOL(register_sound_dsp);/**
 *      unregister_sound_dsp - unregister a DSP device
 *      @unit: unit number to allocate
 *
 *      Release a sound device that was allocated with register_sound_dsp().
 *      The unit passed is the return value from the register function.
 *
 *      Both of the allocated units are released together automatically.
 */

void unregister_sound_dsp(int unit)
{
        return sound_remove_unit(&chains[3], unit);
}

EXPORT_SYMBOL(unregister_sound_dsp);        dsp是一个典型的字符设备,因此编码的主要工作是实现 file_operations 中的 read()、write()、ioctl()等函数。
        dsp 接口 file_operations 中的 read()和 write()函数非常重要,read()函数从音频控制器中获取录音数据到缓冲区并复制到用户空间,write()函数从用户空间复制音频数据到内核空间缓冲区并最终发送到音频控制器。
        dsp 接口 file_operations 中的 ioctl()函数处理对采样率、量化精度、DMA 缓冲区块大小等参数设置 I/O 控制命令的处理。
        数据从缓冲区复制到音频控制器的过程中,数据从缓冲区复制到音频控制器的过程中,通常会使用 DMA,DMA 对声卡非常重要。
        例如,在放音时,驱动设置完 DMA 控制器的源数据地址(内存中的 DMA 缓冲区)、目的地址(音频控制器 FIFO)和 DMA 的数据长度,DMA 控制器会自动发送缓冲区的数据填充 FIFO,直到发送完相应的数据长度后才中断一次。
        在 OSS 驱动中,通常值得推荐的方法是建立存放音频数据的环形缓冲区(ring buffer)。此外,在 OSS 驱动中,一般会将一个较大的 DMA 缓冲区分成若干个大小相同的块(这些块也被称为“片段”,即 fragment),驱动程序使用 DMA 每次在声音缓冲区和声卡之间搬移一个 fragment。在用户空间,可以使用 ioctl()系统调用来调整块的大小和个数。
    除read()、write()和 ioctl()外,dsp 接口的 poll()函数通常也需要被实现,以向用户反馈目前能否读写 DMA 缓冲区。
    在 OSS 驱动初始化过程中,会调用 register_sound_dsp()和 register_sound_mixer()注册 dsp 和mixer 设备;在模块卸载的时候,会调用 unregister_sound_dsp()和 unregister_sound_mixer()注销 dsp 和mixer 设备;如代码清单 17.2。
代码清单 17.2 OSS 驱动初始化注册 dsp 和 mixer 设备
 static int xxx_init(void)
 {
         struct xxx_state *s = &xxx_state;
         ...
         /* 注册 dsp 设备 */
         if ((audio_dev_dsp = register_sound_dsp(&xxx_audio_fops, - 1)) < 0)
                 goto err_dev1;
         /* 注册 mixer 设备 */
         if ((audio_dev_mixer = register_sound_mixer(&xxx_mixer_fops, - 1)) < 0)
                 goto err_dev2;
          ...
 }
void _ _exit xxx_exit(void) {
        /* 注销 dsp 和 mixer 设备 */
        unregister_sound_dsp(audio_dev_dsp);
        unregister_sound_mixer(audio_dev_mixer);
         ...
 }
 Linux OSS 驱动结构的简图,如图 17.4所示。

图 17.4Linux OSS 驱动结构
17.3.4 OSS 用户空间编程
1.dsp 编程
对 OSS 驱动声卡的编程使用 Linux 文件接口函数,如图 17.5 所示。

dsp 接口的操作一般包括如下几个步骤。
(1)打开设备文件/dev/dsp。
    采用何种模式对声卡进行操作必须在打开设备时指定,对于不支持全双工(发送数据的同时也能够接收数据,两者同步进行)的声卡,使用只读或者只写的方式打开,只有支持全双工的声卡,才能以读写的方式打开,这还依赖于驱动程序的具体实现。Linux 允许应用程序多次打开或者关闭与声卡对应的设备文件,从而很方便地在放音状态和录音状态之间进行切换。
(2)如果有需要,设置缓冲区大小。
    运行在 Linux 内核中的声卡驱动程序专门维护了一个缓冲区,其大小会影响到播放和录音时的效果,使用 ioctl()系统调用可以对它的尺寸进行恰当设置。调节驱动程序中缓冲区大小不是必须的,如果没有特殊要求,一般采用默认的缓冲区大小。如果想设置缓冲区的大小,通常应紧跟在设备文件打开之后,因为对声卡的其他操作有可能会导致驱动程序无法再修改其缓冲区的大小。
(3)设置声道(channel)数量。
    根据硬件设备和驱动程序具体情况,可设置为单声道或立体声。
(4)设置采样格式和采样频率
    采样格式包括 AFMT_U8(无符号 8 位)、AFMT_S8(有符号 8 位)、AFMT_U16_LE(小端模式,无符号 16 位)、AFMT_U16_BE(大端模式,无符号 16 位)、AFMT_MPEG、AFMT_AC3等。使用 SNDCTL_DSP_SETFMT IO 控制命令可以设置采样格式。
    大多数声卡支持的采样频率范围一般为 5kHz~44.1kHz 或者 48kHz,并不意味着该范围内的所有连续频率都会被硬件支持,在 Linux 系统下进行音频编程时最常用到的几种采样频率是 11025Hz、16000Hz、22050Hz、32000Hz 和 44100Hz。使用 SNDCTL_DSP_SPEED IO控制命令设置采样频率。
(5)读写/dev/dsp 实现播放或录音。
    代码清单 17.3的程序实现了利用/dev/dsp 接口进行声音录制和播放的过程,功能是先录制几秒钟音频数据,将其存放在内存缓冲区中,然后再进行播放。
代码清单 17.3 OSS dsp 接口应用编程范例
 #define LENGTH 3 /* 存储秒数 */
 #define RATE 8000 /* 采样频率 */
 #define SIZE 8 /* 量化位数 */
 #define CHANNELS 1 /* 声道数目 */
/* 用于保存数字音频数据的内存缓冲区 */
 unsigned char buf[LENGTH *RATE * SIZE * CHANNELS / 8];
 int main()
 {
             int fd; /* 声音设备的文件描述符 */
             int arg; /* 用于 ioctl 调用的参数 */
             int status; /* 系统调用的返回值 */
             /* 打开声音设备 */
             fd = open("/dev/dsp", O_RDWR);// 全双工

             /* 设置采样时的量化位数 */
             arg = SIZE;
             status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);

             /* 设置采样时的通道数目 */
             arg = CHANNELS;
             status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);

             /* 设置采样率 */
             arg = RATE;
             status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);

             /* 循环,直到按下[Ctrt+c] */
             while (1) {
                     printf("Say something: ");
                     status = read(fd, buf, sizeof(buf)); /* 录音 */

                     printf("You said: ");
                     status = write(fd, buf, sizeof(buf)); /* 放音 */

                     /* 在继续录音前等待放音结束 */
                     status = ioctl(fd, SOUND_PCM_SYNC, 0);

             }
 }