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

はじめに

先日つくった正弦波を生成するsingen~を拡張して、もう少し複雑なexternにしていきます。正弦波の生成ができたので、それらを複数重ねて音作りをする加算合成シンセにしてみましょう。

とは言ってもいきなり難しいものは無理なので、下図のように基音の周波数倍音の数スペクトルのスロープ(傾き)の3つをコントロールできるものにします。一般的な加算合成法ではそれぞれの正弦波を独立してコントロールできるようにするのですが、今日のところは整数倍の成分の合成だけを考えます。つまり各倍音の周波数は基音の周波数によって決まり、独立したコントロールはできません。

これを数式で書くと以下のようになります。音合成の教科書などには必ずと言っていいほど載っている式で、スペクトルの傾きを0にするとすべての成分音のレベルが等しくなります。また、スペクトルの傾きを1にすると鋸歯波になります。(-1)k-1はなくてもいいのですが、時刻ゼロのときに振幅ゼロの鋸歯波の波形が得られるので入れています。

以下が完成したexternを使用しているイメージ。複数(multiple)の正弦波(sin)を生成(generate)するのでmulsingen~と命名しました。出力のあたりがごちゃごちゃしていますが、Pd-extendedのoutput~のようなことをしているだけです。

プログラムの説明

先日つくった正弦波を生成するsingen~との違いが大きいところだけを説明します。

クラス定義

static t_class *mulsingen_tilde_class;
typedef struct _mulsingen_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 */
  t_float numT;    /* number of tones */
  t_float slope;   /* spectral slope */

  double phase;   /* current phase */
  double sr;      /* sampling frequency */
  
  t_inlet *x_in2;
  t_inlet *x_in3;

  t_outlet *x_out;
} t_mulsingen_tilde;

基本周波数(f0)に加えて成分音の数(numT)とスペクトルの傾き(slope)を保持するために変数を宣言します。第1インレットは自動的にできるのですが、第2インレット以降は自分で作らないといけません。そのためにx_in2x_in3も用意しました。

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

/* constructor */
void *mulsingen_tilde_new(t_floatarg f) {
  t_mulsingen_tilde *x = (t_mulsingen_tilde *)pd_new(mulsingen_tilde_class);
  
  x->f0 = f;
  x->x_in2 = floatinlet_new(&x->x_obj, &x->numT);
  x->x_in3 = floatinlet_new(&x->x_obj, &x->slope);
  
  x->x_out = outlet_new(&x->x_obj, &s_signal);

  x->sr = sys_getsr();
  return (void *)x;
}

/* destructor */
void mulsingen_tilde_free(t_mulsingen_tilde *x) {
  outlet_free(x->x_out);
  inlet_free(x->x_in3);
  inlet_free(x->x_in2);
}

コンストラクタで第2・3インレットを数値型として作成します。ここで、インレットから入ってきた数値がクラスのどの変数に保存されるのかを指示します。ここでPd側につたえておくだけで、使用時には自動的に値が入ってきます。数値範囲のチェックをしたり、ルックアップテーブルを作りたいなど、入力に応じて何かをする場合には別途関数を準備しないといけませんが、今回はパスします。

デストラクタでのメモリ開放も忘れずに。

Pdへのオブジェクトの登録・DSPツリーへの登録

インレットの個数は増えていますが、入出力される信号の数はsingen~と変わらないので、この部分に変更なし。

信号処理部分

基本部分は大幅に変化しません。(singen~と違ってクラス内にサンプリング周波数を保持するようにしたりしていますが、これはどっちでもいいです。)

  R = (int)(x->sr / 2 / (double)x->f0);
  if (R > (int)x->numT) {
    R = (int)x->numT;
  }

ここではエイリアシングが発生しないように倍音数の上限を決めて、それを超えないようにしています。まずナイキスト周波数(サンプリング周波数の半分)までにいくつ倍音を入れられるかを計算してRとします。指定された成分音の数(numT)がRよりも小さい場合にのみRをnumTで書き換えます。この時点でRにはエイリアシングが発生しない最大値と指定した成分音個数のうち数の少ない方が入ります。

    tmp = 0;
    for (i=1; i<=R; i++) {
      tmp += (t_sample)(sin((double)i * TWOPI * x->phase / x->sr) / pow(i, (double)(x->slope)) * pow(-1, i-1));
    }
    *out++ = tmp / 2.0;

