从零写OS内核|350:音频子系统——ALSA 是怎么播放声音的

摘要:音频是计算机最常用的外设功能之一,从播放音乐到视频通话都离不开它。本篇文章从 PCM 原理、HDA 音频架构、到 Linux ALSA 驱动和 aplay 实战,带你理解数字音频的编解码链路。


1. PCM:数字音频的本质

1.1 采样、量化、编码

模拟声音是连续变化的波形。要变成数字信号,需要三步:

Bash
模拟信号(连续)→ 采样(离散时间)→ 量化(离散幅度)→ 编码(二进制)

采样率(Sample Rate):每秒采多少个点。常见:

  • 44100 Hz(CD 音质)
  • 48000 Hz(DVD/专业音频)
  • 96000 Hz(Hi-Res)

位深(Bit Depth):每个采样点用多少 bit 描述幅度。常见:

  • 16 bit(CD 音质)
  • 24 bit(专业录音)
  • 32 bit float(DSP 内部处理)

声道(Channels):1=单声道,2=立体声,5.1=环绕声,7.1=…

1.2 PCM 数据流

Bash
时间 →  0    1    2    3    4    5    6    7
      ┌────┬────┬────┬────┬────┬────┬────┬────┐
声道L │ S0 │ S1 │ S2 │ S3 │ S4 │ S5 │ S6 │ S7 │  ← 16-bit LE
声道R │ S0'│ S1'│ S2'│ S3'│ S4'│ S5'│ S6'│ S7'│  ← 16-bit LE
      └────┴────┴────┴────┴────┴────┴────┴────┘

对于 44100Hz/16bit/立体声:
Bytes per second = 44100 × 2 × 2 = 176,400 bytes/sec
Bitrate = 1.411 Mbps

1.3 有趣的 ASCII 波形

Bash
原始模拟波形:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          ╱╲        ╱╲╱╲      ╱╲
~~~~~~~~ ╱    ╲____╱      ╲__╱  ╲________________

PCM 采样 (4-bit 量化,仅作示意):
采样点: *   *   *   *   *   *   *   *
        ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓
     ┌──┬──┬──┬──┬──┬──┬──┬──┐
     │0307│0F│0B│0307│0F│0B│  (4-bit sample values)
     └──┴──┴──┴──┴──┴──┴──┴──┘

1.4 交叉存取(Interleaved)vs 非交叉(Non-interleaved)

Interleaved(交叉存取)——最常见格式:

Bash
[L0][R0][L1][R1][L2][R2]...
  ↑   ↑
  立体声帧(一对采样点)

Non-interleaved——给多通道或复杂布局用:

Bash
[L0][L1][L2][L3]...
[R0][R1][R2][R3]...

2. ALSA:Linux 音频架构

2.1 ALSA 分层

Bash
用户空间
├── aplay / arecord (alsa-utils)
├── PulseAudio / PipeWire (声音服务器)
└── libasound (alsa-lib, 应用编程接口)
    ↓
内核 ALSA Core (sound/core/pcm_native.c)
├── PCM Native API (playback/capture open/read/write)
├── Control API (音量、开关)
└── Raw MIDI API
    ↓
平台驱动层
├── HDA Intel (sound/pci/hda)
├── USB Audio (sound/usb)
├── ASoC (sound/soc/) - 嵌入式
└── 虚拟声卡 (snd-virmidi, snd-dummy)
    ↓
硬件 (HDA Codec / I2S Codec / USB Audio Class)

2.2 ALSA 设备节点

Bash
$ ls -la /proc/asound/cards
 0 [PCH            ]: HDA-Intel - Intel Haswell Audio
                      Intel Haswell Audio at 0xf7d34000 irq 50

$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: ALC269VC Analog [ALC269VC Analog]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

$ ls -la /dev/snd/
crw-rw---- 1 root audio 116,  2 May 29 09:00 pcmC0D0p   ← card 0, device 0, playback
crw-rw---- 1 root audio 116,  3 May 29 09:00 pcmC0D0c   ← card 0, device 0, capture
crw-rw---- 1 root audio 116,  4 May 29 09:00 controlC0  ← card 0, control device

设备名格式:pcmC{B}D{D}{p/c}

  • C{B} = card number
  • D{D} = device number
  • p = playback, c = capture

2.3 /proc/asound 信息

Bash
# 查看 PCM 硬件参数
$ cat /proc/asound/pcm
00-00: ALC269VC Analog : ALC269VC Analog : playback 1 : capture 1

# 查看声卡编解码器
$ cat /proc/asound/card0/codec#0
Codec: Realtek ALC269VC
Address: 0
Function Id: 0x1
Vendor Id: 0x10ec0269
...

3. HDA:高清音频架构

3.1 HDA 总线结构

HDA(High Definition Audio,最初叫 Azalia)是 Intel 在 2004 年提出的取代 AC’97 的音频总线标准:

Bash
      CPU                    HDA Controller              Codec
 ┌──────────┐             ┌──────────────┐          ┌──────────┐
 │ Memory   │             │              │          │          │
 │ DMA desc │────────────►│  DMA Engine  │─────────►│  DAC     │──►Speaker
 │ buffers  │             │              │          │  ADC     │◄──Mic
 └──────────┘             │  Command/    │          │          │
                           │  Response Q │          │  Pin     │──►HP Jack
                           └──────┬───────┘          └──────────┘
                                  │
                          64-byte frame
                          (HDA link)

HDA 是点对点的:每个 HDA Codec 有一个独立的输出引脚(通常叫 pin),连接 Speaker、Headphone、麦克风。HDA link 是固定 64 字节/帧的高速串行链路。

