Chapter 17:
MSP パラメータの取り扱い

リアルタイムシグナルプロセッシングは、シグナルの演算を行っているだけではありません。DSP ルーチンには、入力されるパラメータの変更に対応することも期待されます。このタスクは、 多くの場合 、パラメータの更新として述べられています。Max では、DSP パラメータは主としてコントロールメッセージです。sig~ や line~ オブジェクトを使ってコントロールメッセージをシグナルに変換することも可能ですし、シグナルオブジェクトの内部状態を直接更新することも可能です。2番目のアプローチは、アウトプットの不連続の可能性という不利な点を持っていますが、多くのアプリケーションにとって、あるいは、ユーザが実験を行っている場合には、より簡単で、言うまでもなく効率的です。多くの MSP オブジェクトは内部状態の情報にアクセスするために、パフォームメソッドに対して自分自身へのポインタを渡す必要があります。例えば、float による係数の指定を受け取ることができるフィルタオブジェクトでは、これらの係数に対するアクセスが可能になるように、パフォームルーチンに対して自分自身へのポインタを渡す必要があります。

フィルタの例

例として、ここでは ,オブジェクトに格納されている係数を使う、lop~ というシンプルなローバスフィルタオブジェクトの dsp メソッド と パフォームメソッドを示しています。最初に、係数はオブジェクトの右インレットで指定され、このインレットは float のみを受け取り、シグナルは受け付けないものと仮定しましょう。このことは、あなたが追加のインレットとパラメータを受け取るメソッドを宣言しているということを意味します。これがオブジェクトの宣言文です。

