SuperCollider自主練 - 基本編

ここしばらく、openFrameworksやProcessingを主につかっているうちに、音から遠ざかってしまってる。もともとは、コンピュータ音楽が専門なのに…。ということで、この夏もういちど音と向きあおうと思いたち、SuperColliderに再入門してみようかと思い立つ。教材は、MIT Pressから刊行された"The SuperCollider Book“。この本を読みながら、SCの基本をもういちどおさらいしてみたい。

まずは、最初の章から順番に。

Chapter 1: Beginner’s Tutorial

1.1 Hello World

まずは、基本から。

初心者向けチュートリアルといいながら、いきなり冒頭でオシレータ、LFO、エンベロープなどの音響合成の基礎知識を前提としていると宣言される。この突き放し感がSCらしい感じ。1章のねらいは言語の成り立ちを詳細に学ぶというよりは、実際に音のでるサンプルを通してSCで出来ることを体験していこうという主眼のよう。

まずは、Hello Worldと、サーバーのブート:

"Hello world"
Server.default = s = Server.internal.boot;

そして、実際に音の出るサンプル。シンプルだけど出てくる音はなかなか面白い。

play({SinOsc.ar(LFNoise0.kr(12, mul: 600, add: 1000), 0.3)})

ここで簡単にプログラムの実行方法や停止のやりかた、ボリュームの上げ下げなどの基本操作の解説と、エラーメッセージの読み方など。さらにもう一つ音の出るサンプル。

play({RLPF.ar(Dust.ar([12, 15]), LFNoise1.ar(1/[3, 4], 1500, 1600), 0.02)})

ここで、Dust.arの仲の12と15や、LFNoise1の3と4の数値をいろいろ変化させてみろ、との指示。こうやって実際に数値を変えてすぐに実践できるのがSCの良さの一つかもしれない。

ここまでで、オーディオシグナル".ar"の意味や関数"{…}“の意味などの説明は一切ないが、まあまずは体験しながらということなのだろう。

次にいままでは1行で書いていたプログラムを機能ごとに改行してインデントした、加算合成のサンプル。こうやって書くとわかりやすい。

play({
    var sines = 10, speed = 6;
    Mix.fill(sines, 
        {arg x; 
            Pan2.ar(
                SinOsc.ar(x+1*100, 
                    mul: max(0, 
                        LFNoise1.kr(speed) +
                        Line.kr(1, -1, 30)
                    )
                ), rand2(1.0))})/sines})

このサンプルでは、新たに関数の中での変数(var)の使用や、引数(arg)なども出てきているが、ここでもあまり具体的な解説はなし。「この記述がギリシャ語のように感じるかもしれないが、おいおい学んでいきましょう」という感じ。

1.2 メッセージと引数

メッセージと引数という概念の紹介。

メッセージとは、複数のコンマで区切られたパラメータのリストをもった小文字で始まる命令。

message(arg1, arg2, arg3…)

メッセージは、大文字で始まる言葉(つまりクラスのことか?)の後に接続されることもある。

SinOsc.ar(arglist)
Mix.fill(arglist)

他のOOP言語に慣れていると、逆にオブジェクトに対するメッセージだけでなくメッセージ単体で使用できることの方が奇異に感じるが、ここらへんはSmallTalkから来ているものなのだろうか?

メッセージの例としてランダムな値の生成について。単純な乱数と指数範囲での乱数exprandの紹介など。引数にfloat()小数点以下の数)を入れると出力もfloatになり、整数(int)だと出力結果もintとなるなど。

rand(100)
exprand(1.0, 100.0)

例えば、10から100のランダムの数値を使用する場合でも、音程をランダムに生成する場合にはexprandomを使うべきという話。なぜなら我々の耳は音程を指数的に知覚しているから。こうした乱数の説明も、音を基本にしているところがSC的。

ランダムシードを指定して、常に同じランダム数列を得る方法もある。これは知らなかった…

thisThread.randSeed = 666; {rand(10.0)} ! 10;

そのあとは様々なメッセージの紹介。くりかえし、小数点の桁の切り捨て、ソートなど。

dup("echo", 20)
round([3.141, 5.9265, 358.98], 0.01)
sort([23, 54, 678, 1, 21, 91, 34, 78])
round(dup({exprand(1, 10)}, 100), 0.1)
sort(round(dup({exprand(1, 10)}, 100), 0.1))

こういったメッセージを必要に応じてすぐに使用できるようになると、気の効いたプログラムがつくれそう。

1.3 ネスティング(Nesting)

ネスティングについて。SCではあるメッセージの出力結果を、そのまま別のメッセージの引数として使用できる。それをさらに別のメッセージの引数にしていってというように、どんどんネストしていけるという話。

実例がわかり易い。

exprand(1.0, 1000.0)
dup({exprand(1.0, 1000.0)}, 100)
sort(dup({exprand(1.0, 1000.0)}, 100))
round(sort(dup({exprand(1.0, 1000.0)}, 100)), 0.01)

こうしたネストの使用を音響合成の例でみてみる。

play(
    {
        CombN.ar(
            SinOsc.ar(
                midicps(
                    LFNoise1.ar(3, 24, 
                        LFSaw.ar([5, 5.123], 0, 3, 80)
                    )
                ), 
                0, 0.4), 
            1, 0.3, 2)
    }
)

様々なユニットジェネレータがネストされている。