3.2 HDA 编解码器信息

Bash
$ cat /proc/asound/card0/codec#0
Codec: Realtek ALC269VC
Vendor Id: 0x10ec0269
Subsystem Id: 0x10431019
Revision Id: 0x100100

Pin Default Config: 0x411111f0 (Speaker)
  Port: Line outs (Jack)
  Color: Black
  Association: 0

Node 0x04 (Pin Complex):
  Amplitude caps: 0x00
  Pin cap: 0x400107f7 (HP Out, PresIn, EAPD)

3.3 HDA 命令流

C
// 向 Codec 发送 HDA 命令(简化版)
void hda_codec_write(struct hda_codec *codec, hda_nid_t nid, 
                     unsigned int verb, unsigned int payload) {
    u32 cmd = (nid << 20) | (verb << 8) | (payload & 0xFF);
    // 写 HDA_REG_GCTL? 不,这里是通过 CORB/RIRB 环
    // 实际是:写 CORB (Command Outbound Ring Buffer)
}

// HDA Verb 常见命令:
// 0x705 GET_POWER_STATE     - 读功耗状态
// 0x706 SET_POWER_STATE     - 置功耗状态  
// 0x70C GET_SDI_SELECT      - 读 SDI 选择
// 0x71C GET_PROC_COEF       - 读处理系数(用于音量等)
// 0x720 SET_PROC_COEF       - 设置处理系数
// 0xF0C GET_GPIO_DATA       - 读 GPIO

4. 实战:aplay 和 WAV 文件

4.1 播放一个 WAV 文件

Bash
# 首先准备一个测试音调(可以用 sox 生成)
$ sudo apt install sox -y
$ sox -n -r 44100 -c 2 test.wav synth 2 sine 440

# 用 aplay 播放
$ aplay -D plughw:0,0 test.wav
Playing WAVE 'test.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo

# 详细参数查看
$ aplay -D plughw:0,0 test.wav -v
Playing WAVE 'test.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Plug PCM: Hardware PCM card 0 'HDA Intel PCH' device 0 subdevice 0
Its setup is:
  stream       : PLAYBACK
  access       : RW_INTERLEAVED
  format       : S16_LE
  subformat    : STD
  channels     : 2
  rate         : 44100
  exact rate   : 44100 (44100/1)
  buffer_size  : 44100
  period_size  : 11025
  period_pos   : 0x0

4.2 参数解释

  • format: S16_LE = 有符号 16 bit 小端序
  • buffer_size: 44100 bytes = 1 秒的播放缓冲(2 channels × 44100 samples/sec × 2 bytes/sample)
  • period_size: 11025 = 每 11025 bytes(约 0.25 秒)触发一次 DMA 中断

4.3 arecord:录音

Bash
# 录制 5 秒音频
$ arecord -D plughw:0,0 -f cd -d 5 recording.wav
Recording WAVE 'recording.wav' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo

# 查看可用录音设备
$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: ALC269VC Analog [ALC269VC Analog]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

4.4 用 amixer 调节音量

Bash
# 查看所有可调控件
$ amixer -c 0 contents
numid=3,iface=MIXER,name='Master Playback Volume'
  ; type=INTEGER,access=rw---R--,values=2,min=0,max=87,step=1
numid=2,iface=MIXER,name='Master Playback Switch'
  ; type=BOOLEAN,access=rw------,values=1

# 查看简化输出
$ amixer -c 0
Simple mixer control 'Master',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback 0 - 87
  Mono: Playback 46 [53%] [-16.50dB] [on]

# 调音量到 50%
$ amixer -c 0 set Master 50%

# 静音
$ amixer -c 0 set Master mute

# 取消静音
$ amixer -c 0 set Master unmute

5. ALSA PCM 中间件:PulseAudio 和 PipeWire

5.1 为什么需要声音服务器?

ALSA 是内核驱动层,应用程序直接用 ALSA 比较底层。桌面环境需要混音(多个应用同时出声)、音量控制、重采样、蓝牙音频等高级功能,于是有了 PulseAudio 和 PipeWire:

Bash
应用 → libasound → ALSA → PulseAudio/PipeWire → ALSA → HDA/Codec → Speaker

5.2 PipeWire(现代方案)

PipeWire 2020 年代逐步取代 PulseAudio,它的优势:

  • 支持专业音频(低延迟)
  • 统一处理音频 + 视频(摄像头等)
  • 支持 Flatpak 沙盒应用访问音频
  • 兼容 PulseAudio 和 JACK 客户端

Bash
$ pactl info | grep "Server Name"
Server Name: PulseAudio (on PipeWire 0.3.65)

$ pw-cli list-objects Node

6. wandos 对比:无音频子系统

wandos 目前没有音频子系统实现。音频的复杂度在于:它需要 DMA(直接内存访问)、中断处理、编解码器配置、以及应用层混音。这些在 wandos 当前阶段优先级较低。wandos 的目标是先实现基本的 PIT(定时器)和简单的 beep 驱动,用于系统启动提示音。


公众号底部

📢 关注公众号【内核栈】,获取更多 OS/内核干货

💡 本文属于「从零写OS内核」系列,欢迎回顾:

  • 篇 349:《SPI 总线——高速串行通信》
  • 篇 348:《I2C 总线——嵌入式系统的血管》
  • 篇 347:《显示器——从 Framebuffer 到 GPU》

🔗 回复「ALSA」获取 ALSA 编程示例代码和 PCM 参数说明


作者:内核栈 | 原创出品
本文永久链接:https://kernelstack.dev/350-alsa

最后修改: 2024年1月30日

作者

评论

发表评论

您的邮箱地址不会被公开。