Chapter 16:
MSP コードの書き方

この章では、MSP エクスターナルを書くために必要となる基本的な情報について説明します。

インクルードファイル

典型的な MSP オブジェクトの場合、ソースファイルの最初に次のような行が必要です。

#include "ext.h" // Max エクスターナルための、標準のインクルードファイル #include "z_dsp.h" // MSP 情報を持っています

インクルードファイル z_dsp.h は、他の多くのインクルードファイルを参照しています。それらについては、関連する箇所で述べていきます。

オブジェクト構造体の定義

MSP オブジェクトは、最初のフィールドとして、t_object ではなく t_pxobject を持っています。t_pxobject t_object にいくつかのフィールドを追加したものですが、最も特徴的なものは、MSP オブジェクトのインレットでシグナルと float のどちらでも受信することができるようにするために用いられるプロキシの配列の箇所です。プロキシについて不明な点がある場合は、Chapter 6 および buddy エクスターナルオブジェクトのサンプルコードを参照して下さい。一般に、MSP はプロキシの使用に関する細かい処理の大部分を実行してくれます。ユーザインターフェイスオブジェクトは t_pxbox という良く似たヘッダを使いますが、これは標準のユーザインターフェイスオブジェクトのヘッダ t_boxt_pxobject のフィールドと結合したものです。この両方の構造体は、共にインクルードファイル z_proxy.h で定義されています。

これは、MSP エクスターナルオブジェクトの宣言の例です。