1.4 レシーバー.メッセージ、コメント (Receiver.message, Comments)

1.3の例では、大文字で始まる命令にメッセージがドット(.)で接続されていた。SinOsc.ar、LFNoise1.ar など。これらはユニットジェネレータ(UGens)と呼ばれ、デジタルオーディオに特化した数値のストリームを出力する。これらはより広範な定義でいうと「レシーバー(Receiver)」と呼ばれるもの。レシーバーは、引数をもったメッセージからの指令によって動作する。

例えば、

LFNoise1.kr(10,100)

これは「-100から100の範囲でランダムな値を生成するタイプ1の低周波のノイズ生成器でノイズを1秒に10個生成する」という意味になる。

数(Numbers)、関数(functions)、配列(arrays)、文字列(strings)もメッセージをドットで継いで何をするか伝えることができる。

[45, 13, 10, 498, 78].sort
"echo".dup(20)
50.midicps
444.cpsmidi
100.rand
{100.rand}.dup(50)
[1.001, 45.827, 187.18].round(0.1)
"I've just picked up a fault in the AE35 unit".speak

などなど。

ところで、ちょっと前のサンプルで出てきた、rand(100) と 100.rand はどう違うのか? 結論からいうと両者は同じ。rand(100) は関数的な表記法。100.rand はメッセージによる表記になる。どちらの記述方法を使うかはケースバイケース。

メッセージを利用した書式は、ドットで継いでいくことでネストを実現できる。

1000.0
1000.0.rand
1000.0.rand.round(0.01)
1000.0.rand.round(0.01).post
{1000.0.rand.round(0.01).postln}.dup(100).plot
{1000.0.rand.round(0.01).postln}.dup(100).postln.sort.plot
1000.0.rand.round(0.01).postln.asString.speak

1.5 エンクロージャー (Enclosures)

ここまでで出てきた、4種類のエンクロージャー(つまり括弧)を整理。個人的には、この括弧の種類によって何をしているのか正しく類推できるようになるのが、SC理解のポイントのような気がしている。

これらの括弧の開始と終了をマッチさせていかないといけないよという話。エディタで括弧の先頭や末尾でダブルクリックするとその括弧の範囲が選択されるので、それを利用するのも一つの方法。

クォーテイション “”, ‘’

“” ダブルクォーテイションは文字列をあらわす。それに対して'‘シングルクォーテイションは、シンボルをあらわす 'aSymbol’ シンボルはパラメータのラベルなどに用いられる。ちなみに、'aSymbol' と \aSymbol は同じ。

(parentheses) 丸括弧

() 丸括弧は引数のリストをあらわす

msg(arg1, arg2, arg3…)

また丸括弧は計算の順序を指定する際にも使用される。

5 + 10 * 4

これは、普通の数学では45になるが、SCでは60になる(意外…)。SCでは演算子の種類を考慮せず左から右に評価される。かけ算を先に計算させる場合は明示的に

5 + (10 * 4)

と書かなくてはならない。丸括弧はまた二項演算とメッセージを一体化する際にも使用される。

(5 + 10).squared

[brackets] 大括弧

[]大括弧はアイテムの集合(collection)をあらわす。集合の一種Arrayは数字、テキスト、関数、パッチ全体など様々な種類のアイテムを含むことができる。さらには、一つのArrayでデータの種類を混ぜることも可能。

またArrayには様々なメッセージが定義されている。reverse, scramble, mirror, rotate, midicps, choose, permuteなど。

実際に提示されている例が、音列の操作と関連づけられていて楽しい。

[0, 11, 10, 1, 9, 8, 2, 3, 7, 4, 6, 5].reverse // 12音技法の逆行形
12 - [0, 11, 10, 1, 9, 8, 2, 3, 7, 4, 6, 5].reverse // 逆行形の反行形
[0, 2, 4, 5, 6, 7, 9, 11].scramble // ダイアトニックスケール
[60, 62, 64, 67, 69].mirror // ペンタトニック
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].rotate
[60, 62, 64, 65, 67, 69, 71].midicps.round(0.1) // MIDIから周波数へ
[1, 0.75, 0.5, 0.25, 0.125].choose // 音の長さなど?
0.125 * [1, 2, 3, 4, 5, 6, 7, 8].choose // クォンタイズ
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].permute(6) // 順序の変更

{} 中括弧

{}中括弧は関数(functions)をあらわす。まず初めの例として以下の2つの結果の違いについて。

exprand(1, 1000.0)
{exprand(1, 1000.0)}

上のほうは、毎回評価する度に1から1000の間の指数的な乱数を出力する。ところが下の例では、常に"a Function"という出力がされる。では関数(Function)とは何なのか? 以下のサンプルで考察してみる。

dup(rand(1000.0), 5)   // ランダムに数を選択して、それをくりかえす
dup({rand(1000.0)}, 5) // ランダムに選択するという関数(機能)を、くりかえす

上の行は、ランダムに選択された数値を5回同じ値でくりかえす。それに対して下の行の場合は、毎回違う値を5回くりかえすことになる。(つまりは、ランダムな値をピックアップするという機能をくりかえしているということか?)

下の行のケースは、下記のように理解するとわかりやすい。

[rand(1000.0), rand(1000.0), rand(1000.0), rand(1000.0), rand(1000.0)]

関数は様々なメッセージを解釈する。plot, play, scope, dup… など。

