虚拟声卡

一、 虚拟声卡是什么?

虚拟声卡是一种软件产品,它只对声音数字信号进行处理。虽然它不能产生声音,但可以用来实现声音的传输、存储或混音等功能。

1) 虚拟声卡工作原理:

虚拟声卡通过软件技术实现了软声卡及声卡的驱动程序

虚拟声卡软件在计算机中为每一个虚拟声卡开辟一块内存,

应用程序可以通过虚拟声卡的音频输出端将音频流存储到开辟的内存中,同时可以通过虚拟声卡的输入端读取内存中的音频流。

每一个虚拟声卡的音频输入输出端的内部都是直连的,声音源程序产生(播放)声音,将声音送到虚拟声卡的音频输出端,虚拟声卡驱动程序直接将声音传到虚拟声卡的音频输入端。

应用程序 <---------->虚拟声卡(输出端 ----->一块内存 ----->虚拟声卡驱动程序   ----->音频输入 )

2) 虚拟声卡中声音的编码方式 pcm

3)根据实现方式大致可将虚拟设备分为两类

一类是在原有特定物理硬件设备的基础上采用软件的方式对这些设备加以抽象,如虚拟内存、虚拟存储器、虚拟网卡;

另一类则仅以纯软件编程的方式实现虚拟设备,这类虚拟设备主要有:虚拟光驱、虚拟声卡等。

虚拟设备 从单一功能设备-》多个功能设备-》通过网络完成将多个设备虚拟化发展。

虚拟设备技术 从逻辑角度而不是物理角度来对资源进行配置。

 

二、虚拟声卡实现技术

 通过对虚拟声卡源代码的分析,根据虚拟设备编程技术奖虚拟声卡的实现划为以下三个模块:

1)内核模式驱动:内核模式驱动程序是虚拟声卡技术的核心部分,执行大部分的响应用户需求的任务,内核模式驱动处理请求并填充好的数据包返回给用户模式驱动。(是被用户模式驱动的客户进程服务可以构造很多设备对象,并与他们虚连接)

2)用户模式驱动:用户模式驱动程序准备请求数据包并将数据包传给处理主要任务的内核模式驱动(用户模式驱动程序通过维持一个用户列表,驱动程序支持多用户操作。还具有内核模式驱动的服务管理功能,虚拟混频器)

3)控制面板程序:应用函数与用户模式驱动通信。 可以从内核驱动获得数据和修改内核驱动状态。

 

三、基于虚拟声卡(只处理声音数字信号)的各种异构(通信协议、语音编码)VOIP网络电话互通设计(通过开放API设置客户端声卡音频的输入和输出,获取PCM格式音频流)及实现 (解决VOIP通信技术在各个通信软件之间不能互通的问题的一个思路) 产品和业务互通 自动答录机和电话转接等功能(可以将一种网络电话输出的声音信息直接传给另一种网络电话的输入端)。

 

四、linux 虚拟声卡 dummy 创建

应用工程师需要用到一张虚拟声卡,以前没有接触过,这里mark一下。

内核配置如下:

  │     -> Device Drivers                                                                     
  │       -> Sound card support (SOUND [=y])                                                  
  │         -> Advanced Linux Sound Architecture (SND [=y])                                     
  │           -> Generic sound devices (SND_DRIVERS [=y])   

虚拟声卡_第1张图片

# aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: Dummy [Dummy], device 0: Dummy PCM [Dummy PCM]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 1: imxspdif [imx-spdif], device 0: S/PDIF PCM snd-soc-dummy-dai-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 2: imxhdmisoc [imx-hdmi-soc], device 0: i.MX HDMI Audio Tx hdmi-hifi-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0

 

 

DTS配置

dummy codec 驱动

RK系列SDK -- dummy codec虚拟声卡注册  https://blog.csdn.net/hb9312z/article/details/103315401

[RK3399][Android7.1] 调试笔记 --- 虚拟声卡驱动添加 https://blog.csdn.net/kris_fei/article/details/85237331

 

五、声卡和 PCM 设备的建立过程

