实时音频编程(一)

2023-10-30

系列文章目录


简介

初入音频坑时,对于"实时音频编程"并无基本认识,也从未意识到音频编程在某些场景下是”较为特殊的“,只是觉得写出来的代码没有 bug、没有内存泄露、接口易用就已经满足要求了,至于 real-time safe 是啥,根本没听过。直到开始接触音频播放系统,才开始慢慢接触到了 real-time 的概念,周边的同事也分享了更多关于实时音频编程的知识。

网络上对于实时音频编程的内容不多,主要有:

以上两份资料之前也只是草草看过,学习了一些基本概念,最近在 review 代码时发现 real-time 的概念在组内没有很好的普及,于是打算重新整理相关资料并总结成文并分享出来。本文的主要内容是上述两份资料总结,添加一些具体例子帮助理解。有兴趣的同学推荐观看原文。


实时系统

我们首先要搞清楚 real-time 的含义,它意味着什么。

A system is said to be real-time if the total correctness of an operation depends not only upon its logical correctness, but also upon the time in which it is performed.[1].
如果一个操作的全部正确性不仅取决于其逻辑正确性,而且还取决于执行该操作的时间,那么这个系统就被称为实时系统 – wiki

一个实时系统是指计算的正确性不仅取决于程序的逻辑正确性,也取决于结果产生的时间,如果系统的时间约束条件得不到满足,则认为系统失效 – 百度百科

根据定义,在实时系统下,系统的正确性 = 逻辑正确 + 满足时间要求。一个实时操作系统面对变化的负载(从最小到最坏的情况)时必须确定性地保证满足时间要求。请注意,必须要满足确定性,而不是要求速度足够快!就拿 windows pc 机来举例,机器空闲的状态下,pc 响应时间非常快,但是一旦后台程序变多,cpu 负载增加后,响应速速会大大降低,甚至出现卡死等情况,所以说 windows 无法满足确定性,它不是实时系统。


实时系统的分类

根据响应时间,以及错误响应时间所产生的影响,可以将实时系统分为三类:

  1. 强实时系统。强实时系统必须是对即时的事件作出反应,绝对不能错过事件处理时限。若有任务实例未在截止期限内完成,则会对系统造成不可估量的损失。例如测控领域就是要求强或接近强实时系统
  2. 准实时系统。允许系统偶尔超时,但这可能会降低系统的服务质量。但若任务超时,该任务的计算结果没有任何意义。
  3. 弱实时系统。通常是指允许任务超时,但超时后的计算结果仍有一定的意义,并且其意义随着超时时间的增加而下降。

实时音频系统

数字音频的工作原理是向声卡或音频接口的数模转换器(DAC)播放持续的音频样本流。这些样本是以一个恒定的速率播放的,称为采样率。对于CD播放器,采样率是44100Hz,也就是每秒44100个采样点。每一秒钟都是相同的速率,不快也不慢,每秒刚刚有 44100 个采样点。如果你的声卡在DAC需要时没有下一个样本,你的音频就会出现 glitch。

在常用的操作系统中,软件通常不会向 DAC 发送单个采样,它将向驱动或者操作系统提供一个音频缓存 buffer。例如,这个 buffer 大小为 256,那么它可能以 179.26Hz(44100/256) 的速率处理音频缓存 buffer。然后,系统的底层以 44100Hz 的速度将 buffer 里的采样一个一个的送入 DAC 中。

关于播放,我们举个实际的例子(完整代码在 0_playback.cpp)使用 PortAduio 进行音频播放。PortAudio 是一个简洁的跨平台的音频 I/O 库,目前支持 Windows、Mac OSX、Linux(很遗憾,不支持 Android)。它使用回调机制来处理音频请求。

PortAudio 只需要两步就能进行音频播放:

  1. 编写回调函数,在回调函数中将需要播放的数据填入 Buffer 中
  2. Pa_OpenStream 打开音频流,并注册回调函数。

