Pure DataのPatchからDrumlogueのSynthユニットを作ってみた(3)MIDI入力で鳴らす

引き続き、Pure DataのパッチをhvccでCのコードに変換してDrumlogueの上で動かす実験です。前回も書きましたが、このCに変換したPure Dataのパッチを、以下では「Hvコンテキスト」と呼びます。

前回は、Drumlogueのノブから送ったパラメータをHvコンテキストに送って、音を変化させました。

MIDIのコントロール情報も、同じようにしてHvコンテキストに渡すことができますので、今回はそれをやってみます。

まず、元となるPure Dataのパッチはこちらです。

この記事で紹介したものと似ていますが、エンベロープ信号を生成している部分は、vline~がhvccではサポートされないので、line~を使用しています。

上から見ていきましょう。

noteinの出力の左側はノート番号、中央はベロシティ、右側はチャンネル番号です。チャンネル番号は今回使いません。ノートオフの場合は、ベロシティは0になります。

パッチの左半分では、ノート番号は周波数に変換されてオシレータに与えられます。オシレータで作ったサイン波はline~の出力と掛け算され(VCAに相当)、DACに送られます。

パッチの右半分、selectからline~までの部分はエンベロープ信号を作ります。line~の出力は、

・ノートONイベントの場合:selectの右側にベロシティを出力 → /がベロシティの1/127を出力 → line~がその値をそのまま出力

・ノートOFFイベントの場合:selectの左側にBangを出力 → メッセージボックスオブジェクトが[0 250]を出力 → line~が250msecかけて0へ減衰していく信号を出力

となります。これは、リリースタイムが250msecのエンベロープとなっています。(ベロシティに関わりなく250msecかけて減衰するので、普通のエンベロープとは違う動作ですが。)

以下、もう少し動作を詳しく書いておきます。

noteinのベロシティ出力がつながっているselectオブジェクトは、パラメータ(0)と入力が一致した場合には左側からBangメッセージを出力し、一致しない場合は入力をそのまま右側から出力します。Bangメッセージは、何も情報は与えませんが、メッセージを受け取ったオブジェクトはその性質に従った何らかの出力を行います。

selectで信号は2つに分岐しましたが、まず先に、その分岐した情報を両方受け取るline~の説明をしておきます。

line~のパラメータは最大3つですが、今回はselectの左側からは2つ、右側からは1つのパラメータを渡しています。これらのどちらかがline~に渡されます。selectでどちらか一方がキックされるので、両方同時に動作することはありません。

パラメータが2つの場合、1つ目が終了時の値、2つ目が「現在の値から終了時の値まで変化するのに要する所要時間」です。2つ目のパラメータは省略可能で、省略した場合は0とみなされます。つまり即座に終了時の値になるということです。

上のパッチでは、line~のパラメータは[0 250]という2つの数値か、[/ 127]の出力である1つの数値のいずれかです。

[0 250]は、line~に「現在の値から250msecかけて0になる」という信号を出力させます。
[/ 127]は、ベロシティを127で割ったものを出力します。従ってline~の1つ目のパラメータは1/127~1.0で、2つ目のパラメータは無し(0)です。

さて、このパッチをhvcc -n midiでC言語に変換する(-nオプションはコンテキストに付ける名前)と、出力されるファイルは以下のようになります。だいぶ多くのファイルが出力されていますが、メインはHeavy_midi.*です。

$ ls
HeavyContext.cpp           HvControlUnop.c    HvMessageQueue.c
HeavyContext.hpp           HvControlUnop.h    HvMessageQueue.h
HeavyContextInterface.hpp  HvControlVar.c     HvSignalLine.c
Heavy_midi.cpp             HvControlVar.h     HvSignalLine.h
Heavy_midi.h               HvHeavy.cpp        HvSignalPhasor.c
Heavy_midi.hpp             HvHeavy.h          HvSignalPhasor.h
HvControlBinop.c           HvHeavyInternal.h  HvSignalVar.c
HvControlBinop.h           HvLightPipe.c      HvSignalVar.h
HvControlCast.c            HvLightPipe.h      HvTable.c
HvControlCast.h            HvMath.h           HvTable.h
HvControlIf.c              HvMessage.c        HvUtils.c
HvControlIf.h              HvMessage.h        HvUtils.h
HvControlSlice.c           HvMessagePool.c
HvControlSlice.h           HvMessagePool.h

今回実装するものの概略は下図の通りです。

図の左側、Render()を実行する部分は実装済みです。

ノブは、今回はGateOn()が呼ばれたときのノート番号を指定するために使います。前回はパラメータをHvコンテキストに送信しましたが、今回は設定値としてSynthユニットの中で保持するだけで、Hvコンテキストには送信しません。

NoteOn/NoteOff、GateOn/GateOffの4つのイベントに対応するコードは、いずれもHvコンテキストのnoteinオブジェクトにメッセージを送信します。GateOn/GateOffでは、ノブで指定したノート番号を使います。