{LFNoise0.ar}.play  // ランダムな数列を再生(ノイズ)
{LFNoise0.ar(10000)}.plot // ランダムな数列をプロット
{LFNoise0.ar(10000)}.scope // 再生して波形を表示

{100.rand}.dup(10) // 10コのランダムな数を選択
{100.rand} ! 10 // 上と同じ
{100.rand}.dup(10).postln.plot // 10個の数を選択し、表示した後プロット
{100.rand}.dup(100).sort.plot // 100個の数を選択し、ソートした後プロット

まとめ

ここまで出てきた括弧の機能をまとめると…

1.6 マルチチャンネル

「1.5 エンクロージャー」のセクションでは、大括弧[]は集合をあらわすという説明だった。ところが、SCの大括弧にはもう一つ意味があって、マルチチャンネルをあらわすという話。

UGen(ユニットジェレータ)の引数にArrayが用いられた場合には、いかなる場合にもマルチチャンネルとみなされる。

{Blip.ar(25, LFNoise0.kr(5, 12, 14), 0.3)}.play // 単一チャンネル
{Blip.ar(25, LFNoise0.kr([5, 10], 12, 14), 0.3)}.play // ステレオ
{Blip.ar(25, LFNoise0.kr([5, 10, 2, 25], 12, 14), 0.3)}.play // 4ch
{Blip.ar(25, LFNoise0.kr([5, 4, 7, 9, 5, 1, 9, 2], 12, 14), 0.3)}.play // 8ch

1.7 ヘルプ

大文字で始まるアイテム(SinOsc, LFSaw, LFNoise, PMOsc, Array, Mix …etc.)や、多くのメッセージ(midi cps, max, loop, random …)はヘルプを持つ。ヘルプを参照したい単語を選択し、cmd-dヘプルを参照できる。

新たなUGen、PMOscを例にヘルプの使いかたを考察。PMOScは位相を変調できるオシレータ、つまりFM合成に利用できる。少ないパラメータで豊かな倍音をもった音響を生成できる。

{PMOsc.ar(440, 550, 7)}.play

PMOscのヘルプを参照すると、その引数の内容が示されている。

ar(carfreq, modfreq, pmindex = 0, modphase = 0, mul = 1, add = 0)

※ キャリア周波数, モジューレタ周波数, インデックス値, モジュレータの位相、全体のスケール、直流成分

既に初期値が設定されている引数(pmindex, mod phase, mil, add)は省略することが可能であるということを意味している。

ネスティングを使用して、より面白いサウンドにしてみる。MouseXとMouseYを使用して、マウスからの入力でモジュレータの周波数とインデックス値を変更できるようにしている。

{PMOsc.ar(440, MouseY.kr(1, 550), MouseX.kr(1, 15))}.play

引数の記述に関するきまりごと。引数はあらかじめ指定された順番で記入していかなくてはならない。ひとつ飛ばしたり順番を変更は基本的にできない。

{PMOsc.ar(100, 500, 10, 0, 0.5)}.play

しかし、キーワードのあとにコロンを付けて引数を指定するという別の方法もある。こうすることで、確実に引数の内容を指定することができ、記述する順番なども関係なくなる。

{PMOsc.ar(carfreq: 100, modfreq: 500, pmindex: 10, mul: 0.5)}.play
{PMOsc.ar(carfreq: 100, mul: 0.5, pmindex: 10, modfreq: 500)}.play //順番変更

ヘルプの活用の実践として、もうひとつ別のサンプルで考察。

{
    Blip.ar(
        TRand.kr( // frequency or VCO
            100, 1000, // range
            Impulse.kr(Line.kr(1, 20, 60))), // trigger
        TRand.kr( // number of harmonics or VCF
            1, 10, // range
            Impulse.kr(Line.kr(1, 20, 60))), // trigger
        Linen.kr( // mul, or amplitude, VCA
            Impulse.kr(Line.kr(1, 20, 60)), // trigger
            0, // attack
            0.5, // sustain level
            1/Line.kr(1, 20, 60)) // trigger
        )
}.play

VCO、VCF、VCAという構成の古典的なアナログシンセサイザーを模した減算合成のサンプル。ネストされた要素の内側から外側へと読み解いていく。

3つのLine.krのインスタンスが、それぞれImpulseへ数を送出している。それらが、LinenとTRandをトリガーしている。2つのTRandは、Blipの周波数とハーモニクスの数をランダムに決定している。

ここで、.krと.arの違いについての解説。UGenへのメッセージ.krと.arは、生成される数値のストリームの数を決定している。.arはオーディオレイトの略で、サンプリング周波数と同じ数のストリームを生成(44.1kHzだったら毎秒44100のストリーム)。.krはコントロールレイトの略で、一秒に1ダースほどしか数を生成しない。.krをコントロールに関する部分に使用する計算量の節約になる。

もう1つのImpulseは、Linenをトリガーしている。これはエンベロープと呼ばれるもので、音量の変化の形を作りだしている。Linenの引数は順番に、トリガー、アタックタイム、サステイン(持続)レベル、ディケイ(減衰)タイム。

このサンプルでは、3つのImpulseと2つのTRandと4つのLineといったUGenがそれぞれ独立して使用されてる。これらを連動させていくにはどうすれば良いのか? そのためには、変数を用いなければならない。

1.8 変数