在使用 Pa_OpenStream 时,我们需要指定音频 buffer 的大小,以上面 256 为例,我们的回调函数必须在不到 5.8ms(256/44100) 的时间内计算好每一个 buffer。无论你的代码被如何调用,它必须在限定的时间内提供这些音频采样点,否则,你就会听到刺耳的 glitch。我们在 1_playback_underrun.cpp 中模拟了这种情况,它回调函数中使用 sleep 使得无法在限定时间内给到 buffer,
这时候播放就出现了糟糕的杂音。

说个题外话,人耳对声音是非常敏感的,对于播放的音频,只要有一个是异常的,它都能分辨出来。例如 sine_glitch.wav,这是一个 sine 波,仅在 2s 处修改了一个采样的值,也就是这一个采样,你的耳朵明显能听出来这段音频是有瑕疵的。


什么会产生 glitch ?

人耳对 glitch 非常敏感,我们当然不希望出现 glitch,如果我们的缓冲 buffer 是 5ms 的话,那么代码必须在 5ms 内处理完音频。

glitch 产生的原因,归根结底就一个原因:代码处理时间比缓冲 buffer 还要长。 这可能是因为你的代码太慢了,无法实时运行,但我们的关注点并不在算法优化上。假设你的代码足够高效,可以实时运行,或者你有能力对它进行优化,使它足够快。

这里我们关注的是那些运行时间无法预测代码,也就是说你无法预测这个函数需要多久时间才能完成。或许是你选择的算法不合适,或者你不了解算法的行为。不管什么原因,结果都是一样的:你的代码运行时间比缓冲 buffer 要长,导致了 glitch。

因此,实时音频编程的基本规则可以简单地总结为:如果你不知道这要花多久时间,那就不要做。

那么有哪些操作会导致 glitch 呢?下面我们将详细的讨论它们。

阻塞

“不要做任何阻塞音频回调线程的事情”。做任何让你的音频代码在系统中等待其他东西的事情都是阻塞的。这可能是获取一个mutex,等待一个资源,如semaphore,等待其他线程或进程做一些事情,等待从磁盘上读取数据,等待一个网络套接字。很明显,其中一些等待时间并不明确,某些执行时间肯定比几毫秒长。下面我将更详细地讨论这些具体的阻塞类型。

记住,你不仅要避免直接编写阻塞的代码,关键是你要避免调用第三方或操作系统的代码,这些代码可能会在内部阻塞。

算法的最坏时间复杂度

假设一个理想的情况:音频回调中的每一行代码都是自己写的,没有调用任何可能阻塞 api 或第三方代码。

即便如此,你可能仍然有一个问题:软件效率通常是以平均时间复杂性来分析的。例如,在许多应用中,一个算法在99.9%的时间里运行得超快,但偶尔需要1000倍的时间,可能仍然被认为是"目前最快的算法"。如果你在音频回调中偶然发生了需要 1000 倍运行时间的情况,你可能会出现 glitch。出于这个原因,你应该总是考虑你的代码的最坏情况下的执行时间。

在这里需要记住的另一件事是,许多操作系统和库函数是使用平均情况下的优化算法实现的。在C++中,许多STL容器方法就属于这一类。通用的内存分配算法和垃圾回收器也有不可预测的时间行为,即使它们不使用锁。

当你的音频程序需要与 GUI 界面、网络后者磁盘 IO 进行交互时,很难避免并发。例如,你的 GUI 程序以某种形式控制着音频算法的某个参数,你需要在 GUI 线程与音频线程之间通信;或者你想要实时的绘制音频波形图。

你可能非常自然地会想到通过一个锁来保护两个线程的共享数据,但你不应该在音频回调中使用它们。这里有三个原因。

不使用锁的第一个原因:优先级倒置

假设你的 GUI 线程与音频回调线程共享一个锁,为了让音频回调及时处理,你需要等待 GUI 线程释放该锁。GUI 线程的优先级比音频回调线程优先级低得多,所以它可能被系统上任意进程打断,音频回调将不得不等待其他线程完成,直到 GUI 线程释放锁。这一过程中,虽然音频回调有着最高优先级,但它却不得不等待其他线程完成,这就是优先级倒置。

