音频应用编程-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

音频应用编程

在这里插入图片描述

Linux 下 ALSA 框架概述

ALSA 简介:ALSA 是 Advanced Linux Sound Architecture(高级的 Linux 声音体系)的缩写

地位与功能:现已成为 Linux 下的主流音频体系架构,提供音频和 MIDI 支持,替代了旧版本中的 OSS(开放声音系统)

框架设计:ALSA 是 Linux 系统下标准且先进的音频驱动框架,设计复杂,采用分离和分层思想

应用层 API:在应用层,ALSA 提供了一套标准 API(alsa-lib),应用程序调用这些 API 即可控制底层音频硬件设备,实现播放、录音等功能

  • alsa 音频示意图

alsa-lib 库介绍

性质与功能

  • alsa-lib 是 Linux 应用层的 C 语言函数库

  • 它为音频应用程序开发提供了一套统一、标准的接口

API 接口

  • 应用程序只需调用这一套 API 即可完成对底层声卡设备的操控,如播放与录音

  • 隐藏了驱动层的实现细节,简化了应用程序的实现难度

开发者便利

  • 用户空间的 alsa-lib 对应用程序提供了统一的 API 接口

  • 开发者无需直接读写音频设备节点

使用说明文档

  • 地址:https://www.alsa-project.org/alsa-doc/alsa-lib/

alsa-lib 库的功能模块

  • alsa-lib 库提供了丰富的 API 接口,根据功能和作用进行了分类

  • 模块列表:
    ⚫ Global defines and functions:包括一些全局的定义,譬如函数、宏等;
    ⚫ Constants for Digital Audio Interfaces:数字音频接口相关的常量;
    ⚫ Input Interface:输入接口;
    ⚫ Output Interface:输出接口;
    ⚫ Error handling:错误处理相关接口;
    ⚫ Configuration Interface:配置接口;
    ⚫ Control Interface:控制接口;
    ⚫ PCM Interface:PCM 设备接口;
    ⚫ RawMidi Interface:RawMidi 接口;
    ⚫ Timer Interface:定时器接口;
    ⚫ Hardware Dependant Interface:硬件相关接口;
    ⚫ MIDI Sequencer:MIDI 音序器;
    ⚫ External PCM plugin SDK:外部 PCM 插件 SDK;
    ⚫ External Control Plugin SDK:外部控制插件 SDK;
    ⚫ Mixer Interface:混音器接口;
    ⚫ Use Case Interface:用例接口;
    ⚫ Topology Interface:拓扑接口。

  • 重点模块

    • PCM Interface(PCM 设备接口)

      • PCM Interface 提供了与 PCM(脉冲编码调制)设备相关的操作接口

      • 主要操作

        • 设备管理

          • 打开 PCM 设备

          • 关闭 PCM 设备

        • 参数配置

          • 配置 PCM 设备的硬件参数

          • 配置 PCM 设备的软件参数

        • 设备控制

          • 启动 PCM 设备

          • 暂停 PCM 设备

          • 恢复 PCM 设备

        • 数据操作

          • 写入数据到 PCM 设备

          • 从 PCM 设备读取数据

      • 该模块下还包含了一些子模块,用于细化和扩展 PCM 设备的操作和控制功能

        • 点击模块名称可以查看到该模块提供的 API 接口有哪些以及相应的函数说明
    • Error Interface(错误处理接口)

      • 该模块提供了关于错误处理相关的接口,譬如函数调用发生错误时,可调用该模块下提供的函数打印错误描述信息
    • Mixer Interface(混音器接口)

      • 提供了关于混音器相关的一系列操作接口,譬如音量、声道控制、增益等

sound 设备节点

设备节点文件

  • 在基于 ALSA 音频驱动框架注册的 sound 设备会在 /dev/snd 目录下生成相应的设备节点文件

    • controlC0

      • 用于声卡控制的设备节点,如通道选择、混音器、麦克风的控制等

      • C0 表示声卡 0(card0)

    • pcmC0D0c

      • 用于录音的 PCM 设备节点

      • C0 表示 card0(声卡 0),D0 表示 device 0(设备 0),c 表示 capture(录音)

    • pcmC0D0p

      • 用于播放的 PCM 设备节点

      • C0 表示 card0(声卡 0),D0 表示 device 0(设备 0),p 表示 playback(播放)

    • pcmC0D1c

      • 用于录音的 PCM 设备节点

      • 对应声卡 0 中的录音设备 1

    • pcmC0D1p

      • 用于播放的 PCM 设备节点

      • 对应声卡 0 中的播放设备 1

    • timer

      • 定时器设备节点
  • Mini 板不支持音频、 没有板载音频编解码芯片,所以本章实验例程无法在 Mini 板上进行测试

应用程序与 alsa-lib

  • 我们编写的应用程序虽然调用 alsa-lib 库函数去控制底层音频硬件,但最终也是落实到对 sound 设备节点的 I/O 操作,只不过 alsa-lib 已经帮我们封装好了

  • 在 Linux 系统的 /proc/asound 目录下,有很多文件记录了系统中声卡相关的信息

重要文件说明

  • cards 文件

    • 通过 cat /proc/asound/cards 命令查看 cards 文件的内容,可列出系统中可用的、注册的声卡

    • 阿尔法板子上只有一个声卡(WM8960 音频编解码器),它的编号为 0,即 card0

    • 系统中注册的所有声卡在 /proc/asound/ 目录下将存在一个相应的目录,如 card0

  • devices 文件

    • 列出系统中所有声卡注册的设备,包括 control、pcm、timer、seq 等

    • 使用 cat /proc/asound/devices 命令查看

  • pcm 文件

    • 列出系统中的所有 PCM 设备,包括 playback 和 capture

    • 使用 cat /proc/asound/pcm 命令查看

alsa-lib 库移植

需要移植移植 alsa-lib 和 alsa-utils

alsa-utils 工具集

  • 功能概述:alsa-utils 提供了一系列用于测试和配置声卡的工具

  • 主要工具

    • aplay:
      用于测试音频播放功能

      • 可以使用 aplay 播放 wav 格式的音频文件

      • 程序运行后会开始播放音乐

      • 自动切换功能

        • ALPHA 开发板支持喇叭和耳机自动切换功能

          • 如果不插耳机,默认从喇叭播放音乐

          • 插上耳机后,喇叭会停止播放,切换为耳机播放音乐

      • aplay 工具的格式限制

        • 支持格式

          • aplay 工具只能解析 wav 格式的音频文件
        • 不支持格式

          • aplay 工具不支持 mp3 格式的音频文件解码,因此无法用 aplay 工具播放 mp3 音频文件
    • alsamixer 是一个字符图形化的配置工具,用于配置声卡的混音器

      • 在开发板串口终端运行 alsamixer 命令即可打开图形化配置界面

      • 声卡选择

        • 左上角“Card: wm8960-audio”表示当前配置的声卡为 wm8960-audio

        • 如果系统中注册了多个声卡,可以按 F6 进行选择

      • 操作说明

        • 按下 H 键可查看界面的操作说明
      • 配置项显示

        • 不同声卡支持的混音器配置选项不同,与具体硬件相关

        • 左上角 View 处提示:View: F3:[Playback] F4: Capture F5: All

          • 当前显示的是[Playback]播放的配置项,通过 F4 按键切换为 Capture录音、或按 F5 显示所有配置项
        • 在终端按下 F4 或 F5 按键时,可能会直接退出配置界面,可能是 F4 或 F5 快捷键被其它程序占用

          • 建议在 Ubuntu 系统下使用 ssh 远程登录开发板,然后在 Ubuntu ssh 终端执行 alsamixer 程序
        • 配置项选择与修改

          • 左上角 Item 处提示:Item: Headphone [dB gain: -8.00, -8.00]

            • 表示当前选择的是 Headphone 配置项,可通过键盘上的 LEFT(向左)和 RIGHT(向右)按键切换到其它配置项。

            • 当用户对配置项进行修改时,只能修改被选中的配置项,中括号[dB gain: -7.00, -7.00]中的内容显示了该配置项当前的配置值

        • 配置项详解

          • Headphone

            • 耳机音量,使用上(音量增加)、下(音量降低)按键可以调节播放时耳机输出的音量大小

            • 可以通过 Q(左声道音量增加)、Z(左声道音量降低)按键单独调节左声道音量或通过 E(右声道音量增加)、C(右声道音量降低)按键单独调节右声道音量

          • Headphone Playback ZC

            • 耳机播放 ZC(交流),通过 M 键打开或关闭 ZC
          • Speaker

            • 喇叭播放音量,音量调节方法与 Headphone 相同
          • Speaker AC

            • 喇叭 ZC,通过上下按键可调节大小
          • Speaker DC

            • 喇叭 DC,通过上下按键可调节大小
          • Speaker Playback ZC

            • 喇叭播放 ZC,通过 M 键打开或关闭 ZC
          • Playback

            • 播放音量,播放音量作用于喇叭、也能作用于耳机,能同时控制喇叭和耳机的输出音量。调节方法与 Headphone 相同
          • Capture

            • 采集音量,也就是录音时的音量大小,调节方法与 Headphone 相同
        • 配置文件

          • 开发板出厂系统中有一个配置文件 /var/lib/alsa/asound.state,这是 WM8960 声卡的配置文件

          • 每当开发板启动进入系统时会自动读取该文件加载声卡配置;而每次系统关机时,又会将声卡当前的配置写入到该文件中进行保存

          • 加载与保存操作是通过 alsactl 工具完成的

编写一个简单地 alsa-lib 应用程序

前言

  • alsa-lib 提供了许多库函数

  • ALSA 提供了一些参考资料帮助应用程序开发人员快速上手 alsa-lib 和进行应用编程

    • ALSA 0.9 如何使用

      • https://users.suse.com/~mana/alsa090_howto.html

      • 这份文档介绍了如何使用 alsa-lib 编写简单的音频应用程序,包括 PCM 播放音频和 PCM 录音

    • ALSA 示例代码

      • https://www.alsa-project.org/alsa-doc/alsa-lib/examples.html

      • 点击对应源文件即可查看源代码

一些基本概念

  • 样本长度(Sample)

    • 样本长度是采样位数,也称为位深度(Bit Depth、Sample Size、Sample Width)

    • 指计算机在采集和播放声音文件时,所使用数字声音信号的二进制位数

    • 常见的样本长度有 8bit、16bit、24bit 等

  • 声道数(Channel)

    • 分为单声道(Mono)和双声道/立体声(Stereo)

    • 1 表示单声道,2 表示立体声

  • 帧(Frame)

    • 帧记录了一个声音单元,其长度为样本长度与声道数的乘积

    • 对于单声道:一帧 = 样本长度 * 1;双声道:一帧 = 样本长度 * 2

    • 例如,样本长度为 16bit 的双声道,一帧的大小为 16 * 2 / 8 = 4 个字节

  • 采样率(Sample Rate)

    • 也叫采样频率,是指每秒钟采样次数

    • 常见的采样率有

      • 8KHz - 电话所用采样率

      • 22.05KHz - FM 调频广播所用采样率

      • 44.1KHz - 音频 CD,也常用于 MPEG-1 音频(VCD、SVCD、MP3)所用采样率

      • 48KHz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率

  • 交错模式(Interleaved)

    • 交错模式是一种音频数据的记录方式,分为交错模式和非交错模式

    • 在交错模式下,数据以连续帧的形式存放,即首先记录完帧 1 的左声道样本和右声道样本,再记录帧 2 的左声道样本和右声道样本

    • 多数情况下,一般使用交错模式

  • 周期(Period)

    • 周期是音频设备处理(读、写)数据的单位

    • 每次读或写一个周期的数据,一个周期包含若干个帧

    • 例如,周期的大小为 1024 帧,则表示音频设备进行一次读或写操作的数据量大小为 1024 帧

  • 缓冲区(Buffer)

    • 数据缓冲区,一个缓冲区包含若干个周期

    • 音频设备底层驱动程序使用 DMA 来搬运数据,buffer 中有多个 period

    • 每当 DMA 搬运完一个 period 的数据就会触发一次中断

    • 为了减少延迟,ALSA 把缓存区拆分成多个周期,以周期为传输单元进行传输数据

    • 周期大小要合适,在延迟可接受的情况下,尽量设置大一些,但需根据实际应用场合而定

    • buffer/period/frame/sample 之间的关系示例图

  • 数据之间的传输

    • PCM 播放情况下

      • 数据流动方向

        • 应用程序向 buffer 中写入音频数据

        • 音频设备从 buffer 中读取数据进行播放

      • 指针机制

        • write pointer:指向当前应用程序写 buffer 的位置

        • read pointer:指向当前音频设备读 buffer 的位置

        • pointer 指向 buffer 起始位置

      • 初始状态

        • 播放之前,buffer 缓冲区没有数据,write pointer 和 read pointer 都指向 buffer 的起始位置
      • 数据写入过程

        • 应用程序向 buffer 写入多少帧数据,write pointer 就向前移动多少帧

        • 当写入一个周期的数据时,write pointer 移动一个周期

        • 当 write pointer 移动到 buffer 末尾时,会回到 buffer 的起始位置,形成环形缓冲区

      • 数据读取过程

        • 播放开始前,read pointer 指向 buffer 的起始位置

        • 每次读取一个周期的数据,read pointer 向前移动一个周期

        • 当 read pointer 移动到 buffer 末尾时,会回到 buffer 的起始位置,形成循环

      • 读写配合

        • 应用程序需要向 buffer 中写入音频数据,音频设备才能读取数据进行播放

        • 当 buffer 数据满时,应用程序不能再写数据,需等待音频设备播放完一个周期的数据腾出空间

    • PCM 录音情况下

      • 数据流动方向

        • 音频设备向 buffer 中写入音频数据(DMA 搬运)

        • 应用程序从 buffer 中读取数据

      • 指针机制

        • write pointer:指向音频设备写 buffer 的位置

        • read pointer:指向应用程序读 buffer 的位置

      • 初始状态

        • 录音开始前,buffer 缓冲区没有数据,write pointer 和 read pointer 都指向 buffer 的起始位置
      • 数据写入过程

        • 音频设备向 buffer 写入多少帧数据,write pointer 就向前移动多少帧

        • 音频设备每次写入一个周期的数据,write pointer 移动一个周期

        • 当 write pointer 移动到 buffer 末尾时,会回到 buffer 的起始位置,形成循环

      • 数据读取过程

        • 录音开始前,read pointer 指向 buffer 的起始位置

        • 应用程序从 buffer 读取多少帧数据,read pointer 就向前移动多少帧

        • 当 read pointer 移动到 buffer 末尾时,会回到 buffer 的起始位置,形成循环

      • 读写配合

        • 音频设备需要向 buffer 中写入音频数据,应用程序才能读取数据

        • 当 buffer 中没有数据时,应用程序需等待音频设备写入一个周期的数据

  • Over and Under Run

    • 环形缓冲区的工作状态

      • 声卡在工作状态下,环形缓冲区 buffer 中的数据总是连续地在音频设备和应用程序缓存区间传输
    • Overrun 的情况

      • 在录音例子中,如果应用程序读取数据不够快,环形缓冲区 buffer 中的数据已经被音频设备写满了,而应用程序还未来得及读走,那么数据将会被覆盖

      • 这种数据的丢失被称为 overrun

    • Underrun 的情况

      • 在播放例子中,如果应用程序写入数据到环形缓冲区 buffer 中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放)

      • 这样的错误被称为 underrun(欠载)

    • ALSA 文档中的统称

      • 在 ALSA 文档中,将这两种情形统称为 “XRUN”
    • 应用程序的设计策略

      • 适当地设计应用程序可以最小化 XRUN 并且可以从中恢复过来

