ALSA-LINUX音频框架学习笔记

一、ALSA概述

ALSA 是 Advanced Linux Sound Architecture(高级的 Linux 声音体系)的缩写,目前已经成为了 linux下的主流音频体系架构,提供了音频和 MIDI 的支持,替代了原先旧版本中的 OSS(开发声音系统);ALSA 是 Linux 系统下一套标准的、先进的音频驱动
框架,那么这套框架的设计本身是比较复杂的,采用分离、分层思想设计而成。
在应用层,ALSA 为我们提供了一套标准的 API,应用程序只需要调用这些 API 就可完成对底层音频硬
件设备的控制,譬如播放、录音等,这一套 API 称为 alsa-lib。如下图所示:
在这里插入图片描述
alsa-lib 是一套 Linux 应用层的 C 语言函数库,为音频应用程序开发提供了一套统一、标准
的接口,应用程序只需调用这一套 API 即可完成对底层声卡设备的操控,譬如播放与录音。
用户空间的 alsa-lib 对应用程序提供了统一的 API 接口,这样可以隐藏驱动层的实现细节,简化了应用
程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。所以本章,对于我们来说,学习音频应
用编程其实就是学习 alsa-lib 库函数的使用、如何基于 alsa-lib 库函数开发音频应用程序。
ALSA 提供了关于 alsa-lib 的使用说明文档,其链接地址为:https://www.alsa-project.org/alsa-doc/alsa-lib/

二、基本知识

主要是与音频相关的基本概念,因为在 alsa-lib 应用编程中会涉及到这些概念,所以先给大家进行一个
简单地介绍。
样本长度 (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 帧,假设一帧为 4 个字节,那么也就是 1024*4=4096 个字节数据。
一个周期其实就是两次硬件中断之间的帧数,音频设备每处理(读或写)完一个周期的数据就会产生一
个中断,所以两个中断之间相差一个周期,关于中断的问题,稍后再向大家介绍!
缓冲区(buffer )
数据缓冲区,一个缓冲区包含若干个周期,所以 buffer 是由若干个周期所组成的一块空间。下面一张图
直观地表示了 buffer、period、frame、sample(样本长度)之间的关系,假设一个 buffer 包含 4 个周期、而
一个周包含 1024 帧、一帧包含两个样本(左、右两个声道):
在这里插入图片描述
音频设备底层驱动程序使用 DMA 来搬运数据,这个 buffer 中有 4 个 period,每当 DMA 搬运完一个
period 的数据就会触发一次中断,因此搬运整个 buffer 中的数据将产生 4 次中断。
如果数据缓存区 buffer 很大,一次传输整个 buffer 中的数据可能会导致不可接受的延迟,因为
一次搬运的数据量越大,所花费的时间就越长,那么必然会导致数据从传输开始到发出声音(以播放为例)
这个过程所经历的时间就会越长,这就是延迟。为了解决这个问题,ALSA 把缓存区拆分成多个周期,以周
期为传输单元进行传输数据。
所以,周期不宜设置过大,周期过大会导致延迟过高;但周期也不能太小,周期太小会导致频繁触发中
断,这样会使得 CPU 被频繁中断而无法执行其它的任务,使得效率降低!所以,周期大小要合适,在延迟
可接受的情况下,尽量设置大一些,不过这个需要根据实际应用场合而定,有些应用场合,可能要求低延迟、
实时性高,但有些应用场合没有这种需求。
数据 之间的传输
这里再介绍一下数据之间传输的问题,这个问题很重要,大家一定要理解,这样会更好的帮助我们理解
代码、理解代码的逻辑。
PCM 播放 情况下
在播放情况下,buffer 中存放了需要播放的 PCM 音频数据,由应用程序向 buffer 中写入音频数据,buffer
中的音频数据由 DMA 传输给音频设备进行播放,所以应用程序向 buffer 写入数据、音频设备从 buffer 读取
数据,这就是 buffer 中数据的传输情况。
图 28.5.2 中标识有 read pointer 和 write pointer 指针,write pointer 指向当前应用程序写 buffer 的位置、
read pointer 指向当前音频设备读 buffer 的位置。在数据传输之前(播放之前),buffer 缓冲区是没有数据的,
此时 write/read pointer 均指向了 buffer 的起始位置,也就是第一个周期的起始位置,如下所示:
在这里插入图片描述
应用程序向 buffer 写入多少帧数据,则 write pointer 指针向前移动多少帧,当应用程序向 buffer 中写入
一个周期的数据时,write pointer 指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周
期,以此类推!当 write pointer 移动到 buffer 末尾时,又会回到 buffer 的起始位置,以此循环!所以由此可
知,这是一个环形缓冲区。
以上是应用程序写 buffer 的一个过程,接着再来看看音频设备读 buffer(播放)的过程。在播放开始之
前,read pointer 指向了 buffer 的起始位置,也就是第一个周期的起始位置。音频设备每次只播放一个周期
的数据(读取一个周期),每一次都是从 read pointer 所指位置开始读取;每读取一个周期,read pointer 指
针向前移动一个周期,同样,当 read pointer 指针移动到 buffer 末尾时,又会回到 buffer 的起始位置,以此
构成一个循环!
应用程序需要向 buffer 中写入音频数据,音频设备才能读取数据进行播放,如果 read pointer 所指向的
周期并没有填充音频数据,则无法播放!当 buffer 数据满时,应用程序将不能再写入数据,否则就会覆盖之
前的数据,必须要等待音频设备播放完一个周期,音频设备每播放完一个周期,这个周期就变成空闲状态
了,此时应用程序就可以写入一个周期的数据以填充这个空闲周期。
PCM 录音情况下
在录音情况下,buffer 中存放了音频设备采集到的音频数据(外界模拟声音通过 ADC 转为数字声音),
由音频设备向 buffer 中写入音频数据(DMA 搬运),而应用程序从 buffer 中读取数据,所以音频设备向
buffer 写入数据、应用程序从 buffer 读取数据,这就是录音情况下 buffer 中数据的传输情况。
回到图 28.5.2 中,此时 write pointer 指向音频设备写 buffer 的位置、read pointer 指向应用程序读 buffer
的位置。在录音开始之前,buffer 缓冲区是没有数据的,此时 write/read pointer 均指向了 buffer 的起始位置,
也就是第一个周期的起始位置,如图 28.5.3 中所示。
音频设备向 buffer 写入多少帧数据,则 write pointer 指针向前移动多少帧,音频设备每次只采集一个周
期,将采集到的数据写入 buffer 中,从 write pointer 所指位置开始写入;当音频设备向 buffer 中写入一个周
期的数据时,write pointer 指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以
此类推!当 write pointer 移动到 buffer 末尾时,又会回到 buffer 的起始位置,以此构成循环!
以上是音频设备写 buffer 的一个过程,接着再来看看应用程序读 buffer 的过程。在录音开始之前,read
pointer 指向了 buffer 的起始位置,也就是第一个周期的起始位置。同样,应用程序从 buffer 读取了多少帧
数据,则 read pointer 指针向前移动多少帧;从 read pointer 所指位置开始读取,当 read pointer 指针移动到
buffer 末尾时,又会回到 buffer 的起始位置,以此构成一个循环!
音频设备需要向 buffer 中写入音频数据,应用程序才能从 buffer 中读取数据(录音),如果 read pointer
所指向的周期并没有填充音频数据,则无法读取!当 buffer 中没有数据时,需要等待音频设备向 buffer 中写
入数据,音频设备每次写入一个周期,当应用程序读取完这个周期的数据后,这个周期又变成了空闲周期,
需要等待音频设备写入数据。
Over and Under Run
当一个声卡处于工作状态时,环形缓冲区 buffer 中的数据总是连续地在音频设备和应用程序缓存区间
传输,如下图所示:
在这里插入图片描述
上图展示了声卡在工作状态下,buffer 中数据的传输情况,总是连续地在音频设备和应用程序缓存区间
传输,但事情并不总是那么完美、也会出现有例外;譬如在录音例子中,如果应用程序读取数据不够快,环
形缓冲区 buffer 中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种
数据的丢失被称为 overrun。在播放例子中,如果应用程序写入数据到环形缓冲区 buffer 中的速度不够快,
缓存区将会“饿死”(缓冲区中无数据可播放);这样的错误被称为 underrun(欠载)。在 ALSA 文档中,
将这两种情形统称为"XRUN",适当地设计应用程序可以最小化 XRUN 并且可以从中恢复过来。

三、开发基本流程

1.初始化PCM设备

1.打开PCM设备
2.实例化hwparams对象
3.获取 PCM 设备当前硬件配置,对 hwparams 进行初始化
4.设置访问类型: 交错模式
5.设置数据格式: 有符号 16 位、小端模式
6.设置采样率
7.设置声道数: 双声道 
8.设置周期大小: period_size
9.设置周期数(驱动层 buffer 的大小): periods
10.使配置生效
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;
}

