Max/MSPのgen~からエクスポートしたコードをDaisyで使う
更新: 現在はDaisyを発売するElectroSmithより、gen~→Daisyのワークフローであるoopsyが整備されています。そのため本記事のように直接C++を弄る必要性はほとんどなくなりました
Daisyとは
STM32ベースのArduinoIDEとかで開発できるマイコンボードで、DSPライブラリやユーロラックモジュールなどいろいろ整ってます。
Genとは
Cycling'74 Maxで使える低レベル信号処理のコードを作れる機能です。パッチの名前は.maxpat
でなく.gendsp
になります。おまけ的にC++にエクスポートできる機能があるのでこれを使うとvstの信号処理とかに応用できたりする。けど最近Cyclingの開発では放置気味なので今後の将来が若干心配。
Daisy開発環境のセットアップ
英語いける人は基本以下のリンクを参照してやっていきましょう(今回はとりあえずmacOSを想定しています)
Arduino IDEをインストール
brew cask arduino
でも良い。
STM32のボードをArduino IDEにいれる
これに従う。
STM32CubeProgrammerを入れる
↑のExtra Stepに書いてある部分。DFUでアップロードするために必要です
でメールアドレスを入力するとDLリンクが送られてくるので開く。(Macの場合.appがあるけど/Applicationに移動せずそのまま開きます)。
なおJavaが必要です。つらい。brew install openjdk
とかで入れます。
開くとApplicationsにProgrammerアプリ本体がアップロードされます。
DaisyDuinoライブラリを入れる
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のボード上にRESET
とBOOT
の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ファイルをベースにしました。
比較のために左チャンネルだけに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