【TinyALSA全解析(三)】tinyplay、tincap、pcm_open源码解析

tinyplay、tincap、pcm_open源码解析

  • 一、本文的目的
  • 二、tinyplay.c源码分析
  • 三、tinycap.c源码分析
  • [四、pcm.c如何调度到Linux Kernel](#四、pcm.c如何调度到Linux Kernel)
    • [4.1 pcm_open解析](#4.1 pcm_open解析)
      • [4.1.1 pcm_open的主要流程](#4.1.1 pcm_open的主要流程)
      • [4.1.2 流程说明](#4.1.2 流程说明)
      • [4.1.3 调用方法](#4.1.3 调用方法)
    • [4.2 pcm_write解析](#4.2 pcm_write解析)

/*****************************************************************************************************************/

声明: 本博客内容均由https://blog.csdn.net/weixin_47702410原创,转载or引用请注明出处,谢谢!

创作不易,如果文章对你有帮助,麻烦点赞 收藏支持~感谢

/*****************************************************************************************************************/

一、本文的目的

本文分析tinyalsa中的的tinyplay和tinycap源码,tinyplay和tinycap是两个小工具。会分析这两个工具主要是调用pcm.c和pcm.h实现访问Linux kernel的硬件接口。

本文先解析一下这两个工具的源代码,再分析一下pcm.c这个文件是怎么访问到底层的。

学习这两个工具的意义不仅仅是对这两个工具有所了解,在安卓的音频架构中,Android Audio HAL层的代码对硬件的访问也会通过pcm.c文件进行访问。

tinyalsa的具体源码完整可以见Android 12_r8官方源码网站。本文以安卓12的r8分支进行讲解,安卓9-安卓14的tinyalsa的实现是大同小异的,掌握安卓12的TinyALSA源码再去看其它版本的安卓源码也是可以很快入手的。

二、tinyplay.c源码分析

tinyplay的主要流程如下:

概述一下上面的流程图,tinyplay的主要行为是:

  1. 读取播放的文件

  2. 解析wav文件,遍历了WAV文件中的每个数据块(chunk),直到找到音频数据块或无其他块可读。每个WAV文件都由RIFF数据块组成,每个块都有一个类型标识符(id)和一个大小字段(sz)。

    根据块的类型标识符(id),有三种可能的情况:

    • 如果ID是ID_FMT,这表示块是"fmt "块,它会存储音频流的格式信息,包括样本率、比特率等。代码读取这个块的内容到chunk_fmt结构体。如果实际上块的大小大于结构体的大小,代码会跳过剩下的字节。

    • 如果ID是ID_DATA,这表示块包含了实际的音频样本数据。在这个情况下,暂停块的读取(通过设置more_chunks 为0)并保持块的数据大小。

    • 对于其他未知的块类型,代码简单地跳过它。

  3. 最后调用pcm.c中的ops进一步的去对硬件进行访问,涉及硬件有关操作的函数主要是pcm_open、pcm_write函数。pcm_frames_to_bytes函数用于将音频帧数转换为字节数(此时有音频的位深、声道数信息,就可以计算所需帧数据占用多少字节空间)。

三、tinycap.c源码分析

tinycap的主要流程如下:

概述一下上面的流程图,tinycap的主要行为是:

  1. 以wb模式打开文件,wb模式表示以二进制模式打开一个文件进行写入操作。如果文件存在,则文件被截断为零长度(即文件的内容会被全部删除)。如果文件不存在,则创建一个新文件。
  2. 读取命令行的参数并存储在结构体struct wav_header中(对应tinycap,定义的变量名字为header,所有的内容将会存储在变量header中)
  3. wav文件格式有文件的头部信息,在wav文件的头部信息中有data_sz 字段用于记录整个音频数据占用字节,故此头部信息可以在录音完成后再计算写入,此处代码先跳过写wav的头部信息,先读取录音数据后再写入头部信息。
  4. 调用pcm.c中的ops进一步的去对硬件进行访问,主要使用pcm_read函数去读录音数据,pcm_open和pcm_close对设备进行开关。音频的处理一般是以帧为单位处理的,但是在malloc内存 还有 读取数据的时候(pcm_read)的时候,是以字节为单位。因此用pcm_frames_to_bytes函数转换帧数为字节数去申请内存,然后会用pcm_bytes_to_frames将读取的字节转换为帧以保存在wav头部信息中。
  5. 写入wav文件的头部信息

四、pcm.c如何调度到Linux Kernel

在 pcm.c 中,TinyALSA 使用了一些系统调用(如 open(), ioctl(), mmap(), close() 等)来访问和控制 ALSA 音频设备。这些系统调用是 Linux 内核提供的接口,应用程序可以通过这些接口与内核进行交互。

其中open() 系统调用用于打开 ALSA 设备,ioctl() 系统调用用于控制 ALSA 设备(如设置音频参数),mmap() 系统调用用于映射 ALSA 设备的内存,以便应用程序可以直接访问这些内存,close() 系统调用用于关闭 ALSA 设备。

Tip:
ALSA是位于Linux Kernel层面的音频系统。
TinyALSA是AOSP(Android Open Source Project)的一部分。
TinyALSA位于ALSA的上层,他们之间的关系是使用关系。
TinyALSA使用 ALSA 提供的接口与 Linux 内核进行交互。

本文就以pcm_open和pcm_write函数为例进行讲解,pcm_close和pcm_read函数的源码其实是类似的操作。熟悉pcm_open和pcm_write函数后,再去看pcm.c文件中的其它函数源码会有举一反三的效果。

参考源码:Android 12_r8官方pcm.c文件源码

4.1 pcm_open解析

4.1.1 pcm_open的主要流程

概述一下上面的流程图,tinycap的主要行为是:

  1. 创建一个新的PCM对象,并将传入的配置和标志复制到该对象中。
  2. 接下来,函数通过snd_utils_get_dev_node函数获取PCM设备的节点,并通过snd_utils_get_node_type函数获取设备的类型。根据设备类型,函数选择相应的操作集(ops)。
  3. 函数尝试打开PCM设备。如果打开失败,函数将清理已分配的资源并返回错误。如果设备成功打开,函数将获取设备的信息,并设置硬件参数(hw_params)。这些参数包括音频格式、子格式、周期大小、样本位数、帧位数、通道数、周期数和采样率。
  4. 函数设置软件参数(sw_params)。这些参数包括时间戳模式、周期步长、启动阈值、停止阈值、可用最小值、传输对齐、静音阈值、静音大小和边界。
  5. 函数尝试映射硬件状态。如果映射失败,函数将清理已分配的资源并返回错误。如果映射成功,函数将返回打开的PCM对象。

4.1.2 流程说明

  1. 函数选择相应的操作集(ops)主要有两种:
C 复制代码
//plug_ops
struct pcm_ops plug_ops = {
    .open = pcm_plug_open,
    .close = pcm_plug_close,
    .ioctl = pcm_plug_ioctl,
    .mmap = pcm_plug_mmap,
    .munmap = pcm_plug_munmap,
    .poll = pcm_plug_poll,
};
//hw_ops
struct pcm_ops hw_ops = {
    .open = pcm_hw_open,
    .close = pcm_hw_close,
    .ioctl = pcm_hw_ioctl,
    .mmap = pcm_hw_mmap,
    .munmap = pcm_hw_munmap,
    .poll = pcm_hw_poll,
};

hw_ops指的就是操作硬件设备的一组函数。plug_ops是处理plug插件的一组函数。

Tip: plug 是 ALSA 中的一种插件,它可以自动进行音频格式转换,例如从一个采样率转换到另一个采样率,或者从一个音频格式转换到另一个音频格式。参考ALSA官方文档中的:ALSA官方文档

  1. pcm_hw_open访问硬件的方式

主要就是通过节点访问到Linux kernel层面

c 复制代码
static int pcm_hw_open(unsigned int card, unsigned int device,
                unsigned int flags, void **data,
                __attribute__((unused)) void *node)
{
//...
    snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
             flags & PCM_IN ? 'c' : 'p');
    fd = open(fn, O_RDWR|O_NONBLOCK);
//...
}
  1. 设置的硬件和软件参数的含义

总结表格:

参数 类型 描述
SNDRV_PCM_HW_PARAM_FORMAT 硬件 PCM数据的格式,例如16位、24位、32位等
SNDRV_PCM_HW_PARAM_SUBFORMAT 硬件 PCM数据的子格式,通常设置为SNDRV_PCM_SUBFORMAT_STD,表示标准的线性PCM格式
SNDRV_PCM_HW_PARAM_PERIOD_SIZE 硬件 每个周期的帧数
SNDRV_PCM_HW_PARAM_SAMPLE_BITS 硬件 每个样本的位数
SNDRV_PCM_HW_PARAM_FRAME_BITS 硬件 每个帧的位数,等于样本位数乘以通道数
SNDRV_PCM_HW_PARAM_CHANNELS 硬件 音频数据的通道数
SNDRV_PCM_HW_PARAM_PERIODS 硬件 缓冲区中周期的数量
SNDRV_PCM_HW_PARAM_RATE 硬件 音频数据的采样率
sparams.tstamp_mode 软件 时间戳模式
sparams.period_step 软件 周期步长
sparams.start_threshold 软件 启动阈值
sparams.stop_threshold 软件 停止阈值
sparams.avail_min 软件 可用的最小帧数
sparams.xfer_align 软件 传输对齐
sparams.silence_threshold 软件 静音阈值
sparams.silence_size 软件 静音大小
sparams.boundary 软件 边界,定义了循环缓冲区的大小

硬件参数说明:

SNDRV_PCM_HW_PARAM_FORMAT:这是PCM数据的格式,例如16位、24位、32位等。

SNDRV_PCM_HW_PARAM_SUBFORMAT:这是PCM数据的子格式,通常设置为SNDRV_PCM_SUBFORMAT_STD,表示标准的线性PCM格式。

SNDRV_PCM_HW_PARAM_PERIOD_SIZE:这是每个周期的帧数。一个周期是音频数据的一个块,当一个周期的数据被播放或录制后,驱动程序将生成一个中断。

SNDRV_PCM_HW_PARAM_SAMPLE_BITS:这是每个样本的位数,它与SNDRV_PCM_HW_PARAM_FORMAT相关。

SNDRV_PCM_HW_PARAM_FRAME_BITS:这是每个帧的位数,它等于样本位数乘以通道数。

SNDRV_PCM_HW_PARAM_CHANNELS:这是音频数据的通道数,例如立体声有两个通道。

SNDRV_PCM_HW_PARAM_PERIODS:这是缓冲区中周期的数量。缓冲区是存储音频数据的内存区域,它由多个周期组成。

SNDRV_PCM_HW_PARAM_RATE:这是音频数据的采样率,例如44100Hz或48000Hz。

软件参数说明:

sparams.tstamp_mode:这是时间戳模式,设置为SNDRV_PCM_TSTAMP_ENABLE表示启用时间戳。

sparams.period_step:这是周期步长,通常设置为1。

sparams.start_threshold:这是启动阈值,当可用的帧数达到这个值时,播放或录制将开始。

sparams.stop_threshold:这是停止阈值,当可用的帧数达到这个值时,播放或录制将停止。

sparams.avail_min:这是可用的最小帧数,当可用的帧数达到这个值时,驱动程序将生成一个中断。

sparams.xfer_align:这是传输对齐,通常设置为周期大小的一半。

sparams.silence_threshold:这是静音阈值,当可用的帧数低于这个值时,驱动程序将生成静音数据。

sparams.silence_size:这是静音大小,它定义了静音数据的长度。

sparams.boundary:这是边界,它定义了循环缓冲区的大小。

4.1.3 调用方法

举个例子,如果想用音频节点0-0播放48K 2ch u16bit声音,可以这样设置参数:

c 复制代码
unsigned int card = 0;
unsigned int device = 0;
unsigned int flags = PCM_OUT;
struct pcm_config config = {
    .channels = 2,
    .rate = 48000,
    .format = PCM_FORMAT_S16_LE,
    .period_size = 1024,
    .period_count = 4,
};
struct pcm *pcm = pcm_open(card, device, flags, &config);

4.2 pcm_write解析

源码的解析如下:

c 复制代码
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
    // 定义一个snd_xferi结构体,用于存储音频数据和帧数
    struct snd_xferi x;

    // 检查PCM设备是否是输入设备,如果是,返回错误
    if (pcm->flags & PCM_IN)
        return -EINVAL;

    // 设置snd_xferi结构体的buf字段为传入的数据
    x.buf = (void*)data;
    // 设置snd_xferi结构体的frames字段为传入的数据量除以每帧的字节数
    x.frames = count / (pcm->config.channels *
                        pcm_format_to_bits(pcm->config.format) / 8);

    // 进入一个无限循环
    for (;;) {
        // 检查PCM设备是否正在运行
        if (!pcm->running) {
            // 如果不是,尝试准备PCM设备
            int prepare_error = pcm_prepare(pcm);
            // 如果准备失败,返回错误
            if (prepare_error)
                return prepare_error;
            // 尝试向PCM设备写入初始数据
            if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x))
                // 如果写入失败,返回错误
                return oops(pcm, errno, "cannot write initial data");
            // 如果写入成功,将PCM设备标记为正在运行,并返回0
            pcm->running = 1;
            return 0;
        }
        // 如果PCM设备正在运行,尝试向设备写入数据
        if (pcm->ops->ioctl(pcm->data, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)) {
            // 如果写入失败,重置PCM设备的prepared和running字段
            pcm->prepared = 0;
            pcm->running = 0;
            // 检查错误是否是EPIPE(管道破裂)
            if (errno == EPIPE) {
                // 如果是,增加underruns计数,并根据PCM设备的flags字段决定是否重新开始
                pcm->underruns++;
                if (pcm->flags & PCM_NORESTART)
                    return -EPIPE;
                continue;
            }
            // 如果不是EPIPE错误,返回错误
            return oops(pcm, errno, "cannot write stream data");
        }
        // 如果写入成功,返回0
        return 0;
    }
}

主要就是参数和环境检查,然后就调用ioctl进行处理。

相关推荐
Winston Wood2 分钟前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽6 分钟前
Android 项目模型配置管理
android
wowocpp10 分钟前
ubuntu 22.04 硬件配置 查看 显卡
linux·运维·ubuntu
山河君22 分钟前
ubuntu使用DeepSpeech进行语音识别(包含交叉编译)
linux·ubuntu·语音识别
鹏大师运维27 分钟前
【功能介绍】信创终端系统上各WPS版本的授权差异
linux·wps·授权·麒麟·国产操作系统·1024程序员节·统信uos
xinghuitunan28 分钟前
蓝桥杯顺子日期(填空题)
c语言·蓝桥杯
筱源源29 分钟前
Elasticsearch-linux环境部署
linux·elasticsearch
Half-up30 分钟前
C语言心型代码解析
c语言·开发语言
帅得不敢出门33 分钟前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
懒大王就是我1 小时前
C语言网络编程 -- TCP/iP协议
c语言·网络·tcp/ip