实时操作系统实现了特殊的机制来避免优先级倒置。例如,通过暂时将锁持有者的优先级提升到等待锁的最高线程的优先级。在Linux上,这可以通过使用带有RT preempt补丁的内核来实现。但是,如果你希望你的代码可以移植到所有的通用操作系统上,那么你就不能依赖实时操作系统的功能。

不使用锁的第二个原因:执行时间可能超时

使用锁可能会导致优先级倒置的问题,但如果你还是在考虑使用锁来同步数据,那么要注意一点:音频回调将不得不等待所有被锁保护的代码完成,然后才能继续。实际上,除了上下文切换的开销外,这些被保护的关键代码可能相当的复杂,你知道这些代码的执行时间吗?记住,我们这里讨论的是最坏时间复杂度。例如,在 C++ 中,你不会想这么做:

mutex.lock()。
my_data_vector.push_back( 10 ); // 可能分配内存,并且复制大量数据
mutex.unlock()。

如果 my_data_vector 是一个std::vector,当 vector 的内部存储空间已满时,调用push_back()将导致内存被分配,所有现有元素被复制到新的内存中。这显然会导致处理时间的激增。大多数非实时代码在某些时候会有这样的表现。看起来简单的代码很容易出现非确定性的时间行为。

不使用锁的第三个原因:复杂的线程调度器

除了优先级倒置和关键代码执行时间无法估计外,线程调度器的复杂性也是我们应该避免使用锁的原因之一。

很少有人确切知道调度器是如何实现的,不管是什么操作系统,调度器的实现可能随着每个操作系统的发布而改变。这些通用的操作系统调度器并不要求或保证表现出实时行为。它们没有被认证用于航空电子系统或医疗设备。没有政府或司法机构对它们的实时性进行问责。

作者在这方面的一般立场是,应该避免与操作系统的线程调度器进行任何形式的互动。避免在你的音频回调中调用任何同步函数。调度器采用了复杂多样的算法,你不希望给他们额外的理由来取消实时音频线程的调度。

内存分配

不要音频线程中分配内存,禁止 new、delete、malloc、free 等操作,或者任何分配内存的函数,以及任何可能调用这些函数的程序。原因有三:

  1. 内存分配器可能有锁,用于同步不同线程之间的数据。
  2. 内存分配器可能不得不向操作系统要更多的内存,操作系统可能有自己的锁,或者更糟糕的是,它决定从硬盘中获取内存,这导致你不得不等待更久。
  3. 内存分配器使用的算法需要的时间无法预测。

关于内存,有一些明确的解决方案

  1. 预先分配所有内存
  2. 只在非实时线程中执行动态分配
  3. 预先分配一大块内存,然后实现自己的动态内存分配器,只在音频线程中调用

等待硬件或”外部“时间

你可能没有直接写等待硬件的代码,但你可能会写磁盘 I/O 相关的代码,磁盘 I/O 需要等待磁盘头找到正确的问题,这可能需要一些时间(平均 8ms)。这意味着不能再音频回调中执行文件 I/O 操作。类似的规则也适用于其他任务,例如显卡上的垂直中断同步或者网络 I/O。正如前面所说,如果你不知道这要花多长时间,那么就不要做。


总结

总结全文,我们得出在实时音频回调中执行的代码的几条经验法则:

  1. 不要申请或者释放内存
  2. 不要使用锁
  3. 不要进行文件读写,或者其他方式的 I/O(这包括任何 print 或者 NSLog,或者 GUI API)
  4. 不要调用那些可能造成阻塞的系统 api
  5. 不要运行那些执行时间不确定,或者最坏时间复杂度有激增的代码
  6. 不要调用任何有上述行为的代码
  7. 不要调用任何你不信任的代码