打开 PCM 设备

  • 在应用程序中包含 alsa-lib 库的头文件 <alsa/asoundlib.h>,以便调用 alsa-lib 库函数和使用相关宏

  • 打开 PCM 设备

    • 调用函数 snd_pcm_open()

    • int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)

      • pcmp

        • snd_pcm_t 用于描述一个 PCM 设备

        • snd_pcm_open 函数会打开参数 name 所指定的设备,实例化 snd_pcm_t 对象,并将对象的指针(PCM 设备的句柄)通过 pcmp 返回

      • name

        • 指定 PCM 设备的名字

        • alsa-lib 库函数使用逻辑设备名而不是设备文件名

        • 命名方式为 “hw:i,j”,其中 i 表示声卡的卡号,j 表示设备号。例如,“hw:0,0” 表示声卡 0 上的 PCM 设备 0

        • “hw:0,0” 在播放情况下对应 /dev/snd/pcmC0D0p,在录音情况下对应 /dev/snd/pcmC0D0c

        • 其他常用的命名方式包括 “plughw:i,j”、“default” 等

      • stream

        • 指定流类型,有两种类型

        • SND_PCM_STREAM_PLAYBACK:表示播放

        • SND_PCM_STREAM_CAPTURE:表示采集

      • mode

        • 指定打开模式,通常设置为 0 表示默认打开模式(阻塞方式)

        • 也可以设置为 SND_PCM_NONBLOCK,表示以非阻塞方式打开设备

      • 返回值

        • 设备打开成功时,snd_pcm_open 函数返回 0

        • 开失败时,返回一个小于 0 的错误编号,可以使用 alsa-lib 提供的库函数 snd_strerror() 来得到对应的错误描述信息

  • 关闭 PCM 设备

    • 与 snd_pcm_open 相对应的函数为 snd_pcm_close(),用于关闭 PCM 设备

    • int snd_pcm_close(snd_pcm_t *pcm);

  • 使用示例

    • 调用 snd_pcm_open()函数打开声卡 0 的 PCM 播放设备 0

    • snd_pcm_t *pcm_handle = NULL;
      int ret;

ret = snd_pcm_open(&pcm_handle, “hw:0,0”, SND_PCM_STREAM_PLAYBACK, 0);
if (0 > ret) {
fprintf(stderr, “snd_pcm_open error: %s\n”, snd_strerror(ret));
return -1;
}

设置硬件参数

  • 设备设置的两大类

    • 硬件配置

      • 采样率(sampling rate)

      • 声道数(number of channels)

      • 格式(format)

      • 访问类型(access type)

      • 周期大小(period size)

      • 缓冲区大小(buffer size)

    • 软件配置

      • 这里使用默认配置即可
  • 实例化 snd_pcm_hw_params_t 对象

    • 描述 PCM 设备硬件配置

      • alsa-lib 使用 snd_pcm_hw_params_t 数据类型来描述 PCM 设备的硬件配置参数
    • 实例化对象的方法

      • 在配置参数之前,需要实例化一个 snd_pcm_hw_params_t 对象

      • 可以使用以下两种函数来实例化

        • snd_pcm_hw_params_malloc()

        • snd_pcm_hw_params_alloca()

    • 实例化代码示例

      • nd_pcm_hw_params_t *hwparams = NULL;

snd_pcm_hw_params_malloc(&hwparams);


snd_pcm_hw_params_alloca(&hwparams);

- 函数的区别

	- 两者的区别类似于 C 库函数 malloc 和 alloca 之间的区别

	- malloc 分配堆内存,需要手动释放

	- alloca 分配栈内存,函数执行完自动释放

- 其他实例化方法

	- 可以直接使用 malloc() 或 alloca() 来分配一个 snd_pcm_hw_params_t 对象

	- 也可以定义全局变量或栈自动变量来实例化

- 释放内存

	- 与 snd_pcm_hw_params_malloc 或 snd_pcm_hw_params_alloca 相对应的函数是 snd_pcm_hw_params_free

	- snd_pcm_hw_params_free() 用于释放 snd_pcm_hw_params_t 对象占用的内存空间

	- 函数原型如下:

void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj)

  • 初始化 snd_pcm_hw_params_t 对象

    • 实例化完成后的操作

      • 在 snd_pcm_hw_params_t 对象实例化完成之后,需要对其进行初始化操作
    • 初始化函数

      • 调用 snd_pcm_hw_params_any() 对 snd_pcm_hw_params_t 对象进行初始化操作
    • 函数作用

      • snd_pcm_hw_params_any() 会使用 PCM 设备当前的配置参数来初始化 snd_pcm_hw_params_t 对象
    • 初始化代码示例

      • snd_pcm_hw_params_any(pcm_handle, hwparams);

      • pcm_handle

        • PCM 设备的句柄
      • hwparams

        • 需要初始化的 snd_pcm_hw_params_t 对象的指针
  • 对硬件参数进行设置

    • 设置硬件参数

      • alsa-lib 提供了一系列的 snd_pcm_hw_params_set_xxx 函数,用于设置 PCM 设备的硬件参数
    • 获取硬件参数

      • alsa-lib 同样也提供了一系列的 snd_pcm_hw_params_get_xxx 函数,用于获取硬件参数
    • (1)设置 access 访问类型:snd_pcm_hw_params_set_access()

      • snd_pcm_hw_params_set_access() 用于设置 PCM 设备的访问类型

      • 函数原型

        • int snd_pcm_hw_params_set_access(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          snd_pcm_access_t access);

        • pcm

          • PCM 设备的句柄
        • params

          • 要设置的 snd_pcm_hw_params_t 对象的指针
        • access

          • 指定设备的访问类型,是一个 snd_pcm_access_t 类型常量

            • snd_pcm_access_t 枚举类型

              • enum snd_pcm_access_t {
                SND_PCM_ACCESS_MMAP_INTERLEAVED = 0, //mmap access with simple interleaved channels
                SND_PCM_ACCESS_MMAP_NONINTERLEAVED, //mmap access with simple non interleaved
                channels
                SND_PCM_ACCESS_MMAP_COMPLEX, //mmap access with complex placement
                SND_PCM_ACCESS_RW_INTERLEAVED, //snd_pcm_readi/snd_pcm_writei access
                SND_PCM_ACCESS_RW_NONINTERLEAVED, //snd_pcm_readn/snd_pcm_writen access
                SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED
                };
          • 通常将访问类型设置为 SND_PCM_ACCESS_RW_INTERLEAVED,即交错访问模式,通过 snd_pcm_readi/snd_pcm_writei 对 PCM 设备进行读/写操作

        • 函数返回值

          • 调用成功返回 0;失败将返回一个小于 0 的错误码

          • 可以通过 snd_strerror() 函数获取错误描述信息

      • 使用示例

        • int ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
          if (0 > ret) {
          fprintf(stderr, “snd_pcm_hw_params_set_access error: %s\n”, snd_strerror(ret));
          }
    • (2)设置数据格式:snd_pcm_hw_params_set_format()

      • snd_pcm_hw_params_set_format() 用于设置 PCM 设备的数据格式

      • 函数原型

        • int snd_pcm_hw_params_set_format(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          snd_pcm_format_t format);

        • pcm

          • PCM 设备的句柄
        • params

          • 要设置的 snd_pcm_hw_params_t 对象的指针
        • format

          • 指定数据格式,该参数是一个 snd_pcm_format_t 类型常量

            • enum snd_pcm_format_t {
              SND_PCM_FORMAT_UNKNOWN = -1,
              SND_PCM_FORMAT_S8 = 0,
              SND_PCM_FORMAT_U8,
              SND_PCM_FORMAT_S16_LE,
              SND_PCM_FORMAT_S16_BE,
              SND_PCM_FORMAT_U16_LE,
              SND_PCM_FORMAT_U16_BE,
              SND_PCM_FORMAT_S24_LE,
              SND_PCM_FORMAT_S24_BE,
              SND_PCM_FORMAT_U24_LE,
              SND_PCM_FORMAT_U24_BE,
              SND_PCM_FORMAT_S32_LE,
              SND_PCM_FORMAT_S32_BE,
              SND_PCM_FORMAT_U32_LE,
              SND_PCM_FORMAT_U32_BE,
              SND_PCM_FORMAT_FLOAT_LE,
              SND_PCM_FORMAT_FLOAT_BE,
              SND_PCM_FORMAT_FLOAT64_LE,
              SND_PCM_FORMAT_FLOAT64_BE,
              SND_PCM_FORMAT_IEC958_SUBFRAME_LE,
              SND_PCM_FORMAT_IEC958_SUBFRAME_BE,
              SND_PCM_FORMAT_MU_LAW,
              SND_PCM_FORMAT_A_LAW,
              SND_PCM_FORMAT_IMA_ADPCM,
              SND_PCM_FORMAT_MPEG,
              SND_PCM_FORMAT_GSM,
              SND_PCM_FORMAT_S20_LE,
              SND_PCM_FORMAT_S20_BE,
              SND_PCM_FORMAT_U20_LE,
              SND_PCM_FORMAT_U20_BE,
              SND_PCM_FORMAT_SPECIAL = 31,
              SND_PCM_FORMAT_S24_3LE = 32,
              SND_PCM_FORMAT_S24_3BE,
              SND_PCM_FORMAT_U24_3LE,
              SND_PCM_FORMAT_U24_3BE,
              SND_PCM_FORMAT_S20_3LE,
              SND_PCM_FORMAT_S20_3BE,
              SND_PCM_FORMAT_U20_3LE,
              SND_PCM_FORMAT_U20_3BE,
              SND_PCM_FORMAT_S18_3LE,
              SND_PCM_FORMAT_S18_3BE,
              SND_PCM_FORMAT_U18_3LE,
              SND_PCM_FORMAT_U18_3BE,
              SND_PCM_FORMAT_G723_24,
              SND_PCM_FORMAT_G723_24_1B,
              SND_PCM_FORMAT_G723_40,
              SND_PCM_FORMAT_G723_40_1B,
              SND_PCM_FORMAT_DSD_U8,
              SND_PCM_FORMAT_DSD_U16_LE,
              SND_PCM_FORMAT_DSD_U32_LE,
              SND_PCM_FORMAT_DSD_U16_BE,
              SND_PCM_FORMAT_DSD_U32_BE,
              SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_DSD_U32_BE,
              SND_PCM_FORMAT_S16 = SND_PCM_FORMAT_S16_LE,
              SND_PCM_FORMAT_U16 = SND_PCM_FORMAT_U16_LE,
              SND_PCM_FORMAT_S24 = SND_PCM_FORMAT_S24_LE,
              SND_PCM_FORMAT_U24 = SND_PCM_FORMAT_U24_LE,
              SND_PCM_FORMAT_S32 = SND_PCM_FORMAT_S32_LE,
              SND_PCM_FORMAT_U32 = SND_PCM_FORMAT_U32_LE,
              SND_PCM_FORMAT_FLOAT = SND_PCM_FORMAT_FLOAT_LE,
              SND_PCM_FORMAT_FLOAT64 = SND_PCM_FORMAT_FLOAT64_LE,
              SND_PCM_FORMAT_IEC958_SUBFRAME = SND_PCM_FORMAT_IEC958_SUBFRAME_LE,
              SND_PCM_FORMAT_S20 = SND_PCM_FORMAT_S20_LE,
              SND_PCM_FORMAT_U20 = SND_PCM_FORMAT_U20_LE
              };
          • 常用的数据格式包括 SND_PCM_FORMAT_S16_LE,即有符号 16 位、小端模式

      • 支持性测试

        • 音频设备不一定支持用户所指定的格式。用户可以在设置之前调用 snd_pcm_hw_params_test_format() 函数测试 PCM 设备是否支持某种格式

        • if (snd_pcm_hw_params_test_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE)) {
          // 返回一个非零值表示不支持该格式
          } else {
          // 返回 0 表示支持
          }

    • (3)设置声道数:snd_pcm_hw_params_set_channels()

      • 调用 snd_pcm_hw_params_set_channels() 函数来设置 PCM 设备的声道数

        • int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          unsigned int val
          )

        • pcm:指向 PCM 设备句柄的指针

        • params:指向硬件参数结构体的指针

        • val:指定声道数量

          • val=2 表示双声道(立体声)
        • 成功返回 0,失败返回小于 0 的错误码

      • 使用示例

        • ret = snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2);
          if (0 > ret)
          fprintf(stderr, “snd_pcm_hw_params_set_channels error: %s\n”, snd_strerror(ret));

        • 调用 snd_pcm_hw_params_set_channels,设置声道数为 2(立体声)

        • 检查返回值 ret,如果小于 0,则打印错误信息

    • (4)设置采样率大小:snd_pcm_hw_params_set_rate()

      • 调用 snd_pcm_hw_params_set_rate() 函数来设置 PCM 设备的采样率

        • int snd_pcm_hw_params_set_rate(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          unsigned int val,
          int dir
          )

        • pcm:指向 PCM 设备句柄的指针

        • params:指向硬件参数结构体的指针

        • val:指定采样率大小(例如,44100)

        • dir:用于控制采样率的方向

          • dir = -1:实际采样率小于参数 val 指定的值

          • dir = 0:实际采样率等于参数 val 指定的值

          • dir = 1:实际采样率大于参数 val 指定的值

        • 成功返回 0;失败将返回小于 0 的错误码

      • 使用示例

        • ret = snd_pcm_hw_params_set_rate(pcm_handle, hwparams, 44100, 0);
          if (0 > ret)
          fprintf(stderr, “snd_pcm_hw_params_set_rate error: %s\n”, snd_strerror(ret));

        • 调用 snd_pcm_hw_params_set_rate,设置采样率为 44100,方向为 0(实际采样率等于 44100)

        • 检查返回值 ret,如果小于 0,则打印错误信息

    • (5)设置周期大小:snd_pcm_hw_params_set_period_size()

      • 周期定义

        • 周期是指音频数据处理的一个基本单位,用帧来衡量

        • 例如,一个周期可以是 1024 帧

      • 调用 snd_pcm_hw_params_set_period_size() 函数来设置 PCM 设备的周期大小

        • int snd_pcm_hw_params_set_period_size(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          snd_pcm_uframes_t val,
          int dir);

        • pcm:指向 PCM 设备句柄的指针

        • params:指向硬件参数结构体的指针

        • val:指定周期大小,单位是帧(例如,1024 帧)

          • 参数 val 的单位是帧,而不是字节
        • dir:用于控制周期大小的方向,与 snd_pcm_hw_params_set_rate() 函数的 dir 参数意义相同

      • snd_pcm_uframes_t 类型用于表示帧的数量

      • 使用示例

        • ret = snd_pcm_hw_params_set_period_size(pcm_handle, hwparams, 1024, 0);
          if (0 > ret)
          fprintf(stderr, “snd_pcm_hw_params_set_period_size error: %s\n”, snd_strerror(ret));

        • 调用 snd_pcm_hw_params_set_period_size,设置周期大小为 1024 帧,方向为 0

        • 检查返回值 ret,如果小于 0,则打印错误信息

    • (6)设置 buffer 大小:snd_pcm_hw_params_set_buffer_size()

      • 调用 snd_pcm_hw_params_set_buffer_size() 函数来设置 PCM 设备的 buffer 大小

        • int snd_pcm_hw_params_set_buffer_size(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          snd_pcm_uframes_t val);

          • pcm:指向 PCM 设备句柄的指针

          • params:指向硬件参数结构体的指针

          • val:指定 buffer 的大小,以帧为单位

            • 例如,如果 buffer 大小是 16 个周期,每个周期 1024 帧,则 val 等于 16 * 1024 = 16384 帧
          • 成功返回 0;失败返回一个小于 0 的错误码

        • 使用示例

          • ret = snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, 16*1024);
            if (0 > ret)
            fprintf(stderr, “snd_pcm_hw_params_set_buffer_size error: %s\n”, snd_strerror(ret));

          • 调用 snd_pcm_hw_params_set_buffer_size,设置 buffer 大小为 16384 帧

          • 检查返回值 ret,如果小于 0,则打印错误信息

      • 调用 snd_pcm_hw_params_set_periods() 函数来设置 buffer 大小,以周期为单位

        • int snd_pcm_hw_params_set_periods(snd_pcm_t *pcm,
          snd_pcm_hw_params_t *params,
          unsigned int val,
          int dir);

          • pcm:指向 PCM 设备句柄的指针

          • params:指向硬件参数结构体的指针

          • val:指定 buffer 的大小,以周期为单位

            • 例如,val 为 16 表示 buffer 大小为 16 个周期
          • dir:用于控制周期大小的方向,与 snd_pcm_hw_params_set_rate() 函数的 dir 参数意义相同

          • 成功返回 0;失败将返回一个小于 0 的错误码

        • 使用示例

          • ret = snd_pcm_hw_params_set_periods(pcm_handle, hwparams, 16, 0);
            if (0 > ret)
            fprintf(stderr, “snd_pcm_hw_params_set_periods error: %s\n”, snd_strerror(ret));

          • 调用 snd_pcm_hw_params_set_periods,设置 buffer 大小为 16 个周期

          • 检查返回值 ret,如果小于 0,则打印错误信息

      • snd_pcm_hw_params_set_buffer_size() 和 snd_pcm_hw_params_set_periods() 的区别在于参数的单位不同:前者以帧为单位,后者以周期为单位

    • (7)安装/加载硬件配置参数:snd_pcm_hw_params()

      • 参数设置完成后,调用 snd_pcm_hw_params() 函数加载/安装配置,并将配置参数写入硬件使其生效

        • int snd_pcm_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);

        • pcm:指向 PCM 设备句柄的指针

        • params:指向硬件参数结构体的指针

        • 成功返回 0,失败将返回一个小于 0 的错误码

      • 内部行为

        • 调用 snd_pcm_hw_params() 之后,其内部会自动调用 snd_pcm_prepare() 函数

        • PCM 设备的状态被更改为 SND_PCM_STATE_PREPARED

      • 设备状态

        • PCM 设备有多种不同的状态,SND_PCM_STATE_PREPARED 是其中一种

        • 调用 snd_pcm_prepare() 函数会使得 PCM 设备处于 SND_PCM_STATE_PREPARED 状态,也就是一种准备好的状态

      • 使用示例

        • ret = snd_pcm_hw_params(pcm_handle, hwparams);
          if (0 > ret)
          fprintf(stderr, “snd_pcm_hw_params error: %s\n”, snd_strerror(ret));

        • 调用 snd_pcm_hw_params() 函数,将配置参数写入硬件

        • 检查返回值 ret,如果小于 0,则打印错误信息

  • 读/写数据

    • 概述

      • 读写操作

        • PCM 播放:调用 snd_pcm_writei() 函数向播放缓冲区 buffer 中写入音频数据

          • snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm,
            const void *buffer,
            snd_pcm_uframes_t size
            )

          • 将 buffer 中的数据写入到驱动层的播放环形缓冲区中

          • size 指定写入数据的大小,以帧为单位

          • 通常,每次调用 snd_pcm_writei() 写入一个周期的数据

        • PCM 录音:调用 snd_pcm_readi() 函数从录音缓冲区 buffer 中读取数据

          • snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm,
            void *buffer,
            snd_pcm_uframes_t size
            )

          • 从驱动层的录音环形缓冲区中读取数据到 buffer 中

          • size 指定读取数据的大小,以帧为单位

          • 通常,每次调用 snd_pcm_readi() 读取一个周期的数据

        • 通用参数

          • pcm:PCM 设备的句柄

          • buffer:应用程序的缓冲区指针

          • size:指定读写数据的大小,以帧为单位

          • 调用 snd_pcm_writei() 或 snd_pcm_readi() 成功时,返回实际读取/写入的帧数.调用失败时,返回一个负数的错误码

      • 注意事项

        • 即使调用成功,实际读取/写入的帧数不一定等于 size 指定的帧数

        • 当发生信号或 XRUN(欠载/超载)时,返回的帧数可能会小于 size

      • 缓冲区区分

        • buffer 参数指的是应用程序的缓冲区,不要与驱动层的环形缓冲区混淆
    • 阻塞与非阻塞

      • 打开设备时的模式选择

        • 调用 snd_pcm_open() 打开设备时可以选择阻塞或非阻塞方式
      • 阻塞方式

        • 阻塞方式读写

          • 如果设备以阻塞方式打开,则 snd_pcm_readi() 和 snd_pcm_writei() 也会以阻塞方式进行读写操作
        • PCM 录音(阻塞方式)

          • 当 buffer 缓冲区中没有数据可读时,调用 snd_pcm_readi() 将会阻塞,直到音频设备向 buffer 中写入采集到的音频数据
        • PCM 播放(阻塞方式)

          • 当 buffer 缓冲区中的数据满时,调用 snd_pcm_writei() 将会阻塞,直到音频设备从 buffer 中读走数据进行播放
        • snd_pcm_readi() 和 snd_pcm_writei() 会根据数据是否可读/可写而阻塞,确保操作完成

      • 非阻塞方式

        • 非阻塞方式读写

          • 如果设备以非阻塞方式打开,则 snd_pcm_readi() 和 snd_pcm_writei() 也会以非阻塞方式进行读写操作
        • PCM 录音(非阻塞方式)

          • 当 buffer 缓冲区中没有数据可读时,调用 snd_pcm_readi() 不会阻塞,而是立即返回错误
        • PCM 播放(非阻塞方式)

          • 当 buffer 缓冲区中的数据满时,调用 snd_pcm_writei() 不会阻塞,而是立即返回错误
        • snd_pcm_readi() 和 snd_pcm_writei() 不会等待数据准备好,而是立即返回,可能返回错误码表示操作未完成

    • snd_pcm_readn 和 snd_pcm_writen

      • 适用于交错模式(interleaved)读写数据

      • 交错模式(Interleaved)

        • 数据存储格式:多个声道的音频数据交错在一起,每个采样点的数据依次存储。例如,左声道和右声道的数据依次排列

        • 使用 snd_pcm_readi 和 snd_pcm_writei

      • 非交错模式(Non-Interleaved)

        • 数据存储格式:每个声道的音频数据独立存储,各自的采样点数据连续排列

        • 使用 snd_pcm_readn 和 snd_pcm_writen

      • 非交错模式下的读写函数

        • 需要使用 snd_pcm_readn 和 snd_pcm_writen 函数进行读写操作
  • 示例代码之 PCM 播放

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_PLAYBACK_DEV    "hw:0,0"

