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 = 0256 & 255 = 0,也就是说 2^N & (2^N - 1) = 0,且对于 2^N - 1 以外的其他数字,& 不改变其值,即2^N & x = x 。这和我们第一版实现中 write_index 的更新策略有异曲同工之妙。我们称 2^N - 1maskwrite_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++ 实现的主要内容,如果未能解决你的问题,请参考以下文章

音效处理Vibrato 简介

音频处理Channel Vocoder 算法简介

音频处理Channel Vocoder 算法简介

音效处理Compressor 压缩器算法简介

音效处理Compressor 压缩器算法简介

音效处理Reverb 混响算法简介