在可能的情况下,有几件事你应该做:

  1. 使用最坏时间复杂度友好的算法
  2. 在许多音频采样中摊销计算,以平滑CPU的使用,而不是使用偶尔有长处理时间的 "突发 "算法。
  3. 在一个非实时线程中预先分配或预先计算数据
  4. 采用非共享的、仅在音频回调中使用的数据结构,这样你就不需要考虑共享、并发和锁的问题。

参考资料

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实时音频编程(一) 的相关文章

  • 音频处理——详解PCM数据格式

    目录 知识储备什么是PCM采样采样率重采样 量化编码PCM常用指标 PCM数据流 知识储备 音频处理 音频编码原理简介 音频处理 音频处理的基本概念 什么是PCM PCM全称Pulse Code Modulation xff0c 翻译一下是
  • 【C】借助DirectSound进行流的形式无缝播放的例子

    DirectSound是DirectX的一个组件 用于播放声音 BGM等 和DirectMusic不一样 DirectSound用于播放波形声音 WAV无损等 而不是midi音乐 通常大家使用DirectSound是直接把一个声波数据全部载
  • NVIDIA VIDEO CODEC SDK

    转自 https developer nvidia com nvidia video codec sdk NVIDIA GPU 硬件decoder和encoder是独立于cuda cores NVIDIA GPUs contain one
  • WAV文件格式详解

    概述 Waveform Audio File Format WAVE 又或者是因为WAV后缀而被大众所知的 它采用RIFF Resource Interchange File Format 文件格式结构 通常用来保存PCM格式的原始音频数据
  • HTML5实现音频和视频嵌入

    简介 HTML5未出来之前 在线的音频和视频都是借助Flash或者第三方工具实现的 现在HTML5也支持了这方面的功能 在一个支持HTML5的浏览器中 不需要安装任何插件就能播放音频和视频 原生的支持音频和视频 为HTML5注入了巨大的发展
  • 基于线性预测的语音编码原理解析

    早期的音频系统都是基于声音的模拟信号实现的 在声音的录制 编辑和播放过程中很容易引入各种噪声 从而导致信号的失真 随着信息技术的发展 数字信号处理技术在越来越多领域得到了应用 数字信号更是具备了易于存储和远距离传输 没有累积失真 抗干扰能力
  • C#音频采集 (笔记)

    using System using System Collections Generic using System Text using System IO using System Threading using Microsoft D
  • AMR文件格式的解释

    一 什么是AMR AMR WB 全称Adaptive Multi Rate和Adaptive Multi Rate Wideband 主要用于移动设备的音频 压缩比比较大 但相对其他的压缩格式质量比较差 由于多用于人声 通话 效果还是很不错
  • OGG流媒体文件格式分析

    摘自 http www studa net yingyong 080505 16283240 html 摘 要 流媒体文件格式在流媒体系统中占有重要地位 设计合理的文件格式是提高流媒体服务器工作效率最直接和最有效的办法 该文在剖析常用流媒体
  • 六、Audio-ALSA架构中的codec

    一 codec简介 处理器如果既想 听到 外界的声音 又想向外界传达自己的 心声 那么就需要同时用到 DAC 和 ADC 这两款芯片 那是不是买两颗 DAC 和 ADC 芯片就行了呢 答案肯定是可以的 但是音频不单单是能出声 能听到就行 我
  • javacv-ffmpeg播放视频里的音频

    在javacv封装的ffmpeg包中 可以通过FFMpegFrameGrabber捕获image数据和sample数据 而且捕获的都是译码后的 下面通过一个Demo来实现如何处理sample数据 1 首先实例化一个FFMpegFrameGr
  • 声音识别的 ImageNet 诞生了

    转自 https www zhihu com question 56816282 answer 150639596 谷歌机器感知研究小组 Machine Perception Research 最新发布了一个大规模的音频数据集AudioSe
  • Linux车机平台pulseaudio多alsasink配置

    https www freedesktop org wiki Software PulseAudio 官网上的介绍是这样的 pulseaudio 是一个POSIX操作系统上的声音系统 是音频应用的代理 它允许你对音频数据 在从应用传递到硬件
  • Java把V3音频文件转化为wav文件的算法的代码

    将写内容过程经常用到的内容段做个备份 如下内容内容是关于Java把V3音频文件转化为wav文件的算法的内容 import java io BufferedInputStream import java io BufferedOutputSt
  • 麦克输入

    如图所示 单端输入只有一个输入引脚ADCIN 使用公共地GND作为电路的返回端 ADC的采样值 ADCIN电压 GND的电压 0V 这种输入方式优点就是简单 缺点是如果vin受到干扰 由于GND电位始终是0V 所以最终ADC的采样值也会随着
  • 基于音频和文本的多模态语音情感识别(一篇极好的论文,值得一看哦!)

    基于音频和文本的多模态语音情感识别 语音情感识别是一项具有挑战性的任务 在构建性能良好的分类器时 广泛依赖于使用音频功能的模型 本文提出了一种新的深度双循环编码器模型 该模型同时利用文本数据和音频信号来更好地理解语音数据 由于情感对话是由声
  • LabVIEW 读写和缩放音频文件

    LabVIEW 提供了多种方式来读取和写入 WAV 格式的音频文件 完成本模块后 您将能够使用位于 Programming Graphics Sound Sound Files 中的 Simple Read 和 Simple Write 用
  • Android 14 CarAudioService

    文章目录 新功能 AudioMirring oemCarService 新功能 AudioMirring 简单的说就是两个bus输出的是同一个音频数据 构建的流程是 一个输入src的bus 和两个输出dst的bus 通过setParamte
  • FMOD Core API 指南

    目录 3 Core API 指南 3 1 什么是 Core API 3 2 链接的插件 3 2 1 静态 3 2 2 动态 3 3 API 功能
  • 免费音效素材网站,一次性介绍清楚

    不管是在游戏 电影 电视剧 短视频还是音频中 合适的音效能够更好的表达内容和渲染氛围 今天给大家分享几个免费音效素材 感兴趣的话可以接着往下看 一 制片帮素材 找音效 制片帮素材不仅有海量的优质视频素材 还有丰富的音效资源 分类清晰 更重要

