ChucKによる1bit音楽
ChucK
http://chuck.cs.princeton.edu/
長じてminiAudicleが装備され、使いやすくなった。
pdやscと比較してChucKで注目した点
- サンプル単位の演算
- 物理モデル音源
- 制御構造
特にサンプル単位の演算というのはpdのfexpr~以来で、何に使うんだという話だが、
例えばカオス式のようなものを簡単に鳴らすことができるだろう。
カオス式はscでもStandardNなどUGenとして用意されているが、式自体をいじる方法がついにわからない。
もっとも、目ぼしいところは全てUGenで網羅しているのだろうが。
scのヘルプからStandardN (standard map chaotic generator) の説明をそのまま臨書してみる。
Step s => dac; 1.0 => float r; // iteration rate in samples 1.0 => float k; // perturbation amount .5 => float x; // initial value of x .5 => float y; // initial value of y while( 1 ) { r::samp => now; (k * Math.sin(x) + y) % (2*pi) => y; (x + y) % (2*pi) => x; x => s.next; }
これがそのまま鳴ったので少々驚く。
物理モデリングについては出来合いの音源が豊富にある。
ということで、では音響合成についてどこまで深く突っ込めるのかが主な関心となる。
以下はアナログ風キックの例。
SinOsc s => dac; ADSR a => blackhole; a.set(1::samp, 0::ms, 1, 1000::ms); a.keyOn(); 1::samp => now; a.keyOff(); now + 1000::ms => time later; while( now < later ) { a.value() => s.gain; Math.pow(a.value(), 50) * 3000 + 50 => s.freq; 1::ms => now; }
この記述に至るまでかなり難航したが、これでまともなのかもよくわからない。かつ、この手の用例はまだ少ない。
examplesではサウンドファイルでドラムパターンを鳴らすというようなことをやっているが、ではそのサウンドファイルはChucKではどうやって作れるのか。
音作りに関する機能の網羅性が明らかになれば制作環境としてもっと伸びると思う。
制御構造についてはあくまでpdやscとの比較だが、普通にわかりやすい。よりアルゴリズム作曲に適しているかも知れない。
1bit音楽へ
ChucKはサンプル単位の演算が可能である。2サンプルごとに振幅の+1と-1を切り替えてみる。
Step s => dac; 1 => int val; while(1) { -1 *=> val => s.next; 2::samp => now; }
サンプリング周波数44100Hzの環境では、これは11025Hzの矩形波となる。
デジタルオーディオはサンプリング周波数の半分の周波数まで再生できる。
1サンプルごとに切り替えると再生の上限である22050Hzだが、これは私の可聴域を超えるので聴こえない。
切り替え間隔のサンプル数を増加させれば周波数は下がる。切り替えごとにサンプル数をインクリメントしてみる。
Step s => dac; int val, len; 1 => val; while(1) { -1 *=> val => s.next; len::samp => now; len++; }
急激に下がるのでアナログシンセのキック風の音になる。次は切り替え1000回ごとにインクリメントする。
Step s => dac; int val, len; 1 => val; while(1) { int cnt; len++; while(cnt<1000) { -1 *=> val => s.next; len::samp => now; cnt++; } }
明らかな下降音階が聴こえる。これはサンプリング周波数の下方倍音列だろうか。
次は切り替え回数ではなく、一定時間でインクリメントする方法を考える。1/4秒間隔。
Step s => dac; int val, len; 1 => val; while(1) { int cnt; len++; 11025/len => float d; while(cnt < d) { -1 *=> val => s.next; len::samp => now; cnt++; } }
切り替えサンプル数を数列で与えてみる。
Step s => dac; int val, idx; 1 => val; [1,2,3,4,5,6,7] @=> int len[]; while(1) { -1 *=> val => s.next; len[idx]::samp => now; (idx+1)%len.cap() => idx; }
数列が短い場合、この場合は一つの音色として聴こえる。
恐らく数列の合計が基音の周波数として認識されるが、ここに一つ数字を加えても単純に周波数が下がるわけではない。
Step s => dac; int val, idx; 1 => val; [1,2,3,4,5,6,7,8] @=> int len[]; while(1) { -1 *=> val => s.next; len[idx]::samp => now; (idx+1)%len.cap() => idx; }
数列の長さが偶数か奇数かによって、矩形波ひいては基音の聴こえ方が変わるのだろう。
次は一定時間ごとに数列を延ばしてみる。基音の変化が明らかである。
Step s => dac; int val, len, rate; 1 => val; 2 => len; 44100/2 => rate; while(1) { int cnt, total; int n; while(n < len) { n +=> total; n++; } Math.min(total, rate)$int => total; <<<total>>>; while(cnt < rate/total) { int m; while(m < len) { -1 *=> val => s.next; m::samp => now; m++; } cnt++; } len++; }
ところでここでは+1か-1かの二つの振幅しか扱っていないので、原理としては1bitオーディオということになる。
サンプル数を乱数にしてみる。
Step s => dac; 1 => int val; while(1) { Math.rand2(1, 10) => int x; -1 *=> val => s.next; x::samp => now; }
単なるノイズに聴こえる。値の範囲を変えることで帯域が変わるが、ノイズには変わりない。
当然ながら、ここに反復を導入すると個々のピッチが明らかになってくる。
Step s => dac; 1 => int val; while(1) { Math.rand2(1, 10) => int x; int i; while(i < 20) { -1 *=> val => s.next; x::samp => now; i++; } }
1bit音楽というものがあるとすれば*1、単に+1と-1を切り替えるサンプル数の列として表すことができるだろう。
振幅の要素を欠いた純粋な持続の列である。
単純な算術的操作によって興味深い構造を作る方法はいくつか考えられる。
二つの異なる周期の同時使用は最も単純な例の一つだろう。
cyc(83,101,11,67,3); fun void cyc (int ai, int am, int bi, int bm, int mul) { Step s => dac; 1 => int val; int a, b; while(1) { int i; while(i < b*mul) { -1 *=> val => s.next; a::samp => now; i++; } (a+ai)%am => a; (b+bi)%bm => b; } }
異なる周期によるインクリメントの例。
Step s => dac; 1 => int val; int a, b, c; while(1) { int d; while(d < 1000) { -1 *=> val => s.next; (a+1)%211 => a; a::samp => now; -1 *=> val => s.next; (b+1)%163 => b; b::samp => now; -1 *=> val => s.next; (c+70)%71 => c; c::samp => now; d++; } 5000::samp => now; }
(原文2009年4月、加筆転載)
*1:有名な試みとしてはhttp://www.onebitmusic.com