PureDataのexternalを作ってみた(前編)

はじめに

MacPureDataのオーディオexternal objectを作ってみたメモ。日本語で書かれた情報が少なかったので、「無いよりはマシ」程度で書き残しておきます。いろいろと間違いがあると思います。指摘していただければ幸いです。

まず参考にしたのはPd-externals-HOWTOです。この文書では4つのexternを説明してくれます。

  1. bangが来たらhelloを表示するもの。内部に状態を保存しない、とても単純なオブジェクトの例。
  2. 内部に数を保存するカウンターの例。bangがやってきたら、1を加算して返す。と同時に新しい(1が加算された)数を保存しておく。
  3. カウンターの機能を拡張して、下限・上限の設定、加算する数の変更、カウンター数値を下限値にリセット、などを可能にする例。可変個数のメッセージが来た時の対応や、インレットを増やす方法などを説明。
  4. オーディオ信号の加工をする例。2つのオーディオ信号が入ってきて、それらのミックスバランスを変化させられるextern。(筆者は「pan~」と呼んでるけど「mixer~」のほうが良かったと思う)

今回はこれらとは少し違う、「osc~」のような「周波数の数値入力に対して正弦波をオーディオ信号として出力するオブジェクト」を作ってみます。正弦波(sin波)を生成(generate)するので、名前は「singen~」とします。最終的にはこんな感じで使いたい。

プログラムの説明

PdではC言語オブジェクト指向な雰囲気に書くのが流儀のようです。そのため、クラスの宣言、定義、コンストラクタ・デストラクタなどを書いてゆきます。

ヘッダ部

#include <math.h>
#include "m_pd.h"

ヘッダファイルの読み込みです。Pd externを作るときにはm_pd.hが必須です。Mac版のPdだとアプリケーション・パッケージの中に入っているので、コンパイル時にパスを通します(僕の環境では/Applications/Pd-0.4x-x.app/Contents/Resources/include/m_pd.hにありました)。あと、sin関数を使いたいのでmath.hも。

#define TWOPI 6.28318530717959

正弦波を生成するときに2πを使うので、定数として用意しておきます。

クラス定義

static t_class *singen_tilde_class;

typedef struct _singen_tilde {
  t_object x_obj;
  t_float f;

  t_float f0;
  double phase;

  t_outlet *x_out;
} t_singen_tilde;

ここではクラス(構造体)の宣言をします。C++Javaのようなクラスとの違いは、メソッド(関数)が含まれないことです。これはC言語なのでしょうがないですね。なので、ここにはデータの宣言だけが入ることになります。

中身の先頭にあるx_objはそのオブジェクトの様々な情報を持っていて、どのexternでも必須です。おまじないとして必ず最初に書いておきます。オーディオ・オブジェクトはインレットにオーディオ信号が入ってくることを想定しています(自動的にオーディオ入力のインレットが作られます)。信号ではなく数値が入ってきた場合に備えるダミーの変数としてfを作っています。ここまでは常套句で、信号を扱うオブジェクトの場合はほぼ同じようになると思います。

次のf0phaseは僕がオブジェクト内部に保管しておきたいデータです。それぞれ、生成したいサイン波の周波数と、正弦波の中で「いま再生している箇所の位相」を指す数値です。後者をどう使うかは、あとで詳しく説明します。t_〇〇〇という名前の変数や型はm_pd.hで宣言されています。クラスの内部だけで使う変数であれば自由な型が使えますが、[singen~]を呼び出すPdパッチ側に公開してデータのやりとりをする変数はt_〇〇〇を使って宣言します。phaseは外部に公開しないのでdouble型にしていますが、t_float型にしておいても構わないと思います。

最後に信号を出力するためのアウトレットがいるので、その宣言です。(このクラスではインレットの明示をしていませんが、信号を扱うオブジェクトの場合はひとつめのインレットは自動的に準備されますので指定する必要はありません)

コンストラクタとデストラク

void *singen_tilde_new(t_floatarg f) {
  t_singen_tilde *x = (t_singen_tilde *)pd_new(singen_tilde_class);
  
  x->f0 = f;
  
  x->x_out = outlet_new(&x->x_obj, &s_signal);
  
  return (void *)x;
}