随机推荐

  • python基础编程—综合实训

    一 实训要求 要求完成下列程序的功能 给出程序的源代码及运行效果截图 1 编写程序 根据下表提供的数据编写程序 实现快递计费系统 快递行业高速发展 我们邮寄物品变得方便快捷 某快递点提供华东地区 华南地区 华北地区的寄件服务 其中华东地区编
  • 1-linux初始化-hadoop

    系统初始化 1 网络配置 编辑文件 vi etc sysconfig network scripts ifcfg 网卡名 插入 IPADDR 192 168 200 10 GATEWAY 192 168 200 2 NETMASK 255
  • 最全idea java连接mysql数据库中文乱码解决方案

    相信不少人都遇到过中文乱码的问题 今天整理一篇解决乱码问题的文章分享给大家 中文乱码总逃脱不了编码格式以及匹配的问题 1 修改idea文件编码格式 idea File settings editor File Encodings 2 如果还
  • 第一批被AI淘汰的人已经出现,你距离失业还有多远?

    在我之前的文章有提到过 当chat GPT横空出世之后 AI必然会迎来大爆发 在chat GPT推出后 百度随即推出文心一言 而各个互联网大厂也纷纷入局 这将会导致一大批人失业 为什么各个互联网大厂会纷纷入局 他们不是人傻钱多 而是看到其背
  • 【Redis】主从复制和哨兵模式

    主从复制 主从复制 主机数据更新后根据配置和策略 自动同步到备机的master slaver机制 Master以写为主 Slave以读为主 作用 读写分离 性能扩展 容灾快速恢复 主从复制的配置 在我的 myredis目录中 存在redis
  • 太阳能路灯实际功率怎么计算?

    太阳能路灯实际功率怎么计算 李老师给朋友们分享测量法和计算法两个方式来得出太阳能路灯实际功率 一 最简单的就是太阳能路灯正常亮灯的时候用钳流表测试控制器负载输出端的电流和电压 两者相乘 得到实际输出功率 直流钳流表 二 通过太阳能板功率和锂
  • 2021-11-05 Leetcood 160 每日一题

    R星校长 已知两个单链表的头节点 headA 和 headB 请你找出并返回两个单链表相交的起始节点 如果两个链表没有交点 返回 null 杰哥我用白话翻译过来 链表A和链表B相交于C节点 我们返回C节点的地址 如果两个链表之间没有交点 程
  • jmeter性能测试常用插件简介

    jmeter性能测试常用插件简介 jmeter作为一个开源的接口性能测试工具 相对于商业性软件loadrunner来说 优点是更加灵活方便 操作简单 但相对来说 专业性和对性能参数的分析相对比较薄弱 但是通过第三方插件 就能将一些重要的参数
  • OpenCV 的浅拷贝和深拷贝,千万不要踩坑了!

    最近在使用 OpenCV 进行图像处理时 遇到一个小坑 这里总结一下 希望能对你有帮助 将一个变量赋给另一个变量 直接使用 符号即可 不过在 OpenCV 中就会遇到问题 Mat a b a b 都为 Mat 类型 这类拷贝方法是浅拷贝 没
  • 矩阵的逆以及实际应用

    矩阵逆的实际应用 最常见的是解线性方程组Ax b 如果A可逆 则解是x A 1 b 这类例子太多了 例如 小花 小明钱包里面各有几百元 小花金额的2倍和小明的金额 加起来是300元 小花金额的10倍比小明的金额对9倍 还多100元 则联立方
  • C语言之:数组的定义和初始化必备练习题

    1 作业标题 688 关于一维数组初始化 下面哪个定义是错误的 作业内容 A int arr 10 1 2 3 4 5 6 B int arr 1 2 3 4 5 6 C int arr 1 2 3 4 5 6 D int arr 10 0
  • Centos7安装Python2.7

    1 删除现有Python root test rpm qa grep python xargs rpm ev allmatches nodeps 强制删除已安装程序及其关联 root test whereis python xargs rm
  • 若依系统去redis

    1 注释 application yml文件中关于redis中所有的代码 2 修改framework模块下 RedisConfig该文件 删除也可以 3 在common模块中新建MyCache继承Cache类 package com xxx
  • 使用html+js书写分页功能

  • 【华为OD机试】拼接URL(C++ Python Java)2023 B卷

    时间限制 C C 1秒 其他语言 2秒 空间限制 C C 262144K 其他语言524288K 64bit IO Format lld 题目描述 给定一个URL前缀和URL后缀 通过 分割 需要将其连接为一个完整的URL 如果前缀结尾和后
  • Transformer背景介绍

    目录 Transformer的诞生 Transformer的优势 Transformer的市场 Transformer的诞生 论文地址 Transformer的优势 Transformer的市场
  • 解决el-table大数据表格卡顿问题

    解决el table大数据表格卡顿问题 使用umyui中的表格组件 使用ux grid组件 记录滚动条位置 使用umyui中的表格组件 umyui官网 当u table元素中注入data对象数组后 添加use virtual属性开启虚拟 同
  • MySQL基础(一)SQL基础

    DDL Data Definition Languages 数据定义语言 常用关键字包括create drop alter等 0 连接数据库 mysql u root pconnection id 表示的是连接次数1 创建数据库 CREAT
  • Android 应用安装成功之后删除apk文件

    问题 在应用开发中遇到需要这样的需求 在用户下载我们的应用安装之后删除安装包 解决 android会在每个外界操作APK的动作之后发出系统级别的广播 过滤器名称 android intent action package ADDED and
  • 实时音频编程(一)

    系列文章目录 实时音频编程 一 实时音频编程 二 实践与技巧 文章目录 系列文章目录 简介 实时系统 实时系统的分类 实时音频系统 什么会产生 glitch 阻塞 算法的最坏时间复杂度 锁 不使用锁的第一个原因 优先级倒置 不使用锁的第二个