R個の正弦波の重ね合わせを行っています。上記の数式通りにやっています。最後に2.0で割っているのは、振幅の範囲がプラスマイナス1を超えないようにするためです。

コード全体

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

#define TWOPI 6.28318530717959

/* class declaration (data space) */
static t_class *mulsingen_tilde_class;
typedef struct _mulsingen_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 */
  t_float numT;    /* number of tones */
  t_float slope;   /* spectral slope */

  double phase;   /* current phase */
  double sr;      /* sampling frequency */
  
  t_inlet *x_in2;
  t_inlet *x_in3;

  t_outlet *x_out;
} t_mulsingen_tilde;



/* constructor */
void *mulsingen_tilde_new(t_floatarg f) {
  t_mulsingen_tilde *x = (t_mulsingen_tilde *)pd_new(mulsingen_tilde_class);
  
  x->f0 = f;
  x->x_in2 = floatinlet_new(&x->x_obj, &x->numT);
  x->x_in3 = floatinlet_new(&x->x_obj, &x->slope);
  
  x->x_out = outlet_new(&x->x_obj, &s_signal);

  x->sr = sys_getsr();
  return (void *)x;
}

/* destructor */
void mulsingen_tilde_free(t_mulsingen_tilde *x) {
  outlet_free(x->x_out);
  inlet_free(x->x_in3);
  inlet_free(x->x_in2);
}



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

  int i;
  int R;
  double ss;
  t_sample tmp;

  ss = (double)(x->slope);

  x->f0 = (t_float)(*in++);
  R = (int)(x->sr / 2 / (double)x->f0);
  if (R > (int)x->numT) {
    R = (int)x->numT;
  }

  while (n--) {
    tmp = 0;
    for (i=1; i<=R; i++) {
      tmp += (t_sample)(sin((double)i * TWOPI * x->phase / x->sr) / pow(i, (double)(x->slope)) * pow(-1, i-1));
    }
    *out++ = tmp / 2.0;
    
    x->phase += (double)(x->f0);
    while (x->sr < x->phase) {
      x->phase -= x->sr;
    }
    if (x->phase < 0) {
      x->phase = 0;
    }
  }
  
  return (w + 5);
}



/* register mulsingen_tilde to the dsp tree */
void mulsingen_tilde_dsp(t_mulsingen_tilde *x, t_signal **sp) {
  dsp_add(mulsingen_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 mulsingen_tilde_setup(void) {
  mulsingen_tilde_class = class_new(gensym("mulsingen~"),
                    (t_newmethod)mulsingen_tilde_new,
                    (t_method)mulsingen_tilde_free,
                    sizeof(t_mulsingen_tilde),
                    CLASS_DEFAULT,
                    A_DEFFLOAT, 0);
  CLASS_MAINSIGNALIN(mulsingen_tilde_class, t_mulsingen_tilde, f);

  class_addmethod(mulsingen_tilde_class,
          (t_method)mulsingen_tilde_dsp,
          gensym("dsp"), A_CANT, 0);
  
  post("mulsingen~ (c) 2016 hoge hoge");
}

コンパイルとリンク

これもsingen~と同様の方法でOK。

まとめ

リアルタイムに正弦波を生成しているので、かなり計算量が多いです。MacBook Pro, Late 2013(Core i7, 2.6 GHz)上でActivity Monitorで確認したところ、50本程度の成分音でCPUの約25%、100本で約40%、200本で約75%を使います。約300本で計算が間に合わなくなって音飛びがし始めました。僕の用途では100本も使わないので十分です。

ただ、これをシンセとして使おうとすると不十分です。ざっくり考えて、20 Hzなら可聴周波数範囲に約1000本の倍音が作れるので、この速度ではまだまだ足りません。さらにポリフォニック音源化したければなおさらです。毎回sin関数でいちいち計算しているのが遅さの原因なので、解決法としてはウェーブテーブルを使うことが考えられます。実際にPureDataの「osc~」ではウェーブテーブルを使っています。興味がある人はソースコードを読んでみてはいかがでしょうか。

後日談:同じことをMaxのgen~を使ってやってみました↓

marui.hatenablog.com