上記で宣言したクラスが実際にオブジェクトとして生成されるときにまず実行されるのがこのコードです。pd_new()が生成のためのコードですが、この部分は常套句ですので自分の作っているexternにあわせて型の名前を書き換えます。大事なのはアウトレットの生成部分(outlet_new())でしょうか。第1引数はオブジェクト自身、第2引数は出力がオーディオ信号であることを表します。

void singen_tilde_free(t_singen_tilde *x) {
  outlet_free(x->x_out);
}

こちらは対応するデストラクタです。コンストラクタの中で動的に生成したアウトレットをここで廃棄します。これをしておかないとメモリリークが発生してしまいます。

Pdへのオブジェクトの登録

void singen_tilde_setup(void) {
  singen_tilde_class = class_new(gensym("singen~"),
                    (t_newmethod)singen_tilde_new,
                    (t_method)singen_tilde_free,
                    sizeof(t_singen_tilde),
                    CLASS_DEFAULT,
                    A_DEFFLOAT, 0);

  class_addmethod(singen_tilde_class,
          (t_method)singen_tilde_dsp,
          gensym("dsp"), A_CANT, 0);
  
  CLASS_MAINSIGNALIN(singen_tilde_class, t_singen_tilde, f);

  post("singen~ (c) 2016 hoge hoge");
}

Pdに「私はこれこれこういうexternですよ」と伝える部分だと理解しています。具体的にclass_new()の引数は、オブジェクト名称、コンストラクタ関数名、デストラクタ関数名、クラス(構造体)の大きさ、クラスの種類(いつもデフォルトでいいみたい)と続きます。そのあとにオブジェクトが取れる引数を列挙します。今回は数値型が一つでいいのでA_DEFFLOATだけです。他にも引数が欲しい時にはここに追加していきます。最後に「これ以上の引数はないよ」を示すゼロを置きます。

class_addmethod()では、クラスにsingen_tilde_dsp()メソッドを追加します。これはPd側に、このクラスは信号処理を行うものであることを伝えるというイメージです。

続くCLASS_MAINSIGNALIN()は一つ目のインレットには信号が入ってくることを宣言するマクロだそうです。クラス名、構造体の型名、入力が信号でなかったときに数値として引き受けるダミー変数名の3つを指定しています。

post("singen~ (c) 2016 hoge hoge");は、オブジェクトが読み込まれた時にPd画面に表示するメッセージです。自分の好きなものに書き換えましょう。もちろん、この1行を削除して静かに起動するようにしてもいいと思います。

DSPツリーへの登録

void singen_tilde_dsp(t_singen_tilde *x, t_signal **sp) {
  dsp_add(singen_tilde_perform,
      4, /* number of following pointers */
      x,
      sp[0]->s_vec, /* in signal */
      sp[1]->s_vec, /* out signal */
      sp[0]->s_n);  /* length of the signal vector (s_vec) */
}

実際の信号処理を請け負うsingen_tilde_perform()dsp_add()で登録します。singen_tilde_dsp()にはt_singen_tilde *xt_signal **spの二つの引数があります。xにはクラスのデータ部分、spには入出力信号へのポインタが入ってきます。spの中はインレットとアウトレットの信号の配列が左上を起点に時計回りの順に入っています(下図参照)。

今回はインレットとアウトレットがひとつずつなのでsp[0]->s_vecがインレット、sp[1]->s_vecがアウトレットです。もしより多くのインレット・アウトレットを使う時にはこの部分がsp[2]sp[3]……と増えていきます。(それに合わせてポインタ数の4も増やします)

sp[0]->s_nには各信号配列の長さが入っていますので、これも渡してあげます。この中身はデフォルトでs_n=64なのですが、Pdパッチ上でblock~などを使って変更される可能性があるので決め打ちにできません。

信号処理部分

実際の信号処理を行う部分です。少し長いので、分割しながら説明します。