SCでは、小文字のa〜zは、宣言なしに変数として使用できる。SCの変数は、数値、文字列、UGen、関数など様々な値を格納できる。代入にはイコール(=)を用いる。

変数を使用した簡単なサンプル。

(
a = 440;
b = 3;
c = "math operations";
[c, a, b, a*b, a + b, a.pow(b), a.mod(b)]
)

これは、以下のプログラムと同じ。

["math operations", 440, 3, 440*3, 440 + 3, 440.pow(3), 440.mod(3)]

次にマウスの位置でSinOscのエンベロープを変化させる例。変数を使用することで、Impuluseのタイミングと減衰時間を一致させている。

(
{
r = MouseX.kr(1/3, 10);
SinOsc.ar(mul: Linen.kr(Impulse.kr(r), 0, 1, 1/r))
}.play
)

さらに変数を使用したより複雑なサンプル。「1.7 ヘルプ」で使用した減算合成のサンプルを変数を使用して書き直している。

(
p = { // 変数pに関数を代入している
r = Line.kr(1, 20, 60); // 演奏タイミング
// r = LFTri.kr(1/10) * 3 + 7;
t = Impulse.kr(r); // トリガー
// t = Dust.kr(r);
e = Linen.kr(t, 0, 0.5, 1/r); // エンベロープを生成
f = TRand.kr(1, 10, t); // ランダムな値をトリガーから生成
// f = e + 1 * 4;
Blip.ar(f*100, f, e) // 生成された値でBlipを合成
}.play
)

p.free;  // 演奏の終了

変数を使用することで、すぐに全体の挙動を変更できる。たとえば、r = Line.kr(1, 20, 60); を r = LFTri.kr(1/10) * 3 + 7; に変更するだけで演奏タイミングが変化する。

周波数も、f = TRand.kr(1, 10, t); から f = e + 1 * 4; とするだけですぐに変化する。

ここで、テスト。

(
{ // carrier and modulator not linked
    r = Impulse.kr(10);
    c = TRand.kr(100, 5000, r);
    m = TRand.kr(100, 5000, r);
    PMOsc.ar(c, m, 12)*0.3
}.play
)

(
{
    var rate = 4, carrier, modRatio; // declare variables
    carrier = LFNoise0.kr(rate) * 500 + 700;
    modRatio = MouseX.kr(1, 2.0);
    // modulator expressed as ratio, therefore timbre
    PMOsc.ar(carrier, carrier*modRatio, 12)*0.3
}.play
)

問題

  1. ステレオにせよ
  2. indexの範囲を、Linke.krを使って1から12まで変化するようにせよ
  3. それぞれのreceiver.messageのペアの引数を定義せよ
  4. 演奏スピードを、また別のLine.krを使用して、1から20まで変化するようにせよ
  5. 全ての引数をキーワード付きにせよ

実際に解いてみた。

ステレオにせよ。

(
{ // carrier and modulator not linked
    r = Impulse.kr([10, 10]); // ステレオに
    c = TRand.kr(100, 5000, r);
    m = TRand.kr(100, 5000, r);
    PMOsc.ar(c, m, 12)*0.3
}.play
)

(
{
    var rate = 4, carrier, modRatio; // declare variables
    carrier = LFNoise0.kr([rate, rate]) * 500 + 700; // ステレオに
    modRatio = MouseX.kr(1, 2.0);
    // modulator expressed as ratio, therefore timbre
    PMOsc.ar(carrier, carrier*modRatio, 12)*0.3
}.play
)

indexの範囲を、Linke.krを使って1から12まで変化するようにせよ

(
{ // carrier and modulator not linked
    r = Impulse.kr([10, 10]); // ステレオに
    c = TRand.kr(100, 5000, r);
    m = TRand.kr(100, 5000, r);
    i = Line.kr(1, 12, 60); // 
    PMOsc.ar(c, m, i)*0.3
}.play
)

(
{
    var rate = 4, carrier, modRatio, index; // declare variables
    carrier = LFNoise0.kr([rate, rate]) * 500 + 700; // ステレオに
    modRatio = MouseX.kr(1, 2.0);
    index = Line.kr(1, 12, 60); 
    // modulator expressed as ratio, therefore timbre
    PMOsc.ar(carrier, carrier*modRatio, index)*0.3
}.play
)

それぞれのreceiver.messageのペアの引数を定義せよ。※これはちょっと面倒なんで省略

演奏スピードを、また別のLine.krを使用して、1から20まで変化するようにせよ

(
{ // carrier and modulator not linked
    t = Line.kr(1, 20, 60);
    r = Impulse.kr([t, t]); // ステレオに
    c = TRand.kr(100, 5000, r);
    m = TRand.kr(100, 5000, r);
    i = Line.kr(1, 12, 60); // 
    PMOsc.ar(c, m, i)*0.3
}.play
)

(
{
    var rate, carrier, modRatio, index; // declare variables
    rate = Line.kr(1, 20, 60);
    carrier = LFNoise0.kr([rate, rate]) * 500 + 700; // ステレオに
    modRatio = MouseX.kr(1, 2.0);
    index = Line.kr(1, 12, 60); 
    // modulator expressed as ratio, therefore timbre
    PMOsc.ar(carrier, carrier*modRatio, index)*0.3
}.play
)

全ての引数をキーワード付きにせよ。→ これも省略…

1.9 Synthの定義 (Synth Definitions)