以下はlogue SDKでの実装の概略(ダミー実装からの差分のみ)です。

config.mkにはhvccで生成されたコードを追加します。また、コンパイルオプションとしてNEONを有効にします。

config.mk:

# C sources
CSRC = header.c Hv/HvControlBinop.c Hv/HvControlUnop.c Hv/HvMessagePool.c Hv/HvSignalVar.c Hv/HvControlCast.c Hv/HvControlVar.c Hv/HvMessageQueue.c Hv/HvTable.c Hv/HvControlIf.c Hv/HvLightPipe.c Hv/HvSignalLine.c Hv/HvUtils.c Hv/HvControlSlice.c Hv/HvMessage.c Hv/HvSignalPhasor.c

# C++ sources
CXXSRC = unit.cc Hv/HeavyContext.cpp Hv/Heavy_midi.cpp Hv/HvHeavy.cpp
UDEFS = -DHV_SIMD_NEON

header.cで使用するパラメータを宣言します。今回はノート番号だけです。

header.c:

        {0, 127, 60, 60, k_unit_param_type_midi_note, 0, 0, 0, {"Note"}},

synth.hに追加するのは、大きくは以下の2点です。

・ノート番号を保持するためにメンバ変数を1つ追加します。また、setParameter()とgetParameter()でこの変数を書き込み/読み出します。

・NoteOn()、NoteOff()、GateOn()、GateOff()でHvコンテキストにノートオン・ノートオフのメッセージを送信します。メッセージを受信するのはnoteinオブジェクトで、このオブジェクトのハッシュ値はあらかじめ決まっています

synth.h:

#include "Heavy_midi.h"
#define __hv_notein 0x67E37CA3
  inline int8_t Init(const unit_runtime_desc_t * desc) {
(略)
    hvContext_ = hv_midi_new(desc->samplerate);
    if (hv_getNumOutputChannels(hvContext_) != 2)
	return k_unit_err_geometry;

    return k_unit_err_none;
  }
  fast_inline void Render(float * out, size_t frames) {
    hv_processInlineInterleaved(hvContext_, NULL, out, frames);
  }
  inline void setParameter(uint8_t index, int32_t value) {
    (void)value;
    switch (index) {
      case 0:
	  note_ = value;
	break;
      default:
        break;
    }
  }

  inline int32_t getParameterValue(uint8_t index) const {
    switch (index) {
      case 0:
          return note_;
      default:
        break;
    }
    return 0;
  }
  inline void NoteOn(uint8_t note, uint8_t velocity) {
    hv_sendMessageToReceiverV(hvContext_, __hv_notein, 0, "fff", 1.f * note, 1.f * velocity, 1.f);
  }

  inline void NoteOff(uint8_t note) {
    hv_sendMessageToReceiverV(hvContext_, __hv_notein, 0, "fff", 1.f * note, 0.f , 1.f);
  }

  inline void GateOn(uint8_t velocity) {
    hv_sendMessageToReceiverV(hvContext_, __hv_notein, 0, "fff", 1.f * note_, 1.f * velocity, 1.f);
  }

  inline void GateOff() {
    hv_sendMessageToReceiverV(hvContext_, __hv_notein, 0, "fff", 1.f * note_, 0.f , 1.f);
  }
 private:
  /*===========================================================================\
*/
  /* Private Member Variables. */
  /*===========================================================================\
*/
  HeavyContextInterface* hvContext_;
  uint8_t note_;

上記2点目の、オブジェクトにメッセージを送信する関数ですが、前回はfloat型のパラメータを送るhv_sendFloatToReceiver()という関数を使っていました。今回は、1つのメッセージでMIDIノート番号とベロシティとMIDIチャネルという複数のパラメータを送っています。それにはhv_sendMessageToReceiverV()という関数を使います。この関数はprintf()関数などと同様に、不定長のメッセージを扱います。この関数のパラメータは、

第一引数:Hvコンテキスト
第二引数:レシーバのハッシュ値
第三引数:メッセージ送信までのディレイ(msec)
第四引数:フォーマット文字列
第五引数以降:実際に渡すパラメータ

となっています。

フォーマット文字列は、”f”(float型)、”s”(シンボル=文字列型)、”b”(bang)のいずれかが使えます。例えばフォーマット文字列が”sfb”なら、シンボル、float型、bangからなるメッセージが作られます。ちなみに、あらかじめメッセージを作成しておいて、それを送信することも可能です。その場合はhv_sendMessageToReceiver()関数を使います。

今回で、Pure Dataを使ってDrumlogueでSynthユニットを作成するための最低限のパーツは揃ったかと思います。

次回は、DrumlogueではなくNTS-1 mkII上で、Pure Dataのパッチ(から作ったHvコンテキスト)を動かしてみます。

コメント