VST
JUCE
DSP
JUCEDay 15

JUCEのSynthesiserクラスを使ってVSTiを作る

JUCEでVSTiを作る

JUCEを使えば簡単にVSTを作れますがVSTi(所謂ソフトシンセ)も比較的簡単に作ることができます。

ソフトシンセの場合、同時発音とか自力で実装しようとすると多少めんどくさい問題が出てきますが、JUCEの場合Synthesiserというその辺の機能を使いやすくしてくれているクラスがあります。
今回はそのSynthesiserクラスを使ったサイン波を鳴らす簡単なシンセを実装しました。

Synthesiserクラス

SyhthesiserクラスはVSTiなどシンセサイザープラグインの作成に特化したクラスです。
SyhthesiserクラスはSynthesiserSoundクラスとSynthesiserVoiceクラスを使うことでソフトシンセサイザーを実現します。
ここら辺は@COx2さんが下記の記事で解説して下さっているので割愛します。

https://qiita.com/COx2/items/e50e8f29bea633c6e5b0#c-1-%E8%A7%A3%E8%AA%ACjucesynthesiser%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

今回実装したシンセ

今回実装したシンセは以下のようなものです。
・ノートオンを受け取るとそのピッチのサイン波を生成し、10ミリ秒で最大音量になる。
・ノートオフを受け取ると100ミリ秒かけて音が消える。
・同時発音数8音
全体のソースコードはここにあります。

実際に書く部分

今回はデフォルトのコードに加えてMySynth.hとMySynth.cppというファイルを作成しました。
そこにSynthsiserSoundクラス、SynthsiserVoiceクラス、Synthsiserクラスの三つをまとめて記述しています。
今回のような波形生成部分を自分で作るタイプのシンセの場合書くコードのほとんどはSynthsiserVoiceクラスになるので他二つのクラスについては

MySynthSound::MySynthSound()    {}
MySynthSound::~MySynthSound()   {}
MySynth::MySynth(AudioProcessor &p)
{
    clearVoices();
    for (int i = 0; i < 8; i++)
    {
        addVoice(new MySynthVoice());
    }
    clearSounds();
    addSound(new MySynthSound());
}
MySynth::~MySynth() {}

というようにほとんど空の状態となります。

主に実装した部分

自分で音の生成部分まで実装する場合、編集する部分はほとんどSynthesiserVoiceクラスのrenderNextBlock関数です。
今回のSynthesiserVoiceクラスは以下のようになっています。

MySynth.h
class MySynthVoice : public SynthesiserVoice
{
public:
    MySynthVoice();
    ~MySynthVoice();
    bool canPlaySound(SynthesiserSound* sound) override;
    void startNote(int midiNoteNum, float velocity,
        SynthesiserSound* sound, int /*currentPitchWheelPos*/) override;
    void stopNote(float /*velocity*/, bool allowTailOff) override;
    void pitchWheelMoved(int /*newVal*/) override {}
    void controllerMoved(int /*controllerNum*/, int /*newVal*/) override {}
    void renderNextBlock(AudioBuffer<float> &outputBuffer, int startSample, int numSamples) override;

private:
    double phase;
    double delta;
    double attack;
    double release;
    float maxLevel;
    float currentLevel;
    bool bRelease;
};
MySynth.cpp
MySynthVoice::MySynthVoice()
{
    phase = 0;
    delta = 0;
    maxLevel = 0;
    attack = 0;
    release = 0;
    currentLevel = 0;
    bRelease = false;
}

MySynthVoice::~MySynthVoice()   {}

bool MySynthVoice::canPlaySound(SynthesiserSound *sound)
{
    return dynamic_cast<MySynthSound*> (sound) != nullptr;
}

void MySynthVoice::startNote(int midiNoteNum, float velocity,
    SynthesiserSound* sound, int /*currentPitchWheelPos*/)
{
    phase = 0;
    delta = (double)(2.0 * double_Pi * MidiMessage::getMidiNoteInHertz(midiNoteNum) / getSampleRate());
    maxLevel = velocity * 0.15;
    attack = maxLevel / (getSampleRate() / 100.0);
    release = maxLevel / (getSampleRate() / 10.0);
    currentLevel = 0;
    bRelease = false;
}

void MySynthVoice::stopNote(float /*velocity*/, bool allowTailOff)
{
    if (allowTailOff)
    {
        bRelease = true;
    }
    else
    {
        bRelease = true;
        clearCurrentNote();
    }
}

void MySynthVoice::renderNextBlock(AudioBuffer<float> &outputBuffer, int startSample, int numSamples)
{
    for (int i = startSample; i < numSamples + startSample; i++)
    {
        if (currentLevel < maxLevel && !bRelease)
        {
            currentLevel += attack;
        }
        else if (bRelease && currentLevel > 0)
        {
            currentLevel -= release;
        }
        float currentSample = (float) (sin(phase) * currentLevel);
        for (int j = 0; j < outputBuffer.getNumChannels(); j++)
        {
            outputBuffer.addSample(j, i, currentSample);
        }
        phase = fmod(phase + delta, 2.0 * double_Pi);
    }
}

ノート音信号が送られてきたときstartNote関数が実行されます。ここでmidiノート番号やvelocityを受け取れるのでそこから周波数などを決めます。
そしてrenderNextBlock関数内で波形の生成計算やエンベロープなどの計算をします。
最後にノートオフ信号が来た時にstopNote関数が実行されます。ここで音を止めてしまってもいいのですが、それだとぶつ切りになってしまうので今回はrenderNextBlock関数内で100ミリ秒かけて音量をさげるようにしています。

まとめ

今回紹介したもの以外にもいくつか自分でシンセを実装してみましたが、めんどくさい箇所をかなり省略できるので便利でした。自分で作ったプラグインのみで曲を作るのはMaxMspなどでの曲作りともまた違った楽しみを感じたので皆さんもぜひ試してみてください。