t_int *singen_tilde_perform(t_int *w) {
  t_singen_tilde *x   = (t_singen_tilde *)(w[1]);
  t_sample       *in  =       (t_sample *)(w[2]);
  t_sample       *out =       (t_sample *)(w[3]);
  int               n =              (int)(w[4]);

  double samprate;
  samprate = sys_getsr();

  x->f0 = (t_float)(*in++);

wには上記dsp_add()で登録した通りの順番でデータ類が入っているので、使いやすい変数名でそれら(へのポインタ)を受け取ります。繰り返しになりますが、xはsingen~オブジェクトに関わる情報(特にここではf0phaseを使っていきます)、inoutは入出力信号へのポインタで、各々の長さがnに入ります(上記のs_nと同じです)。

また、Pd実行時のサンプリング周波数を何度か参照するのでsamprateを準備します。sys_getsr()が現在のサンプリング周波数を取ってくる関数です。

一応Pd側に登録したクラスではインレットへの入力は信号型ということになっていますが、実際に入ってくる情報は周波数なので数値で受け取りたいです。そこで、inに入ってきた信号?を数値型にキャストしてデータとして保持します。これが正しいやり方なのか分からないのですが、singen~オブジェクトにナンバーボックスから数値を送ってもsig~を経由させても同様に動いているようなので、今の所これでヨシとしています。

  while (n--) {
    *out++ = (t_sample)(sin(TWOPI * x->phase / samprate));
    
    x->phase += (double)(x->f0);
    while (samprate < x->phase) {
      x->phase -= samprate;
    }
    if (x->phase < 0) {
      x->phase = 0;
    }
  }

ここが信号の生成を行っている心臓部です。while()nで指定された回数(デフォルトで64サンプル)ぶんの信号を生成してoutに入れていきます。見慣れない方法で正弦波を作っているように見えるかもしれないので、説明します。この部分は「Pdのexternを作る」こととはあまり関係がないので、以下は飛ばしてしまってもいいです。

*out++で始まる行では、1 Hzの正弦波からphaseが指す位置の値を取ってきています。下図では灰色の点(時刻i)を取ってきているとしましょう。次に、whileループの次回繰り返しの時に取る位置(時刻i+1の赤色の点)を計算しておきます(コードではx->phase +=で始まる行)。ここでは取る位置を生成したい周波数(x->f0)のぶんだけ増加させています。その後、phaseがサンプリング周波数を超えるようなことがないような処理をしています。

これは、周波数が1秒間の周期の数であることを利用しています。例えばサンプリング周波数が44100 Hzで所望の正弦波の周波数が441 Hzであったとしましょう。コードでは1 Hzの正弦波(つまり信号長は44100サンプル)から値を取ってくることになっていますので、441サンプルごとに信号を取っていくことになります。44100サンプル÷441サンプル=100なので、ループが100回まわると1周期になります。outからは100回のループ(=100サンプル)で1周期の正弦波が出力されるということです。ここで、「周波数とは1秒間の周期の数である」を思い出して、1秒あたりの周期の数を計算すると、1秒あたりのサンプル数(44100)÷1周期あたりのサンプル数(100)=441となり、周波数が441 Hzだと分かりました。

原理としてはウェーブテーブルからの読み出しと同じですが、ウェーブテーブルを使う代わりにsin関数を毎回呼び出すという(シンプルながら計算量が増える)ことをしています。

さて、最後です。

  return (w + 5);
}

ポインタwの参照先を5だけ増やします。これはdsp_add()の引数の数と一致させます。

コード全体

#include <math.h>
#include "m_pd.h"

#define TWOPI 6.28318530717959

/* class declaration (data space) */
static t_class *singen_tilde_class;
typedef struct _singen_tilde {
  t_object x_obj;  /* always necessary to hold data about the object */
  t_float f;       /* dummy variable to get a float value from the 1st inlet */

  t_float f0;      /* fundamental frequency */
  double phase;    /* current phase */

  t_outlet *x_out;
} t_singen_tilde;



/* constructor */
void *singen_tilde_new(t_floatarg f) {
  t_singen_tilde *x = (t_singen_tilde *)pd_new(singen_tilde_class);
  
  x->f0 = f;
  
  x->x_out = outlet_new(&x->x_obj, &s_signal);
  
  return (void *)x;
}

/* destructor */
void singen_tilde_free(t_singen_tilde *x) {
  outlet_free(x->x_out);
}