/************************************
 WAV音频文件解析相关数据结构申明
 ************************************/
typedef struct WAV_RIFF {
    char ChunkID[4];         		/* "RIFF" */
    u_int32_t ChunkSize;        	/* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];             	/* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];         	/* "fmt " */
    u_int32_t Subchunk1Size;      	/* 16 for PCM */
    u_int16_t AudioFormat;        	/* PCM = 1*/
    u_int16_t NumChannels;       	/* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;        	/* 8000, 44100, etc. */
    u_int32_t ByteRate;           	/* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;        	/* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;      	/* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;
static FMT_t wav_fmt;

typedef struct WAV_DATA {
    char Subchunk2ID[4];         	/* "data" */
    u_int32_t Subchunk2Size;      	/* data size */
} __attribute__ ((packed)) DATA_t;

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;            	//pcm句柄
static unsigned int buf_bytes;               	//应用程序缓冲区的大小(字节为单位)
static void *buf = NULL;                  	//指向应用程序缓冲区的指针
static int fd = -1;                         	//指向WAV音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024;  	//周期大小(单位: 帧)
static unsigned int periods = 16;             	//周期数(设备驱动层buffer的大小)

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;   // 定义PCM硬件参数对象
    int ret;    // 定义返回值变量

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(驱动层buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;    // 定义RIFF chunk结构体
    DATA_t wav_data;    // 定义数据chunk结构体
    int ret;    // 定义返回值变量

    // 打开WAV文件
    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }

    /* 读取RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }
    // 校验RIFF chunk ID和格式
    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
        strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 读取sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }
    // 校验fmt chunk ID
    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf("  file name:     %s\n", file);
    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

    /* 定位到sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
                SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }
    // 查找数据chunk
    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

        /* 找到sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;
        // 定位到下一个chunk
        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }

    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    int ret;    // 定义返回值变量

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 打开WAV音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);

    /* 初始化PCM Playback设备 */
    if (snd_pcm_init())
        goto err1;

    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err2;
    }

    /* 播放 */
    for ( ; ; ) {

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes); //从音频文件中读取数据
        if (0 >= ret)   // 如果读取出错或文件读取完毕
            goto err3;

        ret = snd_pcm_writei(pcm, buf, period_size);    // 写数据到PCM设备
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto err3;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto err3;
            }
        }
    }

err3:
    free(buf);     //释放内存
err2:
    snd_pcm_close(pcm); //关闭pcm设备
err1:
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE);
}

- 代码

	- 校验命令行参数,确保用户传入一个 WAV 文件路径

	- 调用 open_wav_file 函数解析 WAV 文件

		- 打开 WAV 文件,读取 RIFF chunk 并进行校验

		- 读取 fmt chunk 并进行校验

		- 打印 WAV 文件的格式信息

		- 定位到 data chunk,并寻找 data chunk

	- 调用 snd_pcm_init 函数初始化 PCM 播放设备

		- 打开 PCM 设备,初始化硬件参数

		- 实例化hwparams对象

		- 获取PCM设备当前硬件配置,对hwparams进行初始化

		- 设置参数

			- 设置访问类型: 交错模式

			- 设置数据格式: 有符号16位、小端模式

			- 设置采样率

			- 设置声道数: 双声道

			- 设置周期大小: period_size

			- 设置周期数(驱动层buffer的大小): periods

			- 使配置生效

	- 申请缓冲区用于存放从音频文件读取的数据

	- 使用循环从音频文件中读取数据并写入到 PCM 设备进行播放

		- 如果读取数据失败或文件读取完毕,退出循环

		- 如果写入数据失败,调整读取位置并继续

	- 错误处理和资源释放

		- 在出错的情况下,释放分配的缓冲区和关闭 PCM 设备及文件描述符

- 编译

	- 编译示例代码方法

		- 编译时需要指定两个路径:ALSA 库头文件路径和库文件路径,以及链接库的名称

		- ${CC} -o testApp testApp.c -Ixxx -Lyyy -lzzz

			- xxx 表示头文件的路径

			- yyy 表示库文件的路径

			- zzz 表示链接库的名称

	- ALSA 库的路径问题

		- 交叉编译工具已经在安装目录下的 sysroots/cortexa7hf-neon-poky-linux-gnueabi 目录中包含了 alsa-lib

		- 示例安装路径为 /opt/fsl-imx-x11/4.1.15-2.1.0

		- 该目录下有 lib 和 usr 两个目录,分别对应 Linux 系统根目录下的 lib 和 usr

	- 头文件和库文件位置

		- usr/include/alsa 目录中包含了 alsa-lib 的头文件

		- usr/lib 目录中包含了 alsa-lib 的库文件libasound.so

	- 交叉编译器的配置

		- 交叉编译器已经将这些路径添加到了其搜索路径中

		- 使用 echo ${CC} 可以查看环境变量 CC 的内容,其中包含交叉编译器

		- 交叉编译器 arm-poky-linux-gnueabi-gcc 使用了 --sysroot 选项,指定了 sysroots/cortexa7hf-neon-poky-linux-gnueabi 目录作为目标平台的根目录

		- --sysroot 选项会将 usr/include 添加到头文件搜索路径中、将 lib 和 usr/lib 添加到库文件搜索路径中

	- 编译应用程序

		- 编译应用程序时,只需指定链接库即可

			- ${CC} -o testApp testApp.c -lasound

- 测试

	- 拷贝可执行文件和音频文件

		- 将编译得到的可执行文件拷贝到开发板 Linux 系统的 /home/root 目录下

		- 将一个 WAV 音频文件也拷贝到 /home/root 目录下

	- 声卡混音器配置

		- 在进行测试之前,需要对声卡混音器进行配置

		- 使用 amixer 工具进行声卡配置

			- # 打开耳机播放 ZC

amixer sset ‘Headphone Playback ZC’ on

打开喇叭播放 ZC