ALSA表示高级Linux声音体系结构(Advanced Linux Sound Architecture)。它由一系列内核驱动,应用程序编译接口(API)以及支持Linux下声音的实用程序组成。这篇文章里,我将简单介绍 ALSA项目的基本框架以及它的软件组成。主要集中介绍PCM接口编程,包括您可以自动实践的程序示例。

ALSA体系结构

ALSA API可以分解成以下几个主要的接口: 
1 控制接口:提供管理声卡注册和请求可用设备的通用功能 
2 PCM接口:管理数字音频回放(playback)和录音(capture)的接口。本文后续总结重点放在这个接口上,因为它是开发数字音频程序最常用到的接口。 
3 Raw MIDI接口:支持MIDI(Musical Instrument Digital Interface),标准的电子乐器。这些API提供对声卡上MIDI总线的访问。这个原始接口基于MIDI事件工作,由程序员负责管理协议以及时间处理。 
4 定时器(Timer)接口:为同步音频事件提供对声卡上时间处理硬件的访问。 
5 时序器(Sequencer)接口 
6 混音器(Mixer)接口

声音缓存和数据传输

每个声卡都有一个硬件缓存区来保存记录下来的样本。当缓存区足够满时,声卡将产生一个中断。内核声卡驱动然后使用直接内存(DMA)访问通道将样本传送到内存中的应用程序缓存区。类似地,对于回放,任何应用程序使用DMA将自己的缓存区数据传送到声卡的硬件缓存区中。 
这样硬件缓存区是环缓存。也就是说当数据到达缓存区末尾时将重新回到缓存区的起始位置。ALSA维护一个指针来指向硬件缓存以及应用程序缓存区中数据操作的当前位置。从内核外部看,我们只对应用程序的缓存区感兴趣,所以本文只讨论应用程序缓存区。 
应用程序缓存区的大小可以通过ALSA库函数调用来控制。缓存区可以很大,一次传输操作可能会导致不可接受的延迟,我们把它称为延时(latency)。 为了解决这个问题,ALSA将缓存区拆分成一系列周期(period)(OSS/Free中叫片断fragments).ALSA以period为单元来 传送数据。 

一个缓存区分解成周期,然后是帧,然后是样本。

Over and Under Run

当一个声卡活动时,数据总是连续地在硬件缓存区和应用程序缓存区间传输。但是也有例外。在录音例子中,如果应用程序读取数据不够快,循环缓存区将会被新的 数据覆盖。这种数据的丢失被称为overrun.在回放例子中,如果应用程序写入数据到缓存区中的速度不够快,缓存区将会”饿死”。这样的错误被称 为”underrun”。在ALSA文档中,有时将这两种情形统称为”XRUN”。适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。

 

声卡和 PCM 设备的建立过程

声卡驱动中,一般挂载着多个逻辑设备,看看我们计算机的声卡驱动有几个逻辑设备:

$ cat /proc/asound/devices

嵌入式系统中,通常我们更关心 PCM 和 CTL 这两种设备。

设备节点如下: 

$ ll /dev/snd

 

Codec、Platform、Machine 驱动的组成部分及其注册过程,这三者都是物理设备相关的,频驱动的中间层,由于这些并不是真正的物理设备,故我们称之为逻辑设备。

PCM 逻辑设备,我们又习惯称之为 PCM 中间层或 pcm native,起着承上启下的作用:往上是与用户态接口的交互,实现音频数据在用户态和内核态之间的拷贝;往下是触发 codec、platform、machine 的操作函数,实现音频数据在dma_buffer <-> cpu_dai <-> codec 之间的传输。

 1.声卡结构概述

