Delay line 延遲線
今天我們將討論 Delay 和 Vibrato 兩種音頻特效的技術原理和實現細節。
Delay 和 Vibrato 都是基於 Delay line 實現的。Delay line 作爲音頻特效中重要的基礎組件,它很容易實現,並且稍作修改就能夠應用實現於不同的音效。
Delay line 非常簡單,它能功能是將一個信號進行延遲。通過使用多條 delay line,並加以不同的信號延遲,然後將這些信號相加在一起,我們就能夠創建大量的音頻特效。
在模擬信號中,delay line 的實現相當複雜,需要引入物理擴展(例如彈簧)來延遲波的傳播。
在數字信號中,delay line 通常使用 ”循環緩衝區” 的數據結構來實現延遲。循環緩衝區本質上可以用一個數組實現,用一個索引來指向下一個存放信號的位置,當索引超過緩衝區大小時,將其重新置於開始位置。這樣一來,就像往一個圈裏順時針填數據,當我們需要延遲信號時,計算逆時針回退的個數即可。
下面是 delay line 的一種實現,更多細節大家可以參看代碼
template <typename T>
class DelayLine
{
public:
void clear() noexcept
{
std::fill(raw_data_.begin(), raw_data_.end(), T(0));
}
/**
* return the size of delay line
*/
size_t size() const noexcept
{
return raw_data_.size();
}
/**
* resize the delay line
*
* @note resize will clears the data in delay line
*/
void resize(size_t size) noexcept
{
raw_data_.resize(size);
least_recent_index_ = 0;
clear();
}
/**
* push a value to delay line
*/
void push(T value) noexcept
{
raw_data_[least_recent_index_] = value;
least_recent_index_ = (least_recent_index_ == 0) ? (size() - 1):(least_recent_index_ - 1);
}
/**
* returns the last value
*/
T back() const noexcept
{
return raw_data_[(least_recent_index_ + 1) % size()];
}
/**
* returns value with delay
*/
T get(size_t delay_in_samples) const noexcept
{
return raw_data_[(least_recent_index_ + 1 + delay_in_samples) % size()];
}
/**
* Returns interpolation value
*/
T getInterpolation(float delay) const noexcept
{
int previous_sample = static_cast<int>(std::floorf(delay));
int next_sample = static_cast<int>(std::ceilf(delay));
float fraction = static_cast<float>(next_sample) - delay;
return fraction*get(previous_sample) + (1.0f-fraction)*get(next_sample);
}
/**
* set value in specific delay
*/
void set(size_t delay_in_samples, T new_val) noexcept
{
raw_data_[(least_recent_index_ + 1 + delay_in_samples) % size()] = new_val;
}
private:
size_t least_recent_index_{0};
std::vector<T> raw_data_;
};
Delay 延遲
Delay 音效非常簡單,但應用非常廣泛。最簡單的情況下,將聲音進行延遲並與原始信號相加就可以使的樂器的聲音更加生動活潑,或者用更長時間的延遲,來達到二重奏的效果。很多熟悉的音效(例如 Chorus、Flanger、Vibrato 和 Reverb)也是基於 Delay 實現的。
Basic Delay 基本延遲
Basic delay 會在指定延遲時間後播放音頻。根據應用的不同,延遲時間可能從幾毫秒到幾秒,甚至更長。這裏是一小段 basic delay 算法結果。Basic delay 通常也被稱爲 Echo(回聲)效果。
Basic delay 算法原理部分很簡單,通常是將帶有延遲的信號與原始信號相加。其中 表示輸出信號,表示原始信號,表示延遲(單位是採樣個數),表示延遲信號的增益
利用 Z 變換,得到傳遞函數爲:
因爲傳遞函數 的所有極點都在單位圓內,因此 basic delay 在所有情況下都是穩定的。
Dalay with Feedback 反饋延遲
Basic delay 使用場景比較受限,因爲它僅僅產生單個回聲。大多數音頻延遲單元還具有反饋控制,它能夠將延遲輸出的信號再發送到輸入,如下圖。反饋使聲音不斷重複,如果反饋增益小於 1,那麼每次回聲都會變得更加安靜。從理論上講,回聲將永遠重複,但它們最終會變得非常安靜,以至於你無法聽到。
根據上圖,我們可以寫出反饋延遲的差分方程:
然後可以轉換爲只與 和 相關差分方程:
計算其傳遞方程:
因此,系統的極點在 ,這就說明當 時系統是穩定的。這個結果符合直覺,因爲只有反饋增益小於1時,回聲纔會隨着時間越來越小。
這裏是反饋延遲的算法輸出。可以聽到反饋延遲效果很像我們在大山裏喊叫的效果,比起 basic delay,反饋延遲有多次回聲,每次回聲音量逐漸變小。
反饋延遲的實現大致如下,通過 delay line 來獲取延遲信號,並且往 delay line 中記錄帶有反饋的信號。如果 feedback
爲0,那麼反饋延遲將退化爲 basic delay。
void DelayEffect::processBlock(AudioBuffer<float> &buffer)
{
const int num_channels = buffer.getNumChannels();
const int num_samples = buffer.getNumSamples();
for(int c = 0; c < num_channels; ++c)
{
float* channel_data = buffer.getWritePointer(c);
auto& dline = dlines_[c];
size_t delay_samples = delay_length_in_sample_[c];
for(int i = 0; i < num_samples; ++i)
{
const float in = channel_data[i];
const float delay_val = dline.get(delay_samples);
float out = 0.0f;
out = (dry_mix_ * in + wet_mix_ * delay_val);
dline.push( in + feedback_ * delay_val);
channel_data[i] = out;
}
}
}
Vibrato 顫音
顫音指的是音調週期性微小變化。傳統意義上,顫音並不是音效效果,而是歌手和樂器演奏者使用的一種技術。例如在小提琴上,通過在指板上有節奏地前後搖動手指,稍微改變琴絃的長度來產生顫音。但是在音頻信號中,我們可以使用調製的 delay line 來實現顫音。
前面提到的兩種延遲算法,它們的延遲長度是固定,不隨時間變化的那種。顫音與它們最大的不同在於其延遲長度隨着時間變化而變化,而這種變化會導致音調的變化,這裏我們舉個例子來說明,假設:
這時候 :
也就是說,原來信號頻率提升了 倍。如果 那就是降低了頻率
頻率的變化和 無關,只和 有關
Low-Frequency Oscillator 低頻振盪器
爲了達到顫音的效果,我們需要模擬那種音調週期性變化的感覺,而延遲長度的變化會引起音調變化,因此如果我們讓延遲長度發生週期性變化,那麼音調也是週期性變化的。
爲了延遲的長度週期性變化,我們可以用一個低頻振盪器(Low-Frequency Oscillator; LFO)來控制它,以正弦 LFO 爲例,公式爲:
其中,調節變換的範圍;是 LFO 的頻率,影響音調的變化週期;表示採樣率。音調的變化可以計算爲:
因此音調的變化是個週期函數,一會上一會下的。
這裏是顫音算法的輸出結果,顫音應用到人聲上會產生一種滑稽的效果,挺有趣的。
顫音的實現大致如下,通過 lfo 來得到延遲的長度,根據長度從 delay line 中獲取數據。有一點與之前不同的是,當延遲長度不是整數時,我們採用了插值的方法,這樣可以讓信號更加平滑。
void VibratoEffect::processBlock(AudioBuffer<float> &buffer) {
const int num_channels = buffer.getNumChannels();
const int num_samples = buffer.getNumSamples();
float phase = 0.0f;
assert(num_channels <= dlines_.size());
for(int c = 0; c < num_channels; ++c)
{
phase = phase_;
float* channel_data = buffer.getWritePointer(c);
auto& dline = dlines_[c];
for(int i = 0; i < num_samples; ++i)
{
const float in = channel_data[i];
// get delay from lfo
float delay_second = sweep_width*lfo_.lfo(phase, LFO::WaveformType::kWaveformSine);
float delay_sample = delay_second * getSampleRate();
// get interpolation delay value
channel_data[i] = dline.getInterpolation(delay_sample);
// push input to delay line
dline.push(in);
// update phase
phase += lfo_freq*invert_sample_rate_;
if(phase >= 1.0f)
{
phase -= 1.0f;
}
}
}
phase_ = phase;
}
總結
以上介紹了延遲和顫音兩種音效,並給出了詳細實現,它們都是基於 delay line 實現的,簡單卻又有用。