amixer sset ‘Speaker Playback ZC’ on
amixer sset ‘Speaker AC’ 3
amixer sset ‘Speaker DC’ 3

音量设置

amixer sset Headphone 105,105 //耳机音量设置
amixer sset Playback 230,230 //播放音量设置
amixer sset Speaker 118,118
//喇叭音量设置

打开左右声道

amixer sset ‘Right Output Mixer PCM’ on //打开右声道
amixer sset ‘Left Output Mixer PCM’ on //打开左声道

		- 声卡设置

			-  

		- 配置声卡设置完成之后,运行测试程序

			-  

			- 程序运行之后,对传入的 WAV 文件进行解析

			- 将解析得到的音频格式信息打印出来

			- 开发板上的喇叭将开始播放音乐,如果连接了耳机,则音乐将通过耳机播放
  • 示例代码值 PCM 录音
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_CAPTURE_DEV    "hw:0,0"

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;            	//pcm句柄
static snd_pcm_uframes_t period_size = 1024; 	//周期大小(单位: 帧)
static unsigned int periods = 16;             	//周期数(buffer的大小)
static unsigned int rate = 44100;             	//采样率

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_CAPTURE_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    unsigned char *buf = NULL;
    unsigned int buf_bytes;
    int fd = -1;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 初始化PCM Capture设备 */
    if (snd_pcm_init())
        exit(EXIT_FAILURE);

    /* 申请读缓冲区 */
    buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err1;
    }

    /* 打开一个新建文件 */
    fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
        goto err2;
    }

    /* 录音 */
    for ( ; ; ) {

        //memset(buf, 0x00, buf_bytes);   //buf清零
        ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
            goto err3;
        }

        // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数
        ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
        if (0 >= ret)
            goto err3;
    }

err3:
    close(fd);  //关闭文件
err2:
    free(buf);     //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    exit(EXIT_FAILURE);
}

- 代码

	- 参数校验

	- 初始化PCM设备。调用函数 snd_pcm_init() 对 PCM 设备进行初始化

		- 使用 ALSA 库函数 snd_pcm_open() 打开 PCM 录音设备

		- 申请一个 hwparams 对象,用于保存硬件参数

		- 初始化 hwparams 对象,获取当前PCM设备的硬件配置

		- 设置硬件参数

			- 设置访问类型: 交错模式

			- 设置数据格式: 有符号16位、小端模式

			- 设置采样率

			- 设置声道数: 双声道

			- 设置周期大小: period_size

			- 设置周期数(buffer的大小): periods

			- 使配置生效,并释放 hwparams 对象占用的内存

		- 如果在任何步骤出错,释放相关资源并关闭PCM设备

	- 申请读缓冲区

		- 使用 malloc() 申请一个缓冲区,用于存放从驱动层环形缓冲区读取的音频数据。缓冲区的大小为周期大小(1024帧)乘以每帧的字节大小(16位双声道,即4字节)

	- 打开新建文件

		- 使用 open() 打开一个新的文件,以保存录制的音频数据。打开文件时使用了 O_CREAT | O_EXCL 标志,确保文件不存在时才创建

	- 音频录制循环

		- 进入一个无限循环,从PCM设备的环形缓冲区读取音频数据,并将其写入文件中

			- 调用 snd_pcm_readi() 从环形缓冲区读取一个周期的音频数据

			- 如果读取成功,将读取到的数据写入文件

			- 如果读取失败或写入失败,跳转到错误处理部分

	- 在任何错误情况下,执行错误处理和资源释放操作,并退出程序

- 编译

	- ${CC} -o testApp testApp.c -lasound

- 测试

	- 将编译生成的可执行文件拷贝到开发板的 Linux 系统 /home/root 目录下

	- 声卡配置

		- 在运行测试程序之前,需要对声卡进行配置

		- 配置使用 amixer 工具

			- 创建一个名为 mic_in_config.sh 的 shell 脚本

			- 在脚本中输入声卡配置命令

				- #!/bin/sh

#正点原子@ALIENTEK
#设置捕获的音量
amixer cset name=‘Capture Volume’ 40,40

#PCM
amixer sset ‘PCM Playback’ on
amixer sset ‘Playback’ 256

#耳机/喇叭(扬声器)设置播放音量,直流/交流
#Turn on Headphone
amixer sset ‘Headphone Playback ZC’ on
#Set the volume of your headphones(98% volume,127 is the MaxVolume)
amixer sset Headphone 125,125
#Turn on the speaker
amixer sset ‘Speaker Playback ZC’ on
#Set the volume of your Speaker(98% volume,127 is the MaxVolume)
amixer sset Speaker 125,125
#Set the volume of your Speaker AC(80% volume,100 is the MaxVolume)
amixer sset ‘Speaker AC’ 4
#Set the volume of your Speaker AC(80% volume,5 is the MaxVolume)
amixer sset ‘Speaker DC’ 4

#音频输入,左声道管理
#Turn on Left Input Mixer Boost
amixer sset ‘Left Input Mixer Boost’ on
amixer sset ‘Left Boost Mixer LINPUT1’ on
amixer sset ‘Left Input Boost Mixer LINPUT1’ 127
amixer sset ‘Left Boost Mixer LINPUT2’ on
amixer sset ‘Left Input Boost Mixer LINPUT2’ 127
#Turn off Left Boost Mixer LINPUT3
amixer sset ‘Left Boost Mixer LINPUT3’ off
amixer sset ‘Left Input Boost Mixer LINPUT3’ 0

#音频输入,右声道管理,全部关闭
#Turn on Right Input Mixer Boost
amixer sset ‘Right Input Mixer Boost’ off
amixer sset ‘Right Boost Mixer RINPUT1’ off
amixer sset ‘Right Input Boost Mixer RINPUT2’ 0
amixer sset ‘Right Boost Mixer RINPUT2’ off
amixer sset ‘Right Input Boost Mixer RINPUT2’ 0
amixer sset ‘Right Boost Mixer RINPUT3’ off
amixer sset ‘Right Input Boost Mixer RINPUT3’ 0

			- 设置脚本权限并运行

				- 给予 mic_in_config.sh 脚本可执行权限

					- chmod 777 mic_in_config.sh //给予可执行权限 

./mic_in_config.sh //运行

				- 运行该脚本

					- 声卡配置(录音)

	- 配置解释

		- 左右声道的 Mixer Boost 配置

			- 配置根据硬件设计的要求进行

			- 详细信息可以参考《I.MX6U 嵌入式 Linux 驱动开发指南》文档的音频驱动章节

	- 执行测试程序

		- 录音

	-  录音过程

		- 测试程序开始录音,用户可以对着开发板上的麦克风(MIC)讲话

		- 程序会将讲话的内容录制下来

	- 停止录音

		- 要停止录音,需要终止进程,可以按 Ctrl+C 终止应用程序

		- 录音结束后,会在当前目录下生成 cap.wav 音频文件

	- 文件格式说明

		- 生成的 cap.wav 文件是纯音频数据文件,不是标准的 WAV 格式文件,因为缺少头部信息

		- 如果程序检测到该文件不是 WAV 格式文件,会直接退出

		- 因此,不能直接使用上一节的测试程序来播放 cap.wav 文件

	- 播放录制的音频

		- 可以对上一节的示例代码进行修改,或者直接使用 aplay 工具播放录制的音频

		- 使用 aplay 播放命令如下

			- aplay -f cd cap.wav

	- 如果录音正常,使用 aplay 播放时,会听到录制的声音

	- LINE_IN 测试

		- 除了麦克风,开发板底板上还有一个 LINE_IN 接口,用于线路输入

			- 上图中,左边的是耳机接口,右边的是 LINE_IN 接口

			- LINE_IN 接口支持音频输入

		- 使用本测试程序对 LINE_IN 接口进行测试,采集 LINE_IN 接口输入的音频

		- 测试设备连接

			- 使用一根 3.5mm 公对公音频线进行连接

				- 一头连接到手机或电脑

				- 另一头连接到 LINE_IN 接口上

		- 音频播放与采集

			- 手机或电脑端播放音乐

			- 音频数据通过 LINE_IN 接口输入到开发板,被我们的应用程序采集(录制)

		- 在测试之前,需要对声卡进行配置

			- amixer sset Capture 58,58              //录制音量大小 

amixer sset ‘ADC PCM’ 200,200 //PCM ADC

左声道 Mixer Boost 管理

amixer sset ‘Left Input Mixer Boost’ off
amixer sset ‘Left Boost Mixer LINPUT1’ off
amixer sset ‘Left Input Boost Mixer LINPUT1’ 0
amixer sset ‘Left Boost Mixer LINPUT2’ on
amixer sset ‘Left Input Boost Mixer LINPUT2’ 5
amixer sset ‘Left Boost Mixer LINPUT3’ off
amixer sset ‘Left Input Boost Mixer LINPUT3’ 0

右声道 Mixer Boost 管理

amixer sset ‘Right Input Mixer Boost’ on
amixer sset ‘Right Boost Mixer RINPUT1’ off
amixer sset ‘Right Input Boost Mixer RINPUT1’ 0
amixer sset ‘Right Boost Mixer RINPUT2’ off
amixer sset ‘Right Input Boost Mixer RINPUT2’ 0
amixer sset ‘Right Boost Mixer RINPUT3’ on
amixer sset ‘Right Input Boost Mixer RINPUT3’ 5

		- 开始测试

			- 配置好声卡之后即可进行测试

			- 执行程序,手机或电脑端播放音乐

			- 开发板采集从 LINE_IN 接口输入的音频数据

		- 测试方式与麦克风(MIC)测试类似

使用异步方式

概述

  • 上小节中的示例代码都采用了同步方式进行读写

  • 同步读写的缺点

    • 同步读写会导致应用程序无法执行其它任务
  • 异步读写的优势

    • 异步读写可以让应用程序在等待数据时执行其它任务,提高程序的效率和响应能力
  • 使用异步方式进行读写非常简单,只需要注册异步处理函数即可

    • snd_async_add_pcm_handler()函数

      • 通过该函数注册一个异步处理函数即可实现异步读写

        • int snd_async_add_pcm_handler(snd_async_handler_t **handler,
          snd_pcm_t *pcm,
          snd_async_callback_t callback,
          void *private_data
          )

          • handler:用于描述一个异步处理对象,调用函数会实例化一个 snd_async_handler_t 对象,并通过 *handler 返回其指针

          • pcm:pcm 设备的句柄

          • callback:异步处理函数(回调函数),函数指针类型为 snd_async_callback_t

            • 在异步处理函数中,通过调用 snd_async_handler_get_pcm() 函数获取 PCM 设备的句柄
          • private_data:传递给异步处理函数的私有数据,数据类型由用户定义

            • 在异步处理函数中,通过调用 snd_async_handler_get_callback_private() 函数获取私有数据
      • 实现异步 I/O 需要完成以下三件事

        • 使能异步 I/O

        • 设置异步 I/O 的所有者

        • 注册信号处理函数(如 SIGIO 信号或其它实时信号)

      • 使用示例

        • static void snd_playback_async_callback(snd_async_handler_t *handler)
          {
          snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取 PCM 句柄


      }

int main(void)
{

      ......

     snd_async_handler_t *async_handler = NULL; 
    /*  注册异步处理函数   */

     ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL); 
     if (0 > ret)
     fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));

     ......

}

	- 异步回调函数的触发

		- 对于播放,当环形缓冲区有空闲周期时,驱动程序会发送信号,应用程序跳转到异步回调函数执行

		- 对于录音,当环形缓冲区有数据可读时,驱动程序会发送信号,应用程序跳转到异步回调函数执行

	- 在播放情况下,通常会先填满环形缓冲区,当音频设备播放完一个周期时,会产生一个空闲周期,应用程序接收到信号后跳转到异步回调函数执行

- snd_pcm_avail_update()函数

	- 在异步处理函数中,通常会使用 snd_pcm_avail_update() 函数

		- snd_pcm_sframes_t snd_pcm_avail_update(snd_pcm_t *pcm);

	- 录音情况下的使用

		- 用于获取当前可读取的帧数

	- 播放情况下的使用

		- 用于获取当前可写入的帧数

PCM 播放示例-异步方式

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_PLAYBACK_DEV    "hw:0,0"

/************************************
 WAV音频文件解析相关数据结构申明
 ************************************/
typedef struct WAV_RIFF {
    char ChunkID[4];           	/* "RIFF" */
    u_int32_t ChunkSize;         	/* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];              	/* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];         	/* "fmt " */
    u_int32_t Subchunk1Size;      	/* 16 for PCM */
    u_int16_t AudioFormat;       	/* PCM = 1*/
    u_int16_t NumChannels;       	/* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;        	/* 8000, 44100, etc. */
    u_int32_t ByteRate;           	/* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;         	/* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;     	/* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;
static FMT_t wav_fmt;

typedef struct WAV_DATA {
    char Subchunk2ID[4];         	/* "data" */
    u_int32_t Subchunk2Size;     	/* data size */
} __attribute__ ((packed)) DATA_t;

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;            	//pcm句柄
static unsigned int buf_bytes;                	//应用程序缓冲区的大小(字节为单位)
static void *buf = NULL;                  	//指向应用程序缓冲区的指针
static int fd = -1;                         	//指向WAV音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024; 	//周期大小(单位: 帧)
static unsigned int periods = 16;            	//周期数(设备驱动层buffer的大小)

/************************************
 static静态函数
 ************************************/
static void snd_playback_async_callback(snd_async_handler_t *handler)
{
    snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄
    snd_pcm_sframes_t avail;
    int ret;

    avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充
    while (avail >= period_size) {  //我们一次写入一个周期

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes);
        if (0 >= ret)
            goto out;

        ret = snd_pcm_writei(handle, buf, period_size);
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto out;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置 重新读取没有播放出去的数据
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto out;
            }
        }

        avail = snd_pcm_avail_update(handle);   //再次获取、更新avail
    }

    return;
out:
    snd_pcm_close(handle); //关闭pcm设备
    free(buf);
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE); //退出程序
}

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    snd_async_handler_t *async_handler = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(驱动层环形缓冲区buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小

    /* 注册异步处理函数 */
    ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
    if (0 > ret) {
        fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;
    DATA_t wav_data;
    int ret;

    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }

    /* 读取RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
        strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 读取sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf("  file name:     %s\n", file);
    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

    /* sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
                SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

        /* 找到sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;

        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }

    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    snd_pcm_sframes_t avail;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 打开WAV音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);

    /* 初始化PCM Playback设备 */
    if (snd_pcm_init())
        goto err1;

    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err2;
    }

    /* 播放:先将环形缓冲区填满数据 */
    avail = snd_pcm_avail_update(pcm);  //获取环形缓冲区中有多少帧数据需要填充
    while (avail >= period_size) {  //我们一次写入一个周期

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes);
        if (0 >= ret)
            goto err3;

        ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto err3;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto err3;
            }
        }

        avail = snd_pcm_avail_update(pcm);  //再次获取、更新avail
    }

    for ( ; ; ) {
        /* 主程序可以做一些其它的事,当环形缓冲区有空闲周期需要写入数据时
         * 音频设备驱动程序会向应用程序发送SIGIO信号
         * 接着应用程序跳转到snd_playback_async_callback()函数执行 */
        //do_something();
        sleep(1);
    }