typedef struct _lop { t_pxobject x_obj; // ヘッダ float x_coeff; // 係数 float x_m1; // フィルタメモリ } t_lop;

これは、初期化ルーチンです。

void main(void) { setup(&lop_class, lop_new, (method)dsp_free, (short)sizeof(t_lop), 0L, A_DEFFLOAT, 0); addftx(lop_ft1,1); // 右インレットメソッドのバインド addmess(lop_dsp,"dsp", A_CANT, 0); dsp_initclass(); }

次は、右インレットのメソッドです。

void lop_ft1(t_lop *x, double f) { x->x_coeff = f; }

インスタンス生成ルーチンです。左インレットに1つのシグナルインプットを持っています。そのため、シグナルインプットの数として、dsp_setup に 1 が渡されています。

void *lop_new(double initial_coeff) { t_lop *x = newobject(lop_class); dsp_setup((t_pxobject *)x, 1); floatin((t_object *)x,1); outlet_new((t_object *)x,"signal"); x->x_coeff = initial_coeff; // 係数の初期化 x->x_m1 = 0.;// 以前の状態の初期化 return x; }

次は dsp メソッドです。これは、MSP に対して、オブジェクトのポインタ、インプットベクタ、アウトプットベクタ、ベクタサイズをパフォームルーチンに渡すよう指示しています。シグナル接続数のカウントは必要とされていません。実際、必要ならば、count パラメータなしでメソッドの宣言を行うことができます。

void lop_dsp(t_lop *x, t_signal **sp, short *count) { dsp_add(lop_perform1, 4, x, sp[0]->s_vec, sp[1]->s_vec, sp[0]->s_n); }

最後は、パフォームルーチンです。この例の中で、後ほど、これに代わるパフォームメソッドを書くため、このメソッドは lop_perform1 という名前になっています。フィルタ係数をオブジェクト構造体から取り出し、ローカル変数に置く方法に注意して下さい。可能なベクタサイズは最高2048までのため、この方法は演算のループの間にオブジェクトから値を読み込むより効果的です。パフォームルーチンは割り込みレベルで実行されるため、ルーチンの途中で係数が変更されないことは保証されています。同様なことが、オブジェクト内に保存されるフィルタのメモリに関しても言えます。

t_int *lop_perform1(t_int *w) { t_lop *x = (t_lop *)(w[1]); // オブジェクトは最初のアーギュメント、インプットはその次 float *in = (float *)(w[2]); // その後にアウトプットが続きます float *out = (float *)(w[3]); long n = w[4]; // ベクタサイズ float xm1 = x->x_m1; // 以前の状態を記録するローカル変数 float coeff = x->x_coeff,val;// 係数 // フィルタ演算 while (n--) { val = *in++; *out++ = coeff * (val + xm1); xm1 = val; } x->x_m1 = xm1; // 次回のために古い状態を再保存 return w + 5; }

ここでは、係数の値として float あるいはシグナルを受け取ることができるなようにフィルタを書き直します。シグナルベクタから係数値を読み取る頻度をどの程度にしたいかによって2つの方法があります。まず、1つのパフォームルーチンを使う方法で書いて見ましょう。このパフォームルーチンは、係数をシグナルから得るか、あるいはオブジェクトに格納された float の値から得るかを決定します。この方法では係数はシグナルベクタの最初の値からだけ読み込まれ、それに続く値は無視されます。

オブジェクトにフィールドを追加します。このフィールドによって、dsp ルーチンが右インレットへのシグナルの接続(接続されているか、いないか)を探索した結果をパフォームルーチンに報告します。

typedef struct _lop { t_pxobject x_obj; float x_coeff; float x_m1; short x_connected; } t_lop;

MSP は右インレットでシグナルまたは float の値を得るためにプロキシを使用するため、私たちの初期化ルーチンを少しだけ変更する必要があります。

addftx(lop_ft1,1);

上の行を次の行で置き換えます。

addfloat(lop_float);

名前の変更以外、float メソッドは以前のものと同じです。次は、2つのシグナルインレットを指定するように変更された インスタンス生成ルーチンです。追加のインレットの生成を削除し、dsp_setup の呼出しの中で指定するシグナルインレットの数を2に変更しています。dsp_setup はプロキシを使用して、右インレットを生成します。

void *lop_new(double initial_coeff) { t_lop *x = newobject(lop_class); dsp_setup((t_pxobject *)x,2); // 以前の例から変更した部分 outlet_new((t_object *)x,"signal"); x->x_coeff = initial_coeff; // 係数の初期化 x->x_m1 = 0.; // 前の状態の初期化 return x; }

これは、変更された dsp メソッドです。2つのシグナルインレットを持つため、アウトプットのベクタは sp[1] ではなく sp[2] になっています。

void lop_dsp(t_lop *x, t_signal **sp, short *count) { x->x_connected = count[1]; // 右インレットがシグナルかどうかをこの中に保存 dsp_add(lop_perform2, 5, x, sp[0]->s_vec, sp[1]->s_vec, sp[2]->s_vec, sp[0]->s_n); }

次は、修正されたパフォームルーチンです。スタック上で渡されるシグナルベクタの最初の値を使用するか、あるいは格納されている float の値を使用するかを、オブジェクトの x_connected フィールドの値によって決めています。

t_int *lop_perform2(t_int *w) { t_lop *x = (t_lop *)(w[1]); float *in = (float *)(w[2]); // シグナル、或いは格納された係数を使います。 float coeff = x->x_connected? *(float *)(w[3]) : x->x_coeff; float *out = (float *)(w[4]); long n = w[5]; float xm1 = x->x_xm1,val; while (n--) { val = *in++; *out++ = coeff * (val + xm1); xm1 = val; } x->x_m1 = xm1; // 次回のために古い状態を保存 return w + 6; // ここでは5個のアーギュメントがあるため、6を加える }

2つめの方法は、2つの異なったパフォームルーチンを使うことです。dsp メソッドは、右インプットに接続されたシグナルの数に基づいて、どちらのルーチンを使用するかを決定します。t_lop 構造体から x_connected フィールドを 取り除いた以外は、以前の lop~ の実装からdsp メソッドとパフォームメソッドを変更しただけです。次は、修正された dsp メソッドです。ここでは、以前定義したオリジナルの lop_perform1 メソッドを参照しています。オブジェクトに追加のインプットシグナルがある場合でも、左インプットシグナルベクタとアウトプットシグナルベクタだけを渡すことによって、lop_perform1 を使うことができます。lop_perform1 は他のインプットシグナルベクタがあることを感知しません。。

void lop_dsp(t_lop *x, t_signal **sp, short *count) { if (count[1]) dsp_add(lop_perform3,5,x, sp[0]->s_vec, sp[1]->s_vec, sp[2]->s_vec, sp[0]->s_n); else dsp_add(lop_perform1, 4, x, sp[0]->s_vec, sp[2]->s_vec,sp[0]->s_n); // skip unused sp[1] signal }

最後は、入力される係数シグナルからの全ての値を用いてローパスフィルタのアウトプットを計算する lop_perform3 です。これは、内部に格納された係数については何も感知しません。

t_int *lop_perform3(t_int *w) { t_lop *x = (t_lop *)(w[1]); float *in = (float *)(w[2]); float *coeff = (float *)(w[3]); float *out = (float *)(w[4]); long n = w[5]; float xm1 = x->x_xm1,val; while (n--) { val = *in++; // 係数シグナルベクタの個々の値を使用 *out++ = *coeff++ * (val + xm1); xm1 = val; } x->x_m1 = xm1; // 次回のために、古い状態を再保存 return w + 6;

}