回顾下 ASoC 是如何注册声卡的,详细请参考章节 5. ASoC machine driver,这里仅简单陈述下:

  • Machine 驱动初始化时,.name = "soc-audio" 的 platform_device 与 platform_driver 匹配成功,触发soc_probe() 调用;
  • 继而调用 snd_soc_register_card(): 
    • 为每个音频物理链路找到对应的 codec、codec_dai、cpu_dai、platform 设备实例,完成 dai_link 的绑定;
    • 调用 snd_card_create() 创建声卡;
    • 依次回调 cpu_dai、codec、platform 的 probe() 函数,完成物理设备的初始化;
  • 随后调用 soc_new_pcm(): 
    • 设置 pcm native 中要使用的 pcm 操作函数,这些函数用于驱动音频物理设备,包括 machine、codec_dai、cpu_dai、platform;
    • 调用 snd_pcm_new() 创建 pcm 逻辑设备,回放子流和录制子流都在这里创建;
    • 回调 platform 驱动的 pcm_new(),完成音频 dma 设备初始化和 dma buffer 内存分配
  • 最后调用 snd_card_register() 注册声卡。

关于音频物理设备部分(Codec/Platform/Machine)不再累述,下面详细分析声卡和 PCM 逻辑设备的注册过程。

上面提到声卡驱动上挂着多个逻辑子设备,有 pcm 音频数据流、control 混音器、midi 迷笛、timer 定时器、sequencer 音序器等。

这些与声音相关的逻辑设备都在结构体 snd_card 管理之下可以说 snd_card 是 alsa 中最顶层的结构。我们再看看 alsa 声卡驱动的大致结构图(不是严格的 UML 类图,有结构体定义、模块关系、函数调用,方便标示结构模块的层次及关系):

snd_card