err3:
    free(buf);     //释放内存
err2:
    snd_pcm_close(pcm); //关闭pcm设备
err1:
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE);
}

  • 异步回调函数的注册

    • 在 snd_pcm_init() 函数中,调用了 snd_async_add_pcm_handler() 函数注册异步回调函数 snd_playback_async_callback()
  • 可写入数据时的处理

    • 当驱动层环形缓冲区可写入数据时,跳转到 snd_playback_async_callback() 函数执行
  • 异步回调函数中的操作

    • 在 snd_playback_async_callback() 函数中

      • 首先调用 snd_pcm_avail_update() 获取当前可写入多少帧数据

      • 然后在 while 循环中

        • 调用 read() 读取音频文件的数据

        • 调用 snd_pcm_writei() 向环形缓冲区写入数据

        • 每次循环写入一个周期,直到把缓冲区写满

      • 最后退出回调函数

  • 主函数中的初始化操作

    • 回到 main() 函数中,在进入 for 循环之前,先将环形缓冲区填满

    • 填满缓冲区的操作代码与回调函数中的相同

PCM 录音示例-异步方式

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_CAPTURE_DEV    "hw:0,0"

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;            	//pcm句柄
static unsigned int buf_bytes;              	//应用层缓冲区的大小(字节为单位)
static void *buf = NULL;                  	//指向应用层缓冲区的指针
static int fd = -1;                         	//输出文件的文件描述符
static snd_pcm_uframes_t period_size = 1024; 	//周期大小(单位: 帧)
static unsigned int periods = 16;             	//周期数(驱动层环形缓冲区的大小)
static unsigned int rate = 44100;             	//采样率

/************************************
 static静态函数
 ************************************/
static void snd_capture_async_callback(snd_async_handler_t *handler)
{
    snd_pcm_t *handle = snd_async_handler_get_pcm(handler);
    snd_pcm_sframes_t avail;
    int ret;

    avail = snd_pcm_avail_update(handle);   //检查有多少帧数据可读
    while (avail >= period_size) {  //每次读取一个周期

        //memset(buf, 0x00, buf_bytes);   //buf清零
        ret = snd_pcm_readi(handle, buf, period_size);//读取PCM数据 一个周期
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
            goto out;
        }

        // snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数
        ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
        if (0 >= ret)
            goto out;

        avail = snd_pcm_avail_update(handle);   //再次读取、更新avail
    }

    return;
out:
    snd_pcm_close(handle); //关闭pcm设备
    free(buf);
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE); //退出程序
}

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    snd_async_handler_t *async_handler = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_CAPTURE_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    /* 注册异步处理函数 */
    ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_capture_async_callback, NULL);
    if (0 > ret) {
        fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 初始化PCM Capture设备 */
    if (snd_pcm_init())
        exit(EXIT_FAILURE);

    /* 申请读缓冲区 */
    buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err1;
    }

    /* 打开一个新建文件 */
    fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
        goto err2;
    }

    /* 录音 */
    ret = snd_pcm_start(pcm);       //开始录音
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_start error: %s\n", snd_strerror(ret));
        goto err3;
    }

    for ( ; ; ) {
        /* 主程序可以做一些其它的事,当环形缓冲区有数据可读时
         * 音频设备驱动程序会向应用程序发送SIGIO信号
         * 接着应用程序跳转到snd_capture_async_callback()函数执行、读取数据 */
        //do_something();
        sleep(1);
    }

err3:
    close(fd);  //关闭文件
err2:
    free(buf);     //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    exit(EXIT_FAILURE);
}

  • 在 main() 函数中,调用了 snd_pcm_start() 函数

    • 用于启动 PCM 设备
  • 在录音情况下,调用 snd_pcm_start() 函数开始录音

  • 在播放情况下,调用 snd_pcm_start() 函数开始播放

使用 poll()函数

使用 poll I/O 多路复用来实现读写数据

  • I/O 多路复用是一种高级 I/O

  • 获取计数:snd_pcm_poll_descriptors_count

    • 用于获取 PCM 句柄的轮询描述符计数

    • int snd_pcm_poll_descriptors_count(snd_pcm_t *pcm);

  • 分配 struct pollfd 对象

    • 为每一个轮询描述符分配一个 struct pollfd 对象

    • struct pollfd *pfds = NULL;
      int count;

/* 获取 PCM 句柄的轮询描述符计数 /
count = snd_pcm_poll_descriptors_count(pcm);
if (0 >= count) {
fprintf(stderr, “Invalid poll descriptors count\n”);
return -1;
}
/
分配内存 */
pfds = calloc(count, sizeof(struct pollfd));
if (NULL == pfds) {
perror(“calloc error”);
return -1;
}

  • 填充 struct pollfd:snd_pcm_poll_descriptors

    • 调用 snd_pcm_poll_descriptors()函数对 struct pollfd 对象进行填充(初始化)

      • 函数原型:
        int snd_pcm_poll_descriptors(
        snd_pcm_t *pcm,
        struct pollfd *pfds,
        unsigned int space
        );

      • 参数 space 表示 pfds 数组中的元素个数

    • /* 填充 pfds */
      ret = snd_pcm_poll_descriptors(pcm, pfds, count);
      if (0 > ret)
      return -1;

  • poll+snd_pcm_poll_descriptors_revents

    • 一切准备完成之后,可以调用 poll() 函数监视 PCM 设备是否有数据可读或可写

    • 当有数据可读或可写时,poll() 函数返回

    • 调用 snd_pcm_poll_descriptors_revents() 函数获取文件描述符中返回的事件类型

      • int snd_pcm_poll_descriptors_revents(
        snd_pcm_t *pcm,
        struct pollfd *pfds,
        unsigned int nfds,
        unsigned short *revents
        );

      • nfds 表示 pfds 数组中元素的个数

      • 通过参数 revents 返回文件描述符中返回的事件

    • 将返回的事件类型与 poll 的 events 标志进行比较,以确定是否可读或可写

    • 注意事项

      • 不要直接读取 struct pollfd 对象中的 revents 成员变量

      • snd_pcm_poll_descriptors_revents() 函数会对 poll() 系统调用返回的 revents 掩码进行“分解”以纠正语义(POLLIN 表示读取,POLLOUT 表示写入)

    • 使用示例

      • for ( ; ; ) {
        ret = poll(pfds, count, -1); // 调用 poll
        if (0 > ret) {
        perror(“poll error”);
        return -1;
        }
        ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
        if (0 > ret)
        return -1;
        if (revents & POLLERR) // 发生 I/O 错误
        return -1;
        if (revents & POLLIN) { // 表示可读取数据
        // 从 PCM 设备读取数据
        }
        if (revents & POLLOUT) { // 表示可写入数据
        // 将数据写入 PCM 设备
        }
        }

PCM 播放示例代码

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <poll.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_PLAYBACK_DEV    "hw:0,0"

/************************************
 WAV音频文件解析相关数据结构申明
 ************************************/
typedef struct WAV_RIFF {
    char ChunkID[4];                /* "RIFF" */
    u_int32_t ChunkSize;            /* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];                 /* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];            /* "fmt " */
    u_int32_t Subchunk1Size;        /* 16 for PCM */
    u_int16_t AudioFormat;          /* PCM = 1*/
    u_int16_t NumChannels;          /* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;           /* 8000, 44100, etc. */
    u_int32_t ByteRate;             /* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;           /* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;        /* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;
static FMT_t wav_fmt;

typedef struct WAV_DATA {
    char Subchunk2ID[4];            /* "data" */
    u_int32_t Subchunk2Size;        /* data size */
} __attribute__ ((packed)) DATA_t;

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;               //pcm句柄
static unsigned int buf_bytes;                  //应用程序缓冲区的大小(字节为单位)
static void *buf = NULL;                    //指向应用程序缓冲区的指针
static int fd = -1;                             //指向WAV音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024;    //周期大小(单位: 帧)
static unsigned int periods = 16;               //周期数(设备驱动层buffer的大小)

static struct pollfd *pfds = NULL;
static int count;

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(驱动层buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小
    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;
    DATA_t wav_data;
    int ret;

    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }

    /* 读取RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
        strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 读取sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf("  file name:     %s\n", file);
    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

    /* sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
                SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

        /* 找到sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;

        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }

    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

static int snd_pcm_poll_init(void)
{
    int ret;

    /* 获取PCM句柄的轮询描述符计数 */
    count = snd_pcm_poll_descriptors_count(pcm);
    if (0 >= count) {
        fprintf(stderr, "Invalid poll descriptors count\n");
        return -1;
    }

    /* 分配内存 */
    pfds = calloc(count, sizeof(struct pollfd));
    if (NULL == pfds) {
        perror("calloc error");
        return -1;
    }

    /* 填充pfds */
    ret = snd_pcm_poll_descriptors(pcm, pfds, count);
    if (0 > ret)
        return -1;

    return 0;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    unsigned short revents;
    snd_pcm_sframes_t avail;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 打开WAV音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);

    /* 初始化PCM Playback设备 */
    if (snd_pcm_init())
        goto err1;

    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err2;
    }

    /* I/O多路复用poll初始化 */
    if (snd_pcm_poll_init())
        goto err3;

    for (;;) {

        ret = poll(pfds, count, -1);//调用poll
        if (0 > ret) {
            perror("poll error");
            goto err3;
        }

        ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
        if (0 > ret)
            goto err3;
        if (revents & POLLERR)
            goto err3;
        if (revents & POLLOUT) {    //可写数据
            avail = snd_pcm_avail_update(pcm);//获取环形缓冲区中有多少帧数据需要填充
            while (avail >= period_size) {  //我们一次写入一个周期

                memset(buf, 0x00, buf_bytes);   //buf清零
                ret = read(fd, buf, buf_bytes);
                if (0 >= ret)
                    goto err3;

                ret = snd_pcm_writei(pcm, buf, period_size);
                if (0 > ret) {
                    fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
                    goto err3;
                }
                else if (ret < period_size) {
                    if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                        perror("lseek error");
                        goto err3;
                    }
                }

                avail = snd_pcm_avail_update(pcm);   //再次获取、更新avail
            }
        }
    }

err3:
    free(buf);     //释放内存
err2:
    snd_pcm_close(pcm); //关闭pcm设备
err1:
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE);
}

  • I/O多路复用poll初始化

    • 获取PCM句柄的轮询描述符计数

    • 分配内存

    • 填充pfds

  • 进入无限循环进行播放

    • 调用 poll 等待文件描述符的事件

    • 检查事件类型,处理错误事件

    • 如果可写,计算并更新可用帧数 avail

      • 循环读取WAV文件数据到缓冲区,并写入PCM设备

      • 更新可用帧数,继续循环

PCM 录音示例代码

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <poll.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_CAPTURE_DEV    "hw:0,0"

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;               //pcm句柄
static snd_pcm_uframes_t period_size = 1024;    //周期大小(单位: 帧)
static unsigned int periods = 16;               //周期数(buffer的大小)
static unsigned int rate = 44100;               //采样率

static struct pollfd *pfds = NULL;
static int count;

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_CAPTURE_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int snd_pcm_poll_init(void)
{
    int ret;

    /* 获取PCM句柄的轮询描述符计数 */
    count = snd_pcm_poll_descriptors_count(pcm);
    if (0 >= count) {
        fprintf(stderr, "Invalid poll descriptors count\n");
        return -1;
    }

    /* 分配内存 */
    pfds = calloc(count, sizeof(struct pollfd));
    if (NULL == pfds) {
        perror("calloc error");
        return -1;
    }

    /* 填充pfds */
    ret = snd_pcm_poll_descriptors(pcm, pfds, count);
    if (0 > ret)
        return -1;

    return 0;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    unsigned char *buf = NULL;
    unsigned int buf_bytes;
    unsigned short revents;
    snd_pcm_sframes_t avail;
    int fd = -1;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 初始化PCM Capture设备 */
    if (snd_pcm_init())
        exit(EXIT_FAILURE);

    /* 申请读缓冲区 */
    buf_bytes = period_size * 4;    //字节大小 = 周期大小*帧的字节大小 16位双声道
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err1;
    }

    /* 打开一个新建文件 */
    fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));
        goto err2;
    }

    /* I/O多路复用poll初始化 */
    if (snd_pcm_poll_init())
        goto err3;

    /* 开始录音 */
    ret = snd_pcm_start(pcm);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_start error: %s\n", snd_strerror(ret));
        goto err3;
    }

    for (;;) {

        ret = poll(pfds, count, -1);//调用poll
        if (0 > ret) {
            perror("poll error");
            goto err3;
        }

        ret = snd_pcm_poll_descriptors_revents(pcm, pfds, count, &revents);
        if (0 > ret)
            goto err3;
        if (revents & POLLERR)
            goto err3;
        if (revents & POLLIN) { //可读数据
            avail = snd_pcm_avail_update(pcm);   //检查有多少帧数据可读
            while (avail >= period_size) {  //每次读取一个周期

                ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期
                if (0 > ret) {
                    fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));
                    goto err3;
                }

                ret = write(fd, buf, ret * 4);    //将读取到的数据写入文件中
                if (0 >= ret)
                    goto err3;

                avail = snd_pcm_avail_update(pcm);   //再次读取、更新avail
            }
        }
    }

err3:
    close(fd);  //关闭文件
err2:
    free(buf);     //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    exit(EXIT_FAILURE);
}

  • I/O多路复用poll初始化

    • 获取PCM句柄的轮询描述符计数

    • 分配内存

    • 填充pfds

  • 启动PCM捕获设备 snd_pcm_start()

  • 进入无限循环进行捕获

    • 使用 poll(pfds, count, -1) 进行I/O多路复用等待事件

    • 检查返回的事件类型,处理错误事件

    • 如果是可读事件,计算并更新可用帧数 avail

    • 循环读取数据到缓冲区 buf,并写入文件

      • 更新可用帧数,继续循环

PCM 设备的状态

简介

  • ALSA(Advanced Linux Sound Architecture)库提供了 snd_pcm_state() 函数用于获取 PCM 设备的当前状态

  • snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm);

