3. 音频采集与播放

任务目标

在嵌入式Linux设备上,通过ALSA(Advanced Linux Sound Architecture)接口完成一条最小音频通路:从麦克风采集PCM音频数据,立即写入声卡播放设备,实现回声侦听(听到自己说话的声音),以此验证音频采集与播放硬件链路正常。

环境与依赖

  • Linux内核已启用ALSA驱动,设备节点存在:/dev/snd/pcmC0D0c(采集)与 /dev/snd/pcmC0D0p(播放)
  • 安装ALSA用户态库及头文件:
    sudo apt-get install libasound2-dev
  • 编译工具:gcc

核心实现

以下代码打开默认采集设备(default,通常为麦克风)与默认播放设备(default,通常为扬声器),配置为16bit单声道、采样率16000Hz,以周期(period)为单位循环读取PCM数据并原样写入播放设备。缓冲区大小设为8个period,以降低因调度延迟导致的音频断裂。

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

#define SAMPLE_RATE 16000
#define CHANNELS 1
#define PERIOD_SIZE 512          /* 每个period的帧数 */
#define PERIODS 8                /* 缓冲区包含的period数 */

static int set_hw_params(snd_pcm_t *pcm, snd_pcm_uframes_t *period_size) {
    snd_pcm_hw_params_t *hw;
    snd_pcm_hw_params_alloca(&hw);
    if (snd_pcm_hw_params_any(pcm) < 0) return -1;
    if (snd_pcm_hw_params_set_access(pcm, hw, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) return -1;
    if (snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S16_LE) < 0) return -1;
    if (snd_pcm_hw_params_set_channels(pcm, hw, CHANNELS) < 0) return -1;
    unsigned int rate = SAMPLE_RATE;
    if (snd_pcm_hw_params_set_rate_near(pcm, hw, &rate, 0) < 0) return -1;
    if (snd_pcm_hw_params_set_period_size_near(pcm, hw, period_size, 0) < 0) return -1;
    if (snd_pcm_hw_params_set_periods(pcm, hw, PERIODS, 0) < 0) return -1;
    if (snd_pcm_hw_params(pcm, hw) < 0) return -1;
    return 0;
}

int main(void) {
    snd_pcm_t *capture = NULL, *playback = NULL;
    int err;

    /* 打开采集与播放设备 */
    if ((err = snd_pcm_open(&capture, "default", SND_PCM_STREAM_CAPTURE, 0)) < 0) {
        fprintf(stderr, "无法打开采集设备: %s\n", snd_strerror(err));
        return 1;
    }
    if ((err = snd_pcm_open(&playback, "default", SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
        fprintf(stderr, "无法打开播放设备: %s\n", snd_strerror(err));
        snd_pcm_close(capture);
        return 1;
    }

    /* 配置硬件参数 */
    snd_pcm_uframes_t period_size = PERIOD_SIZE;
    if (set_hw_params(capture, &period_size) < 0 ||
        set_hw_params(playback, &period_size) < 0) {
        fprintf(stderr, "硬件参数配置失败\n");
        snd_pcm_close(capture);
        snd_pcm_close(playback);
        return 1;
    }

    /* 分配缓冲区:一个period的帧数 × 2字节(16bit) × 通道数 */
    size_t bytes_per_frame = 2 * CHANNELS;
    char *buf = malloc(period_size * bytes_per_frame);
    if (!buf) {
        fprintf(stderr, "内存分配失败\n");
        snd_pcm_close(capture);
        snd_pcm_close(playback);
        return 1;
    }

    printf("开始音频回环测试(按 Ctrl+C 退出)...\n");
    printf("参数: %u Hz, %d 通道, 16bit, period=%lu 帧\n",
           SAMPLE_RATE, CHANNELS, (unsigned long)period_size);

    /* 主循环:采集→播放 */
    while (1) {
        snd_pcm_sframes_t n = snd_pcm_readi(capture, buf, period_size);
        if (n == -EPIPE) {
            snd_pcm_prepare(capture);   /* 处理欠载 */
            continue;
        } else if (n < 0) {
            fprintf(stderr, "采集错误: %s\n", snd_strerror(n));
            break;
        }

        snd_pcm_sframes_t w = snd_pcm_writei(playback, buf, n);
        if (w == -EPIPE) {
            snd_pcm_prepare(playback);  /* 处理欠载 */
        } else if (w < 0) {
            fprintf(stderr, "播放错误: %s\n", snd_strerror(w));
            break;
        }
    }

    free(buf);
    snd_pcm_drain(playback);
    snd_pcm_close(capture);
    snd_pcm_close(playback);
    return 0;
}

编译与运行

gcc -o alsa_loop alsa_loop.c -lasound
./alsa_loop

验证方法

  1. 运行程序后,对着麦克风说话,若能立即从扬声器/耳机听到自己的声音(有轻微延迟属正常),说明采集与播放通路正常。
  2. 程序持续运行不退出,且终端无错误输出,说明PCM数据流未中断。
  3. 按 Ctrl+C 终止程序,若终端无“采集错误”或“播放错误”打印,则判定通过。

常见问题

  • 打开设备失败:确认声卡驱动已加载,可用 aplay -l 和 arecord -l 查看可用设备;若默认设备不对,将代码中的 "default" 替换为具体设备名(如 "plughw:0,0")。
  • 听到爆音或卡顿:可增大 PERIODS 至16,或使用 snd_pcm_hw_params_set_buffer_time_near 增加缓冲时间;若CPU负载高,可尝试将 PERIOD_SIZE 增至1024。
  • 只有采集或只有播放:检查麦克风/扬声器是否被其他程序占用(如PulseAudio),可临时停止音频服务:sudo systemctl stop pulseaudio。