snd_cards:记录着所注册的声卡实例,每个声卡实例有着各自的逻辑设备,如 PCM 设备、CTL 设备、MIDI 设备等,并一一记录到 snd_card 的 devices 链表上 
snd_minors:记录着所有逻辑设备的上下文信息,它是声卡逻辑设备与系统调用 API 之间的桥梁;每个 snd_minor 在逻辑设备注册时被填充,在逻辑设备使用时就可以从该结构体中得到相应的信息(主要是系统调用函数集file_operations

2.声卡的创建

声卡实例通过函数 snd_card_create() 来创建,其函数原型:

  1. int snd_card_create(int idx, const char *xid,

  2. struct module *module, int extra_size,

  3. struct snd_card **card_ret)

注释非常详细,简单说下:

  • idx:声卡的编号,如为 -1,则由系统自动分配
  • xid:声卡标识符,如为 NULL,则以 snd_card 的 shortname 或 longname 代替
  • card_ret:返回所创建的声卡实例的指针

如下声卡信息:

$ cat /proc/asound/cards
 0 [PCH            ]: HDA-Intel - HDA Intel PCH
                      HDA Intel PCH at 0xf7c30000 irq 47

  • number:0
  • id:PCH
  • shortname:HDA Intel PCH
  • longname:HDA Intel PCH at 0xf7c30000 irq 47

 3.逻辑设备的创建

当声卡实例建立后,接着可以创建声卡下面的各个逻辑设备了。每个逻辑设备创建时,都会调用 snd_device_new() 生成一个 snd_device 实例,并把该实例挂到声卡 snd_card 的 devices 链表上。alsa 驱动为各种逻辑设备提供了创建接口,如下:

Device Interface
PCM snd_pcm_new()
CONTROL snd_ctl_create()
MIDI snd_rawmidi_new()
TIMER snd_timer_new()
SEQUENCER snd_seq_device_new()
JACK snd_jack_new()

4.声卡的注册

当声卡下的所有逻辑设备都已经准备就绪后,就可以调用 snd_card_register() 注册声卡了:

  • 创建声卡的 sysfs 设备;
  • 调用 snd_device_register_all() 注册所有挂在该声卡下的逻辑设备;
  • 建立 proc 信息文件和 sysfs 属性文件。

int snd_card_register(struct snd_card *card)     

 // 创建 sysfs 设备,声卡的 class 将会出现在 /sys/class/sound/ 下面

  1. if (!card->card_dev) {

  2. card->card_dev = device_create(sound_class, card->dev,

  3. MKDEV(0, 0), card,

  4. "card%i", card->number);

  5. // 遍历挂在该声卡的所有逻辑设备,回调各 snd_device 的 ops->dev_register() 完成各逻辑设备的注册

  6. if ((err = snd_device_register_all(card)) < 0)

       snd_cards[card->number] = card; // 把该声卡实例保存到 snd_cards 数组中

至此完成了声卡及声卡下的所有逻辑设备的注册,用户态可以通过系统调用来访问这些设备了。

 

5. PCM 设备的创建

最后我们简单描述下 PCM 设备的建立过程:

snd_soc_instantiate_card

snd_pcm_set_ops:设置 PCM 设备的操作接口,设置完成后,在 PCM 设备层即可访问操作底层音频物理设备。 
snd_pcm_new

  • 创建一个 PCM 设备实例 snd_pcm
  • 创建 playback stream 和 capture stream,旗下的 substream 也同时建立;
  • 调用 snd_device_new() 把 PCM 设备挂到声卡的 devices 链表上。

我们再看看 PCM 设备的系统调用:onst struct file_operations snd_pcm_f_ops[2] = {

snd_pcm_f_ops 作为 snd_register_device_for_dev() 的参数被传入,并被记录在 snd_minors[minor] 中的字段 f_ops 中。snd_pcm_f_ops[0] 是回放使用的系统调用接口,snd_pcm_f_ops[1] 是录制使用的系统调用接口。

 

6.Frame & Period

后面章节将分析 dma buffer 的管理,其中细节需要对音频数据相关概念有一定的了解。因此本章说明下音频数据中的几个重要概念:

  • Sample:样本长度,音频数据最基本的单位,常见的有 8 位和 16 位;
  • Channel:声道数,分为单声道 mono 和立体声 stereo;
  • Frame:帧,构成一个完整的声音单元,所谓的声音单元是指一个采样样本,Frame = Sample * channel
  • Rate:又称 sample rate,采样率,即每秒的采样次数,针对帧而言;
  • Period Size:周期,每次硬件中断处理音频数据的帧数,对于音频设备的数据读写,以此为单位;
  • Buffer Size:数据缓冲区大小,这里指 runtime 的 buffer size,而不是结构图 snd_pcm_hardware 中定义的 buffer_bytes_max;一般来说 buffer_size = period_size * period_count, period_count 相当于处理完一个 buffer 数据所需的硬件中断次数。

下面一张图直观的表示 buffer/period/frame/sample 之间的关系:

      read/write pointer
               |
               v
+-----------------------------+--------------+--------------+
|              |              |              |              |buffer = 4 periods
+--------------+--------------+--------------+--------------+
                     ^
                     |
               +---+---+------+
               |   |   |  ... |period = 1024 frames
               +---+---+------+
                     ^
                     |
                   +---+
                   |L|R|frame = 2 samples (left + right)
                   +---+
 
                        sample = 2 bytes (16bit)
 

这个 buffer 中有 4 个 period,每当 DMA 搬运完一个 period 的数据就会出生一次中断,因此搬运这个 buffer 中的数据将产生 4 次中断。ALSA 为什么这样做?因为数据缓存区可能很大,一次传输可能会导致不可接受的延迟;为了解决这个问题,alsa 把缓存区拆分成多个周期,以周期为单元传输数据。

Frames & Periods

敏感的读者会察觉到 period 和 buffer size 在 PCM 数据搬运中扮演着非常重要的角色下面引用两段来自 alsa 官网对 Period 的详细解释:Retrieved from “http://alsa.opensrc.org/Period”

 buffer_size = period_size * periods 
- period_bytes = period_size * bytes_per_frame 
- bytes_per_frame = channels * bytes_per_sample

简单说下 Frame 和 Period 要点:

  • Frame:帧,构成一个完整的声音单元,它的大小等于 sample_bits * channels;
  • Peroid:周期大小,即每次 dma 运输处理音频数据的帧数。如果周期大小设定得较大,则单次处理的数据较多,这意味着单位时间内硬件中断的次数较少,CPU 也就有更多时间处理其他任务,功耗也更低,但这样也带来一个显著的弊端——数据处理的时延会增大。

hrtimer 模拟 PCM 周期中断

在 4.2.1. pcm operations 章节中,我们提到:每次 dma 传输完成一个周期的数据传输后,都要调用 snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经传送到 FIFO 上了,然后再次调用 dma 传输音频数据…如此循环。

但有些 Platform 可能由于设计如此或设计缺陷,dma 传输完一个周期的数据不会产生硬件中断。这样系统如何知道什么时候传输完一个周期的数据了呢?我们提到 I2S 总线传输一个周期的数据所需的时间,这其实也是 dma 搬运一个周期的数据所需的时间,这很容易理解:I2S FIFO 消耗完一个周期的数据,dma 才接着搬运一个周期的数据到 I2S FIFO。

因此我们可以用定时器来模拟这种硬件中断:

  1. 触发dma搬运数据时,启动定时器开始计时;
  2. 当定时到 1 * period_size / sample_rate,这时 I2S 已传输完一个周期的音频数据了,进入定时器中断处理:调用snd_pcm_period_elapsed() 告知 pcm native 一个周期的数据已经处理完毕了,同时准备下一次的数据搬运;
  3. 继续执行步骤 1…

为了更好保证数据传输的实时性,建议采用高精度定时器 hrtimer。

两家芯片在传输音频数据时需要用定时器模拟周期中断,一是 MTK 的智能手机处理器,二是 Freescale 的 i.MX 系列处理器。后者已经合入 Linux 内核代码,具体见:sound/soc/imx/imx-pcm-fiq.c

// 定时器中断处理例程
static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)

{
    ...
 
    /* If we've transferred at least a period then report it and
     * reset our poll time */
    if (delta >= iprtd->period) {
        snd_pcm_period_elapsed(substream); // 告知 pcm native 一个周期的数据已经处理完毕
        iprtd->last_offset = iprtd->offset;
    }
 
    hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns)); // 重新计时,poll_time_ns:I2S 传输一个周期的数据所需的时间
    return HRTIMER_RESTART;
}
 
