📢

Max/MSPのgen~からエクスポートしたコードをDaisyで使う

2020/10/09に公開

更新: 現在はDaisyを発売するElectroSmithより、gen~→Daisyのワークフローであるoopsyが整備されています。そのため本記事のように直接C++を弄る必要性はほとんどなくなりました

https://github.com/electro-smith/oopsy

Daisyとは

https://www.electro-smith.com/daisy
STM32ベースのArduinoIDEとかで開発できるマイコンボードで、DSPライブラリやユーロラックモジュールなどいろいろ整ってます。

Genとは

Cycling'74 Maxで使える低レベル信号処理のコードを作れる機能です。パッチの名前は.maxpatでなく.gendspになります。おまけ的にC++にエクスポートできる機能があるのでこれを使うとvstの信号処理とかに応用できたりする。けど最近Cyclingの開発では放置気味なので今後の将来が若干心配。

https://cycling74.com/tutorials/gen~-for-beginners-part-1-a-place-to-start

Daisy開発環境のセットアップ

英語いける人は基本以下のリンクを参照してやっていきましょう(今回はとりあえずmacOSを想定しています)

https://github.com/electro-smith/DaisyWiki/wiki/1a.-Getting-Started-(Arduino-Edition)

Arduino IDEをインストール

https://www.arduino.cc/en/Main/software

brew cask arduinoでも良い。

STM32のボードをArduino IDEにいれる

https://github.com/stm32duino/wiki/wiki/Getting-Started

これに従う。

STM32CubeProgrammerを入れる

↑のExtra Stepに書いてある部分。DFUでアップロードするために必要です

https://www.st.com/en/development-tools/stm32cubeprog.html

でメールアドレスを入力するとDLリンクが送られてくるので開く。(Macの場合.appがあるけど/Applicationに移動せずそのまま開きます)。

なおJavaが必要です。つらい。brew install openjdk とかで入れます。
開くとApplicationsにProgrammerアプリ本体がアップロードされます。

DaisyDuinoライブラリを入れる

https://github.com/electro-smith/DaisyWiki/wiki/1a.-Getting-Started-(Arduino-Edition)#install-the-daisyduino-library

Sketch->Include Library->Manage Libraryからライブラリマネージャを立ち上げてDaisyDuinoで検索、インストール。

書き込み方法

Tool->Board でGeneric STM32H7 Seriesを選ぶ(これがない場合はSTM32duinoのインストールができてない)
Tool->Board-> part numberで Daisy Seedを選ぶ
Tool-> USB Support(if available)でCDC (generic 'Serial' supersede U(S)ART)
Tool-> USB SpeedではとりあえずLow/Full Speed
Tools->Upload MethodでSTM32CubeProgrammer (DFU)

注意点

普通のArduinoボードと違う点としてtools->portでUSBポートを選択する必要がありません(というか選択肢に見えないはずです)。無くてもアップロードされるので大丈夫。

またDaisyはseedのボード上にRESETBOOTの2つのボタンがついており、BOOTを押しながらRESETを押し離すと書き込みモードになり音が止まり、スケッチが書き込めるようになります。忘れてるとアップロードエラーになります。

Genで作ったパッチをexportする

まず適当なフォルダに適当な.maxpatファイルと.gendspファイルを作りましょう。今回はgendspのexamplesの中からフィルターのコードを引っ張ってきました。

フィルターの周波数とQの設定がinputになっていてちょっと使いづらいので、それぞれparamsに差し替えたパッチを作りました。

このgendspをmaxpat側で[gen~ filter.gendsp]オブジェクトを作って読み込んだら、exportcode filter.cppというメッセージを送ります。最初の一回はエクスポート先のフォルダを聞かれるので適当なところに指定してください。

Arduinoスケッチのフォルダを作る

適当なフォルダに適当な名前の.inoファイルを作ります。ここにgendspでエクスポートしたコードを一緒に突っ込んでください。

