典型的数字音频系统电路组成如图所示:
通过PCM、 IIS 或 AC97 这些音频接口连接外部的音频编解码器即可实现声音的 AD 和 DA 转换,图中的功放完成模拟信号的放大功能。音频编解码器是数字音频系统的核心,主要指标有:
1、采样频率
采样频率是每秒钟的采样次数,理论上采样频率越高,转换精度越高,目前主流的采样频率是48KHZ
2、量化精度
量化精度是指对采样数据分析的精度,比如24bit精度就是指将标准电平信号按照2^24进行分析,量化精度越高,声音就越逼真。
1、PCM接口
PCM(脉冲编码调制)接口,该接口由时钟脉冲(BCLK)、帧同步信号(FS)及接收数据(DR)和发送数据(DX)组成。在 FS 信号的上升沿,数据传输从 MSB(Most Significant Bit)开始, FS 频率等于采样率。 FS 信号之后开始数据字的传输,单个的数据位按顺序进行传输,一个时钟周期传输一个数据字。发送 MSB 时,信号的等级首先降到最低,以避免在不同终端的接口使用不同的数据方案时造成 MSB 的丢失。PCM接口需要每个音频通道获得一个独立的数据队列。
2、IIS接口
IIS接口在一个称为LRCLK(Left/Right Clock)的信号机制中经过多路转换将两路音频信号变成单一的数据队列。当 LRCLK 为高时,左声道数据被传输; LRCLK 为低时,右声道数据被传输。与 PCM 相比, IIS 更适合于立体声系统。
3、AC97接口
AC'97 不只是一种数据格式,用于音频编码的内部架构规格,它还具有控制功能。AC'97 采用 AC-Link 与外部的编解码器相连, AC-Link 接口包括位时钟(BITCLK)、同步信号校正(SYNC) 和从编码到处理器及从处理器中解码(SDATDIN 与 SDATAOUT) 的数据队列。
AC'97数据帧以 SYNC 脉冲开始,包括 12 个 20 位时间段(时间段为标准中定义的不同的目的服务)及 16 位“tag”段,共计 256 个数据序列。例如,时间段“1”和“2”用于访问编码的控制寄存器,而时段“3”和“4”分别负载左、右两个音频通道。“tag”段表示其他段中哪一个包含有效数据。把帧分成时间段使传输控制信号和音频数据仅通过 4 根线到达 9 个音频通道或转换成其他数据流成为可能。
PCM、 IIS 和 AC97 各有其优点和应用范围,例如在 CD、 MD、 MP3 随身听多采用 IIS 接口,移动电话会采用 PCM 接口,具有音频功能的 PDA 则多使用和 PC 一样的 AC'97 编码格式。
在Linux中,主要有OSS和ALSA两种音频设备驱动框架。
ALSA的主要特点:
支持多种声卡设备。
模块化的内核驱动程序。
支持 SMP 和多线程。
提供应用开发函数库(alsa-lib)以简化应用程序开发。
支持 OSS API,兼容 OSS 应用程序。
ALSA 系统包括驱动包 alsa-driver(内核驱动程序)、开发包 alsa-libs(用户函数库,应用程序使用时应包含头文件 asoundlib.h,并使用共享库libasound.so)、开发包插件 alsa-libplugins、设置管理工具包 alsa-utils(包含一些基于 ALSA 的用于控制声卡的应用程序)、其他声音相关处理小程序包 alsa-tools、特殊音频固件支持包 alsa-firmware、 OSS 接口兼容模拟层工具alsa-oss 共 7 个子项目,其中只有驱动包是必需的。
目前 ALSA 内核提供给用户空间的接口有:
信息接口(Information Interface, /proc/asound);
控制接口(Control Interface, /dev/snd/controlCX);
混音器接口(Mixer Interface, /dev/snd/mixerCXDX);
PCM 接口(PCM Interface, /dev/snd/pcmCXDX);
Raw 迷笛接口(Raw MIDI Interface, /dev/snd/midiCXDX);
音序器接口(Sequencer Interface, /dev/snd/seq);
定时器接口(Timer Interface, /dev/snd/timer)。
这些接口被提供给alsa-lib使用。
对于每个声卡而言,必须创建一个card实例。card管理这个声卡上所有设备(组件),如 PCM、 mixers、 MIDI、 synthesizer 等。
1、创建card
struct snd_card *snd_card_new(int idx, const char *xid,struct module *module, int extra_size);
idx 是 card 索引号, xid 是标识字符串, module 一般为 THIS_MODULE, extra_size 是要分配的额外数据的大小,分配的 extra_size 大小的内存将作为 card->private_data。
2、创建组件
int snd_device_new(struct snd_card *card, snd_device_type_t type,void *device_data, struct snd_device_ops *ops);
当 card 被创建后,设备(组件)能够被创建并关联于该 card。第 1 个参数是 snd_card_new()创建的 card指 针 , 第 2 个 参 数 type 指 的 是 device-level 即 设 备 类 型 , 形 式 为 SNDRV_DEV_XXX , 包 括SNDRV_DEV_CODEC、 SNDRV_DEV_CONTROL、 SNDRV_DEV_PCM、 SNDRV_DEV_ RAWMIDI 等,用户自定义设备的 device-level是 SNDRV_DEV_LOWLEVEL, device_data 是设备数据指针,注意函数 snd_device_ new()本身不会分配设备数据的内存,因此应事先分配,ops参数是 1个函数集(定义为 snd_device_ops结构体)的指针。
3、组件释放
每个 ALSA 预定义的组件在构造时需调用 snd_device_new(),而每个组件的析构方法则在函数集中被包含。
对于 PCM、 AC97 此类预定义组件, 我们不需关心它们的析构, 而对于自定义的组件, 则需要填充 snd_device_ops中的析构函数指针 dev_free,这样,当 snd_card_free()被调用时,组件将自动被释放。
4、芯片特定的数据(Chip-Specific Data)
芯片特定的数据一般以 struct xxxchip 结构体形式组织,这个结构体中包含芯片相关的 I/O 端口地址、资源指针、中断号等,其意义等同于字符设备驱动中的 file->private_data。
定义芯片特定的数据主要有两种方法,一种方法是将 sizeof(struct xxxchip)传入 snd_card_new()作为extra_size 参数,它将自动成为 snd_card 的 private_data 成员,代码如下:
struct xxxchip //芯片特定的数据结构体
{
...
};
card = snd_card_new(index, id, THIS_MODULE, sizeof(struct xxxchip));//创建声卡并申请xxx_ch 内存作为card-> private_data
struct xxxchip *chip = card->private_data;
另一种方法是在snd_card_new()传入给 extra_size 参数 0,再分配 sizeof(struct xxxchip)的内存,将分配内存的地址传入snd_device_new()的 device_data 的参数
struct snd_card *card;
struct xxxchip *chip;
//使用 0 作为第 4 个参数,并动态分配 xxx_chip 的内存
card=snd_card_new(index[dev], id[dev], THIS_MODULE, 0);
...
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
//在 xxxchip 结 构 体 中 , 应 该 包 括 声 卡 指 针
struct xxxchip
{
struct snd_card *card;
...
};
//并将其 card 成员赋值为 snd_card_new()创建的 card 指针
chip->card = card;
static struct snd_device_ops ops =
{
. dev_free = snd_xxx_chip_dev_free, //组件析构
};
...
//创建自定义组件
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
//在析构函数中释放 xxxchip 内存
static int snd_xxx_chip_dev_free(struct snd_device *device)
{
return snd_xxx_chip_free(device->device_data); //释放
}
5、注释/释放声卡
当 snd_card 被准备好以后,可使用 snd_card_register()函数注册这个声卡,如下所示:
int snd_card_register(struct snd_card *card);
对应的 snd_card_free()完成相反的功能,如下所示:
int snd_card_free(struct snd_card *card);
每个声卡最多可以有四个 PCM 实例,一个 PCM 实例对应一个设备文件。 PCM 实例由 PCM 播放和录音流组成,而每个 PCM 流又由一个或多个 PCM 子流组成。
1、PCM实例构造
int snd_pcm_new(struct snd_card *card, char *id, int device,int playback_count, int capture_count, struct snd_pcm ** rpcm);
第 1 个参数是 card 指针,第 2 个是标识字符串,第 3 个是 PCM 设备索引(0 表示第 1 个 PCM 设备),第 4 和第 5 个分别为播放和录音设备的子流数。在每个回调函数中, 可以通过 snd_pcm_substream 的 number 成员得知目前操作的究竟是哪个子流,如下所示:
struct snd_pcm_substream *substream;
int index = substream->number;
一般习惯的做法是在驱动中定义一个PCM“构造函数”,负责PCM实例的创建。
2、设置PCM操作
void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, struct snd_pcm_ops *ops);
第 1 个参数是 snd_pcm 的指针,第 2 个参数是SNDRV_PCM_STREAM_PLAYBACK 或 SNDRV_PCM_STREAM_CAPTURE,而第 3 个参数是 PCM 操作结构体 snd_pcm_ops,这个结构体的定义如下所示:
struct snd_pcm_ops
{
int (*open)(struct snd_pcm_substream *substream);//打开
int (*close)(struct snd_pcm_substream *substream);//关闭
int (*ioctl)(struct snd_pcm_substream * substream,
unsigned int cmd, void *arg);//I/O 控制
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);//硬件参数
int (*hw_free)(struct snd_pcm_substream *substream); //资源释放
int (*prepare)(struct snd_pcm_substream *substream);//准备
//在 PCM 被开始、停止或暂停时调用
int (*trigger)(struct snd_pcm_substream *substream, int cmd);
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);// 当前缓冲区的硬件位置
//缓冲区复制
int (*copy)(struct snd_pcm_substream *substream, int channel,snd_pcm_uframes_t pos,void _ _user *buf, snd_pcm_uframes_t count);
int (*silence)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
struct page *(*page)(struct snd_pcm_substream *substream,
unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
};
snd_pcm_ops 中的所有操作都需事先通过 snd_pcm_substream_chip()获得 xxxchip 指针,例如:
int xxx()
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
...
}
当一个 PCM 子流被打开时, snd_pcm_ops 中的 open()函数将被调用,在这个函数中,至少需要初始化runtime->hw 字段,如下:
static int snd_xxx_open(struct snd_pcm_substream *substream)
{
//从子流获得 xxxchip 指针
struct xxxchip *chip = snd_pcm_substream_chip(substream);
//获得 PCM 运行时信息指针
struct snd_pcm_runtime *runtime = substream->runtime;
...
//初始化 runtime->hw
runtime->hw = snd_xxxchip_playback_hw; //预先定义的硬件描述
return 0;
}
snd_pcm_ops 的 hw_params()成员函数将在应用程序设置硬件参数(PCM 子流的周期大小、缓冲区大小和格式等)的时候被调用,它的形式如下:
static int snd_xxx_hw_params(struct snd_pcm_substream *substream,struct snd_pcm_hw_params *hw_params);
在这个函数中,将完成大量硬件设置,甚至包括缓冲区分配,这时可调用如下辅助函数:
snd_pcm_lib_malloc_pages(substream,params_buffer_bytes(hw_params));
仅当 DMA 缓冲区已被预先分配的情况下,上述调用才可成立。
与 hw_params()对应的函数是 hw_free(),它释放由 hw_params()分配的资源,例如,通过如下调用释放snd_pcm_lib_malloc_pages()缓冲区:
snd_pcm_lib_free_pages(substream);
当 PCM 子流被关闭时, close()函数将被调用。如果 open()函数中分配了私有数据,则在 close()函数中应该释放 substream 的私有数据,代码如下:
static int snd_xxx_close(struct snd_pcm_substream *substream)
{
//释放子流私有数据
kfree(substream->runtime->private_data);
//...
}
当 PCM 被“准备”时, prepare()函数将被调用,在其中可以设置采样率、格式等。 prepare()函数与hw_params()函数的不同在于对 prepare()的调用发生在 snd_pcm_prepare()每次被调用的时候。 prepare()的形式如下:
static int snd_xxx_prepare(struct snd_pcm_substream *substream);
trigger()成员函数在PCM被开始、停止或暂停时使用,函数的形式如下:
static int snd_xxx_trigger(struct snd_pcm_substream *stream,int cmd);
cmd参数定义了具体的行为,在trigger()成员函数中至少要处理SNDRV_PCM_TRIGGER和SNDRV_PCM_TRIGGER_STOP 命 令 , 如 果 PCM 支 持 暂 停 , 还 应 处 理 SNDRV_PCM_TRIGGER_PAUSE_PUSH 和 SNDRV_PCM_TRIGGER_PAUSE_RELEASE 命令。如果设备支持挂起/恢复,当 能 量 管 理 状 态 发 生 变 化 时 将 处 理 SNDRV_PCM_TRIGGER_SUSPEND 和 SNDRV_PCM_TRIGGER_RESUME 这两个命令。注意 trigger()函数是原子的,中途不能睡眠。代码如下:
static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd)
{
switch (cmd)
{
case SNDRV_PCM_TRIGGER_START:
// 开启 PCM 引擎
break;
case SNDRV_PCM_TRIGGER_STOP:
// 停止 PCM 引擎
break;
...//其他命令
default:
return - EINVAL;
}
}
pointer()函数用于 PCM 中间层查询目前缓冲区的硬件位置,该函数以帧的形式返回 0~buffer_size – 1的位置(ALSA 0.5.x 中为字节形式),此函数也是原子的。
3、分配缓冲区
int snd_pcm_lib_preallocate_pages_for_all(struct snd_pcm *pcm,int type, void *data, size_t size, size_t max);
type 参数是 缓 冲区 的类 型 ,包含 SNDRV_DMA_TYPE_UNKNOWN ( 未 知 )、 SNDRV_DMA_TYPE_CONTINUOUS(连续的非 DMA 内存)、 SNDRV_DMA_TYPE_DEV(连续的通用设备)SNDRV_DMA_TYPE_DEV_SG(通用设备 SG-buffer)和 SNDRV_DMA_TYPE_SBUS(连续的 SBUS)。如下
代码将分配 64KB 的缓冲区:
snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,snd_dma_pci_data(chip->pci),64*1024, 64*1024);
4.设置标志
在构造 PCM 实例、设置操作集并分配缓冲区之后,如果有需要,应设置 PCM 的信息标志,例如,如果 PCM 设备只支持半双工,则这样定义标志:
pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;
5.PCM 实例析构
PCM 实例的“析构函数”并非是必须的,因为 PCM 实例会被 PCM 中间层代码自动释放,如果驱动中分配了一些特别的内存空间,则必须定义“析构函数”,“析构函数”会释放“构造函数”中创建的 xxx_private_pcm_data。
static void xxxchip_pcm_free(struct snd_pcm *pcm)
{
/* 从 pcm 实例得到 chip */
struct xxxchip *chip = snd_pcm_chip(pcm);
/* 释放自定义用途的内存 */
kfree(chip->xxx_private_pcm_data);
...
}
static int __devinit snd_xxxchip_new_pcm(struct xxxchip *chip)
{
struct snd_pcm *pcm;
...
/* 分配自定义用途的内存 */
chip->xxx_private_pcm_data = kmalloc(...);
pcm->private_data = chip;
/* 设置“析构函数” */
pcm->private_free = xxxchip_pcm_free;
...
}
6、PCM信息运行时结构体
当PCM子流被打开后,PCM运行时实例(定义为结构体为snd_pcm_runtime)将被分配给这个子流, 这个指针通过 substream->runtime 获得。运行时指针包含各种各样的信息: hw_params及 sw_params 配置的拷贝、缓冲区指针、 mmap 记录、自旋锁等,几乎 PCM 的所有控制信息均能从中取得。
struct snd_pcm_runtime
{
/* 状态 */
struct snd_pcm_substream *trigger_master;
snd_timestamp_t trigger_tstamp; /* 触发时间戳 */
int overrange;
snd_pcm_uframes_t avail_max;
snd_pcm_uframes_t hw_ptr_base; /* 缓冲区复位时的位置 */
snd_pcm_uframes_t hw_ptr_interrupt; /* 中断时的位置*/
/* 硬件参数 */
snd_pcm_access_t access; /* 存取模式 */
snd_pcm_format_t format; /* SNDRV_PCM_FORMAT_* */
snd_pcm_subformat_t subformat; /* 子格式 */
unsigned int rate; /* rate in Hz */
unsigned int channels; /* 通道 */
snd_pcm_uframes_t period_size; /* 周期大小 */
unsigned int periods; /* 周期数 */
snd_pcm_uframes_t buffer_size; /* 缓冲区大小 */
unsigned int tick_time; /* tick time */
snd_pcm_uframes_t min_align; /* 格式对应的最小对齐*/
size_t byte_align;
unsigned int frame_bits;
unsigned int sample_bits;
unsigned int info;
unsigned int rate_num;
unsigned int rate_den;
/* 软件参数 */
struct timespec tstamp_mode; /* mmap 时间戳被更新*/
unsigned int period_step;
unsigned int sleep_min; /* 睡眠的最小节拍 */
snd_pcm_uframes_t xfer_align;
snd_pcm_uframes_t start_threshold;
snd_pcm_uframes_t stop_threshold;
snd_pcm_uframes_t silence_threshold; //Silence 填充阈值
snd_pcm_uframes_t silence_size; /* Silence 填充大小 */
snd_pcm_uframes_t boundary;
snd_pcm_uframes_t silenced_start;
snd_pcm_uframes_t silenced_size;
snd_pcm_sync_id_t sync; /* 硬件同步 ID */
/* mmap */
volatile struct snd_pcm_mmap_status *status;
volatile struct snd_pcm_mmap_control *control;
atomic_t mmap_count;
/* 锁/调度 */
spinlock_t lock;
wait_queue_head_t sleep;
struct timer_list tick_timer;
struct fasync_struct *fasync;
/* 私有段 */
void *private_data;
void(*private_free)(struct snd_pcm_runtime *runtime);
/* 硬件描述 */
struct snd_pcm_hardware hw;
struct snd_pcm_hw_constraints hw_constraints;
/* 中断回调函数 */
void(*transfer_ack_begin)(struct snd_pcm_substream*substream);
void(*transfer_ack_end)(struct snd_pcm_substream *substream);
/* 定时器 */
unsigned int timer_resolution; /* timer resolution */
/* DMA */
unsigned char *dma_area; /* DMA 区域*/
dma_addr_t dma_addr; /* 总线物理地址*/
size_t dma_bytes; /* DMA 区域大小 */
struct snd_dma_buffer *dma_buffer_p; //被分配的缓冲区
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
/* OSS 信息 */
struct snd_pcm_oss_runtime oss;
#endif
};
snd_pcm_runtime 中的大多数记录对被声卡驱动操作集中的函数是只读的,仅仅 PCM 中间层可从更新或修改这些信息,但是硬件描述、中断回调函数、 DMA 缓冲区信息和私有数据是例外的。
snd_pcm_runtime结构体中的重要成员:
(1)硬件描述
硬件描述(snd_pcm_hardware 结构体)包含了基本硬件配置的定义, 需要在 open()函数中赋值。 runtime实例保存的是硬件描述的拷贝而非指针,这意味着在 open()函数中可以修改被拷贝的描述(runtime->hw),
例如:
struct snd_pcm_runtime *runtime = substream->runtime;
...
runtime->hw = snd_xxchip_playback_hw; /* “大众”硬件描述 */
/* 特定的硬件描述 */
if (chip->model == VERY_OLD_ONE)
runtime->hw.channels_max = 1;
snd_pcm_hardware结构体定义如下:
struct snd_pcm_hardware
{
unsigned int info; /* 标识 PCM 设备的类型和能力, 形式为 SNDRV_PCM_ INFO_XXX* /
u64 formats; /* PCM 设备支持的格式,形式为 SNDRV_PCM_FMTBIT_XXX */
unsigned int rates; /* PCM 设备支持的采样率,形式如SNDRV_PCM_RATE_XXX,如果支持连续的采样率,则传递 CONTINUOUS。*/
unsigned int rate_min; /* 最小采样率 */
unsigned int rate_max; /* 最大采样率 */
unsigned int channels_min; /* 最小的通道数 */
unsigned int channels_max; /* 最大的通道数 */
size_t buffer_bytes_max; /* 最大缓冲区大小 */
size_t period_bytes_min; /* 最小周期大小 */
size_t period_bytes_max; /* 最大奏曲大小 */
unsigned int periods_min; /* 最小周期数 */
unsigned int periods_max; /* 最大周期数 */
size_t fifo_size; /* FIFO 字节数 */
};
info 字段至少需要定义是否支持 mmap,当支持时,应设置 SNDRV_PCM_INFO_MMAP 标志;SNDRV_PCM_INFO_PAUSE 意 味 着 设 备 可 支 持 暂 停 操 作 , 而 SNDRV_PCM_INFO_RESUME 意味着设备可支持挂起/恢复操作;当 PCM 子流能被同步,如同步播放和录音流的start/stop,可设置 SNDRV_PCM_INFO_SYNC_START 标志。
PCM 可被应用程序通过 alsa-lib 发送 hw_params 来配置,配置信息将保存在运行时实例中。对缓冲区和周期大小的配置以帧形式存储,而 frames_to_bytes()和 bytes_to_frames()可完成帧和字节的转换,如:
period_bytes = frames_to_bytes(runtime, runtime->period_size);
(2) DMA 缓冲区信息。
包含 dma_area(逻辑地址)、 dma_addr(物理地址)、dma_bytes(缓冲区大小)和 dma_private(被 ALSADMA 分配器使用)。可以由 snd_pcm_lib_malloc_pages()实现, ALSA 中间层会设置 DMA 缓冲区信息的相关字段,这种情况下,驱动中不能再写这些信息,只能读取。如果使用标准的缓冲区分配函数snd_pcm_lib_malloc_pages()分配缓冲区,则我们不需要自己维护 DMA 缓冲区信息。如果缓冲区由自己分配,则需要在 hw_params()函数中管理缓冲区信息,至少需管理 dma_bytes 和 dma_addr,如果支持 mmap,则必须管理 dma_area,对 dma_private 的管理视情况而定。
(3)运行状态。
通过 runtime->status 可以获得运行状态,它是snd_pcm_mmap_status 结构体的指针,例如,通过runtime->status->hw_ptr 可以获得目前的 DMA 硬件指针。此外,通过 runtime->control 可以获得 DMA 应用指针,它指向 snd_pcm_mmap_control 结构体指针。
(4)私有数据。
驱动中可以为子流分配一段内存并赋值给 runtime->private_data,例如:
static int snd_xxx_open(struct snd_pcm_substream *substream)
{
struct xxx_pcm_data *data;
....
data = kmalloc(sizeof(*data), GFP_KERNEL);
substream->runtime->private_data = data; //赋值 runtime->private_data
....
}
(5)中断回调函数:
transfer_ack_begin()和 transfer_ack_end()函数分别在 snd_pcm_period_elapsed()的开始和结束时被调用。
#include
....
/* 播放设备硬件定义 */
static struct snd_pcm_hardware snd_xxxchip_playback_hw =
{
.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* 录音设备硬件定义 */
static struct snd_pcm_hardware snd_xxxchip_capture_hw =
{
.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID),
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_8000_48000,
.rate_min = 8000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 32768,
.period_bytes_min = 4096,
.period_bytes_max = 32768,
.periods_min = 1,
.periods_max = 1024,
};
/* 播放:打开函数 */
static int snd_xxxchip_playback_open(struct snd_pcm_substream*substream)
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_xxxchip_playback_hw;
... // 硬件初始化代码
return 0;
}
/* 播放:关闭函数 */
static int snd_xxxchip_playback_close(struct snd_pcm_substream*substream)
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
// 硬件相关的代码
return 0;
}
/* 录音:打开函数 */
static int snd_xxxchip_capture_open(struct snd_pcm_substream*substream)
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = snd_xxxchip_capture_hw;
... // 硬件初始化代码
return 0;
}
/* 录音:关闭函数 */
static int snd_xxxchip_capture_close(struct snd_pcm_substream*substream)
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
... // 硬件相关的代码
return 0;
}
/* hw_params 函数 */
static int snd_xxxchip_pcm_hw_params(struct snd_pcm_substream*substream, struct
snd_pcm_hw_params *hw_params)
{
return snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));
}
/* hw_free 函数 */
static int snd_xxxchip_pcm_hw_free(struct snd_pcm_substream*substream)
{
return snd_pcm_lib_free_pages(substream);
}
/* prepare 函数 */
static int snd_xxxchip_pcm_prepare(struct snd_pcm_substream*substream)
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/* 根据目前的配置信息设置硬件
* 例如:
*/
xxxchip_set_sample_format(chip, runtime->format);
xxxchip_set_sample_rate(chip, runtime->rate);
xxxchip_set_channels(chip, runtime->channels);
xxxchip_set_dma_setup(chip, runtime->dma_addr, chip->buffer_size, chip
->period_size);
return 0;
}
/* trigger 函数 */
static int snd_xxxchip_pcm_trigger(struct snd_pcm_substream*substream, int cmd)
{
switch (cmd)
{
case SNDRV_PCM_TRIGGER_START:
// do something to start the PCM engine
break;
case SNDRV_PCM_TRIGGER_STOP:
// do something to stop the PCM engine
break;
default:
return - EINVAL;
}
}
/* pointer 函数 */
static snd_pcm_uframes_t snd_xxxchip_pcm_pointer(struct snd_pcm_substream *substream)
{
struct xxxchip *chip = snd_pcm_substream_chip(substream);
unsigned int current_ptr;
/*获得当前的硬件指针*/
current_ptr = xxxchip_get_hw_pointer(chip);
return current_ptr;
}
/* 放音设备操作集 */
static struct snd_pcm_ops snd_xxxchip_playback_ops =
{
.open = snd_xxxchip_playback_open,
.close = snd_xxxchip_playback_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_xxxchip_pcm_hw_params,
.hw_free = snd_xxxchip_pcm_hw_free,
.prepare = snd_xxxchip_pcm_prepare,
.trigger = snd_xxxchip_pcm_trigger,
.pointer = snd_xxxchip_pcm_pointer,
};
/* 录音设备操作集 */
static struct snd_pcm_ops snd_xxxchip_capture_ops =
{
.open = snd_xxxchip_capture_open,
.close = snd_xxxchip_capture_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_xxxchip_pcm_hw_params,
.hw_free = snd_xxxchip_pcm_hw_free,
.prepare = snd_xxxchip_pcm_prepare,
.trigger = snd_xxxchip_pcm_trigger,
.pointer = snd_xxxchip_pcm_pointer,
};
/* 创建一个 PCM 设备 */
static int _ _devinit snd_xxxchip_new_pcm(struct xxxchip *chip)
{
struct snd_pcm *pcm;
int err;
if ((err = snd_pcm_new(chip->card, "xxx Chip", 0, 1, 1, &pcm)) < 0)
return err;
pcm->private_data = chip;
strcpy(pcm->name, "xxx Chip");
chip->pcm = pcm;
/* 设置操作集 */
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_xxxchip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &snd_xxxchip_capture_ops);
/* 分配缓冲区 */
snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
snd_dma_pci_data(chip - > pci), 64 *1024, 64 *1024);
return 0;
}
1、control
control的最主要用途是 mixer,所有的 mixer 元素基于 control 内核 API 实现,在 ALSA 中, control 用 snd_kcontrol结构体描述。创建一个新的 control 至少需要实现 snd_kcontrol_new 中的 info()、 get()和 put()这 3 个成员函数,snd_kcontrol_new 结构体的定义如下:
struct snd_kcontrol_new
{
snd_ctl_elem_iface_t iface; /*接口 ID,定义control的类型,形式为 SNDRV_CTL_ELEM_IFACE_XXX,通常是MIXER,对于不属于mixer的全局控制,使用CARD */
unsigned int device; /* 设备号 */
unsigned int subdevice; /* 子流(子设备)号 */
unsigned char *name; /* 名称(ASCII 格式),control的作用根据名称来区分,对于名称相同的control,根据index区分 */
unsigned int index; /* 索引 */
unsigned int access; /* 访问权限 */
unsigned int count; /* 享用元素的数量 */
snd_kcontrol_info_t *info;
snd_kcontrol_get_t *get;
snd_kcontrol_put_t *put;
unsigned long private_value;
};
2. info()函数
snd_kcontrol_new 结构体中的 info()函数用于获得该 control 的详细信息,该函数必须填充传递给它的第二个参数 snd_ctl_elem_info 结构体, info()函数的形式如下:
static int snd_xxxctl_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo);
snd_ctl_elem_info 结构体的定义如下:
struct snd_ctl_elem_info
{
struct snd_ctl_elem_id id; /* W: 元素 ID */
snd_ctl_elem_type_t type; /*定义control的类型,包括 BOOLEAN、 INTEGER、ENUMERATED、 BYTES、 IEC958 和 INTEGER64 */
unsigned int access; /* R: 值访问权限(位掩码) - SNDRV_CTL_ELEM_ACCESS_* */
unsigned int count; /* 值的计数,control中包含的元素的个数 */
pid_t owner; /* 该 control 的拥有者 PID */
union
{
struct
{
long min; /* R: 最小值 */
long max; /* R: 最大值 */
long step; /* R: 值步进 (0 可变的) */
} integer;
struct
{
long long min; /* R: 最小值 */
long long max; /* R: 最大值 */
long long step; /* R: 值步进 (0 可变的) */
} integer64;
struct
{
unsigned int items; /* R: 项目数 */
unsigned int item; /* W: 项目号 */
char name[64]; /* R: 值名称 */
} enumerated; /* 枚举 */
unsigned char reserved[128];
}
value;
union
{
unsigned short d[4];
unsigned short *d_ptr;
} dimen;
unsigned char reserved[64-4 * sizeof(unsigned short)];
};
3、get()函数
get()函数用于得到 control 的目前值并返回用户空间。
static int snd_xxxctl_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
//从 snd_kcontrol 获得 xxxchip 指针
struct xxxchip *chip = snd_kcontrol_chip(kcontrol);
//从 xxxchip 获得值并写入 snd_ctl_elem_value
ucontrol->value.integer.value[0] = get_some_value(chip);
return 0;
}
snd_ctl_elem_value,定义如下:
struct snd_ctl_elem_value
{
struct snd_ctl_elem_id id; /* W: 元素 ID */
unsigned int indirect: 1; /* W: 使用间接指针(xxx_ptr 成员) */
//值联合体
union
{
union
{
long value[128];
long *value_ptr;
} integer;
union
{
long long value[64];
long long *value_ptr;
} integer64;
union
{
unsigned int item[128];
unsigned int *item_ptr;
} enumerated;
union
{
unsigned char data[512];
unsigned char *data_ptr;
} bytes;
struct snd_aes_iec958 iec958;
}
value; /* 只读 */
struct timespec tstamp;
unsigned char reserved[128-sizeof(struct timespec)];
};
4、put()函数
put()用于从用户空间写入值,如果值被改变,该函数返回 1,否则返回 0;如果发生错误,该函数返回一个错误码。范例如下:
static int snd_xxxctl_put(struct snd_kcontrol *kcontrol, struct
snd_ctl_elem_value *ucontrol)
{
//从 snd_kcontrol 获得 xxxchip 指针
struct xxxchip *chip = snd_kcontrol_chip(kcontrol);
int changed = 0;//默认返回值为 0
//值 被 改 变
if (chip->current_value != ucontrol->value.integer.value[0])
{
change_current_value(chip, ucontrol->value.integer.value[0]);
changed = 1;//返回值为 1
}
return changed;
}
对于 get()和 put()函数而言,如果 control 有多于一个元素,即 count >1,则每个元素都需要被返回或写入。
5.构造 control
当所有事情准备好后,我们需要创建一个 control,调用 snd_ctl_add()和 snd_ctl_new1()这两个函数来完成,这两个函数的原型为:
int snd_ctl_add(struct snd_card *card, struct snd_kcontrol *kcontrol);
struct snd_kcontrol *snd_ctl_new1(const struct snd_kcontrol_new *ncontrol,void *private_data);
snd_ctl_new1() 函 数 用 于 创 建 一 个 snd_kcontrol 并 返 回 其 指针 ,
snd_ctl_add() 函 数 用于 将 创 建的snd_kcontrol 添加到对应的 card 中。
6.变更通知
如果驱动中需要在中断服务程序中改变或更新一个 control,可以调用 snd_ctl_notify()函数,此函数原型为:
void snd_ctl_notify(struct snd_card *card, unsigned int mask, struct snd_ctl_elem_id *id);
该函数的第二个参数为事件掩码(event-mask),第三个参数为该通知的 control 元素 id 指针。例如, 如下语句定义的事件掩码 SNDRV_CTL_EVENT_MASK_VALUE 意味着 control 值的改变被通知:
snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, id_pointer);
1. AC97 实例构造
为了创建一个 AC97 实例,首先需要调用 snd_ac97_bus()函数构建 AC97 总线及其操作,这个函数的原型为:
int snd_ac97_bus(struct snd_card *card, int num, struct snd_ac97_bus_ops *ops,void *private_data, struct snd_ac97_bus **rbus);
该函数的第 3 个参数 ops 是一个 snd_ac97_bus_ops 结构体,其定义如下:
struct snd_ac97_bus_ops
{
void(*reset)(struct snd_ac97 *ac97); //复位函数
//写入函数
void(*write)(struct snd_ac97 *ac97, unsigned short reg, unsigned short val);
//读取函数
unsigned short(*read)(struct snd_ac97 *ac97, unsigned short reg);
void(*wait)(struct snd_ac97 *ac97);
void(*init)(struct snd_ac97 *ac97);
};
接下来,调用 snd_ac97_mixer()函数注册混音器,这个函数的原型为:
int snd_ac97_mixer(struct snd_ac97_bus *bus, struct snd_ac97_template *template, struct snd_ac97 **rac97);
代码所示为 AC97 实例的创建过程:
struct snd_ac97_bus *bus;
//AC97 总线操作
static struct snd_ac97_bus_ops ops =
{
.write = snd_mychip_ac97_write,
.read = snd_mychip_ac97_read,
};
//AC97 总线与操作创建
snd_ac97_bus(card, 0, &ops, NULL, &bus);
//AC97 模板
struct snd_ac97_template ac97;
int err;
memset(&ac97, 0, sizeof(ac97));
ac97.private_data = chip;//私有数据
//注册混音器
snd_ac97_mixer(bus, &ac97, &chip->ac97);
2. snd_ac97_bus_ops 成员函数
snd_ac97_bus_ops 结构体中的 read()和 write()成员函数完成底层的硬件访问, reset()函数用于复位编解码器, wait()函数用于编解码器标准初始化过程中的特定等待,如果芯片要求额外的等待时间,则应实现这个函数, init()用于完成编解码器附加的初始化。
3.修改寄存器
如果需要在驱动中访问编解码器,可使用如下函数:
void snd_ac97_write(struct snd_ac97 *ac97, unsigned short reg, unsigned short value);
int snd_ac97_update(struct snd_ac97 *ac97, unsigned short reg, unsigned short value);
int snd_ac97_update_bits(struct snd_ac97 *ac97, unsigned short reg, unsigned short mask,unsigned short value);
unsigned short snd_ac97_read(struct snd_ac97 *ac97, unsigned short reg);
snd_ac97_update()与 void snd_ac97_write()的区别在于前者在值已经设置的情况下不会再设置,而后者则会再写一次。 snd_ac97_update_bits()用于更新寄存器的某些位,由 mask 决定。
除此之外,还有一个函数可用于设置采样率:
int snd_ac97_set_rate(struct snd_ac97 *ac97, int reg, unsigned int rate);
这个函数的第二个参数 reg 可以是 AC97_PCM_MIC_ADC_RATE、 AC97_PCM_FRONT_DAC_ RATE、AC97_PCM_LR_ADC_RATE 和 AC97_SPDIF,对于 AC97 _SPDIF 而言,寄存器并非真地被改变了,只是相应的 IEC958 状态位将被更新。
4.时钟调整
在一些芯片上,编解码器的时钟频率不是 48000Hz,而是使用 PCI 时钟以节省一个晶振,在这种情况下,我们应该改变 bus->clock 为相应的值,例如 intel8x0 和 es1968 包含时钟的自动测量函数。
ALSA 驱动的声卡在用户空间不宜直接使用文件接口,而应使用 alsa-lib,代码所示为基于ALSA 音频驱动的最简单的播放应用程序
#include
#include
#include
main(int argc, char *argv[])
{
int i;
int err;
short buf[128];
snd_pcm_t *playback_handle; //PCM 设备句柄
snd_pcm_hw_params_t *hw_params; //硬件信息和 PCM 流配置
//打开 PCM,最后一个参数为 0 意味着标准配置
if ((err = snd_pcm_open(&playback_handle, argv[1], SND_PCM_STREAM_PLAYBACK, 0)) < 0)
{
fprintf(stderr, "cannot open audio device %s (%s)\n", argv[1], snd_strerror
(err));
exit(1);
}
//分配 snd_pcm_hw_params_t 结构体
if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0)
{
fprintf(stderr, "cannot allocate hardware parameter structure (%s)\n",snd_strerror(err));
exit(1);
}
//初始化 hw_params
if ((err = snd_pcm_hw_params_any(playback_handle, hw_params)) < 0)
{
fprintf(stderr, "cannot initialize hardware parameter structure (%s)\n",
snd_strerror(err));
exit(1);
}
//初始化访问权限
if ((err=snd_pcm_hw_params_set_access(playback_handle, hw_params,SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
{
fprintf(stderr, "cannot set access type (%s)\n", snd_strerror(err));
exit(1);
}
//初始化采样格式
if ((err=snd_pcm_hw_params_set_format(playback_handle, hw_params,SND_PCM_FORMAT_S16_LE)) < 0)
{
fprintf(stderr, "cannot set sample format (%s)\n", snd_strerror(err));
exit(1);
}
//设置采样率,如果硬件不支持我们设置的采样率,将使用最接近的
if((err=snd_pcm_hw_params_set_rate_near(playback_handle, hw_params, 44100,
0)) < 0)
{
fprintf(stderr, "cannot set sample rate (%s)\n", snd_strerror(err));
exit(1);
}
//设置通道数量
if((err=snd_pcm_hw_params_set_channels(playback_handle, hw_params, 2)) < 0)
{
fprintf(stderr, "cannot set channel count (%s)\n", snd_strerror(err));
exit(1);
}
//设置 hw_params
if((err = snd_pcm_hw_params(playback_handle, hw_params)) < 0)
{
fprintf(stderr, "cannot set parameters (%s)\n", snd_strerror(err));
exit(1);
}
//释放分配的 snd_pcm_hw_params_t 结构体
snd_pcm_hw_params_free(hw_params);
//完成硬件参数设置,使设备准备好
if ((err = snd_pcm_prepare(playback_handle)) < 0)
{
fprintf(stderr, "cannot prepare audio interface for use (%s)\n",
snd_strerror(err));
exit(1);
}
for (i = 0; i < 10; ++i)
{
//写音频数据到 PCM 设备
if ((err = snd_pcm_writei(playback_handle, buf, 128)) != 128)
{
fprintf(stderr, "write to audio interface failed (%s)\n", snd_strerror
(err));
exit(1);
}
}
//关闭 PCM 设备句柄
snd_pcm_close(playback_handle);
exit(0);
}
OSS标准中有两个基本的音频设备:mixer(混音器)和dsp(数字信号处理)。
mixer的作用是将多个信号组成或者叠加在一起,在OSS驱动中,/dev/mixer设备文件是应用程序对mixer进行操作的软件接口。
混音器电路通常由两部分组成:输入混音器(input mixer)和输出混音器(output mixer)。
输入混音器负责从不同的信号源接收模拟信号(混音通道或混音设备)。模拟信号通过增益控制器和由软件控制的音量调节器,在不同的混音通道中进行级别(level)调制,然后被送到输入混音器中进行声音的合成。混音器上的电子开关可以控制哪些通道中有信号与混音器相连,有些声卡只允许连接一个混音通道作为录音的音源, 而有些声卡则允许对混音通道做任意的连接。经过输入混音器处理后的信号仍然为模拟信号,它们将被送到 A/D 转换器进行数字化处理。
当输出混音器对所有的模拟信号进行了混合之后,通常还会有一个总控增益调节器来控制输出声音的大小, 此外还有一些音调控制器来调节输出声音的音调。 经过输出混音器处理后的信号也是模拟信号,它们最终会被送给喇叭或者其他的模拟输出设备。
对混音器的编程包括如何设置增益控制器的级别,以及怎样在不同的音源间进行切换,这些操作通常来讲是不连续的,大部分的操作都是通过 ioctl()系统调用来完成的。/dev/mixer 允许多个应用程序同时访问,并且混音器的设置值会一直保持到对应的设备文件被关闭为止。
DSP也称为编解码器,实现录音和放音(播放),其对应的设备文件是/dev/dsp或/dev/sound/dsp。OSS 声卡驱动程序提供的/dev/dsp 是用于数字采样和数字录音的设备文件,向该设备写数据即意味着激活声卡上的 D/A 转换器进行播放,而向该设备读数据则意味着激活声卡上的 A/D 转换器进行录音。
从 DSP 设备读取数据时,从声卡输入的模拟信号经过 A/D 转换器变成数字采样后的样本,保存在声卡驱动程序的内核缓冲区中,当应用程序通过 read()系统调用从声卡读取数据时,保存在内核缓冲区中的数字采样结果将被复制到应用程序所指定的用户缓冲区中。需要指出的是,声卡采样频率是由内核中的驱动程序所决定的,而不取决于应用程序从声卡读取数据的速度。
向 DSP 设备写入数据时,数字信号会经过 D/A 转换器变成模拟信号,然后产生声音。应用程序写入数据的速度应该至少等于声卡的采样频率,过慢会产生声音暂停或者停顿的现象(即 underflow)。如果用户写入过快的话,它会被内核中的声卡驱动程序阻塞,直到硬件有能力处理新的数据为止。
声卡通常不需要支持非阻塞的I/O操作。
无论是从声卡读取数据,或是向声卡写入数据,事实上都具有特定的格式(format),如无符号 8 位、单声道、 8kHz 采样率,如果默认值无法达到要求,可以通过 ioctl()系统调用来改变它们。通常说来,在应用程序中打开设备文件/dev/dsp 之后,接下去就应该为其设置恰当的格式,然后才能从声卡读取或者写入数据。
int register_sound_mixer(struct file_operations *fops, int dev);
上述函数用于注册一个混音器,第一个参数 fops 即是文件操作接口,第二个参数 dev 是设备编号,如果填入-1,则系统自动分配一个设备编号。 mixer 是一个典型的字符设备,因此编码的主要工作是实现file_operations 中的 open()、 ioctl()等函数。
mixer 接口 file_operations 中的最重要函数是 ioctl(), 它实现混音器的不同 I/O 控制命令。
int register_sound_dsp(struct file_operations *fops, int dev);
上述函数用于注册一个 dsp 设备,第一个参数 fops 即是文件操作接口,第二个参数 dev 是设备编号,如果填入-1,则系统自动分配一个设备编号。 dsp 也是一个典型的字符设备,因此编码的主要工作是实现 file_operations 中的 read()、 write()、 ioctl()等函数。
dsp 接口 file_operations 中的 read()和 write()函数非常重要, read()函数从音频控制器中获取录音数据到缓冲区并复制到用户空间, write()函数从用户空间复制音频数据到内核空间缓冲区并最终发送到音频控制器。
dsp 接口 file_operations 中的 ioctl()函数处理对采样率、量化精度、 DMA 缓冲区块大小等参数设置 I/O控制命令的处理。
dsp接口中的poll()函数通常用于实现向用户反馈目前能否读写DMA缓冲区。
在数据从缓冲区复制到音频控制器的过程中,通常会使用 DMA, DMA 对声卡而言非常重要。例如,在放音时, 驱动设置完 DMA 控制器的源数据地址(内存中的 DMA 缓冲区)、 目的地址(音频控制器 FIFO)和 DMA 的数据长度, DMA 控制器会自动发送缓冲区的数据填充 FIFO,直到发送完相应的数据长度后才中断一次。
在 OSS 驱动中,建立存放音频数据的环形缓冲区(ring buffer)通常是值得推荐的方法。此外,在 OSS驱动中, 一般会将一个较大的 DMA 缓冲区分成若干个大小相同的块(这些块也被称为“段”, 即 fragment),驱动程序使用 DMA 每次在声音缓冲区和声卡之间搬移一个 fragment。在用户空间,可以使用 ioctl()系统调用来调整块的大小和个数。
对OSS驱动声卡的编程使用Linux文件接口函数,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 实现播放或录音。
如下代码清单的程序实现了利用/dev/dsp 接口进行声音录制和播放的过程,它的功能是先录制几秒钟音频数据,将其存放在内存缓冲区中,然后再进行播放。
#include
#include
#include
#include
#include
#include
#include
#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);
if(fd < 0)
{
perror("open of /dev/dsp failed");
exit(1);
}
/* 设置采样时的量化位数 */
arg = SIZE;
status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
if (status == - 1)
perror("SOUND_PCM_WRITE_BITS ioctl failed");
if (arg != SIZE)
perror("unable to set sample size");
/* 设置采样时的通道数目 */
arg = CHANNELS;
status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);
if(status == - 1)
perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
if(arg != CHANNELS)
perror("unable to set number of channels");
/* 设置采样率 */
arg = RATE;
status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
if(status == - 1)
perror("SOUND_PCM_WRITE_WRITE ioctl failed");
/* 循环,直到按下[ControltC]*/
while (1)
{
printf("Say something:\n");
status = read(fd, buf, sizeof(buf)); /* 录音 */
if(status != sizeof(buf))
perror("read wrong number of bytes");
printf("You said:\n");
status = write(fd, buf, sizeof(buf)); /* 放音 */
if(status != sizeof(buf))
perror("wrote wrong number of bytes");
/* 在继续录音前等待放音结束 */
status = ioctl(fd, SOUND_PCM_SYNC, 0);
if(status == - 1)
perror("SOUND_PCM_SYNC ioctl failed");
}
}
声卡上的混音器由多个混音通道组成,它们可以通过驱动程序提供的设备文件/dev/mixer 进行编程。对混音器的操作一般都通过 ioctl()系统调用来完成, 所有控制命令都以 SOUND_MIXER 或者 MIXER 开头,下表列出了常用的混音器控制命令。
对声卡的输入增益和输出增益进行调节是混音器的一个主要作用,目前大部分声卡采用的是 8 位或者16 位的增益控制器, 声卡驱动程序会将它们变换成百分比的形式, 也就是说无论是输入增益还是输出增益,其取值范围都是从 0~100。
(1) SOUND_MIXER_READ 宏。
在进行混音器编程时,可以使用 SOUND_MIXER_READ 宏来读取混音通道的增益大小,例如,如下代码可以获得麦克风的输入增益:
ioctl(fd, SOUND_MIXER_READ(SOUND_MIXER_MIC), &vol);
对于只有一个混音通道的单声道设备来说,返回的增益大小保存在低位字节中。而对于支持多个混音通道的双声道设备来说,返回的增益大小实际上包括两个部分,分别代表左、右两个声道的值,其中低位字节保存左声道的音量,而高位字节则保存右声道的音量。下面的代码可以从返回值中依次提取左右声道的增益大小:
int left, right;
left = vol & 0xff;
right = (vol & 0xff00) >> 8;
(2) SOUND_MIXER_WRITE 宏。
如果想设置混音通道的增益大小,则可以通过 SOUND_MIXER_WRITE 宏来实现,例如下面的语句可以用来设置麦克风的输入增益:
vol = (right << 8) + left;
ioctl(fd, SOUND_MIXER_WRITE(SOUND_MIXER_MIC), &vol);
(3)查询 MIXER 信息。
声卡驱动程序提供了多个 ioctl()系统调用来获得混音器的信息,它们通常返回一个整型的位掩码,其中每一位分别代表一个特定的混音通道,如果相应的位为 1,则说明与之对应的混音通道是可用的。
通过 SOUND_MIXER_READ_DEVMASK 返回的位掩码查询出能够被声卡支持的每一个混音通道, 而通过 SOUND_MIXER_READ_RECMAS 返回的位掩码则可以查询出能够被当作录音源的每一个通道。
大多数声卡提供了多个录音源,通过 SOUND_MIXER_READ_RECSRC 可以查询出当前正在使用的录音源,同一时刻可使用两个或两个以上的录音源,具体由声卡硬件本身决定。相应地,使用SOUND_MIXER_WRITE_RECSRC 可以设置声卡当前使用的录音源, 如下代码可以将 CD 输入作为声卡的录音源使用。
devmask = SOUND_MIXER_CD;
ioctl(fd, SOUND_MIXER_WRITE_RECSRC, &devmask);
此外,所有的混音通道都有单声道和双声道的区别,如果需要知道哪些混音通道提供了对立体声的支持,可以通过 SOUND_MIXER_READ_STEREODEVS 来获得。
以下程序实现了利用/dev/mixer 接口对混音器进行编程的过程,该程序可对各种混音通道的增益进行调节。
#include
#include
#include
#include
#include
#include
/* 用来存储所有可用混音设备的名称 */
const char *sound_device_names[] = SOUND_DEVICE_NAMES;
int fd; /* 混音设备所对应的文件描述符 */
int devmask, stereodevs; /* 混音器信息对应的 bit 掩码 */
char *name;
/* 显示命令的使用方法及所有可用的混音设备 */
void usage()
{
int i;
fprintf(stderr, "usage: %s \n"
"%s \n\n""Where is one of:\n", name, name);
for(i = 0; i < SOUND_MIXER_NRDEVICES; i++)
if ((1 << i) &devmask)
/* 只显示有效的混音设备 */
fprintf(stderr, "%s ", sound_device_names[i]);
fprintf(stderr, "\n");
exit(1);
}
int main(int argc, char *argv[])
{
int left, right, level; /* 增益设置 */
int status; /* 系统调用的返回值 */
int device; /* 选用的混音设备 */
char *dev; /* 混音设备的名称 */
int i;
name = argv[0];
/* 以只读方式打开混音设备 */
fd = open("/dev/mixer", O_RDONLY);
if (fd == - 1)
{
perror("unable to open /dev/mixer");
exit(1);
}
/* 获得所需要的信息 */
status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);//被声卡支持的每一个混音通道
if(status == - 1)
perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);//获取对立体声支持的通道
if (status == - 1)
perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");
/* 检查用户输入 */
if (argc != 3 && argc != 4)
usage();
/* 保存用户输入的混音器名称 */
dev = argv[1];
/* 确定即将用到的混音设备 */
for (i = 0; i < SOUND_MIXER_NRDEVICES; i++)
if (((1 << i) &devmask) && !strcmp(dev, sound_device_names[i]))
break;
if (i == SOUND_MIXER_NRDEVICES)
{
/* 没有找到匹配项 */
fprintf(stderr, "%s is not a valid mixer device\n", dev);
usage();
}
/* 查找到有效的混音设备 */
device = i;
/* 获取增益值 */
if (argc == 4)
{
/* 左、右声道均给定 */
left = atoi(argv[2]);
right = atoi(argv[3]);
}
else
{
/* 左、右声道设为相等 */
left = atoi(argv[2]);
right = atoi(argv[2]);
}
/* 对非立体声设备给出警告信息 */
if ((left != right) && !((1 << i) &stereodevs))
{
fprintf(stderr, "warning: %s is not a stereo device\n", dev);
}
/* 将两个声道的值合到同一变量中 */
level = (right << 8) + left;
/* 设置增益 */
status = ioctl(fd, MIXER_WRITE(device), &level);
if (status == - 1)
{
perror("MIXER_WRITE ioctl failed");
exit(1);
}
/* 获得从驱动返回的左右声道的增益 */
left = level &0xff;
right = (level &0xff00) >> 8;
/* 显示实际设置的增益 */
fprintf(stderr, "%s gain set to %d%% / %d%%\n", dev, left, right);
/* 关闭混音设备 */
close(fd);
return 0;
}
编译上述程序为可执行文件 mixer,执行./mixer