ならば

音とかで遊んでいたログ

エンベロープ

楽器を弾いて鳴らした音は一定の音量や音高を保ったまま鳴り続けるわけではなく、時間が経過するにつれて立ち上がりや減衰がある。この変化の仕方をエンベロープといって、楽器や弾き方によって様々で音色を特徴付ける大きな要因になっている。シンセサイザーには作った音のエンベロープを制御するためのエンベロープ・ジェネレータというモジュールがある。これは音量を制御するために使うことが一番多いらしいが、音の他の要素(例えば、周波数)を変化させるのにも使える。
ChucKには二種類のエンベロープ・ジェネレータが用意されている。

1.Envelope

現在の値から目標値まで指定した変化率で値を更新する(つまり一本の直線で表せる)単純な変化に使う。現在の値は.value、目標値は.target、変化率は.rateというパラメータで読み書きできる。*1
サイン波の音量を制御するプログラム。

SinOsc s => Envelope e => dac;

fun void test(float v, float t, float r)
{
    v => e.value;
    t => e.target;
    r => e.rate;
    // 目標値に到達する時間
    (Std.fabs(t - v) / r)::samp + now => time later;
    <<<"begin:", e.value()>>>;
    // 変化を見るために細かく時間を区切って値を出力
    while(now < later) {
        <<<e.value()>>>;
        100::ms => now;
    }
    <<<"end:", e.value()>>>;
}

test(.3, 1, .000008);  // 立ち上がり
test(.8, .2, .000003); // 減衰

.rateの代わりに継続時間で指定しても勝手に.rateを計算してやってくれる。継続時間は.timeか.durationで読み書きできる。どっちを使ってもいいけど違いは、.timeはfloat型で目標値に到達するまでの時間を秒単位で表すのに対して、.durationはdur型で目標値に到達するまでの時間そのものになる。
エンベロープ・ジェネレータは当然オシレータとは異なる音源からも接続できる。

SndBuf buf => Envelope e => dac;
"foo.wav" => buf.read;
.8 => float T;

0 => e.value;
2 => e.target;
T => e.time;  // 「T::second => e.duration;」と同じ
T::second => now;


Envelopeは音の波形を変化させること以外にも使える。時間を進めるだけでサンプリングごとに自動的に値を更新してくれるので、自分でカウンタを作るよりもスムーズで便利なことがある。Envelopeでパンを制御するプログラムを作ってみた。

SinOsc s => Pan2 p => dac;
Envelope e => blackhole;

fun void chpan(float v, float t, float r)
{
    v => e.value;
    t => e.target;
    r => e.rate;
    (Std.fabs(t - v) / r)::samp + now => time later;
    while(now < later) {
        e.value() => p.pan; 
        100::ms => now;
    }
}

chpan(-.5, .5, .000005);  // 左 -> 右
chpan(.5, -.5, .000005);  // 右 -> 左


最後に注意書き。「.rateの代わりに継続時間で指定しても勝手に.rateを計算してやってくれる」と書いたけど実は最近までこの計算にはバグがあって、0以外の値からの増減は正常に処理されなかった。これは最新バージョン(1.2.1.1)では修正されている。でもminiAudicleの最新バージョン(0.1.3.8)に含まれているChucKは二つ前のバージョンのものなので、未だにこのバグは直っていない。うぉぉ。2008年7月19日訂正:miniAudicle最新バージョン0.1.3.8に含まれているChucKのバージョンは正しくは1.2.1.1。だから0.1.3.8を使えばEnvelopeは正常に動作する。

2.ADSR

Envelopeだと単純すぎるときにはユニットジェネレータADSRを使う。ADSRという言葉自体はエンベロープを四つのパラメータで表した用語。それぞれの意味は次のようになる。

  • Attack: 立ち上がり。キーを押した後、値が最大値に到達するまでの時間
  • Decay: 減衰。Attackで到達した最大値からSustainレベルに減衰するまでの時間
  • Sustain: 持続。Decayの後、キーを離すまで持続する値
  • Release: 減衰。キーを離した後、値が減衰しきって0になるまでの時間

ユニットジェネレータのADSRではそれぞれ、.attackTime、.decayTime、.sustainLevel、.releaseTimeというfloat型のパラメータで読み書きできる。一度に全部セットしたいときは.set(dur attack, dur decay, float sustain, dur release)というのもある。鍵盤を押す/離すに対応する操作は.keyOn/.keyOff。
ホワイトノイズの音量を制御するプログラム。リズムらしきものを吐き出す。

Noise n;
Gain g => dac;
Event event;
150 => int T;

fun void noisEnv(Event e, int a, int d, float s, int r)
{
    n => ADSR env => g;
    env.set(a::ms, d::ms, s, r::ms);
    while(e => now) {
        Std.rand2f(.1, .5) => env.gain;
        env.keyOn();
        Std.rand2f(20, 1.2 * T)::ms => now;
        env.keyOff();
        Std.rand2f(20, 1.2 * T)::ms => now;
    }
}

spork ~ noisEnv(event, 1, 5, 0, 0);
spork ~ noisEnv(event, 0, 50, 0, 0);
spork ~ noisEnv(event, 0, 20, 0, 10);

while(T::ms => now) 
    maybe ? event.signal() : event.broadcast();

まだあれこれ試してないから全く分からないが、ADSRのパラメータ設定は慣れないと思うようにいかないだろう。大まかなコツとかは検索すれば色々見つかる。FlMML(MML記法)の音色パラメータ図解&楽器音テンプレというタイムリーなエントリーもある。ChucKではそのままの設定値は使えないけど、かなり参考になりそう。

*1:もっと単純に鍵盤を押す/離すに対応する操作.keyOn/.keyOffもある。envelope.ckが例