フォルダ構成は以下のスクリーンショットのようになります。filter.cpp,filter.h以外にgen_dspというフォルダの中にライブラリファイルがいくつか吐き出されますが、それもまとめて一つの階層に突っ込みました。

Genのヘッダファイルをいじる

さてこのgenの自動で生成したコードがいくつかdaisyやstmの標準ライブラリの部分とコンフリクトします。うまくマクロの定義とかで解決できるかと思いきや無理っぽかったので、しょうがないですがですがコメントアウトで解決しました。

genlib_ops.hの148~157行

#ifndef WIN32 //ここをコメントアウト
// inline t_sample exp2(t_sample v) { return pow(2., v); }

// inline t_sample trunc(t_sample v) {
// 	t_sample epsilon = (v<0.0) * -2 * 1E-9 + 1E-9;
// 	// copy to long so it gets truncated (probably cheaper than floor())
// 	long val = v + epsilon;
// 	return val;
// }
#endif // WIN32

同じくgenlib_ops.hの621~627行

他のパッチによってはこの上下にあるdbtoaとかmstosampsあたりもコンフリクトするかもしれません。エラーを見つつ対処してください。

// inline t_sample ftom(t_sample in, t_sample tuning=440.) {
// 	return t_sample(69. + 17.31234050465299 * log(safediv(in, tuning)));
// }

// inline t_sample mtof(t_sample in, t_sample tuning=440.) {
// 	return t_sample(tuning * exp(.057762265 * (in - 69.0)));
// }

genlib.cppの153~157行

// NEED THIS FOR WINDOWS: ここをコメントアウト
// void *operator new(size_t size) { return sysmem_newptr(size); }
// void *operator new[](size_t size) { return sysmem_newptr(size); }
// void operator delete(void *p) throw() { sysmem_freeptr(p); }
// void operator delete[](void *p) throw() { sysmem_freeptr(p); }

これで準備完了です。

スケッチを書く

私はDaisy Podを持っているのでそれ用のexamplesからいくつか使えそうなのを探して、今回は以下のSimpleOscillator.inoファイルをベースにしました。

https://github.com/electro-smith/DaisyDuino/blob/master/examples/Pod/SimpleOscillator/SimpleOscillator.ino
元々エンコーダで波形の種類変更、ボタンでオクターブ変更、つまみ1で周波数変更になっていたので、つまみ1でフィルター周波数を変更、つまみ2でレゾナンス変更という形に変えました。
比較のために左チャンネルだけにprocessをかけるようにしています。

#include "DaisyDuino.h"

#include "json.h"
#include "json_builder.h"

#include "genlib.h"
#include "filter.h"

#define NUM_WAVEFORMS 4

DaisyHardware   hw;
Oscillator osc;
CommonState* filter_instance;

uint8_t waveforms[NUM_WAVEFORMS] = {
    Oscillator::WAVE_SIN,
    Oscillator::WAVE_TRI,
    Oscillator::WAVE_POLYBLEP_SAW,
    Oscillator::WAVE_POLYBLEP_SQUARE,
};

static float   freq=1000;
float filterfreq = 1000;
float filterreson = 0.2;
const size_t blocksize = 512;
float** mybuffer;
float          sig;
static int     waveform, octave;