/* real work happens here */
t_int *singen_tilde_perform(t_int *w) {
  t_singen_tilde *x   = (t_singen_tilde *)(w[1]);
  t_sample       *in  =       (t_sample *)(w[2]);
  t_sample       *out =       (t_sample *)(w[3]);
  int               n =              (int)(w[4]);

  double samprate;

  x->f0 = (t_float)(*in++);
  samprate = sys_getsr();
  
  while (n--) {
    *out++ = (t_sample)(sin(TWOPI * x->phase / samprate));
    
    x->phase += (double)(x->f0);
    while (samprate < x->phase) {
      x->phase -= samprate;
    }
    if (x->phase < 0) {
      x->phase = 0;
    }
  }

  return (w + 5);
}



/* register singen_tilde to the dsp tree */
void singen_tilde_dsp(t_singen_tilde *x, t_signal **sp) {
  dsp_add(singen_tilde_perform,
      4, /* number of following pointers */
      x,
      sp[0]->s_vec, /* in signal (which we think is float) */
      sp[1]->s_vec, /* out signal */
      sp[0]->s_n);  /* length of the signal vector (s_vec) */
}



/* generation of a new signal class */
void singen_tilde_setup(void) {
  singen_tilde_class = class_new(gensym("singen~"),
                    (t_newmethod)singen_tilde_new,
                    (t_method)singen_tilde_free,
                    sizeof(t_singen_tilde),
                    CLASS_DEFAULT,
                    A_DEFFLOAT, 0);

  class_addmethod(singen_tilde_class,
          (t_method)singen_tilde_dsp,
          gensym("dsp"), A_CANT, 0);
  
  CLASS_MAINSIGNALIN(singen_tilde_class, t_singen_tilde, f);

  post("singen~ (c) 2016 hoge hoge");
}

コンパイルとリンク

コンパイルMac OS X 10.11.6、Xcode 7.3.1、Pd-0.47-1-64bit.appの環境で行いました。XcodeIDEを使ったわけではなく、コマンドラインツールをターミナルから使いました。

以下のコマンドでsingen~.pd_darwinができます。

$ cc -I/Applications/Pd-0.47-1-64bit.app/Contents/Resources/src -fPIC -arch i386 -arch x86_64 -c -o singen~.o singen~.c
$ cc -dynamic -bundle -undefined dynamic_lookup -arch i386 -arch x86_64 -o singen~.pd_darwin singen~.o

-arch i386 -arch x86_64をつけているのは、32ビット・64ビット両方のバイナリを持つファットバイナリを作るため。Pd-extendedの32ビット版を使ってる人もいるだろうから、両対応している方が優しい。

あとは呼び出す側のPdパッチと同じフォルダに入れておくと使えるようになります(PureData本体の再起動が必要)。

【2023-10-30追記】Apple Silicon対応のビルド方法を「PureDataのexternalを作ってみた(Apple Silicon対応版バイナリを作る) - 丸井綜研」に書きました。

参考

基本的に英語ばかり。

  • 何よりも、まずはPd-externals-HOWTOを読まないと、です。
  • Max SDKのAPI文書は詳しすぎてお腹いっぱいになってしまいます。直接PureDataではないしね。
  • Eric Lyon『Designing Audio Objects for Max/MSP and Pd』は僕も持ってますが、MaxとPd両方をターゲットにオーディオだけをとりあげたextern開発の本です。もし英語ができてお金の余裕があって少し古い情報でもよければ、買ってもいいかもです。付録CD-ROMのコードが古いのですが、2014年にCycling 74のサイトでMax 6用に修正したものが配布されていました。
  • 一番参考になったのはPureDataソースコードの「osc~」。最終的にはソースを読むのが大吉。Mac版アプリケーションにも/Applications/Pd-0.47-1-64bit.app/Contents/Resources/src/d_osc.cに入ってます。

僕のが貸し出し中で確認できなかったのですが、美山千香士さんの『PureData チュートリアル&リファレンス』にもexternの作り方に関する記述があったはず。extern作らない人にもオススメの良書です。


だいぶ長くなってしまいましたが、後編に続きます。

marui.hatenablog.com