PCM 设备的状态枚举 snd_pcm_state_t

  • enum snd_pcm_state_t {
    SND_PCM_STATE_OPEN = 0,
    SND_PCM_STATE_SETUP,
    SND_PCM_STATE_PREPARED,
    SND_PCM_STATE_RUNNING,
    SND_PCM_STATE_XRUN,
    SND_PCM_STATE_DRAINING,
    SND_PCM_STATE_PAUSED,
    SND_PCM_STATE_SUSPENDED,
    SND_PCM_STATE_DISCONNECTED,
    SND_PCM_STATE_LAST = SND_PCM_STATE_DISCONNECTED,
    SND_PCM_STATE_PRIVATE1 = 1024
    }

  • SND_PCM_STATE_OPEN

    • 表示 PCM 设备处于打开状态,如调用 snd_pcm_open() 后
  • SND_PCM_STATE_SETUP

    • 表示设备已初始化,参数已配置好
  • SND_PCM_STATE_PREPARED

    • 表示设备已准备好,可以开始播放或录音。调用 snd_pcm_hw_params() 后设备会处于此状态

    • 函数 snd_pcm_prepare() 也可以使设备进入此状态

      • int snd_pcm_prepare(snd_pcm_t *pcm);

      • 调用成功返回 0,失败返回负数错误码

  • SND_PCM_STATE_RUNNING

    • SND_PCM_STATE_RUNNING

    • 调用 snd_pcm_start() 函数可以启动 PCM 设备

    • 当设备处于 SND_PCM_STATE_PREPARED 状态时,调用 snd_pcm_readi 或 snd_pcm_writei 进行读写数据时,这些函数内部会自动调用 snd_pcm_start() 函数

    • 调用 snd_pcm_drop() 或 snd_pcm_drain() 可使设备停止运行

      • int snd_pcm_drain(snd_pcm_t *pcm);
        int snd_pcm_drop(snd_pcm_t *pcm);

      • 调用成功返回 0,失败返回负值错误码

      • snd_pcm_drop():立即停止 PCM,丢弃挂起的帧

      • snd_pcm_drain():处理完挂起的帧后再停止 PCM

  • SND_PCM_STATE_XRUN

    • 表示发生了 XRUN(欠载/超载),设备处于此状态

    • 调用 snd_pcm_prepare() 可以使设备恢复到 SND_PCM_STATE_PREPARED 状态

  • SND_PCM_STATE_DRAINING

    • 表示设备正在处理挂起的帧,文档解释为“Draining: running (playback) or stopped (capture)”
  • SND_PCM_STATE_PAUSED

    • 表示设备处于暂停状态

    • 调用 snd_pcm_pause() 函数可以暂停或恢复设备

      • int snd_pcm_pause(snd_pcm_t *pcm, int enable);

      • 调用成功返回 0,失败返回负值错误码

    • 可以通过调用 snd_pcm_hw_params_can_pause() 判断设备是否支持暂停功能

      • int snd_pcm_hw_params_can_pause(const snd_pcm_hw_params_t *params);

      • 返回 1 表示支持暂停,返回 0 表示不支持暂停

  • SND_PCM_STATE_SUSPENDED

    • 表示硬件已挂起

    • 调用 snd_pcm_resume() 函数可以从挂起状态恢复

      • int snd_pcm_resume(snd_pcm_t *pcm);

      • 调用成功返回 0,失败返回负值错误码

    • 可以通过 snd_pcm_hw_params_can_resume() 判断硬件是否支持从挂起中恢复

      • int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);

      • 返回 1 表示支持,返回 0 表示不支持

  • SND_PCM_STATE_DISCONNECTED

    • 表示硬件已断开连接
  • 状态之间的转换

    • PCM 设备状态之间的转换关系示意图

    • 调用 snd_pcm_open() 之后,设备处于 SND_PCM_STATE_OPEN 状态

    • 调用 snd_pcm_hw_params() 后,设备依次转变为 SND_PCM_STATE_SETUP 和 SND_PCM_STATE_PREPARED 状态

    • 调用 snd_pcm_start() 后,设备进入 SND_PCM_STATE_RUNNING 状态

    • 发生 XRUN 时,设备进入 SND_PCM_STATE_XRUN 状态,调用 snd_pcm_prepare() 可以恢复到 SND_PCM_STATE_PREPARED 状态

    • 调用 snd_pcm_pause() 可以在 SND_PCM_STATE_RUNNING 和 SND_PCM_STATE_PAUSED 之间切换

    • 调用 snd_pcm_drop() 或 snd_pcm_drain() 后,设备返回到 SND_PCM_STATE_SETUP 状态

PCM 播放示例代码-加入状态控制

  • 代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <signal.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_PLAYBACK_DEV    "hw:0,0"

/************************************
 WAV音频文件解析相关数据结构申明
 ************************************/
typedef struct WAV_RIFF {
    char ChunkID[4];            	/* "RIFF" */
    u_int32_t ChunkSize;         	/* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];             	/* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];            	/* "fmt " */
    u_int32_t Subchunk1Size;     		/* 16 for PCM */
    u_int16_t AudioFormat;          	/* PCM = 1*/
    u_int16_t NumChannels;          	/* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;           	/* 8000, 44100, etc. */
    u_int32_t ByteRate;             	/* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;           	/* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;        	/* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;
static FMT_t wav_fmt;

typedef struct WAV_DATA {
    char Subchunk2ID[4];            	/* "data" */
    u_int32_t Subchunk2Size;        	/* data size */
} __attribute__ ((packed)) DATA_t;

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;               	//pcm句柄
static unsigned int buf_bytes;                  	//应用程序缓冲区的大小(字节为单位)
static void *buf = NULL;                    	//指向应用程序缓冲区的指针
static int fd = -1;                             	//指向WAV音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024; 		//周期大小(单位: 帧)
static unsigned int periods = 16;               	//周期数(设备驱动层buffer的大小)
static struct termios old_cfg;                  	//用于保存终端当前的配置参数

/************************************
 static静态函数
 ************************************/
static void snd_playback_async_callback(snd_async_handler_t *handler)
{
    snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄
    snd_pcm_sframes_t avail;
    int ret;

    avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充
    while (avail >= period_size) {  //我们一次写入一个周期

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes);
        if (0 >= ret)
            goto out;

        ret = snd_pcm_writei(handle, buf, period_size);
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto out;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto out;
            }
        }

        avail = snd_pcm_avail_update(handle);   //再次获取、更新avail
    }

    return;
out:
    snd_pcm_drain(pcm); //停止PCM
    snd_pcm_close(handle); //关闭pcm设备
    tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
    free(buf);
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE); //退出程序
}

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    snd_async_handler_t *async_handler = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(驱动层环形缓冲区buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小

    /* 注册异步处理函数 */
    ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
    if (0 > ret) {
        fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;
    DATA_t wav_data;
    int ret;

    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }

    /* 读取RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
        strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 读取sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf("  file name:     %s\n", file);
    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

    /* sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
                SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

        /* 找到sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;

        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }

    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    snd_pcm_sframes_t avail;
    struct termios new_cfg;
    sigset_t sset;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 屏蔽SIGIO信号 */
    sigemptyset(&sset);
    sigaddset(&sset, SIGIO);
    sigprocmask(SIG_BLOCK, &sset, NULL);

    /* 打开WAV音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);

    /* 初始化PCM Playback设备 */
    if (snd_pcm_init())
        goto err1;

    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err2;
    }

    /* 终端配置 */
    tcgetattr(STDIN_FILENO, &old_cfg);  //获取终端<标准输入-标准输出构成了一套终端>
    memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份
    new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式
    new_cfg.c_lflag &= ~ECHO;   //禁用回显
    tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效

    /* 播放:先将环形缓冲区填满数据 */
    avail = snd_pcm_avail_update(pcm);  //获取环形缓冲区中有多少帧数据需要填充
    while (avail >= period_size) {  //我们一次写入一个周期

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes);
        if (0 >= ret)
            goto err3;

        ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto err3;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto err3;
            }
        }

        avail = snd_pcm_avail_update(pcm);  //再次获取、更新avail
    }

    sigprocmask(SIG_UNBLOCK, &sset, NULL);  //取消SIGIO信号屏蔽

    char ch;
    for ( ; ; ) {

        ch = getchar(); //获取用户输入的控制字符
        switch (ch) {
        case 'q':   //Q键退出程序
            sigprocmask(SIG_BLOCK, &sset, NULL);//屏蔽SIGIO信号
            goto err3;
        case ' ':   //空格暂停/恢复
            switch (snd_pcm_state(pcm)) {

            case SND_PCM_STATE_PAUSED:  //如果是暂停状态则恢复运行
                ret = snd_pcm_pause(pcm, 0);
                if (0 > ret)
                    fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
                break;
            case SND_PCM_STATE_RUNNING: //如果是运行状态则暂停
                ret = snd_pcm_pause(pcm, 1);
                if (0 > ret)
                    fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
                break;
            }
            break;
        }
    }

err3:
    snd_pcm_drop(pcm); //停止PCM
    tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
    free(buf);     //释放内存
err2:
    snd_pcm_close(pcm); //关闭pcm设备
err1:
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE);
}

- 参数检查

- 屏蔽SIGIO信号

	- 初始化信号集,并屏蔽 SIGIO 信号,以避免在初始化过程中收到异步I/O信号

	- 主要是为了程序设计上的安全考虑,等把环形缓冲区填满数据之后,再取消 SIGIO 信号屏蔽

- 打开WAV音频文件

	- 调用 open_wav_file 函数打开并解析WAV音频文件。该函数会检查文件的RIFF、FMT和DATA块,确保文件格式正确,并读取音频文件的格式信息

		- RIFF 块是 WAV 文件的开头部分,它描述了文件的总体结构和类型

		- FMT 块描述了音频数据的格式信息,包括声道数、采样率、位深等

		- DATA 块包含实际的音频数据

- 初始化PCM Playback设备

	- 调用 snd_pcm_init 函数初始化PCM设备(音频播放设备)。在这个函数中,会设置PCM设备的访问模式、数据格式、采样率、声道数、周期大小和周期数,并注册异步I/O回调函数

- 申请读缓冲区

	- 分配用于读取音频数据的缓冲区,大小为一个周期的字节数

- 终端配置

	- 获取终端<标准输入-标准输出构成了一套终端>

	- 备份

	- 将终端设置为非规范模式

	- 禁用回显

	- 使配置生效

- 播放:先将环形缓冲区填满数据

	- 获取环形缓冲区的可用空间,并将音频数据填充到缓冲区中,直到缓冲区满

- 取消SIGIO信号屏蔽

- 用户输入处理循环

	- 进入一个循环,处理用户输入的控制字符。支持的命令包括

		- 'q':退出程序

		- ' '(空格):暂停或恢复播放

- 在发生错误或用户退出时,释放所有分配的资源,包括恢复终端状态、释放缓冲区内存、关闭PCM设备和关闭音频文件
  • 编译

    • ${CC} -o testApp testApp.c -lasound
  • 测试

    • 运行测试程序

snd_pcm_readi/snd_pcm_writei 错误处理

  • 错误码与错误描述

    • 当 snd_pcm_readi 或 snd_pcm_writei 调用出错时,它们会返回一个小于 0(负值)的错误码

    • 可以调用 snd_strerror() 函数获取对应的错误描述信息

  • 示例代码中的错误处理

    • 前面的示例代码中,我们没有对 snd_pcm_readi 或 snd_pcm_writei 的错误返回值做过多、细节的处理

    • 示例代码中,遇到错误时,简单地退出程序

  • 详细的错误处理

    • 当 snd_pcm_readi 或 snd_pcm_writei 出错时,可以根据不同的情况作进一步的处理

    • 在 alsa-lib 文档中,介绍了 snd_pcm_readi 和 snd_pcm_writei 函数的不同错误返回值及其含义

      • snd_pcm_writei 函数的错误返回值描述,nd_pcm_readi()函数与它相同

      • 错误码解释

        • -EBADFD

          • 表示 PCM 设备的状态不对

          • snd_pcm_readi 或 snd_pcm_writei 需要 PCM 设备处于 SND_PCM_STATE_PREPARED 或 SND_PCM_STATE_RUNNING 状态

          • 可以通过适当的状态转换使设备进入正确的状态

        • -EPIPE

          • 表示发生了 XRUN

          • 可以根据实际需要进行处理,例如调用 snd_pcm_drop() 停止 PCM 设备,或者调用 snd_pcm_prepare() 使设备恢复到准备状态

        • -ESTRPIPE

          • 表示硬件发生了挂起,PCM 设备处于 SND_PCM_STATE_SUSPENDED 状态

          • 可以调用 snd_pcm_resume() 从挂起中恢复

          • 如果硬件不支持恢复,可以调用 snd_pcm_prepare() 使设备进入准备状态,或者根据应用需求进行其它处理

  • 通过正确处理这些错误,可以提高音频播放或录制程序的稳定性

混音器设置

概述

  • ALSA 配置工具介绍

    • 之前介绍了 alsa-utils 提供的两个声卡配置工具:alsamixer 和 amixer

    • 这两个工具都是基于 alsa-lib 库函数编写的

  • 如何在自己的应用程序中调用 alsa-lib 库函数来配置声卡混音器,例如进行音量调节

  • ALSA 混音器接口

    • 混音器相关的接口在 alsa-lib 的 Mixer Interface 模块中有介绍

    • 可以通过图示中的 “Mixer Interface” 链接查看混音器相关接口的详细介绍

      • Mixer Interface 模块

      • 可以简单浏览 Mixer Interface 模块下提供的函数

      • 点击函数名可以查看该函数的简单介绍信息

(1)打开混音器:snd_mixer_open

  • 混音器打开函数

    • 使用混音器之前,需要调用 snd_mixer_open() 函数来打开一个空的混音器

    • int snd_mixer_open(snd_mixer_t **mixerp, int mode);

      • mode 指定了打开模式,通常设置为 0 使用默认模式即可

      • 成功返回 0;失败返回一个小于 0 的错误码

  • 混音器数据结构

    • alsa-lib 使用 snd_mixer_t 数据结构来描述混音器

    • 调用 snd_mixer_open() 函数会实例化一个 snd_mixer_t 对象,并通过 mixerp 返回该对象的指针(即混音器的句柄)

  • 使用示例

    • snd_mixer_t *mixer = NULL;
      int ret;
      ret = snd_mixer_open(&mixer, 0);
      if (0 > ret)
      fprintf(stderr, “snd_mixer_open error: %s\n”, snd_strerror(ret));