static void AudioCallback(float **in, float **out, size_t size)
{
    hw.DebounceControls();

    waveform += hw.encoder.Increment();
    waveform = (waveform % NUM_WAVEFORMS + NUM_WAVEFORMS ) % NUM_WAVEFORMS;
    osc.SetWaveform(waveforms[waveform]);

    if(hw.buttons[1].RisingEdge())
        octave++;
    if(hw.buttons[0].RisingEdge())
        octave--;

    octave = DSY_CLAMP(octave, 0, 4);

    // convert MIDI to frequency and multiply by octave size
    filterfreq = analogRead(PIN_POD_POT_1) / 1023.f;
   freq = mtof(1000 * 127 + (octave * 12));
    filter::setparameter(filter_instance,0,filterfreq*20000,nullptr);

    filterreson = analogRead(PIN_POD_POT_2) / 1023.f;
    filter::setparameter(filter_instance,1,filterreson,nullptr);
    
    osc.SetFreq(1000);

    // Audio Loop
    for(size_t i = 0; i < size; i ++)
    {
        // Process
        sig        = osc.Process();
        out[0][i] = sig;
        out[1][i] = sig;
    }
    float** out0 =&out[0]; 
    filter::perform(filter_instance,out0,1,mybuffer,1,size);
    for(size_t i = 0; i < size; i ++)
    {
        // Process
        out[0][i] = mybuffer[0][i]*1;
    }
}

void InitSynth(float samplerate)
{
    osc.Init(samplerate);
    osc.SetAmp(0.1);

    waveform = 0;
    octave   = 0;
    freq=1000;
}

void setup()
{
    float samplerate, callback_rate;
    hw = DAISY.init(DAISY_POD, AUDIO_SR_48K);
//    hw.SetAudioBlockSize(blocksize);

    mybuffer = new float*[1];
    mybuffer[0] = new float[blocksize];
    samplerate = DAISY.get_samplerate();
    auto blocksize =  samplerate/DAISY.get_callbackrate();
    filter_instance = (CommonState*)filter::create(samplerate,blocksize);
    hw.leds[0].Set(false,false,false);
    hw.leds[1].Set(false,false,false);

    InitSynth(samplerate);

    DAISY.begin(AudioCallback);
}

void loop()
{
}

要点

インクルード

#include "json.h"
#include "json_builder.h"
#include "genlib.h"
#include "filter.h"

これをDaisyDuino.hのあとに置いておきます。cppファイルのコンパイルとリンクはよくわからないですがどこかで勝手に処理してくれるみたいなので便利です。

インスタンスの生成

//in global
CommonState* filter_instance;


// in setup()
    samplerate = DAISY.get_samplerate();
    auto blocksize =  samplerate/DAISY.get_callbackrate();
    filter_instance = (CommonState*)filter::create(samplerate,blocksize);

なぜかDaisyがblocksizeを直接getできずcallbackrateというのに変換してでしか受け取れないので再度元に戻す処理を噛ませてインスタンスを立ち上げています。filter::createのnamespaceはgendspの名前と一致するようになっているはず。

パラメータの設定

filter::setparameter(filter_instance,1,filterreson,nullptr);

二番目の引数がgendspの中でparamsを複数作った時にどのparamsをセットするかのindexを指定しています。
getparametername(filter_instance,index)でparamの名前を取得していくのも良いですが、大体は.cppファイルのなかに書いてある定義を直接参照してしまった方が早いかと思われます(そもそも文字列で指定させてくれ)。

オーディオ処理の実装

daisyのオーディオコールバックはstatic void AudioCallback(float **in, float **out, size_t size)でやってきます。

filter::perform(filter_instance,out0,1,mybuffer,1,size);

引数は順番に、インスタンスへのポインタ、入力へのポインタ(float**)、入力チャンネル数、出力へのポインタ(float**)、出力チャンネル数、バッファサイズです。バッファサイズはコールバックのsizeを直接使えます。

inとoutのポインタに同じもの指定して破壊的書き換えができるかと思ったらだめだったので一段バッファを経由する形で処理しています。

これでコンパイル、アップロード

雑感、その他

  • genで作ったcppほぼAPIがCなのでもうちょっとcppっぽいコードも出して欲しい
  • なぜかヘッドフォンアウトがモノラルになってるっぽい?しょうがないのでラインアウトに直接ヘッドフォンを突っ込みました(音量注意)
  • 多分node for maxとかと組み合わせればMax上から自動で書き込みとかもやろうと思えばできる
  • でも当分はdaisy側がその辺のインフラを整備してくれるまで今回の方法で自力でやってた方が楽そう

Discussion