typedef struct _sigobj { t_pxobject x_obj; // ヘッダ float x_val; // 追加するフィールド } t_sigobj;

初期化ルーチンの書き方

初期化ルーチンは、Max エクスターナルのためのクラス情報をセットアップします。あなたのクラスの初期化を行う setup 関数の呼出し(一般的に、すべての Max エクスターナルで最初に行うべきものです)の中で、インスタンス消滅ル−チン(free ルーチン)として dsp_free を渡さなければなりません。これはインスタンス生成ルーチンで割当てたメモリのために独自のインスタンス消滅ルーチン(free ルーチン)を書く必要がない場合であっても、渡さなければなりません。次は、メモリの割り当ても行わず、初期値のアーギュメントも取らないオブジェクトの例です。

setup(&sigobj_class, sigobj_new,(method)dsp_free, (short)sizeof(t_sigobj), 0L, 0);

setup を呼び出した後、初期化ルーチンでは、あなたのオブジェクトの dsp メソッド(後述します)のバインドを行う必要があります。引数の型の指示子は、次のように A_CANT を使います。

addmess(sigobj_dsp, "dsp", A_CANT, 0);

さらに、あなたの MSP エクスターナルのクラスのセットアップを終了するためには、dsp_initclass の呼出しが必要になります。

dsp_initclass

     
  あなたのオブジェクトのクラスが MSP で動作するようにセットアップする場合に、dsp_initclass を使います。
   
  void dsp_initclass(void);
     
 

このルーチンはあなたのオブジェクトの初期化ルーチンで呼び出さなければなりません。これにより、あなたのオブジェクトのクラスに一組のメソッドが追加されます。このメソッドは、DSP コールチェインを組み立てるために MSP によって呼び出されるものです。これらのメソッド関数はあなたのオブジェクトにとって完全に透過的なため、あなたがこれらの関数についての心配をする必要はありません。しかし、signaldrawlineuserconnectenable、といった名前に対して他のものをバインドすることは避けなければなりません。このルーチンはノーマルオブジェクト(インターフェイスオブジェクトでないもの)のためのものです。


dsp_initboxclass

     
  あなたのユーザインターフェイスオブジェクトのクラスが MSP で動作するようにセットアップする場合に、 dsp_initboxclass を使います。
   
  void dsp_initboxclass(void)
     
 

ユーザインターフェイスオブジェクトの初期化(main)ルーチンで、dsp_initclass の代わりに、このルーチンを呼び出します。dsp_initboxclass は、上に挙げた名前に4つのメソッドをバインドするのに加え、bxdsp という名前も使用します。


インスタンス生成ルーチン

一般的な Max の インスタンス生成 ルーチンでは、オブジェクトがインレットとアウトレットを何個持つかを指定します。MSP シグナルオブジェクトも例外ではありませんが、複数のシグナルインレットを必要とする場合、MSP シグナルオブジェクトはプロキシを使います。シグナルインレットが何個必要かは、dsp_setup (ユーザインターフェイス・シグナルオブジェクトの場合は dsp_setupbox)の呼び出しで指定します。必要な条件として、シグナルインレットはすべての非シグナルインレットの左側に置かなければならないということがあります。同様に、すべてのシグナルアウトレット(これは単に、"signal" という型が宣言されるだけですが)はすべての非シグナルアウトレットの左側に置かなければなりません。

これは、2つのシグナルインレットと2つのシグナルアウトレットを持つオブジェクトのための、初期化ルーチンの例です。

void *sigobj_new(void) { t_sigobj *x; x = newobject(sigobj_class); dsp_setup((t_pxobject *)x,2); // オブジェクトとインレットの設定 outlet_new((t_object *)x,"signal"); // アウトレットの設定 outlet_new((t_object *)x,"signal"); return x; }

 典型的な Max オブジェクトと異なり、上に挙げたルーチンの例ではアウトレットへのポインタを格納していない点に注意して下さい。MSP オブジェクトは、ほとんどの場合、シグナルアウトレットを直接参照することはありません。MSP シグナルコンパイラは、すべての Max オブジェクトの最初にある t_object 構造体の内部に格納されている、あなたのオブジェクトのすべてのアウトレットへのポインタを通してアウトレットにアクセスします。

dsp_setup

     
  あなたのオブジェクトクラスのインスタンスを初期化し、MSP に対してインスタンスがシグナルインレットを何個持っているかを知らせるために、dsp_setup を使います。
   
  void dsp_setup(t_pxobject *x, short num_signal_inputs);
     
 

あなたのオブジェクトを生成した後、インスタンス生成ルーチンの中で、newobject によってこのルーチンを呼び出します。オブジェクトを、t_pxobject にキャスト(訳注:型変換)して第1の引数とし、その次にオブジェクトが持つシグナルインプットの数を指定します。 dsp_setup は、t_pxobject ヘッダのフィールドを初期化し、(num_signal_inputs が1より大きい場合は)必要なプロキシを割り当てます。シグナルオブジェクトにはインプットを持たないものもありますが、この場合は、num_signal_input として 0 を渡さなければなりません。dsp_setup を呼び出した後、intinfloatininlet_new を使って、非シグナルインレットを追加して作ることができるようになります。


dsp_setupbox

     
  あなたのユーザインターフェイスオブジェクトクラスのインスタンスを初期化し、MSP に対して何個シグナルインレットを持っているかを報告するために、 dsp_setupbox を使います。
   
  void dsp_setupbox(t_pxbox *x, short num_signal_inputs);
     
 

このルーチンは、dsp_setup のユーザインターフェイスシグナルオブジェクト用バージョンです。


t_pxobject ヘッダの中の特別なビット

t_pxobjectt_pxbox の中には、あなたがセットできる3つのビットがあります。これは、MSP が DSP コールチェインを組み立てる際にオブジェクトがどのように取り扱われるかについて影響を及ぼすものです。これらの設定についての説明は、一度、dsp メソッドや パフォームメソッドについての説明を読んでからの方が、より納得がいくかと思いますが、これらはインスタンス生成ルーチンで設定する必要があるためここで説明しています。t_pxobjectt_pxbox はどちらも z_misc と呼ばれるフィールドを持っています。これはデフォルトでは 0 になりますが、この場合、この後に述べるすべての設定は使用不可になります。

#define Z_NO_INPLACE 1

z_misc のこのビットをセットした場合、コンパイラは、あなたのオブジェクトに渡される全てのシグナルベクタが個別のものであることを保証します。オブジェクトが、パフォームメソッドで用いる1つまたはそれ以上のアウトプットベクタが、1つまたはそれ以上のインプットベクタと同じものであることは一般的です。いくつかのオブジェクトはこの制約を守ることができません。典型的な例では、オブジェクトが複数のインプットとアウトプットのペアを持っていて、他のインプットーアウトプットのペアへ移動する前に、1つのインプットに基づいてすべてのアウトプットを書くような場合に起こります。

#define Z_PUT_LAST 2

z_misc のこのビットをセットした場合、コンパイラはあなたのオブジェクトを、可能な限り DSP コールチェインの後ろに置きます。これは、2つの状況下で役に立ちます。1つは、正しく動作するためには、最初に他のオブジェクトの dsp ルーチンが呼び出された後に、あなたのオブジェクトの dsp ルーチンが呼び出される必要がある場合です。2つめは、あなたのオブジェクトのパフォームルーチンを実行する前に、他のオブジェクトのパフォームルーチンを実行させたい場合です。例えば、遅延時間を最小にするためには、ディレイラインを読みこむオブジェクトにとって、おそらく、その前にディレイラインを書き込むオブジェクトが実行されていることが望ましいでしょう。しかし、このフラグをセットすることによって、特定の順序による結果を保証するものではありません。

#define Z_PUT_FIRST 4

z_misc のこのビットをセットした場合、コンパイラは、あなたのオブジェクトを、可能な限り DSP コールチェインの始まりの近くに置きます。このセッティングは、現在、標準の MSP オブジェクトでは使用されていません。

dsp メソッド

dsp メッセージは、MSP が DSP コールチェインを確立している間、あなたのオブジェクトに送られます。このチェインに何かを追加したい場合、dsp メソッドは DSP コールチェインにあなたのバフォームメソッドを追加する dsp_add を呼び出巣必要があります。メソッドは次のように宣言されなければなりません。

dsp

     
  オブジェクトを DSP コールチェインに含めるために、MSP から呼び出されます。
     
バインディング
     
  addmess (mysigobject_dsp, "dsp", A_CANT, 0);
   
宣言
   
  void mysigobject_dsp (t_sigobj *x, t_signal **sp, short *count);
     
 

dsp メソッドは パフォームメソッドのシグナルインプット、アウトプットを定義する t_signal 構造体の配列を渡されます。t_signal は float のバッファとサイズ s_n を持っています。この s_n はパフォームメソッドに対する個々の呼出しの間に計算されるサンプル数を指定するものです(このサイズはベクタサイズと呼ばれることもあります)。現在の所、ベクタサイズは受け取る全てのシグナルのサンプル数と同じですが、MSP の将来のバージョンでは、異なった、または可変サイズのベクタを受け入れるような、特別なオブジェクトが可能になるかもしれません。シグナルはまた、サンプリングレートを持っています。これは、オブジェクトがサンプリングレートによる計算を行う場合には非常に重要です。このような場合、sys_getsr の呼出しによって得られるグローバルなサンプリングレートを使うより、シグナル個々のサンプリングレートを使います。t_signal 構造体は、z_dsp.h で定義されています。

t_signal の配列に加え、dsp メソッドは個々のインプット、アウトプットの数を指定する配列を渡されます。MSP オブジェクトの中にはこの情報を使って、DSP コールチェインに異なるパフォームメソッドを対応させるものがあります。例えば、 *~ オブジェクトでは、インプットのどちらかにシグナルが接続されていない場合、シグナル×シグナルの演算に定数を用いたシンプルなルーチンを使うことによって、処理の最適化を図っています。この場合には、オブジェクトの内部にある定数を使っていますが、これはアーギュメントとして、あるいは右インレットに送られる float によってセットされます。

dsp メソッドを使って、パフォームルーチンが使う他の内部の変数を初期化したい場合があるかも知れません。例えば、多くのオブジェクトはサンプリングレートによる除算を必要とします。パフォームルーチンの中で除算を行うのは効率が悪いので、サンプリングレートの逆数を dsp ルーチンで計算し、これを保存しておいて、パフォームルーチンの中でこの逆数を掛けることが可能です。オブジェクトのパフォームルーチンのサンプリングレートをグローバルなサンプリングレートと同じであると仮定するのではなく、あなたのシグナルアーギュメントの1つからサンプリングレートを得なければならないということを、もう一度思い出して下さい。


dsp_add

     
  あなたのオブジェクトのパフォームルーチンを DSP コールチェイン に加えるために、dsp_add を使います。
     
  void dsp_add(t_perfroutine p, long argc, ...);
     
 

この関数は、あなたのオブジェクトのパフォームメソッドを DSP コールチェインに追加し、渡される引数を指定します。argc(パフォームメソッドが受け取る引数の数)の後に 追加する引数を続ける必要があります。この引数はすべて long へのポインタでなければなりません。


dsp_addv

     
  あなたのオブジェクトのパフォームルーチンを DSP コールチェインに追加し、その引数を、関数への引数としてではなく配列で指定するために、dsp_addv を使います。
     
  void dsp_addv(t_perfroutine p, long argc, void **vector)
     
 

この関数は、パフォームルーチンに渡したい引数の配列をあなた自身で組み立てることを可能にする、dsp_add の改良型です。

次は、dsp_method の例です。ここでは、接続の数に関する情報には特に注意を払っているわけではありません。これは、2つのインプットと2つのアウトプットを持っています。インプットはシグナルの配列の最初にあり、その後にアウトプットがあります。つまり、sp[0] は左インプット、sp[1] は右インプット、sp[2] は左アウトプット、sp[3] は右アウトプットになります。ここではパフォームメソッドの演算で用いられるサンプリングレートの逆数も格納しています。

void sigobj_dsp(t_sigobj *x, t_signal **sp, short *count) { x->s_1oversr = 1. / (double)sp[0]->s_sr; dsp_add(sigobj_perform, 5, sp[0]->s_vec, sp[1]->s_vec, sp[2]->s_vec, sp[3]->s_vec, sp[0]->s_n); }

上記の dsp_add の呼び出しでは、パフォームルーチンの名前を指定し、その後にパフォームルーチンに渡される引数の数が続き、さらに個々の引数が続きます。シグナルの s_vec フィールドは float によるその配列です。このケースでは、2つのインプットの配列が渡され、次に2つのアウトプットの配列が渡され、続いてベクタサイズが渡されています。慣例として、ほとんどの MSP オブジェクトは最初に入力シグナルを使います。

次に示すのは、より複雑な dsp メソッドです。これは、右のインプットと右のアウトプットが接続されていない場合、異なるパフォームルーチンを使います。オブジェクトは fft~ と似たような処理を行ないますが、虚数部のインプットとアウトプットが接続されていない場合には、FFT の実数部のみを計算するルーチンを使います。この例では、sigobj_perform2は左インプットと左アウトプットのためのシグナルベクタ、およびベクタサイズという3つのアーギュメントを取ります。

void sigobj_dsp(t_sigobj *x, t_signal **sp, short *count) { if (count[1] || count[3])// 右インプットまたは右アウトプットが // 接続されている場合 dsp_add(sigobj_perform, 5, sp[0]->s_vec, sp[1]->s_vec, sp[2]->s_vec, sp[3]->s_vec, sp[0]->s_n); else dsp_add(sigobj_perform2, 3, sp[0]->s_vec, sp[2]->s_vec, sp[0]->s_n); }

あなたの dsp メソッドに渡された カウント配列中の情報に従っている場合でも、t_signal はあなたのオブジェクトに接続されてはいません。t_signal は依然として、有効なサンプルレートやベクタサイズ情報だけでなく、有効なベクタを持っています。

何らかの理由で、DSP コールチェインにいくつかの関数を置きたいと思う場合には、それも可能です。そのためには、単にdsp_add を必要なだけ呼び出すだけです。しかし、このようにした場合、微妙な問題があることを心に留めておいて下さい。たとえば、インプットとアウトプットのシグナルのバッファが同じメモリを指している場合、最初の関数がアウトプットシグナルバッファにデータを書き込むと、インプットシグナルバッファは、同じシグナルバッファを使用する全ての後続のパフォームルーチンによって上書きされてしまいます。


パフォームルーチン

パフォームルーチンはシグナルの値を計算するために繰り返し呼び出されます。MSP は DSP コールチェインの中で、各々のパフォームメソッドを順番に呼び出します。その際、オブジェクトの dsp_add の呼出しによって指定された引数(アーギュメント)が渡されます。しかし、アーギュメントはスタック上に渡されるのではありません。その代わりに、オブジェクトに渡されるアーギュメントの配列へのポインタが渡されます。パフォームルーチンは、dsp_add 呼出しによって最後のアーギュメントが指定された直後に、配列の中へポインタを返さなければなりません。これが行われないと MSP はクラッシュします。あなたのメソッドは次のように宣言されなければなりません。

t_int *sigobj_perform(t_int *w);

MSP は一般に割り込みによってパフォームルーチンを呼び出します。全ての割り込みルーチンと同様、パフォームルーチンは可能な限り効率的に書かなければなりません。パフォームルーチンでは、メモリを移動させるようなルーチンを呼び出すことはできません。また、(デバッグのために)情報を表示する呼出しも行うべきではありません。その理由は、サンプリングレートが 44.1 kHz で、ベクタサイズが 256 サンプルの場合、各々のパフォームルーチンはおよそ 5.8 ミリ秒ごとに呼び出されるためです。しかし、qelem をセットするか、注意深く defer_lowdefer ではありません。MSP の割り込みは Max のスケジューラの割り込みとは異なっているため、defer にはそれが割り込みレベルで実行されていることがわかりません。)を使うことによって、メインレベルまで関数の実行を遅れさせることができます。これは慎重に行う必要があります。なぜなら、5.8 ミリ秒ごとに呼出しを遅れさせる場合、割り込みレベルで大量のメモリを割当てるだけでなく、実行させる必要があるメインイベントレベルの関数で多量の処理の積み残しを生じさせるためです。

これは、2つの入力シグナルを受け取るパフォームメソッドの例です。2つのシグナルは加算されて1つのアウトプットとなり、減算されてもう1つのアウトプットになります。このメソッドは、すでに例として示されている sigobj_dsp と互換性を持つように書かれています。t_int 型はポインタ(PowerPC では int または long)と同じサイズになっています。これは、pd パフォームルーチンとある程度のソースコードでの互換性を持つために用いられています。

dsp_add の呼出しで指定した最初のアーギュメントは、パフォームルーチンに渡される配列の中のオフセット1になる点に注意して下さい。オフセット0にはあなたのパフォームルーチンのアドレスが格納されています。

t_int *sigobj_perform(t_int *w) { float *in1,*in2,*out1,*out2,val,val2; long n; in1 = (float *)(w[1]); // インプット 1 in2 = (float *)(w[2]); // インプット 2, 2番目の引数 out1 = (float *)(w[3]); // 3番目の引数, 1番目のアウトプット out2 = (float *)(w[4]); // 4番目の引数, 2番目のアウトプット n = w[5]; // ベクタサイズ // 演算ループ while (n--) { val = *in1++; val2 = *in2++; *out1++ = val + val2 *out2++ = val - val2; } return w + 6; // 常にアーギュメントインデックスの最大値+1のポインタが返されます。 }

計算ループの中では、アウトプットとインプットのシグナルベクタが同じであれば、結果が正しくなるようにコードが書かれます。しかし、何らかの理由でそうすることができない場合、インプットとアウトプットのシグナルベクタが個別のものであることを、Z_NOINPLACE フラグによって指定することができます。この方法に関しては、前述の「t_pxobject ヘッダの中の特別なビット」というセクションで説明しています(標準の MSP オブジェクトでは、fft~/ifft~ および tapout~ の2つだけがこの機能を必要とします)。

インスタンス消滅ルーチン(free ルーチン)

あなたのノーマルオブジェクトが、メモリの割り当てを行わず、インスタンスが消滅する際にオフにするべきものが何もない場合、初期化(main)ルーチンの中で、setup に dsp_free をインスタンス消滅メソッドとして渡すことができます(ユーザインターフェイスオブジェクトの場合には、box_free を呼び出す必要があるため、オブジェクト自身でメモリの割り当てを行っていなくても、インスタンス消滅ルーチンが必要になります)。あなた自身で インスタンス消滅ルーチンを書く場合、ノーマルオブジェクトでは、その中で dsp_free を呼び出さなければなりません。また、ユーザインターフェイスオブジェクトでは、dsp_freebox を呼び出さなければなりません。

dsp_free

     
  オブジェクトの インスタンス消滅ルーチンの中で、dsp_free を使います。
     
  void dsp_free(t_pxobject *x);
     
 

この関数は、dsp_setup によって割当てられた、プロキシが使用するメモリをすべて解放します。また、シグナル処理がアクティブな場合、DSP コールチェインの再構築が必要であることをシグナルコンパイラに伝えます。あなたのオブジェクトが解放される際に、シグナル処理がオンであるイベントの中で、オブジェクトのパフォームルーチンによって使用されているメモリを解放する前にこの関数を確実に呼び出さなければなりません。


dsp_freebox

     
  ユーザインターフェイスオブジェクトの場合には、dsp_free の代わりに、dsp_freebox を使います。
     
  void dsp_freebox(t_pxbox *x);
     
 

この関数は、dsp_setupbox によって割当てられた、プロキシが使用するメモリをすべて解放します。また、シグナル処理がアクティブな場合、DSP コールチェインの再構築が必要であることをシグナルコンパイラに伝えます。あなたのオブジェクトが解放される際にシグナル処理がオンであるイベントの中で、あなたのオブジェクトのパフォームルーチンによって使用されているメモリを解放する前にこの関数を確実に呼び出さなければなりません。