2.打开对应音频文件

    1.读取 RIFF chunk
    2.读取 sub-chunk-fmt
    3.sub-chunk-data
    4.找到 sub-chunk-data
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;
}

3.申请应用层缓冲区

buf = malloc(buf_bytes);
if (NULL == buf) {
perror("malloc error");
goto err2;
}

4.读写音频文件,实现播放与录音

还可通过alsa-lib 提供的 snd_async_add_pcm_handler()函数注册异步处理函数,实现异步处理。本小节为同步方式实现的录/放音。

播放

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);
        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;
            }
        }
    }

录音

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;
    }

5.关闭文件/PCM设备,释放内存

 close(fd);  //关闭文件
 free(buf);     //释放内存
 snd_pcm_close(pcm); //关闭pcm设备
  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux ALSA-Lib库是用于读取和处理音频的开源库。它提供了一套API,可以让开发者通过 C/C++ 编程语言访问 Linux 系统中的音频设备。 ALSA-Lib 可以实现多种音频设备的读写,包括内置音频硬件,外部 USB 音频设备以及蓝牙音频ALSA-Lib 提供了一个叫做alsa-lib.h的头文件,这个头文件包含了常用的 ALSA-Lib API 函数。开发者可以根据具体需求来选择合适的函数,最常用的是snd_pcm_open()、snd_pcm_hw_params_set_xxx()、snd_pcm_writei()和snd_pcm_close(),这些函数分别用于打开、设置参数、写数据和关闭音频设备。 ALSA-Lib 提供的多种API函数使得开发者可以对音频进行多种高级操作。比如,开发者可以通过snd_pcm_drop()中止当前播放操作,通过snd_pcm_pause()暂停播放,通过snd_pcm_prepare()准备播放,还可以通过调用snd_pcm_avail_update()获取当前音频设备的缓冲区状态。 读取音频数据可以通过snd_pcm_readi()函数实现,这个函数会一次性从设备中读取指定数量的音频采样,并将其存储在一个指定的缓冲区中。开发者还可以选择使用snd_pcm_mmap_readi()和snd_pcm_mmap_begin()来读取音频采样,这两个函数可以实现更高效的读取。 在开发 Linux 音频应用程序时,ALSA-Lib 是非常重要的组件。通过掌握 ALSA-Lib 的 API 函数,开发者可以实现快速、高效地读取和处理音频数据。因此,熟悉 ALSA-Lib 是 Linux 音频开发工程师的必备技能之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值