これまでのサンプルは、音を再生する際に Synth(“temp__0” : 1000) というようなメッセージが出力されていた。

内部的に繋ぎあわされたUGenのセットは、SynthDef(Synthの定義)にまとめられる、そこではどのUgenを用いるのか、また、どのように繋りあうのかが記述されている。SCのサーバーはそれらの定義を解釈して音響を生成する。

実はこれまでの例で用いてきた、{}.play という書式を用いた際には、SCはこれらの処理を目に見えないところで自動的に行い、一時的な名前をつけてSynthを生成し、自動処理していた。

より明白な方法でこの処理を行うことが可能。作成したパッチをSynthDefという形式で包みこんで、名前をつけて特定する。また、この際に名前をつけるだけでなく、出力するバスをOut.arメッセージを使用して指定しなければならない。

{SinOsc.ar}.play // 自動的に一時的なSynth名を生成

このサンプルをSynthDefを使用して書くと、

SynthDef("sine", {Out.ar(0, SinOsc.ar)}).play

となる。右チャンネルに出力する場合は、

SynthDef("sine", {Out.ar(1, SinOsc.ar)}).play

もしくは、下記のように変数を用いて記述も可能。

(
SynthDef("one_tone_only", {
    var out, freq = 440;
    out = SinOsc.ar(freq);
    Out.ar(0, out)
}).add
)

これは下記の命令で再生する。

Synth("one_tone_only");

SynthDefで定義された楽器を呼び出すには2つの方法があることに注意。一つは play メッセージを使用する方法。これは {}.play で音を生成する方法に似ている。もう一つは add メッセージを使用してSynthDefをサーバーに追加する方法。この他に .send(s) や WiteDefFile を使用する方法などがあるが、ここでは割愛。

この例であげた"one_tone_only"は、440Hzのサイン波しか生成できない。たとえば、このSinOscの周波数のように、SynthDefの外部から変化させたい場合は、引数(arg)を用いる。

(
SynthDef("different_tones", {
    arg freq = 440; // 引数freqを宣言し、その初期値を440にする
    var out; 
    out = SinOsc.ar(freq)*0.3;
    Out.ar(0, out)
}).play
)

Synthは、第1引数にはSynthDefで指定した名前、第2引数にはSynthDefで定義された引数(arg)を配列にして渡す。このやり方によって複数の引数を同時に渡すことができる。

["arg1", 10, "arg2", 111 …]

実際に"different_tones"を使用してみる。

Synth("different_tones", ["freq", 550]);
Synth("different_tones", [\freq, 660]); // "freq" と同じ
Synth("different_tones", ["freq", 880]);

// もし引数を指定しなければ、初期値の440となる
Synth("different_tones")

一度の複数の"different_tones"を生成することもできる。

a = Synth("different_tones", ["freq", 64.midicps]);
b = Synth("different_tones", ["freq", 67.midicps]);
c = Synth("different_tones", ["freq", 72.midicps]);

a.set("freq", 65.midicps);
c.set("freq", 71.midicps);
a.set("freq", 64.midicps);  c.set("freq", 72.midicps);

a.free; 
b.free; 
c.free;

PMOscを使用したより実践的なSynthDefの例

(
// まずこのブロックを実行
SynthDef("PMCrotale", {
arg midi = 60, tone = 3, art = 1, amp = 0.8, pan = 0; 
var env, out, mod, freq;

freq = midi.midicps;
env = Env.perc(0, art);
mod = 5 + (1/IRand(2, 6));

out = PMOsc.ar(freq, mod*freq, 
    pmindex: EnvGen.kr(env, timeScale: art, levelScale: tone), 
    mul: EnvGen.kr(env, timeScale: art, levelScale: 0.3));

out = Pan2.ar(out, pan);

out = out * EnvGen.kr(env, timeScale: 1.3*art, 
    levelScale: Rand(0.1, 0.5), doneAction:2);  
Out.ar(0, out); //Out.ar(bus, out);

}).add;
)

// 次に以下の行を実行
Synth("PMCrotale", ["midi", rrand(48, 72).round(1), "tone", rrand(1, 6)])

1.10 バス、バッファー、ノード (Buses, Buffers, Nodes)

バス(Buses)は、オーディオ信号やコントロール信号のルーティングに使用される。SCには128のオーディオバスと4096のコントロールバスが用意されていて、またこの数値は設定で変更することも可能。

デフォルトのオーディオ出力は最初の0番のバスから出力される。Out.ar(0)で使用された方法。また、オーディオ入力は通常は8番のバスが使用される。その他のバスは「プライベートな」バスとして内部のルーティングのために確保されている。

例えば5人のメンバーのバンドがいて、それぞれがリバーブのエフェクトを使用するより、通常はミキサーでルーティングして1つに統合してリバーブをかけようと思うだろう。SCのバスもこれと同じような働きをする。

ここで新たなUGenであるPlayBufを例にして、バスの使用方法を検討している。PlayBufはオーディオファイルを読み込んで再生する。サウンドファイルの場所は、SuperCollider.appからみた相対パスで指定する。

ここで、オーディオを格納する変数にバッファー(Buffers)を使用していることに注意。バッファーはチルダ(~)を先頭につけて宣言される。宣言の際にvarが必要ないのはaからzまでの変数と同様。バッファーとして定義することで、その値はパッチのどこからでも参照できるだけでなく、他のパッチや、さらには他のウィンドウからでも参照できるようになる。

