nanonumです。
max6からgenが導入されパッチングでも低レイヤーでのdspプログラミングが出来るようになりましたが、普段コードを書く人にとってはgenexpr言語を使ったテキストベースでのコーディングの方がわかりやすいかと思います。
ところが言語仕様の解説やマニュアルではDAWやオーディオエフェクト的な用語での解説になっている部分が多く、プログラミング言語の解説として曖昧に感じられる箇所などあるため、「プログラミング用語としてのGenExpr」を理解するためにこの記事を役立てていただければ幸いです。なおMax自身のGen Documentを完全にカバーする内容ではないのでそちらも読みながらお付き合いください。
内容上、何らかのプログラミングが出来る前提の話になっていますが、言語としては比較的シンプルかと思いますのでこれを気にgenexprerを目指すのはいかがでしょうか。ジェンエクスパラー
2015/12/26 データ型について修正、int(), float(), bool()追加
2015/12/08 inoutのイベントシグナルについて修正
もくじ
サウンドプログラミング入門――音響合成の基本とC言語による実装 (Software Design plus)
技術評論社
売り上げランキング: 98,572
エディタ:Codebox
GenExprプログラミングを行う最低限必要なgenオブジェクト。gen編集画面で新規オブジェクトにCodeBoxと名付けると現れる。
コードを変更してから左下アイコンをクリック、もしくはCodeBoxからフォーカスを外すとコンパイルされる。
外枠と中のエディタ部分は別のフォーカスを持ち、外枠フォーカス時は青い線がつきCodeBoxの移動やリサイズが出来るがコードは書けず、エディタ部分フォーカス時は緑色の枠が付き編集可能になる。gen~がロックされている場合もコード選択は出来るが枠もつかず編集もできないのでおかしいと思った場合は確認すること。
後ほど書くがgenのinletとCodeBox内のin1は直接は関係ない。
説明の都合上日本語コメントをつけている場合があるが対応していないので要注意。
実行のイメージ
dspプログラミングではブロックサイズ(512サンプル分など)ごとに処理するパターンが多いが、genexprの場合は(内部的にはともかく)コード後半、関数定義やParamなどを除く実行部分が1サンプルごとにメインループのよう実行される。
require"foo" | |
func(){ | |
return 1; | |
} | |
Param freq(); | |
// 以下の部分がメインループのような扱いになる。—— | |
osc = sin(in1 * TWOPI * freq); | |
out1 = osc; | |
// ここまで———————– |
その関係もあるのかCodeBoxに記述する順序は結構厳しい。
個々の項目については後ほど触れるが
- require
- 関数
- 型指定の変数宣言(Param / Buffer / Data / Delay / Historyなど)
- その他コード、通常の変数など(メインループ)
という順序以外ではエラーになる。
データ型
>数値について
マニュアルによれば数値は全てdoubleになると記述されているが、実際保持している数字によってダイナミックに切り替わっている様子。
明示的に扱うにはint(), float()などのキャスト関数が用意されているがfloat(10000.0)とやってもintっぽい。
counterやaccumなどを1ずつ増やすような使い方をしている箇所で最大値設定などをしないと割とすぐ最大値でオーバーフローする。
// bool値的な書き方をすることはある | |
if(in1 > 0.5){ | |
// do | |
} |
以下の様に比較対象が0の場合以外は符号関係なくtrueとして扱われる。
if(0.00000000000000000000000000000001) > // これはtrue
一応bool()という関数はある bool(0.001) -> 1;
文法・演算など
- if, else if, else使用可能、if内にはスコープがある
- インクリメント / デクリメント演算子が無いのでforでは += 1を使う。
- 三項演算子あり。
- if(!true)的な書き方可
- require, Buffer(後述)などで文字列を使用する場合があるがダブルクオートのみ対応している。シングルクオートだとコンパイルされず、エラーも出ないので注意。
- 行末にはセミコロン必須(requireを除く)
- ゼロ除算はなぜか特に問題無い(?)
- コメントは一般的な
//
を書いた行、もしくは/* */
の間。ただし日本語の処理が下手で表示が崩れるどころかその後コンパイルできなくなる場合があるので絶対に書かないこと(書いてしまったらundo)
変数
型指定が必要なものと不要なものがあり、型指定が必要なものについては記述位置に制限がある。
>ローカル変数
どこで宣言しても問題は無い(ifスコープなどは注意)
value1 = in1 * 10; | |
out1 = value1; |
変数は1度のループでしか有効でないため、実行の度+1させるなど、保持させたい箇所についてはaccum()やcounter()を使うかHistory型の変数を使用する。
a = accum(1); | |
c = counter(1, 0); |
>History型変数
マニュアルではHistoryは1サンプル前のデータを保持し、フィードバックを実装するための型というような書き方がしてあるが、メインループ外に存在している保持可能な変数、と考えた方が早い。cファイルで書き出したコード上でも単なるdouble(をtypedefしたもの)として定義されている。
History count(0); | |
out1 = count; | |
count += 1; |
上記コードはよくあるインクリメントでcounter(1, 0);
と同義だが、0の時の処理や最大値になった場合の処理を書こうとすると上記コードの方が融通がきく。またcounter / accumを整数値で増やしていった時int上限でオーバーフローするのに対してHistoryはdoubleと思われるためオーバーフローしにくい。
配列
配列がないことでおなじみのgenではあるが一応Data、Buffer、delayでどうにかすることは可能。Dataはgen内のみ、bufferの場合mspでbuffer~オブジェクトを作る必要があるがmsp側のデータを書き換えられる。残念ながら一般的な配列用のメソッドが用意されているわけでは無い。
Buffer系とdelayで読み書きの方法違うので注意すること。
>Data
配列として使う場合
//どう見ても重い | |
Data array(1000); | |
total = 0; | |
poke(array, in1, counter(1, 0, 1000); | |
for(i = 0; i < dim(array); i+=1){ | |
total += peek(array, i); | |
} | |
out1 = total; |
Data array(1000);が定義部分、1000サンプル分のデータが確保される。Data及びBufferではdim(array)
でcountやlengthのような感覚でサイズが取得できる、ただしサイズ可変なのはBufferのみ。読み書きについては[buffer~]の場合と同じくpoke()、peek()で行う。チャンネル、補間などについてはマニュアルを参照。
宣言時に初期値を設定できないため(0で埋められる)、定数としてのデータテーブルやあらかじめ特定のwavファイルを読み込ませたい場合は後述のBufferを使うこと。
ちなみに
Data d(4); | |
History flag(0); | |
if(!flag){ | |
poke(d, 0, 0); | |
poke(d, 1, 1); | |
poke(d, 0, 2); | |
poke(d, 1, 3); | |
flag = 1; | |
} | |
out1 = peek(d, counter(1, 0, 4)); |
といういまいちな方法はある。
>Buffer
動作についてはほぼDataと同じだがあらかじめmsp側で作成しておき、そのbuffer~名(例 : myBuff)を宣言時に文字列として指定し使用する。
Buffer buff("myBuff"); // ダブルクオート |
以降gen内では変数buffとして扱うことになる。
・参照するバッファーの変更
参照する外部の[buffer~]オブジェクトを変更することが可能。
先ほどのmyBuffという名前の[buffer~]に追加してmyBuff2という名前のbuffer~オブジェクトを用意しておき、メッセージオブジェクトから(buff myBuff2)というメッセージを[gen~]のオブジェクトボックスに送ることで切り替えることができる。
codebox内からダイナミックに参照するbufferを変更するのは無理そう。
>Delay
またDelayについては配列っぽく捉えるとpush / shiftし続けるキュー型配列と言える。d.write(in1)でin1を書き込み、d.read(10)で10サンプル後のデータを読み込む。readは複数の位置を同時に読み込むことが可能。
Delay d(samplerate); // delay最大値、1秒分のメモリを確保 | |
d.write(in1); | |
out1 = d.read(10) + d.read(20); |
読み込み用カウンタが止められそうになく、常に更新され続けるためキューを貯めておくような使い方は難しそう。
結線なしでbuffer~を書き換えている様子
定数
例えばサンプルレートはsamplerateでもSAMPLERATEでも問題はないが単にどちらでも定義されているだけでケースセンシティブなようなので、sampleRateなどでは未定義エラー。
exportcodeしたらsamplerateも48000に置き換わっているのだけど、動作的に何度か怪しい気がした時あり(検証不足)
関数
以下のように定義する。定義場所はrequireの次となる。
myFunc(arg1, arg2){ | |
return arg1 + arg2; | |
} | |
out1 = myFunc(1, 0); // 関数を実行 |
引数の数は自由、戻り値を複数個にしたい場合は
return arg1, arg2;
のようにカンマで区切る。
この場合関数を呼び出すときに
value1, value2 = myFunc(1, 0);
とすることで複数の戻り値を扱える。
関数内とメインループはスコープが異なるので変数は共有されない。
func(arg){ | |
History prev(); | |
// arg != prev | |
} | |
History prev(); | |
func(prev); |
メモ:return以後は無視されるのでHistoryなどで次回に持ち越したいものはreturnより先で行う。
>関数の謎
例えば440Hzのノコギリ波と540Hzのノコギリ波を同時に鳴らす場合
fn(arg){ | |
subosc = phasor( arg * 100 + 440); | |
return subosc * 0.5; | |
} | |
vca = 0; | |
vca += fn(1); | |
vca += fn(2); | |
out1 = vca; |
このような方法があるが、このオシレーター数を可変にしようとして
for(i = 0; i < 2; i+=1){ | |
vca += fn(i); | |
} |
などと書いてもめちゃくちゃな結果が返ってくる。
オシレーターのユニゾン数やcombフィルターの数を可変にしようとしても残念なことになるので注意。
データ入出力
入力についてはmaxオブジェクトからnumboxなどで数値を入力するためのParam、オーディオシグナルを入力するためのinletの2つがメインとなり、bangは受け取れないためトリガーが必要な場合はclick~などを経由させてオーディオシグナルとして入力させる。出力はoutlet経由のオーディオシグナルのみ、maxコンソールへのprintなども無いのでデバッグ時厄介。
その他、外部buffer~を参照、書き換えることもできるのでこれを入出力として捉えることも出来る。buffer~の読み書きについては結線は必要ない
>Param
gen~外部から数値(buffer名のみテキストも)を送る場合に使用する、オーディオシグナルは入力不可。引数と思ってしまっても差し支え無い。
初期値、最大値 / 最初内の設定が可能。
// 使い方(定義) | |
Param frequency(440, min=10, max=2000); |
frequencyがパラメーター名、カッコ内は初期値、最小値、最大値で、frequency(10 * 10)など計算式が使えた時期があった記憶があるが不安定すぎたのでリテラル値のみにしたほうがよさそう。
最小値及び最大値はそれぞれ省略可能、gen編集画面最下部のリセットボタンで初期値に戻る。
あとはmaxのメッセージボックス経由でnumboxなどからgen~に数値を送る。
// コード中で書き換えできないので注意。 | |
Param length(1000); | |
length = mstosamps(length); |
>inlet
最初に書いた通りgen~のinletとcodeboxのin1 / in2….は別なので注意
紛らわしいので便宜的にgen~オブジェクトの出力箇所をinlet/ outlet、codeboxの入出力を in/outとする。エラーが出ないので自信がないのだけどinlet/outlet/in/out全てそれぞれ16個までな様子。
どちらもオーディオシグナルしか入力できないのでnumboxなどつなぎたい時はsig~など経由させること。 (12/08修正)オーディオシグナルとイベントシグナル両方入力されている場合はオーディオシグナルが優先され、オーディオシグナルの結線を削除するとイベントシグナルが有効になる。どちらにしても内部的にはオーディオシグナルとして扱われる。
ただしイベントシグナルはイベントレートで動くので極力スムーズに変化させたいパラメーターであれば[line~]などを経由してスムース化したオーディオシグナルを繋ぐかgen~内で補間処理を書く。
inlet/outlet
左側が最もシンプルな方法、他のアトリビュートは省略可能。in1ではなくin+スペース+番号。
全てのアトリビュートを設定したものが右側。@commentはgen~オブジェクトのインレットにカーソルを合わせた時にヒントとしてポップアップされる。@defaultは初期値、@maxは最大値で@minは最初内。インレット番号は@indexとしてサジェストされるがそれだとどうも動かない。順番も最初である必要がありそう。この部分はCodeBoxには書けない。
追記:inの範囲はmax=1、inletの範囲は@max 1と微妙に表記が違う。
同じ番号のinlet/outletを複数作ることが可能だが以下のようにすると加算されて3が出力される
in/out
codebox内にin1 / in2とそのまま書く、スペースは不要。CodeBox内に書かれている分の入出力がCodeBoxに追加され、記述がなくなると消える。inに限り同じものを複数書いても問題はない。またinput = in1;というように変数に入れられる。
outに関してはoutletと違い
out1 = 1; | |
out1 = 2; | |
out1 = 3; |
と書いても加算されず最後のout1の値のみが出力される。CodeBoxで新規にoutを作ったからといってgen~自体にアウトレットが増えるわけではないのでCodeBox > outletへ繋ぐこと。よく忘れる
>bangについて
bangはそのままだと入力出来ないので、counterをリセットさせるなどトリガーとして使用したい場合はmspの[click~]オブジェクトを使いオーディオシグナルとして入力する。
よく使うパターンは
reset = change(in1) > 0; |
これで1サンプルのみ値が1になる変数resetが使える。先に書いた通りbool型ではない。
また上記reset変数作っておくと
freq = sah(in1, reset, 0); |
という用に、bangが送られてくるタイミングだけ更新される変数が作りやすい。(sahはサンプル&ホールド関数)
コードの外部化
その前にまず.genexprと.gendspファイルの違いについて。
gendspは.maxpatのgen版jsonファイルでCodeBoxとgen~オブジェクトどちらからも利用することができる。inlet, outletと実行するコードの本体が入っており、genexprは単なるコードだけ書かれたテキストファイル。
>requireによる外部genexprファイルの読み込み
requireを使い外部genexprをインポートする事ができる。
.gendspファイルは読み込めない、またrequireしたgen~内のみで有効になる。
例genexpr中身 ( func.genexpr )
myAdd(arg){ | |
return arg + 1; | |
} |
// 呼び出し、使用例 | |
require("func") | |
myAdd(in1); |
requireはCodeBox内の最初の位置に書く必要がある。シングルクオートでは動かないしエラーも出ないので注意。require周りは以前アップデートすると動かなくなることが多い箇所だったので変な時はとりあえず確認。マニュアルではrequire"func" require"func.genexpr"
など表記方法がいろいろあるが上記コードの書き方が今の所一番安心できている印象。これはあやふや
>アブストラクションを使いgendspを読み込む
gendspを読み込みたい場合はサーチパス内に.gendspファイルを置き、ファイル名をそのまま関数名として使用する。requireなどは不要。中身のcodeboxで関数を使用していたとしてもその名前は関係なくあくまでファイル名=呼び出し関数名となる。
例: decay.gendspファイルをサーチパス内に置くと
decay();
として呼び出す事ができる。
引数はdecay.gendspファイル内のinletへ渡され、帰り値はdecay.gendspファイル内のoutletから返る。
gen~オブジェクト自体の読み込み
patcher, bpatcherのように[gen~ filename]という形でmaxパス内のgendspを読み込むことも可能。
まとめ
全体の雰囲気をお伝えしたかったため個々のオペレーター・関数などについてはほぼ触れませんでしたが一般的なプログラミング言語で用意されている関数やmaxオブジェクトに存在する名前の関数が多いのでそこまで混乱はないかと思われます。
genexpr自体の情報はオンラインにもあまりないとは思いますがプリインストールされるgenサンプルに一般的なエフェクトは揃っているのでそちらも有用。GenExprではなくgenオブジェクトで組まれているものの方が多いですがCode画面を見れば(そのままでは読みづらいものの)コードで確認出来ます。
それからC言語用のコードサンプルや解説などもMaxオブジェクトで組み直すより簡単に済む事が多いはずなのでgithubやsourceforgeでコードを探すのも良いでしょう。
ところで私はGenExprを書き、また鰻重が好きですがNOEL-KIT氏はうなぎが嫌いでありgenExprを書きません。表紙にはベストな選択かと思います。
次回は書ききれなかった愚痴・バッドノウハウ・リファクタリング関連についてやexportしたコードを眺めたりする予定です
サウンドエフェクトのプログラミング―Cによる音の加工と音源合成
オーム社
売り上げランキング: 190,716
C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理
オーム社
売り上げランキング: 73,337