(2)Attach 关联设备:snd_mixer_attach

  • 混音器关联声卡控制设备

    • 调用 snd_mixer_open() 函数打开并实例化了一个空的混音器

    • 接下来需要关联声卡控制设备,调用 snd_mixer_attach() 函数进行关联

      • int snd_mixer_attach(snd_mixer_t *mixer, const char *name);

      • mixer:混音器的句柄(即 snd_mixer_t 对象的指针)

      • name:指定了声卡控制设备的名字,使用逻辑设备名而非设备节点的名字

        • 设备名的命名方式为 “hw:i”,其中 i 表示声卡的卡号

        • 例如 “hw:0” 表示声卡 0 的控制设备,对应 /dev/snd/controlC0 设备

      • 成功返回 0;失败返回一个小于 0 的错误码

  • 调用 snd_mixer_attach() 函数会将参数 name 所指定的控制设备与混音器 mixer 进行关联

  • 使用示例

    • ret = snd_mixer_attach(mixer, “hw:0”);
      if (0 > ret)
      fprintf(stderr, “snd_mixer_attach error: %s\n”, snd_strerror(ret));

(3)注册:snd_mixer_selem_register

  • 调用 snd_mixer_selem_register() 函数注册混音器

    • int snd_mixer_selem_register(
      snd_mixer_t *mixer,
      struct snd_mixer_selem_regopt *options,
      snd_mixer_class_t **classp
      );

    • mixer:混音器的句柄(即 snd_mixer_t 对象的指针)

    • 参数 options 和 classp:在一般使用中可以直接设置为 NULL

    • 成功返回 0;失败返回一个小于 0 的错误码

  • 使用示例

    • ret = snd_mixer_selem_register(mixer, NULL, NULL);
      if (0 > ret)
      fprintf(stderr, “snd_mixer_selem_register error: %s\n”, snd_strerror(ret));

(4)加载:snd_mixer_load

  • 最后需要加载混音器,调用 snd_mixer_load() 函数完成加载

    • int snd_mixer_load(snd_mixer_t * mixer);

    • mixer:混音器的句柄(即 snd_mixer_t 对象的指针)

    • 成功返回 0;失败返回小于 0 的错误码

  • 使用示例

    • ret = snd_mixer_load(mixer);
      if (0 > ret)
      fprintf(stderr, “snd_mixer_load error: %s\n”, snd_strerror(ret));

查找元素

  • 概述

    • 经过上面一系列步骤之后,接下来就可以使用混音器了

    • 在 alsa-lib 中,混音器的配置项被称为元素(element)

    • 不同的配置项对应不同的元素

      • 不同的配置项对应不同的元素

      • Headphone Playback ZC 是一个元素

      • Right Output Mixer PCM 也是一个元素

  • snd_mixer_first_elem 和 snd_mixer_last_elem

    • 元素的数据结构

      • alsa-lib 使用数据结构 snd_mixer_elem_t 来描述一个元素

      • 所以一个 snd_mixer_elem_t 对象就是一个元素

    • 混音器有很多的元素,也就是有很多配置项

    • 获取第一个元素

      • 通过 snd_mixer_first_elem() 函数可以找到混音器的第一个元素

      • snd_mixer_elem_t *snd_mixer_first_elem(snd_mixer_t *mixer);

    • 获取最后一个元素

      • 通过 snd_mixer_last_elem() 函数可找到混音器的最后一个元素

      • snd_mixer_elem_t *snd_mixer_last_elem(snd_mixer_t *mixer);

  • snd_mixer_elem_next 和 snd_mixer_elem_prev

    • 调用 snd_mixer_elem_next() 函数可以获取指定元素的下一个元素

      • snd_mixer_elem_t *snd_mixer_elem_next(snd_mixer_elem_t *elem);
    • 调用 snd_mixer_elem_prev() 函数可以获取指定元素的上一个元素

      • snd_mixer_elem_t *snd_mixer_elem_prev(snd_mixer_elem_t *elem);
    • 遍历混音器中的所有元素

      • 通过 snd_mixer_first_elem() 和 snd_mixer_elem_next(),或者 snd_mixer_last_elem() 和 snd_mixer_elem_prev(),就可以遍历整个混音器中的所有元素

      • snd_mixer_elem_t *elem = NULL;

elem = snd_mixer_first_elem(mixer);//找到第一个元素
while (elem) {

......
......

snd_mixer_elem_next(elem); //找到下一个元素 

}

  • snd_mixer_selem_get_name

    • 调用 snd_mixer_selem_get_name()函数可获取指定元素的名字

      • const char *snd_mixer_selem_get_name(snd_mixer_elem_t *elem);
    • 获取元素的名字之后,进行对比,以确定是否是我们要找的元素

      • const char *name = snd_mixer_selem_get_name(elem);
        if (!strcmp(name, “Headphone”)) {
        //该配置项是"Headphone"
        }
        else {
        //该配置项不是"Headphone"
        }

获取/更改元素的配置值

  • 混音器的配置值有两种主要类型

    • 配置值是在一个范围内的数值

      • 例如:调整音量的大小
    • 配置值是布尔类型(bool)

      • 用于控制开启或关闭

        • 例如

        • 0 表示关闭配置

        • 1 表示使能配置

  • snd_mixer_selem_has_playback_volume/snd_mixer_selem_has_capture_volume

    • 用于判断指定混音器元素的配置值是否为volume 音量类型

      • snd_mixer_selem_has_playback_volume:判断播放音量是否为音量类型

      • snd_mixer_selem_has_capture_volume:判断录音音量是否为音量类型

      • int snd_mixer_selem_has_playback_volume(snd_mixer_elem_t *elem);

      • int snd_mixer_selem_has_capture_volume(snd_mixer_elem_t *elem);

    • 返回 0 表示不是 volume 类型;返回 1 表示是 volume 类型

  • snd_mixer_selem_has_playback_switch/snd_mixer_selem_has_capture_switch

    • 用于判断指定混音器元素的配置值是否为 switch开关类型

      • snd_mixer_selem_has_playback_switch:判断播放开关是否为开关类型

      • snd_mixer_selem_has_capture_switch:判断录音开关是否为开关类型

      • int snd_mixer_selem_has_playback_switch(snd_mixer_elem_t *elem);

      • int snd_mixer_selem_has_capture_switch(snd_mixer_elem_t *elem);

    • 返回 0 表示不是 switch 类型;返回 1 表示是 switch 类型

  • snd_mixer_selem_has_playback_channel/snd_mixer_selem_has_capture_channel

    • 用于判断指定混音器元素是否包含特定通道

      • snd_mixer_selem_has_playback_channel:判断播放元素是否包含指定通道

      • snd_mixer_selem_has_capture_channel:判断录音元素是否包含指定通道

      • int snd_mixer_selem_has_playback_channel(
        snd_mixer_elem_t *elem,
        snd_mixer_selem_channel_id_t channel
        );

      • int snd_mixer_selem_has_capture_channel(
        snd_mixer_elem_t *elem,
        snd_mixer_selem_channel_id_t channel
        );

      • channel:用于指定一个通道,snd_mixer_selem_channel_id_t 是一个枚举类型

        • snd_mixer_selem_channel_id_t 枚举类型

        • 双声道元素通常包含左前(SND_MIXER_SCHN_FRONT_LEFT)和右前(SND_MIXER_SCHN_FRONT_RIGHT)两个声道

        • 单声道设备通常只包含 SND_MIXER_SCHN_MONO,其数值等于 SND_MIXER_SCHN_FRONT_LEFT

    • 单声道判断函数

      • snd_mixer_selem_is_playback_mono:判断播放元素是否为单声道

      • snd_mixer_selem_is_capture_mono:判断录音元素是否为单声道

      • int snd_mixer_selem_is_playback_mono(snd_mixer_elem_t *elem);

      • int snd_mixer_selem_is_capture_mono(snd_mixer_elem_t *elem);

  • snd_mixer_selem_get_playback_volume/snd_mixer_selem_get_capture_volume

    • 用于获取指定混音器元素的音量大小

      • snd_mixer_selem_get_playback_volume:用于获取播放元素的音量大小

      • snd_mixer_selem_get_capture_volume:用于获取录音元素的音量大小

      • int snd_mixer_selem_get_playback_volume(
        snd_mixer_elem_t *elem,
        snd_mixer_selem_channel_id_t channel,
        long *value
        );

int snd_mixer_selem_get_capture_volume(
snd_mixer_elem_t *elem,
snd_mixer_selem_channel_id_t channel,
long *value
);

		- elem:指定相应的混音器元素

		- channel:指定该元素的某个声道

- 获取音量大小

	- 调用 snd_mixer_selem_get_playback_volume() 函数,可以获取 elem 元素的 channel 声道对应的音量大小

	- 获取到的音量值通过 value 返回

- 返回值

	- 函数调用成功返回 0

	- 函数调用失败返回一个小于 0 的错误码

- 譬如,获取左前声道的音量(播放)

	- long value;

snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &value);

  • snd_mixer_selem_set_playback_volume/snd_mixer_selem_set_capture_volume

    • 设置指定元素的音量值

      • 使用 snd_mixer_selem_set_playback_volume 设置播放元素的某个声道的音量

      • 使用 snd_mixer_selem_set_capture_volume 设置录音元素的某个声道的音量

      • int snd_mixer_selem_set_playback_volume(
        snd_mixer_elem_t *elem,
        snd_mixer_selem_channel_id_t channel,
        long value
        );
        int snd_mixer_selem_set_capture_volume(
        snd_mixer_elem_t *elem,
        snd_mixer_selem_channel_id_t channel,
        long value
        );

        • elem:指定要设置音量的元素

        • channel:指定要设置音量的元素的某个声道

        • value:指定音量值

    • 设置所有声道音量

      • 使用 snd_mixer_selem_set_playback_volume_all 一次性设置播放元素所有声道的音量

      • 使用 snd_mixer_selem_set_capture_volume_all 一次性设置录音元素所有声道的音量

      • int snd_mixer_selem_set_playback_volume_all(
        snd_mixer_elem_t *elem,
        long value
        );

int snd_mixer_selem_set_capture_volume_all(
snd_mixer_elem_t *elem,
long value
}:

  • snd_mixer_selem_get_playback_volume_range/snd_mixer_selem_get_capture_volume_range

    • 获取指定元素的音量范围

    • int snd_mixer_selem_get_playback_volume_range(
      snd_mixer_elem_t *elem,
      long *min,
      long *max
      );

int snd_mixer_selem_get_capture_volume_range(
snd_mixer_elem_t *elem,
long *min,
long *max
);

示例程序

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <signal.h>
#include <alsa/asoundlib.h>

/************************************
 宏定义
 ************************************/
#define PCM_PLAYBACK_DEV    "hw:0,0"
#define MIXER_DEV           "hw:0"

/************************************
 WAV音频文件解析相关数据结构申明
 ************************************/
typedef struct WAV_RIFF {
    char ChunkID[4];                	/* "RIFF" */
    u_int32_t ChunkSize;            	/* 从下一个地址开始到文件末尾的总字节数 */
    char Format[4];                 	/* "WAVE" */
} __attribute__ ((packed)) RIFF_t;

typedef struct WAV_FMT {
    char Subchunk1ID[4];             	/* "fmt " */
    u_int32_t Subchunk1Size;          	/* 16 for PCM */
    u_int16_t AudioFormat;            	/* PCM = 1*/
    u_int16_t NumChannels;           	/* Mono = 1, Stereo = 2, etc. */
    u_int32_t SampleRate;             	/* 8000, 44100, etc. */
    u_int32_t ByteRate;               	/* = SampleRate * NumChannels * BitsPerSample/8 */
    u_int16_t BlockAlign;             	/* = NumChannels * BitsPerSample/8 */
    u_int16_t BitsPerSample;          	/* 8bits, 16bits, etc. */
} __attribute__ ((packed)) FMT_t;
static FMT_t wav_fmt;

typedef struct WAV_DATA {
    char Subchunk2ID[4];             	/* "data" */
    u_int32_t Subchunk2Size;          	/* data size */
} __attribute__ ((packed)) DATA_t;

/************************************
 static静态全局变量定义
 ************************************/
static snd_pcm_t *pcm = NULL;                   	//pcm句柄
static snd_mixer_t *mixer = NULL;                  	//混音器句柄
static snd_mixer_elem_t *playback_vol_elem = NULL; 	//播放<音量控制>元素
static unsigned int buf_bytes;                      	//应用程序缓冲区的大小(字节为单位)
static void *buf = NULL;                        	//指向应用程序缓冲区的指针
static int fd = -1;                                 	//指向WAV音频文件的文件描述符
static snd_pcm_uframes_t period_size = 1024;        	//周期大小(单位: 帧)
static unsigned int periods = 16;                   	//周期数(设备驱动层buffer的大小)
static struct termios old_cfg;                      	//用于保存终端当前的配置参数

/************************************
 static静态函数
 ************************************/
static void snd_playback_async_callback(snd_async_handler_t *handler)
{
    snd_pcm_t *handle = snd_async_handler_get_pcm(handler);//获取PCM句柄
    snd_pcm_sframes_t avail;
    int ret;

    avail = snd_pcm_avail_update(handle);//获取环形缓冲区中有多少帧数据需要填充
    while (avail >= period_size) {  //我们一次写入一个周期

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes);
        if (0 >= ret)
            goto out;

        ret = snd_pcm_writei(handle, buf, period_size);
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto out;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto out;
            }
        }

        avail = snd_pcm_avail_update(handle);   //再次获取、更新avail
    }

    return;
out:
    snd_pcm_drain(pcm); //停止PCM
    snd_mixer_close(mixer); //关闭混音器
    snd_pcm_close(handle); //关闭pcm设备
    tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
    free(buf);
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE); //退出程序
}

static int snd_pcm_init(void)
{
    snd_pcm_hw_params_t *hwparams = NULL;
    snd_async_handler_t *async_handler = NULL;
    int ret;

    /* 打开PCM设备 */
    ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_open error: %s: %s\n",
                    PCM_PLAYBACK_DEV, snd_strerror(ret));
        return -1;
    }

    /* 实例化hwparams对象 */
    snd_pcm_hw_params_malloc(&hwparams);

    /* 获取PCM设备当前硬件配置,对hwparams进行初始化 */
    ret = snd_pcm_hw_params_any(pcm, hwparams);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /************** 
     设置参数
    ***************/
    /* 设置访问类型: 交错模式 */
    ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置数据格式: 有符号16位、小端模式 */
    ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置采样率 */
    ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置声道数: 双声道 */
    ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期大小: period_size */
    ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 设置周期数(驱动层环形缓冲区buffer的大小): periods */
    ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));
        goto err2;
    }

    /* 使配置生效 */
    ret = snd_pcm_hw_params(pcm, hwparams);
    snd_pcm_hw_params_free(hwparams);   //释放hwparams对象占用的内存
    if (0 > ret) {
        fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));
        goto err1;
    }

    buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小

    /* 注册异步处理函数 */
    ret = snd_async_add_pcm_handler(&async_handler, pcm, snd_playback_async_callback, NULL);
    if (0 > ret) {
        fprintf(stderr, "snd_async_add_pcm_handler error: %s\n", snd_strerror(ret));
        goto err1;
    }

    return 0;