~houston = Buffer.read(s, "sounds/a11wlk01-44_1.aiff");
~chooston = Buffer.read(s, "sounds/a11wlk01.wav");
{PlayBuf.ar(1, ~houston)}.play;
{PlayBuf.ar(1, ~chooston)}.play;

一度バッファーに読みこまれたサウンドは、バッファー番号、チャンネル数、ファイルパス、フレーム数など様々な情報を取得できる。

[~houston.bufnum, ~houston.numChannels, ~houston.path, ~houston.numFrames];
[~chooston.bufnum, ~chooston.numChannels, ~chooston.path, ~chooston.numFrames];

これらの情報を活用して、ライヒの"Come out"のように、左右のチャンネルで少しずつ位相をずらしながらループ再生するというような効果も簡単に実装できる。

(  // 左右の位相をずらす
{
    var rate, trigger, frames;
    frames = ~houston.numFrames;

    rate = [1, 1.01];
    trigger = Impulse.kr(rate);
    PlayBuf.ar(1, ~houston, 1, trigger, frames * Line.kr(0, 1, 60)) * 
    EnvGen.kr(Env.linen(0.01, 0.96, 0.01), trigger) * rate;
}.play;
)

では、このようにして読みこんだ2つのサウンドを一箇所からコントロールするにはどうすれば良いのか? ひとつは、ビンテージのアナログシンセサイザーのようにソースをモジュールとして作成し、それぞれをバスで接続するという方法がある。これには、Out.ar と In.ar を用いる。それぞれの引数にバスの番号を指定すると入力と出力が接続される。

より一般的な方法は、バスオブジェクトを使用する方法だ。

(
// まだコントロールの使用が開始されていないので、0のままで静止している
~kbus1 = Bus.control; // コントロールバス
~kbus2 = Bus.control; // コントロールバス
{
    var speed, direction;
    speed = In.kr(~kbus1, 1) * 0.2 + 1;
    direction = In.kr(~kbus2);
    PlayBuf.ar(1, ~chooston, (speed * direction), loop: 1);
}.play;
)

// コントロールの開始  
(
{Out.kr(~kbus1, LFNoise0.kr(12))}.play;
{Out.kr(~kbus2, LFClipNoise.kr(1/4))}.play;
)

// 二番目のバッファーを、同じコントロールバスを利用して開始
// ただし、右チャンネルに出力
(
{
    var speed, direction;
    speed = In.kr(~kbus1, 1) * 0.2 + 1;
    direction = In.kr(~kbus2);
    Out.ar(1, PlayBuf.ar(1, ~houston, (speed * direction), loop: 1));
}.play;
)

このように、コントロールバスを用いることでインデックス番号などを気にせずにダイレクトにアサインすることができる。コードの再利用性も高まる。

オーディオバスを用いることで、エフェクトを数珠繋ぎにして、オーディオシグナルをルーティングすることができる。

まずは、オーディオバスを使わないサンプル。

(
{
    Out.ar(0,
        Pan2.ar( PlayBuf.ar(1, ~houston, loop: 1) * 
            SinOsc.ar(LFNoise0.kr(12, mul: 500, add: 600)),
        0.5)
    )
}.play
)

オーディオバッファを、ノイズを周波数にドライブしたサイン波で音量変化させている。

もう一つ別の例。オールパスフィルタを使用して、ディレイラインを生成している。

(
{
var source, delay; 
    source = PlayBuf.ar(1, ~chooston, loop: 1);
    delay =  AllpassC.ar(source, 2, [0.65, 1.15], 10);
    Out.ar(0,
    Pan2.ar(source) + delay
    )
}.play
)

これらの処理をオーディオバスを利用して書き直してみる。まずオーディオバスとコントロールバスの名前を決める。

~delay = Bus.audio(s, 2);
~mod = Bus.audio(s, 2);
~gate = Bus.audio(s, 2);
~k5 = Bus.control;

コントロールバスを開始

{Out.kr(~k5, LFNoise0.kr(4))}.play;

最後の出力に手前でディレイラインを開始

{Out.ar(0, AllpassC.ar(In.ar(~delay, 2), 2, [0.65, 1.15], 10))}.play

ディレイの手前で、音量のモジュレーションを開始

{Out.ar(~delay, In.ar(~mod, 2) * SinOsc.ar(In.kr(~k5)*500 + 1100))}.play    

モジュレーションの手前で、ゲートを設定

{Out.ar([0, ~mod], In.ar(~gate, 2) * max(0, In.kr(~k5)))}.play  

これで、エフェクトのチェーンの部分はできたので、ここに実際にオーディオバッファを再生してみる。

{Out.ar(~gate, Pan2.ar(PlayBuf.ar(1, ~houston, loop: 1), 0.5))}.play;

すると、3つのエフェクトをチェーンにして再生される。さらに別の音を追加することも可能。

{Out.ar(~gate, Pan2.ar(PlayBuf.ar(1, ~chooston, loop: 1), -0.5))}.play;

(こうしたオーディオバスを継いでいく方法は、ライブコーディングなどにも使えそう…)

1.11 配列、反復、論理構造 (Arrays, Iteration, and Logical Expression)