// hw_params 回调,数据传输开始前,先设置 dma 传输参数
static int snd_imx_pcm_hw_params(struct snd_pcm_substream *substream,
                struct snd_pcm_hw_params *params)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct imx_pcm_runtime_data *iprtd = runtime->private_data;
 
    iprtd->size = params_buffer_bytes(params);    // dma 缓冲区大小
    iprtd->periods = params_periods(params);      // 周期数
    iprtd->period = params_period_bytes(params) ; // 周期大小

    iprtd->offset = 0;
    iprtd->last_offset = 0;
    iprtd->poll_time_ns = 1000000000 / params_rate(params) *
                params_period_size(params);       // 计算 I2S 传输一个周期的数据所需的时间
    snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer); // 设置 dma 缓冲区
 
    return 0;
}
 
// trigger 回调,触发 dma 传输或停止
static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    struct imx_pcm_runtime_data *iprtd = runtime->private_data;
 
    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        atomic_set(&iprtd->running, 1);
        hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns), // 准备传输数据,启动定时器,开始计时
              HRTIMER_MODE_REL);
        if (++fiq_enable == 1)
            enable_fiq(imx_pcm_fiq); // 开始 dma 传输
 
        break;
 
    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        atomic_set(&iprtd->running, 0);
 
        if (--fiq_enable == 0)
            disable_fiq(imx_pcm_fiq); // 停止 dma 传输
 
        break;
    default:
        return -EINVAL;
    }
 
    return 0;
}
 

 

 

参考:

VoIP网络中虚拟声卡的研究及应用  http://www.docin.com/p-1406014438.html

linux 虚拟声卡 dummy 创建 http://cadenwu.blog.chinaunix.net/uid-20768928-id-5736361.html

ALSA(Advanced Linux Sound Architecture)声卡编程介绍及实例 https://blog.csdn.net/lell3538/article/details/62057159

Linux ALSA 音频系统:逻辑设备篇   https://blog.csdn.net/sunjing_/article/details/79145402

你可能感兴趣的:(音频)