err2:
    snd_pcm_hw_params_free(hwparams);   //释放内存
err1:
    snd_pcm_close(pcm); //关闭pcm设备
    return -1;
}

static int snd_mixer_init(void)
{
    snd_mixer_elem_t *elem = NULL;
    const char *elem_name;
    long minvol, maxvol;
    int ret;

    /* 打开混音器 */
    ret = snd_mixer_open(&mixer, 0);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_open error: %s\n", snd_strerror(ret));
        return -1;
    }

    /* 关联一个声卡控制设备 */
    ret = snd_mixer_attach(mixer, MIXER_DEV);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_attach error: %s\n", snd_strerror(ret));
        goto err;
    }

    /* 注册混音器 */
    ret = snd_mixer_selem_register(mixer, NULL, NULL);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_selem_register error: %s\n", snd_strerror(ret));
        goto err;
    }

    /* 加载混音器 */
    ret = snd_mixer_load(mixer);
    if (0 > ret) {
        fprintf(stderr, "snd_mixer_load error: %s\n", snd_strerror(ret));
        goto err;
    }

    /* 遍历混音器中的元素 */
    elem = snd_mixer_first_elem(mixer);//找到第一个元素
    while (elem) {

        elem_name = snd_mixer_selem_get_name(elem);//获取元素的名称
        /* 针对开发板出厂系统:WM8960声卡设备 */
        if(!strcmp("Speaker", elem_name) ||  //耳机音量<对喇叭外音输出有效>
           !strcmp("Headphone", elem_name) ||//喇叭音量<对耳机输出有效>
           !strcmp("Playback", elem_name)) {//播放音量<总的音量控制,对喇叭和耳机输出都有效>
            if (snd_mixer_selem_has_playback_volume(elem)) {//是否是音量控制元素
                snd_mixer_selem_get_playback_volume_range(elem, &minvol, &maxvol);//获取音量可设置范围
                snd_mixer_selem_set_playback_volume_all(elem, (maxvol-minvol)*0.9 + minvol);//全部设置为90%

                if (!strcmp("Playback", elem_name))
                    playback_vol_elem = elem;
            }
        }

        elem = snd_mixer_elem_next(elem);
    }

    return 0;

err:
    snd_mixer_close(mixer);
    return -1;
}

static int open_wav_file(const char *file)
{
    RIFF_t wav_riff;
    DATA_t wav_data;
    int ret;

    fd = open(file, O_RDONLY);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));
        return -1;
    }

    /* 读取RIFF chunk */
    ret = read(fd, &wav_riff, sizeof(RIFF_t));
    if (sizeof(RIFF_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验
        strncmp("WAVE", wav_riff.Format, 4)) {
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 读取sub-chunk-fmt */
    ret = read(fd, &wav_fmt, sizeof(FMT_t));
    if (sizeof(FMT_t) != ret) {
        if (0 > ret)
            perror("read error");
        else
            fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验
        fprintf(stderr, "check error: %s\n", file);
        close(fd);
        return -1;
    }

    /* 打印音频文件的信息 */
    printf("<<<<音频文件格式信息>>>>\n\n");
    printf("  file name:     %s\n", file);
    printf("  Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);
    printf("  AudioFormat:   %u\n", wav_fmt.AudioFormat);
    printf("  NumChannels:   %u\n", wav_fmt.NumChannels);
    printf("  SampleRate:    %u\n", wav_fmt.SampleRate);
    printf("  ByteRate:      %u\n", wav_fmt.ByteRate);
    printf("  BlockAlign:    %u\n", wav_fmt.BlockAlign);
    printf("  BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);

    /* sub-chunk-data */
    if (0 > lseek(fd, sizeof(RIFF_t) + 8 + wav_fmt.Subchunk1Size,
                SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {

        /* 找到sub-chunk-data */
        if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验
            return 0;

        if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {
            perror("lseek error");
            close(fd);
            return -1;
        }
    }

    fprintf(stderr, "check error: %s\n", file);
    return -1;
}

static void show_help(void)
{
    printf("<<<<<<<基于alsa-lib音乐播放器>>>>>>>>>\n\n"
           "操作菜单:\n"
           "  q             退出程序\n"
           "  space<空格>   暂停播放/恢复播放\n"
           "  w             音量增加++\n"
           "  s             音量减小--\n\n");
}

/************************************
 main主函数
 ************************************/
int main(int argc, char *argv[])
{
    snd_pcm_sframes_t avail;
    struct termios new_cfg;
    sigset_t sset;
    int ret;

    if (2 != argc) {
        fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 屏蔽SIGIO信号 */
    sigemptyset(&sset);
    sigaddset(&sset, SIGIO);
    sigprocmask(SIG_BLOCK, &sset, NULL);

    /* 打开WAV音频文件 */
    if (open_wav_file(argv[1]))
        exit(EXIT_FAILURE);

    /* 初始化PCM Playback设备 */
    if (snd_pcm_init())
        goto err1;

    /* 初始化混音器 */
    if (snd_mixer_init())
        goto err2;

    /* 申请读缓冲区 */
    buf = malloc(buf_bytes);
    if (NULL == buf) {
        perror("malloc error");
        goto err3;
    }

    /* 终端配置 */
    tcgetattr(STDIN_FILENO, &old_cfg);  //获取终端<标准输入-标准输出构成了一套终端>
    memcpy(&new_cfg, &old_cfg, sizeof(struct termios));//备份
    new_cfg.c_lflag &= ~ICANON; //将终端设置为非规范模式
    new_cfg.c_lflag &= ~ECHO;   //禁用回显
    tcsetattr(STDIN_FILENO, TCSANOW, &new_cfg);//使配置生效

    /* 播放:先将环形缓冲区填满数据 */
    avail = snd_pcm_avail_update(pcm);  //获取环形缓冲区中有多少帧数据需要填充
    while (avail >= period_size) {  //我们一次写入一个周期

        memset(buf, 0x00, buf_bytes);   //buf清零
        ret = read(fd, buf, buf_bytes);
        if (0 >= ret)
            goto err4;

        ret = snd_pcm_writei(pcm, buf, period_size);//向环形缓冲区中写入数据
        if (0 > ret) {
            fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));
            goto err4;
        }
        else if (ret < period_size) {//实际写入的帧数小于指定的帧数
            //此时我们需要调整下音频文件的读位置
            //将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节
            //frame_bytes表示一帧的字节大小
            if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {
                perror("lseek error");
                goto err4;
            }
        }

        avail = snd_pcm_avail_update(pcm);  //再次获取、更新avail
    }

    sigprocmask(SIG_UNBLOCK, &sset, NULL);  //取消SIGIO信号屏蔽

    /* 显示帮助信息 */
    show_help();

    /* 等待获取用户输入 */
    char ch;
    long vol;
    for ( ; ; ) {

        ch = getchar(); //获取用户输入的控制字符
        switch (ch) {
        case 'q':   //Q键退出程序
            sigprocmask(SIG_BLOCK, &sset, NULL);//屏蔽SIGIO信号
            goto err4;
        case ' ':   //空格暂停/恢复
            switch (snd_pcm_state(pcm)) {

            case SND_PCM_STATE_PAUSED:  //如果是暂停状态则恢复运行
                ret = snd_pcm_pause(pcm, 0);
                if (0 > ret)
                    fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
                break;
            case SND_PCM_STATE_RUNNING: //如果是运行状态则暂停
                ret = snd_pcm_pause(pcm, 1);
                if (0 > ret)
                    fprintf(stderr, "snd_pcm_pause error: %s\n", snd_strerror(ret));
                break;
            }
            break;
        case 'w':           //音量增加
            if (playback_vol_elem) {
                //获取音量
                snd_mixer_selem_get_playback_volume(playback_vol_elem,
                        SND_MIXER_SCHN_FRONT_LEFT, &vol);
                vol++;
                //设置音量
                snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
            }
            break;
        case 's':           //音量降低
            if (playback_vol_elem) {
                //获取音量
                snd_mixer_selem_get_playback_volume(playback_vol_elem,
                        SND_MIXER_SCHN_FRONT_LEFT, &vol);
                vol--;
                //设置音量
                snd_mixer_selem_set_playback_volume_all(playback_vol_elem, vol);
            }
            break;
        }
    }

err4:
    snd_pcm_drop(pcm); //停止PCM
    tcsetattr(STDIN_FILENO, TCSANOW, &old_cfg); //退出前恢复终端的状态
    free(buf);     //释放内存
err3:
    snd_mixer_close(mixer); //关闭混音器
err2:
    snd_pcm_close(pcm); //关闭pcm设备
err1:
    close(fd);      //关闭打开的音频文件
    exit(EXIT_FAILURE);
}

  • 参数解析

  • 屏蔽SIGIO信号

    • 使用sigprocmask()屏蔽SIGIO信号,确保在初始化期间不会被打断
  • 打开WAV文件

  • 初始化PCM Playback设备

  • 初始化混音器

    • 打开混音器

    • 关联一个声卡控制设备

    • 注册混音器

    • 加载混音器

    • 遍历混音器中的元素

  • 申请读缓冲区

  • 终端配置

    • 备份当前终端配置,并将终端设置为非规范模式和禁用回显,确保能够实时读取用户输入
  • 播放前准备

    • 调用snd_pcm_avail_update()获取PCM设备的环形缓冲区中可用的帧数

    • 循环将数据写入环形缓冲区,直到缓冲区填满

    • 如果写入过程中出现错误,则跳转到错误处理部分,释放资源并退出程序

  • 取消SIGIO信号屏蔽

  • 显示帮助信息

  • 循环等待用户输入

    • 进入循环,等待用户输入控制字符

    • 根据用户输入的控制字符执行相应操作

      • Q键退出程序

      • 空格暂停/恢复

      • w键:增加播放音量

      • s键:降低播放音量

    • 当用户按下w或s键时

      • snd_mixer_selem_get_playback_volume()获取当前音量并使用snd_mixer_selem_set_playback_volume_all()设置新的音量

回环测试例程

alsaloop工具

  • alsa-utils 提供了一个用于回环测试的工具 alsaloop

  • alsaloop 可以实现边录音边播放的功能

  • 使用 alsaloop --help 命令可以查看 alsaloop 测试程序的使用帮助信息

    • alsaloop 工具使用帮助信息

回环测试原理

  • 回环测试的基本原理是录制音频,然后再播放出来

  • 实际实施时,需要考虑更多因素

录音与播放的差异

  • 对于录音和播放而言,录制一个周期和播放一个周期,硬件处理所花费的时间并不相同

  • 录音涉及 ADC 过程,播放涉及 DAC 过程,这两者处理时间不同

出现XRUN的可能性

  • 由于录音和播放处理时间不同,容易出现 XRUN(缓冲区溢出或欠载)

  • 有效合理地设计应用程序,以最大限度降低 XRUN 情况的发生是很重要的

获取源码

  • 直接下载 alsa-utils 源码包,可以找到 alsaloop 程序的源码

  • 源码包中还包括其他工具的源码,如 aplay、alsamixer、amixer、alsactl 等

    • alsaloop 源码

ALSA 插件(plugin)

概述

  • ALSA提供了一些PCM插件,用于扩展PCM设备的功能和特性

  • 插件负责各种样本转换、通道之间的样本复制等任务

PCM设备名使用逻辑设备名

  • 调用snd_pcm_open()函数时,需要填写PCM设备名

  • alsa-lib库使用逻辑设备名而不是设备节点名

示例程序中的设备名格式

  • 在之前编写的示例程序中,使用了"hw:i,j"这种格式的名字

  • "hw"指定的是一个插件,冒号后面的两个数字(i和j)表示两个参数(第一个参数表示声卡号,第二个参数表示设备号)

alsa.conf配置文件

  • 开发板Linux系统的/usr/share/alsa/目录下有一个名为alsa.conf的文件

    • alsa.conf 文件
  • 该文件是alsa-lib库的配置文件,在调用snd_pcm_open()函数时会加载并解析/usr/share/alsa/alsa.conf文件

其他配置文件

  • alsa.conf文件会加载并解析/etc/asound.conf和~/.asoundrc这两个配置文件

  • 在开发板出厂系统中,有/etc/asound.conf配置文件,但并没有~/.asoundrc文件

配置文件的作用

  • /usr/share/alsa/alsa.conf配置文件作为alsa-lib库函数的主要入口点,对alsa-lib进行了配置并定义了一些基本、通用的PCM插件

  • .asoundrc和asound.conf文件的引入提供用户定制化需求,用户可以在这两个文件中根据自己的需求定义插件

文档参考

  • https://www.alsa-project.org/main/index.php/Asoundrc

  • https://www.alsa-project.org/alsa-doc/alsa-lib/pcm_plugins.html

在开发板出厂系统的/etc/asound.conf文件中定义了很多PCM插件

  • /etc/asound.conf 文件中定义的插件

  • PCM插件的定义

    • pcm.name { }定义了一个插件,name表示插件的名字,例如dmix_48000、dmix_44100、dmix_32000等

    • 点号前面的pcm表示name是一个PCM插件,用于PCM设备

    • 点号前面的pcm表示name是一个PCM插件,用于PCM设备

  • 插件类型

    • 中括号{ }中,type字段指定了插件的类型,alsa-lib支持多种不同的插件类型

    • 不同类型的插件支持不同的功能和特性,例如hw、plughw、mmap_emul、shm、linear、plug、multi、share、dmix、dsnoop、softvol等

      • hw插件

        • hw插件直接与ALSA内核驱动程序通信,提供原始的、无转换的通信方式

        • 应用程序通过alsa-lib库函数直接操作底层音频硬件设置,例如对PCM设备的配置,直接作用于硬件

      • plughw插件

        • plughw插件提供软件特性,如采样率转换,这些特性硬件本身可能不支持

          • 例如,如果应用程序播放的音频文件采样率为48000,但底层音频硬件不支持这种采样率,调用snd_pcm_hw_params_set_rate()函数设置PCM设备采样率为48000会导致错误
        • 使用plughw插件可以解决这个问题,因为它支持采样率转换等软件特性

      • dmix插件

        • dmix插件支持混音功能,能够将多个应用程序的音频数据进行混合

        • 这允许多个音频流同时播放,而不会相互干扰

      • softvol插件

        • softvol插件支持软件音量控制

        • 这意味着音量调整是通过软件实现的,而不是直接通过硬件控制

  • 37
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木木不迷茫(˵¯͒¯͒˵)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值