音楽の演奏は、しばしば集合として表現される。12音技法、倍音構造、モード(ピッチの集合)、旋律、ピッチクラスなど。配列はこうした集合を管理することができる。SCのステレオ出力もこれと同様に配列のひとつ。

配列から要素をとりだすには、.atメッセージを使用する。at()の引数に入るインデックス番号は0から開始しているのに注意。

a = ["C", "C#", "D",  "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B "];
a.at(8);
"Item at index 5 is: ".post; a.at(5).postln; // why didn't it print E?
"Item at index 0 is: ".post; a.at(0).postln; // because we start with 0
do(50, { [0, 2, 4, 5, 7, 9, 11].at(7.rand).postln})
do(50, { ["C", "D", "E", "F", "G", "A", "B"].at(7.rand).postln})

ここで新たにdoというメッセージが登場している。これは反復(iteration)の一種。反復はこの他にも loop, while, for, forby などがある。doの第1引数は反復する数、第2引数は反復する関数。

doをTaskの中に入れることで、一定間隔の時間を置いて反復するようになる。

Task({
    50.do({
        ["C", "D", "E", "F", "G", "A", "B"].at(7.rand).postln;
        1.wait;
    });
}).play

Arrayのデータを別のタイプにマッピングすることも可能。例えば、音程名をMIDIナンバーと対応させる。こうすることでよりコンピュータが理解しやすい形式に変換できる。

