ディストーションのDSPアルゴリズム


目的

オーディオ・プログラミングにおけるDSPがイマイチ定着していないので、その理解を深めるためにデータ信号処理のアルゴリズムをJUCEで実装してみようかと思いました。タイトルのとおり、エフェクターがディストーションです。エレキギターにかけて試してみました。

ソースはこちら

教材

このYouTubeの動画の手順に沿って実装しました。JUCEのバージョンは古いのでちょっとだけ調整が必要でした(下の方に詳しく書いています)が、肝心のSource/PluginProcessor.cpp内のprocessBlock()のコードは大体そのままで大丈夫でした。

アルゴリズム

動画で使われたアルゴリズム:

g(x) = (2/\pi)\arctan(x)

Desmosで書いてみる:

入力(x)が大きくなるにつれて、エフェクターが効くのがわかります。この関数について他人に聞きましたが、これに正弦波を通したら、波形が矩形波の形になっていき、周波数の倍音が上がります。他にディストーションのアルゴリズムが多くあり、これはその中の一つです。

アルゴリズムの実装

音源を処理する前に元のデータがcleanSigに格納されます。
つまみは4つあるので、各つまみの値が取得され、音に影響を与えます。
そのためにアルゴリズムの計算がここで上記の関数と比べてより長くなっています。

Source/PluginProcessor.cpp
...省略...
void DistortionTest2AudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();

    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());

    float drive = state.getRawParameterValue("drive")->load();
    float range = state.getRawParameterValue("range")->load();
    float blend = state.getRawParameterValue("blend")->load();
    float volume = state.getRawParameterValue("volume")->load();

    for (int channel = 0; channel < totalNumInputChannels; ++channel)
    {
        auto* channelData = buffer.getWritePointer (channel);

        for (int sample = 0; sample < buffer.getNumSamples(); sample++)
        {
            float cleanSig = *channelData;

            *channelData *= drive * range;

            // アルゴリズムはここで計算されます
            *channelData = (((((2.0f / float_Pi) * atan(*channelData)) * blend) + (cleanSig * (1.0f / blend))) / 2) * volume;

            channelData++;
        }
    }
}
...省略...

正直は僕にとって計算の部分はちょっと見にくかった(DSPのコードに慣れてないせい?)けど、ちゃんと動いていたのでいじりませんでした。

GUIはどう実装したかに興味ある人は、GitHubのリポジトリをご参照ください。

最新版で動作できるための調整

AudioValueTreeStateの新イニシャライザに合わせる

前は2つの引数がイニシャライザに渡される形(state(*this, nullptr)みたいな形)だったけど、今は4つの引数を渡すことになりました。
PluginProcessor.hで作ったインスタンス変数はstateですので、下記ではstate(第1引数...第4引数)になっています:

Source/PluginProcessor.cpp
...省略...
DistortionTest2AudioProcessor::DistortionTest2AudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", AudioChannelSet::stereo(), true)
                     #endif
                       ),
state(*this, nullptr, "Parameters",
      {
          std::make_unique<AudioParameterFloat> ("drive", "Drive", NormalisableRange<float> (0.0f, 1.0f, 0.0001f), 0),
          std::make_unique<AudioParameterFloat> ("range", "Range", NormalisableRange<float> (0.0f, 3000.0f, 0.0001f), 0),
          std::make_unique<AudioParameterFloat> ("blend", "Blend", NormalisableRange<float> (0.0f, 1.0f, 0.0001f), 0),
          std::make_unique<AudioParameterFloat> ("volume", "Volume", NormalisableRange<float> (0.0f, 3.0f, 0.0001f), 0),
      })
#endif
{
    state.state = ValueTree ("drive");
    state.state = ValueTree ("range");
    state.state = ValueTree ("blend");
    state.state = ValueTree ("volume");
}
...省略...

ScopedPointerのSliderを普通のインスタンス変数に

Source/PluginEditor.hではつまみを普通にSlider driveKnob;として定義しました。そのために、Source/PluginEditor.cppのコードも少し変わったけど、大きな変更はなかったのでここで割愛します。

結果

ちゃんと動いた!やっぱりこういうのを書いて実際に使ってみるのはテンションが上がります。が、音自体がそんなに良いとは思いませんでした。これから色んな関数を見て、僕にとって聴きやすい(カッコいい)音を見つけたいです。

他に試してみたい・見てみたいもの

Ivan Cohen - Fifty shades of distortion (ADC'17)