ならば

音とかで遊んでいたログ

無限加速リズム

無限音階の連続バージョンを作ったリセ(Jean-Claude Risset)は、無限音階と同じ原理をリズムに適用して、そのテンポが加速し続けるかのように聴こえる錯覚も作った。

同じ原理が使える理由は音楽におけるリズムと音高の構造が本質的には同じだからだ。ただしタイムスケールの違いからヒトにはこの二つの構造が別のものとして知覚される。リズムは構造の内部が識別できるくらいにタイムスケールが大きく、逆に音高は識別できないくらい小さい。同じ構造が繰り返されるとして周波数で考えると、大体0.5Hz〜数Hzがリズムに、20Hz〜20kHzが音高に聴こえる。手拍子がリズムに聴こえるのはそれが充分に遅いからで、仮に一秒間に440回手を叩くことができればリズムではなく「ラ」の音に聴こえるはず。ちなみにこの知覚の違いは言葉の違いにも表れていて、リズムだと「速い、遅い」という現象が、音高だと「高い、低い」に対応する。

というわけで、シェパードトーンと似たようにしてリズムのパターンを重ね合わせる。例えば下の図の*のところで音を鳴らすようにする。鳴らす拍を倍々にして重ねるのがポイント。

1拍ごと| * * * * * * * * * * * * * * * * * * * *
2拍ごと| *   *   *   *   *   *   *   *   *   *
4拍ごと| *       *       *       *       *

あとは時間経過につれて全体のテンポを速める。それと同時に、速いリズムはフェードアウト、遅いリズムはフェードインするようにそれぞれの音量を調整すれば錯覚を引き起こせる(かも)。

ChucKのプログラム。相変わらず上手く聴こえるパラメータの設定が難しい。

3 => int n;
2.3 => float baseFreq; // 拍を決める
19 => float length;    // 全体の周期(秒)
"./data/kick.wav" => string fname;
Gain g => dac;
1.0 / n => g.gain;
Event tick;

// 加速するテンポを作り出す
Step step => PulseOsc pulse => ZeroX trigger => blackhole;
Phasor phase => pulse;
1 => step.next;
1 / length => phase.freq;
baseFreq => phase.gain => step.gain;

// 2^i回のイベントごとに指定した外部ファイルを再生
fun void beat(Event e, int i) {
    SndBuf buf => g;
    fname => buf.read;
    Math.pow(2, i) $ int => int d;
    int count;
    i/(n-1) $ float => float offset;
   
    do {
	// 音量は直線的に変化
        1 - Math.fabs(offset - phase.last()/baseFreq) => buf.gain;
        if (++count >= d) {
            0 => buf.pos;
            0 => count;
        }
    } while(e => now);
}

for (int i; i < n; i++) spork ~ beat(tick, i);

while(samp => now) {
    if (trigger.last() != 0.0) {
        tick.broadcast();
    }
}

全体のテンポを決める部分にもユニットジェネレータを使った。実際のタイミング合わせはイベント。本当は trigger => tick みたいな感じでトリガーを直接イベントに接続したかったんだけど、ChucKにはそういう機能がないので仕方なくサンプリングごとにトリガーされたかどうか調べて明示的にイベントを起こした。

テンポ制御部分について。まずは初めて使ったユニットジェネレータのメモ。

  • Phasor:指定した速さで0から1まで直線的に増加する値を出力する。位相の制御に使う
  • PulseOsc:パルス波。1と-1を周期的に出力する。出力値の区間の比率はfloat型パラメータ.widthで設定できる。デフォルトは半々の0.5
  • ZeroX:ゼロ交差検出器。入力値がゼロをまたぐとき、(ゼロになったサンプリングで)その向きにパルスをひとつ出力する。つまり、入力値がマイナスからプラスになる場合1、プラスからマイナスになる場合-1を出力

これらをつないでテンポを計算する。「Step step => PulseOsc pulse」の部分でベースの周波数を作っている。オシレータに対する入力はデフォルトでは周波数同期に使われるので、この部分だけだとstepの出力値がパルス波の周波数になる。これにphaseの出力値を足すことでパルス波の周波数を増加させる。こうして変化する周波数のパルス波をゼロ交差検出器に接続して、周波数に応じた拍を作り出している。


録音したもの。シンプルすぎるんだろうか。