前回までの流れ
今回の目標
簡単なオシレータつきのmono/polyシンセはexamplesにあるので、あえてサンプラーに挑戦します。やはり簡単なサンプラーがあるとエフェクト開発でもちょっとした音声流したり捗る気がします(サイン波は微妙)。雑な仕様は以下の通りです。
- 何個かプリセットのサンプルwavファイルを選べる
- MIDI鍵盤に応じて周波数がドレミ~に変わる
- とりあえず単一サンプルのみ (全音階で同じ, 440Hzを仮定)
- とりあえずループとかADSRエンヴェロープとかはなしだが、ノートがオンの間のみ再生/オフになると停止
VSTインストルメントの作り方
今回も既存のexampleベースでいきます
実装するメソッドはエフェクトのときとほぼ同じで、違うのはLegalIOが0in-1outくらいです。
サンプラーのアルゴリズム
今回は我流で考えたので雑です
- 元サンプル波形のピッチ周波数(p)は440Hzで準備されていると仮定
- 押された鍵盤MIDIノートのピッチに対応するサンプル周波数で伸長する
- 具体的には伸長先の周波数はmidiノート番号d->周波数fの式 $f=2^{(d-69)/12}p$ に従う
- 伸長したサンプルのリサンプリングはとりあえず線形補間で行う
ということでUI上のパラメタもなく、固定のサンプル波形WAVを鍵盤にあわせてピッチシフトするだけのインストルメントです。注意点として、WAVパスの指定が面倒でコンストラクタにハードコードしているので適宜書き換えてください。実装はいつも通りGitHubにあげました。
今回の見どころは、exampleに従ってprocessAudio内でmidiデータをよくあるキーボード押下などのイベント駆動っぽく処理しているところです。midiメッセージは小さくかなり高速に読めるようで、オーディオ処理のように各フレームごとにチャネルをまとめて処理するなどの気を使わなくて良いので、midiノートが鳴っているかisAVoicePlaying
で最初に分岐できて読みやすいですね。
override void processAudio(const(float*)[] inputs, float*[]outputs, int frames, TimeInfo info)
{
// 処理するmidiイベント(msg)を受け取る
foreach(msg; getNextMidiMessages(frames))
{
if (msg.isNoteOn())
{
_voiceStatus.markNoteOn(msg.noteNumber());
_sampleIndex[msg.noteNumber()] = 0;
}
else if (msg.isNoteOff())
_voiceStatus.markNoteOff(msg.noteNumber());
}
// 音符が鳴っていれば、対応する周波数にリサンプルした波形を書き出す
if (_voiceStatus.isAVoicePlaying)
{
auto note = _voiceStatus.lastNotePlayed;
auto rs = _resampled[note];
foreach(smp; 0..frames)
{
foreach (ch; 0.. outputs.length)
{
auto i = _sampleIndex[note];
outputs[ch][smp] = rs[ch][i % $];
}
++_sampleIndex[note];
}
}
else
{
outputs[0][0..frames] = 0;
}
}
この他に二つほどnogc, nothrowな便利モジュールを作りました。やはりgcなしで安全なコードを書くのは難しいですね、C++, Rustを書いてるときみたいにコピー禁止にしたりvectorを使っています。
- resampling: リサンプリング(線形補間)する関数のモジュール
- wav: RIFF形式のwav音源を読み込むモジュール
検証
サンプルは有名な効果音を使いました。Public Domainです。 https://archive.org/details/WilhelmScreamSample
サンプルWAVのピッチシフトも付けた(全然眠れない) pic.twitter.com/Nof76HeB4V
— カリテク (@kari_tech) September 24, 2018
次回
折角なので次は任意のサンプルを読み込むなど、とりあえず前回より凝ったGUIを作りたいと思います