Delay Line 简介及其 C/C++ 实现
Posted 芥末的无奈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Delay Line 简介及其 C/C++ 实现相关的知识,希望对你有一定的参考价值。
文章目录
Delay Line 简介
在音频处理中,Delay Line 是基础功能组件,用于模拟声学音频传播延迟。它是延迟音效(包括 Delay、Vibrator、Phaser等)和音频 Synthesizer 中基本组成部分。Delay Line 的功能是在输入信号和输出信号之间引入一个时间延迟。
此外,有时候我们要对齐两路信号,其中一路有延迟,这时候也可以使用 Delay Line 来实现。
Delay Line 使用
一个简单的 Delay Line 可能包括如下接口:
template <typename T>
class DelayLine
public:
/**
* resize the delay line
*
* @note resize will clears the data in delay line
*/
void resize(size_t size) noexcept;
/**
* return the size of delay line
*/
size_t size() const noexcept;
/**
* push a value to delay line
*/
void push(T value) noexcept;
/**
* returns value with delay
*/
T get(size_t delay_in_samples) const noexcept;
;
使用方法如下:
DelayLine dline;
dline.resize(7);
for(int i = 0; i < 7; ++i)
dline.push(i); // 0,1,2,3,4,5,6
cout << dline.get(2) << endl; // 4
for
循环中将 0~6 数字送入 delay line 中,dline.get(2)
指获取延迟为 2 的数据,即数字 4。
当送入的数据超过了 Delay Line 的 size()
时,旧数据会被覆盖:
for(int i = 0; i < 10; ++i)
dline.push(i); // 7,8,9,3,4,5,6
cout << dline.get(2) << endl; // 7
Delay Line C/C++ 实现
符合直觉的实现
C/C++ 中常用数组来实现 Delay Line,push
的数据如果超过了 size()
那么旧数据被覆盖,因此 push
的实现从直觉来看可以这么实现:
void push(T value) noexcept
buffer[write_index] = T;
++write_index;
if(write_index >= size()) write_index = 0;
当前数据的下标为 write_index
,那么延迟为 N 的数据应该在 write_index - N
,那么 get
可以这么实现:
T get(size_t N) const noexcept
size_t read_index = write_index - N;
return buffer[read_index];
但,很不幸,这是错误的。因为有可能出现 write_index - delay_in_samples < 0
的情况,例如:
for(int i = 0; i < 10; ++i)
dline.push(i); // 7,8,9,3,4,5,6
// 此时 write_index = 2
dline.get(3); // write_index - 3 < 0;
因此需要考虑 write_index - delay_in_samples < 0
情况,一种简单实现可以是这样的:
T get(size_t N) const noexcept
N %= size();
size_t read_index = ((write_index + size()) - N) % size();
return buffer[read_index];
优化后的实现
上述实现中,我们使用了 %
取余运算,这非常符合编程的直觉,但 %
是一种计算耗时较高的运算,它的指令 CPU周期 可以达到加减法的 80 倍,且 Delay line 作为一个基础组件,在音频算法中是经常被频繁调用的。越是基础,我们越需要考虑它的性能。
为了实现高性能的 delay line,我们可以从硬件 DSP 中获取线索,硬件 DSP 中实现了循环数组,但要求数组长度是 2 的次方。
一个数是 2 的次方,那么它的二进制表示由一个 1 和若干 0 组成,例如 8 的二进制是 0000 1000
,而 256 的二进制是 0001 0000 0000
。而比数组长度少一的二进制它全由 1 组成, 例如 7。
8: 0000 1000
7: 0000 0111
256: 0001 0000 0000
255: 0000 1111 1111
对于数组敏感的你肯定发现了:8 & 7 = 0
,256 & 255 = 0
,也就是说 2^N & (2^N - 1) = 0
,且对于 2^N - 1
以外的其他数字,&
不改变其值,即2^N & x = x
。这和我们第一版实现中 write_index
的更新策略有异曲同工之妙。我们称 2^N - 1
为 mask,write_index
的更新策略可以这样:
int mask = 7;
++write_index;
write_index &= mask;
因此 push
函数的高性能版本实现是这样的:
void push(T value) noexcept
buffer[write_index] = T;
++write_index;
write_index &= mask;
有意思的是,这个机制对于 read_index
的更新策略同样适用:
int mask = 7;
int read_index = write_index - N;
read_index &= mask;
如果 write_index - N >= 0
,那么 read_index
是一个介于 0 和 write_index
之间的一个数,那么 &
不改变其值。
如果 write_index - N < 0
,那么 read_index
是个负数。计算机中,整数是用补码表示的,我们假设 read_index = -3
,那么他们的补码为:
readIndex (−3): 1111 1111 1111 1111 1111 1111 1111 1101
mask (7): 0000 0000 0000 0000 0000 0000 0000 0111
readIndex & mask 0000 0000 0000 0000 0000 0000 0000 0101
readIndex & mask = 5
这正是我们需要的值。以上,我们的 get
函数的高性能版本实现为:
T get(size_t N) const noexcept
size_t read_index = (write_index - N) & mask;
return buffer[read_index];
完整的 Delay Line 实现可以参考 Libaa - Delay Line。
总结
Delay Line 是音频处理中的基础组件,用于模拟声学传播延迟,在音效和合成器中经常被使用。它的通常用数组实现,在数组长度为 2 的次方时,Delay Line 可以有一种高性能的实现,详细实现可以参考 Libaa - Delay Line。
参考
以上是关于Delay Line 简介及其 C/C++ 实现的主要内容,如果未能解决你的问题,请参考以下文章