Task({
a = ["C", "C#", "D",  "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
"count, midi, pitch, octave".postln;
    do(50, {arg count;
        p = rrand(36, 72);
        [count, p, a.wrapAt(p), (p/12).round(1) - 1].postln;
    1.wait;
    })
}).play 

Taskを実際に音に応用した例。まず始めに楽器PMCrotaleを定義。

(
SynthDef("PMCrotale", {
arg midi = 60, tone = 3, art = 1, amp = 0.8, pan = 0; 
var env, out, mod, freq;

freq = midi.midicps;
env = Env.perc(0, art);
mod = 5 + (1/IRand(2, 6));

out = PMOsc.ar(freq, mod*freq, 
    pmindex: EnvGen.kr(env, timeScale: art, levelScale: tone), 
    mul: EnvGen.kr(env, timeScale: art, levelScale: 0.3));

out = Pan2.ar(out, pan);

out = out * EnvGen.kr(env, timeScale: 1.3*art, 
    levelScale: Rand(0.1, 0.5), doneAction:2);  
Out.ar(0, out); //Out.ar(bus, out);

}).add;
)

これをTaskを利用してくりかえし演奏する。

(
a = ["C", "C#", "D",  "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
"event, midi, pitch, octave".postln;
r = Task({
    inf.do({ arg count;
        var midi, oct, density;
        density = 1.0; // 100% of the time. Uncomment below for 70%, etc.
        // density = 0.7; 
        // density = 0.3;
        midi = [0, 2, 4, 7, 9].choose;
        // midi = [0, 2, 4, 5, 7, 9, 11].choose;
        // midi = [0, 2, 3, 5, 6, 8, 9, 11] .choose; 
        // midi = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] .choose;
        oct = [48, 60, 72].choose;
        if(density.coin, 
            { // true action
                "".postln;
                [midi + oct, a.wrapAt(midi), 
                (oct/12).round(1)].post;
                Synth("PMCrotale", 
                    ["midi", midi + oct, "tone", rrand(1, 7), 
                    "art", rrand(0.3, 2.0), "amp", rrand(0.3, 0.6), "pan", 1.0.rand2]);
            }, {["rest"].post}); // false action
        0.2.wait; 
    }); 
}).start
)

r.stop; // 停止

この例では、if文による条件判定を使用している。SCでのifはやや特殊な書き方となる。

if(条件式, {trueのときの処理}, {falseのときの処理})

if文による条件式やその使用法などのサンプルあれこれ。

//シンプルな例
if(10 == 10, {"10 is indeed equal to 10"}, {"false"})

//AND条件と、型の判定
if((1 < 20).and(1.isInteger), {"1 is less than 20"}, {"false"})

//1から10までの数を数えて、奇数かどうかを判定
10.do({arg count; [count, if(count.odd, {"odd"}, {"even"})].postln})

次の例は、より音楽的で面白い。MIDI番号をカウントアップしていって、そのノートがCの三和音に含まれるかを判別している。12の剰余計算をすることでオクターブを単位でループしている。

(
84.do({arg count; if([0, 4, 7].includes(count%12), 
{count.post; " is part of a C triad.".postln}, 
{count.post; " is not part of a C traid".postln})})
)

音の出現する確率などを操作する際に重宝できそうな、coin演算子について。

//乱数を使用して50%の確率を発生させる例
50.do({if(1.0.rand.round(0.01).post > 0.5,  {" > 0.5".postln}, {" < 0.5".postln})})

//半分の確率で音を発生させる、という構造の基本
50.do({if(1.0.rand > 0.5,  {"play a note".postln}, {"rest".postln})})

//これをcoinを使用するとこうなる
50.do({if(0.5.coin, {"play a note".postln}, {"rest".postln})})

1.12 どうやって配列を実行(Do)するのか?

doの構文と配列(Array)をどうやって結びつけるのか。カウンタを使用して配列の要素を順番にとりだすサンプル。

[0, 2, 4, 5, 7, 9, 11].do({arg each, count; ["count", count, "each", each].postln})

この構文はよりシンプルに書き換え可能。

[0, 2, 4, 5, 7, 9, 11].do({arg whatever, blech; [blech, whatever].postln})

これを応用し、MIDI番号を音階に変換して出力するサンプル。

(
var pc;
pc = ["C", "C#", "D",  "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
[0, 2, 4, 5, 7, 9, 11].do({arg each; pc.wrapAt(each).postln;})
)

これを実行すると、C, D, E, F, G, A, B と順番に音階が出力される。

doをネストして使用することもできる。doのネストを活用して、12音のマトリクスを生成するサンプル。

(
var row, inversion, pitchClass;

//0から11迄の数値をシャッフルして配列に
row = Array.series(11, 1).scramble.insert(0, 0); 

// 上記の表記の代わりに、例えばウェーベルン作品27のような音列を指定することも可能
// row = [0, 11, 8, 2, 1, 7, 9, 10, 4, 3, 5, 6]; 

row.postln;
inversion = 12 - row;

//ピッチクラス配列に音階の名前を指定
pitchClass = ["C  ", "C# ", "D  ", "Eb ", "E  ", "F  ", "F# ", "G  ", "Ab ", "A  ", "Bb ", "B  "];

//展開        
inversion.do({arg eachInv;
    var trans;
    trans = (row + eachInv); 
    // 音階名を表示
    trans.do({arg scaleDegree; pitchClass.wrapAt(scaleDegree).post});
    //改行を出力
    "".postln;
    });
//改行を出力
"".postln
)

このコードを実行した結果の一例:

[ 0, 3, 5, 7, 4, 6, 10, 8, 9, 1, 2, 11 ]
C  Eb F  G  E  F# Bb Ab A  C# D  B  
A  C  D  E  C# Eb G  F  F# Bb B  Ab 
G  Bb C  D  B  C# F  Eb E  Ab A  F# 
F  Ab Bb C  A  B  Eb C# D  F# G  E  
Ab B  C# Eb C  D  F# E  F  A  Bb G  
F# A  B  C# Bb C  E  D  Eb G  Ab F  
D  F  G  A  F# Ab C  Bb B  Eb E  C# 
E  G  A  B  Ab Bb D  C  C# F  F# Eb 
Eb F# Ab Bb G  A  C# B  C  E  F  D  
B  D  E  F# Eb F  A  G  Ab C  C# Bb 
Bb C# Eb F  D  E  Ab F# G  B  C  A  
C# E  F# Ab F  G  B  A  Bb D  Eb C      

配列を利用してシグナルをミックスすることも可能。Mixを使用する。arメッセージを指定して、1つ目の引数にミックスするシグナルを配列を入れる。例えば、LFNoise1で音量をコントロールした加算合成の例。

(
{
var fund = 220;
Mix.ar(
    [
    SinOsc.ar(220, mul: max(0, LFNoise1.kr(12))),
    SinOsc.ar(440, mul: max(0, LFNoise1.kr(12)))*1/2,
    SinOsc.ar(660, mul: max(0, LFNoise1.kr(12)))*1/3,
    SinOsc.ar(880, mul: max(0, LFNoise1.kr(12)))*1/4,
    SinOsc.ar(1110, mul: max(0, LFNoise1.kr(12)))*1/5,
    SinOsc.ar(1320, mul: max(0, LFNoise1.kr(12)))*1/6
    ]
)*0.3
}.play
)

Mixとあわせて、Arrayのメッセージfillを利用するという方法もある。これは、Arrayの中にUGen(音を生成する関数)の集合を生成できる。第1引数に生成するアイテムの数を指定し、第2引数に音を生成する関数を入れる。例えば、基音の周波数に110Hz間隔でハーモニクスを生成しミックスするという例。

{Mix.ar(
    Array.fill(12, 
        {arg count; 
        var harm;
        harm = count + 1 * 110; //ハーモニクスを生成
            SinOsc.ar(harm, 
                mul: max([0, 0], SinOsc.kr(count+1/4))
                )*1/(count+1)
        })
)*0.7}.play
)

このコードの SinOsc.kr(count+¼)) の部分を、SinOsc.kr(¼, 2pi.rand) とか LFNoise1.kr(1) とか LFNoise0.kr(rrand(1, 5)) などと変えてみると倍音の音量の変化が様々な表情を見せて面白い。

さらに、Array.fill は、Klangを用いると面白い効果を発揮する。Klangは共鳴する倍音構造を指定することで仮想の物理モデルを再現可能。ここでは、ベルのような音を鳴らしている。

(
{
    var scale, specs, freqs, amps, rings, 
    numRes = 5, bells = 20, pan; 
    scale = [60, 62, 64, 67, 69].midicps;
    Mix.fill(bells, {
        //周波数
        freqs = Array.fill(numRes, {rrand(1, 15)*(scale.choose)});
        //音量
        amps = Array.fill(numRes, {rrand(0.3, 0.9)});
        //減衰
        rings = Array.fill(numRes, {rrand(1.0, 4.0)});
        //周波数、音量、減衰からKlangのspecs引数を生成
        specs = [freqs, amps, rings].round(0.01);
        //定位
        pan = (LFNoise1.kr(rrand(3, 6))*2).softclip;
        //Klangを生成
        Pan2.ar( 
            Klank.ar(`specs, Dust.ar(1/6, 0.03)), 
            pan)
    })
}.play;
) 

1.13 シーケンスとしての配列