Electro-smith Daisy seedでリバーブをつくる

5 min read読了の目安(約4500字

Daisyとは?

オーディオ処理に特化したArduino、みたいなやつです。以前にnoteに紹介記事を書いたので興味ある方はこちらもご参照ください。

https://note.com/yutannihilation/n/nd837ab844c2b

Daisy seedと他のボードの違う点

Daisyシリーズには、今回使うSeed以外にPod/Patch/Petalなどがあります。

https://www.electro-smith.com/daisy

Seedがほかと違うのは、ツマミやボタンなどの入出力は自分で用意する必要があるという点です。Pod/Patch/Petalにはすでにツマミやボタンがついているので、その辺はライブラリで抽象化されていて簡単にアクセスできるようになっています。サンプルコードはこれを前提にしているものが多く、Seedで同じことをしようとするときに少し手こずりました。

Parameter

たとえば、DaisyにはParameterというクラスがあります。ADCからの入力を、最大値・最小値・カーブの形状(線形、指数、ログなど)を指定して値にマッピングできるという便利なユーティリティです。サンプルコードを見ると、

lpParam.Init(patch.controls[3], 20, 20000, Parameter::LOGARITHMIC);

(https://github.com/electro-smith/DaisyExamples/blob/c763348d79a83afa53d0176cd4a616dc8d098ad5/patch/verb/ex_verb.cpp#L63)

みたいな感じで指定されていて、patch.controls[3]というのはADCを表すなにかのようです。Patchの場合はこれでいいんですが、Seedの場合はこの「ADCを表すなにか」を扱うために多少コードを書く必要があります。

具体的には。ParameterInit()はこういうシグネチャになっています。

void daisy::Parameter::Init(
  AnalogControl input,
  float         min,
  float         max,
  Curve         curve
)

(https://electro-smith.github.io/libDaisy/classdaisy_1_1_parameter.html#a0dc0293e425569511a73c311bfa54f48)

で、AnalogControlはどうやってつくるかというと、こういうInit()になっています。

void daisy::AnalogControl::Init(
  uint16_t * adcptr,
  float      sr,
  bool       flip = false,
  bool       invert = false,
  float      slew_seconds = 0.002f 
)

(https://electro-smith.github.io/libDaisy/classdaisy_1_1_analog_control.html#a516499f0788d7405024eff550386a59e)

このadcptrの説明を読むと、

is a pointer to the raw adc read value – This can be acquired with dsy_adc_get_rawptr(), or dsy_adc_get_mux_rawptr()

とか書かれてるんですが、実際にはdsy_adc_get_rawptr()は公開されていない関数なので使えません...。

いったいどうすれば??、と混乱したんですが、Patchをラップしているライブラリの中身を覗くとこうなってました。seed.adc.GetPtr()という関数が使えるようです。

controls[i].Init(seed.adc.GetPtr(i), AudioCallbackRate(), true);

(https://github.com/electro-smith/libDaisy/blob/831f3820808f7718c55d6c844489d17bc20c2086/src/daisy_patch.cpp#L225)

これを使うと、こんな感じのコードになります。15〜18ピンにツマミを接続することを想定しています。

#define PIN_REVERB_CONTROL1   15
#define PIN_REVERB_CONTROL2   16
#define PIN_REVERB_CONTROL3   17
#define PIN_REVERB_CONTROL4   18

static DaisySeed seed;

// ADCの設定。配列の長さは必要なツマミの数と同じ。
AdcChannelConfig adcConfig[4];

// ツマミを接続するピンをそれぞれ指定する
adcConfig[0].InitSingle(seed.GetPin(PIN_REVERB_CONTROL1));
adcConfig[1].InitSingle(seed.GetPin(PIN_REVERB_CONTROL2));
adcConfig[2].InitSingle(seed.GetPin(PIN_REVERB_CONTROL3));
adcConfig[3].InitSingle(seed.GetPin(PIN_REVERB_CONTROL4));

// ADCを初期化(4は使うADCの数)
seed.adc.Init(adcConfig, 4);

AnalogControl verb_control1, verb_control2, verb_control3, verb_control4;
Parameter verb_feedback, verb_lp_freq, verb_mix, verb_send;

// 各ADCをParameterに割り当てる
verb_control1.Init(seed.adc.GetPtr(0), sample_rate);
verb_feedback.Init(verb_control1, 0.f, 0.99f, Parameter::LINEAR);

verb_control2.Init(seed.adc.GetPtr(1), sample_rate);
verb_lp_freq.Init(verb_control2, 0.f, 20000.0f, Parameter::EXPONENTIAL);

verb_control3.Init(seed.adc.GetPtr(2), sample_rate);
verb_mix.Init(verb_control3, 0.f, 1.0f, Parameter::LINEAR);

verb_control4.Init(seed.adc.GetPtr(3), sample_rate);
verb_send.Init(verb_control4, 0.f, 1.0f, Parameter::LINEAR);

seed.adc.Start();

こうしてParameterでラップしておくと、ADCの値をリバーブのパラメータとして使う時に、自分で値をマップしなくても、<Parameter>.Process()でマップ済みの値が得られます。

RevebSc

リバーブ用にはReverbScというクラスが用意されています。フィードバックの量と、フィードバックにかけるローパスフィルタのカットオフ周波数、という2つのパラメータを持っています。センドリターンやドライウェットといったところは自分で実装する必要があります。(pre-delayの扱いはちょっとよくわかりませんでした。内部的にはもう少しいくつかパラメータがありそうなので、そのへんを改造すればいいのかもしれません)。https://github.com/electro-smith/DaisyExamples/blob/master/patch/verb/ex_verb.cpp#L63を参考に、こういう感じのコードになります。

ReverbSc verb;

static void AudioCallback(float *in, float *out, size_t size) {
  float sig, sig_tmp, dry_rate, send_rate, wet1, wet2;

  for (size_t i = 0; i < size; i += 2) {
    sig = in[i];

    // dry/wet、sendの量を決める
    dry_rate = verb_mix.Process();
    send_rate = verb_send.Process();

    // sigはdryとして使うので、リバーブに送る分は別の変数sig_tmpに入れておく
    sig_tmp = sig * send_rate;

    // パラメータを設定
    verb.SetFeedback(verb_feedback.Process());
    verb.SetLpFreq(verb_lp_freq.Process());
    
    // リバーブをかける
    verb.Process(sig_tmp, sig_tmp, &wet1, &wet2);

    // dry/wetを合わせる
    out[i] = sig * dry_rate + wet1;
    out[i + 1] = sig * dry_rate + wet2;
  }
}

感想

Daisy Seedは3000円くらいで買えるのでDaisyシリーズの中でいちばん気軽な選択肢ですが、その割にドキュメントのどこを見ればいいかよくわからなくて躓きがちな気がします。気付いたところをちょこちょこメモっていきたいと思います。