今回も、音声処理の部分はクラスが分かれていて複雑に見えます。
いくつかのオブジェクトが連携して発音しているからね~
JUCEプログラミング、Synthの「Build a MIDI synthesiser」のチュートリアルで音声処理の部分について、少し考察してみたいと思います。前回は、具体的に発音に関してプログラムをみて、言語で詳細を理解しようとしましたが、いまいちイメージがつかめていなかったので、さらに図として記載してみたいと思いました。だいぶ、Synthesiserクラスとその周りのクラスの連携具合についてイメージできるようになりました。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEの「Build a MIDI synthesiser」チュートリアルを進めている人
・JUCEでシンセを作成したい人
ノートオン~オフの処理の流れ
単一の音声の発音処理
音声処理の関数の実行順序などをDBGで追ってみたところ、次のような順番で実行されているようです。(図の上から下に向けて時間経過です。)
鍵盤(GUIのMIDIキーボード)を押したときに開始される関数が図の左の流れになります。オンとオフは対になるので、このようになっています。
SineWaveSoundオブジェクトとSineWaveVoiceオブジェクトの関数がSynthesiserオブジェクト「synth」から呼び出されてこのような順番で実行されます。その時に、SineWaveVoiceオブジェクトのprivateなメンバ「angleDelta」と「tailOff」がstartNote関数とstopNote関数の際に設定されます。
renderNextBlockはこの「angleDelta」と「tailOff」の値をみてその時の状況に応じたサイン波を生成するような仕組みになっています。
これは、単一の音だけなので、synthオブジェクトに登録されたSineWaveSoundオブジェクト数分の処理がそれぞれ行われます。
同時発音の処理
ここで改めて、SynthAudioSourceのgetNextAudioBlockを見ると、次のようになっています。
void getNextAudioBlock(const juce::AudioSourceChannelInfo& bufferToFill) override
{
//前回のバッファが残るので、クリアします。
bufferToFill.clearActiveBufferRegion();
//入力MIDIをスキャンして、Midiバッファに格納します。
juce::MidiBuffer incomingMidi;
keyboardState.processNextMidiBuffer(incomingMidi, bufferToFill.startSample,
bufferToFill.numSamples, true);
//発音数分の音声処理を行います。
synth.renderNextBlock(*bufferToFill.buffer, incomingMidi,
bufferToFill.startSample, bufferToFill.numSamples);
}
SynthAudioSourceクラスでSineWaveVoiceクラスのrenderNextBlockの実行が取りまとめられます。
processNextMidiBuffer関数で、Midiバッファにリアルタイムにノートオンとオフがスキャンされます。
synth.renderNextBlockでは、その時の音声ブロックの区間でのノートオンとノートオフに応じた処理が、最大、登録されたSineWaveVoiceのオブジェクトの音数まで、renderNextBlockによってサイン波が生成される、というイメージができました。
今の段階での処理のイメージの流れを図にまとめてみました。
左から右に時間が流れるとして、一番上が音声バッファ(1ブロック512サンプルとしています)です。次に、スキャンしたMIDIバッファ、最後に、準備した同時発音数分のSineWaveVoiceオブジェクトです。
Synthesiserクラスのオブジェクトの「renderNextBlock」関数で、これらの要素が紐づけられて、最大同時発音数分までのサイン波のデータ作成を統合しているようです。
JUCEはクラスの使い方に慣れていかないといけませんね~