クラス書いてみる

一連の処理をクラスという形にまとめておくと、使い回しが効いて便利らしいです。
PsyColliderの起動時に、猛烈な勢いで文字が流れてますが、あの中に「compiling class library..」「compile done」といった文言があって、どうもクラスというのは起動時に読み込まれるもののようです。
ということは、これはもう定番だろう、という処理があるとすれば、クラスにしておけば起動時に読み込まれるので毎回書く必要はないし、実に便利なのですねー
という程度の理解なのですが、ではクラスというものを書いてみようと思います。

書く

新しいウィンドウにこう書きます。

Hoge {			// クラス名
	*hoge {			// クラスメソッド名
		|in|		// 引数
		^(in*2)		// in*2を返す
	}
}

まずクラス名を書き、その中のメソッドの部分に、処理の内容を書きつけます。
クラス直下のメソッドであるクラスメソッド名には*をつけるようです。
処理の内容としては、引数で値や音をもらって、ごにょごにょしたのちに、^で戻してもらいます。
骨組みとしてはこんだけです。

コンパイル

さてPsyColliderでは、これ書いただけだとクラスが使えません。なぬー
書いたらこれを拡張子scのファイルとして、C:\Program Files\PsyCollider\SCClassLibraryの中に保存します。
自分はここにさらにフォルダをひとつ作って、その中に置いてます。意味があるかはわかりません。
保存ができたらAlt+Kします。するとクラスライブラリのコンパイルががっちょんがっちょんと始まります。
ほとんど再起動みたいな感じですが、再起動する手もありますよ。
(当方の環境ですと、何度もAlt+Kしてるとアプリケーションが落ちたりするのですが、Ctrl+Alt+Delでタスクマネージャを見ると、java.exeが顔面蒼白で固まってることなどありますので、この場合一旦落としてやる)
コンパイルが済んだら、起動時の状態になってますので、必要に応じて再度localhost serverを起動します。
このように、非常に回りくどいのですが仕方がない。

実行

いよいよHogeクラスの実行です。正しくはHogeクラスのhogeメソッド実行?なんやわかりませんが

Hoge.hoge(2)

4と出たらば成功です。おめでとうございます。
単に来たものを2倍するだけの変哲もないものですが、わしにもクラスが書けた。
要するに「*2」だけが内容ですので、当然音もつっこめます。

{ Hoge.hoge(SinOsc.ar(1000)) }.play;

るせー。

メソッド

同じクラスにメソッドを複数書けます。というかいろんなメソッドをひとつのクラスにまとめられると言う方がいいのか?
クラスと言えばSinOscなんかも同様に、arとkrというふたつのメソッドがあるということのようです(Alt+Jで見られます)。

Hoge {
	*hoge { |in|
		^(in*2)
	}

	*moge { |in|
		^(in/2)
	}
}

Hogeクラスにはhogeメソッドとmogeメソッドがあるのです。と書いてある。

Hoge.moge(3)

1.5です。おめ。

クラス変数

ていうかもう少し意味のありそうなものを書こうかと思います。とはいえ何を書いたものか…
同じscファイルでいいので、以下を書きます。

Ghpr {
	classvar a, x;		// クラス変数

	*new { |aa, xx|
		a = aa;
		x = xx;
		^x
	}

	*next {
		x = (a*x)*(1-x);
		^x
	}
 }

これはまた何か?ところで、クラスの中で変数を使い回すにはクラス変数と書かねばならぬようです。
ではやってみる。

Ghpr.new(3.7, 0.5)

ここでクラス変数に代入。0.5と言われましたが、これが初期値になります。

Ghpr.next()

0.925です。
もういちど押すと、0.2566875です。nextするたびに計算を繰り返す仕組みです。
では100回まとめてやってみる。

100.do{ Ghpr.next().postln };

どざーっと数字が出たと思いますが、どうにもデタラメっぽい並びです。
randなどないのに、どうしたことでしょうね?
説明は省きます。

音クラス

音クラスという言い方はないと思いますが、クラスにUGen入れたいと思います。
やり方としては、箇条書きでテストしてみて、クラスに書き直す感じ。

Moge {
	*ar { |freq|
		var o;
		o = SinOsc.ar(freq);
		o = Pan2.ar(o, 0);
		^o
	}
}

箇条書きで書いてるのとほとんど変わらんと思います。

{ Moge.ar(200) }.play;

ちゃんと鳴る。
ていうかもう少し意味のありそうなものを書こうかと思います。

Keri {
	*ar {
		var e, o;
		e = EnvGen.ar(Env.perc(0, 0.2)); 
		o = SinOsc.ar(e**10*1000+60, 0, e);
		o = Pan2.ar(o, 0);
		^o
	}
}

こないだのキックです。

{ Keri.ar }.play;

これだけでキックが鳴る。大変に便利。
UGenの入れ子みたいなこともやってみます。

Pong {
	*ar{ |freq, rate=1, amp=1|
		var a, b, c, o;
		a = Impulse.ar(rate);
		b = EnvGen.ar(Env.perc(0, 0.1), a);
		c = SinOsc.ar(freq*2.5, 0, b);
		o = SinOsc.ar(freq, c, b);
		o = Pan2.ar(o, 0, amp);
		^o
	}
}

Gucha {
	*ar { |in, len=2, rate=1|
		var a, b, o;
		a = LFNoise2.ar(rate, len/2, len/2);
		o = DelayL.ar(in, len, a);
		o = Pan2.ar(o, 0);
		^o
	}
}

これをコンパイルして以下を実行。

{ Gucha.ar(Pong.ar(500, 5, 0.5)) }.play;

他人が見たらなんのこっちゃと思うに違いないコード。

{ Gucha.ar(Pong.ar(500, 5, 0.5), rate:100) }.play;

音もなんのこっちゃ。

まとめ

Writing-Classesヘルプを見てますと、もっと難しいことが書いてあり私なぞにはよくわけがわかりません。
というかオブジェクト指向云々も未だよくわかってないのだが。
ともあれ、それらしきものを書いて利用するのはさほど難しくないのではというお話。

Routineで作曲とレンダリング

サイコロを使わない作曲法を考えてますが、考えはじめると難しい
というか適当を以って旨とする本文の趣旨とはだいぶかけ離れてきますな。

(
~va = [0,0,0,0,1];		// 音列
~sc = [0,2,3,5,7,9,10];	// 音階

SynthDef("hoge", {
arg nn;
var
e = EnvGen.ar(Env.perc(0, 1), doneAction:2);
o = SinOsc.ar(nn.midicps, 0, e); 
Out.ar(0, Pan2.ar(o, 0, 0.5));
}).store;

r = Routine {
i = 0;
v = ~va.size;
loop {
	v.do({ |i|
		j = i+v-1%v;
		c = ~va[i] + ~va[j] % 7;
		~va.put(i, c);		// 音列のi番目をcに書き換え
		Synth(\hoge, [\nn, ~sc[c]+72]);
		c.post;
		(c+1/8).wait;
	});
} 
}.play;
)

r.stop;

さてこれはどうやって音を選んでいるのか?
次もやってることはほぼ同じです。ちなみにシンセはFM合成で。

(
Tempo.tempo = 4;
~va = [0,0,0,0,0,0,1];
~sc = [0,3.5,5,7,10.7,12,15.7,17,19,22.7];

SynthDef("hoge", {
arg freq, pan;
var
e = EnvGen.ar(Env.perc(0, 2), doneAction:2);
m = SinOsc.ar(freq*7/3, 0, e);
o = SinOsc.ar(freq, m, e/3);
Out.ar(0, Pan2.ar(o, pan));
}).store;

r = Routine {
v = ~va.size;
loop {
	v.do({ |i|
		j = i+v-1%v;
		c = ~va[i] + ~va[j] % 10;
		~va.put(i, c);
		~va.asString.postln;
		Synth(\hoge, [
			\freq, (~sc[c]+60).midicps,
			\pan, 2/(v-1)*i-1
		]);
		1.wait;
	});
}
}.play;
)

r.stop;

これは何かというと遅れフィボナッチという擬似乱数生成法の真似事です。
配列のいくつか前の数どうしを足してmodするという、偶然性のかけらもない計算ですが、結果はデタラメなようなそうでもないような、なかなか興味深いものです。
ずっと見てると戻ってきます。音列の初期値をいじって遊べます。
えー、Rountineで作曲と言いつつ、あまり込み入ったことはできませんでしたが、こんな感じでいろいろ遊べるっぽい。

Rountineをレンダリング

せっかくなのでこれもレンダリングの方法を考えてみます。
他の方法があるのかも知れぬが、要するにRountineでスコアを書けばいいのだろう。
前のやつを元にして書き換えます。

(
~va = [0,0,0,0,0,0,1];
~sc = [0,3.5,5,7,10.7,12,15.7,17,19,22.7];
~ar = Array.new;	// 空のスコア(配列)
~dur = 60;		// 全体の長さ(秒)
~spd = 0.2;		// 速さ

r = Routine {
v = ~va.size;
t = 0;			// 時間
while({ t < ~dur }, {		// 時間が全体の長さ未満ならば
	v.do({ |i|
		j = i+v-1%v;
		c = ~va[i] + ~va[j] % 10;
		~va.put(i, c);
		~va.asString.postln;
		~ar = ~ar.add(		// スコアに追加
			[t, [\s_new, \hoge, -1, 0, 0,  
			\freq, (~sc[c]+60).midicps,
			\pan, 2/(v-1)*i-1]]
		);
		t = t + ~spd;		// 時間を更新
	});
});
~ar.add([t, [\c_set, 0, 0]]);		// 演奏終わり
"done".postln;
}.play;
)

loopをやめて、予め全体の長さを決めてwhileすることにした。
スコアに追加するメッセージを直して、他は大して変更ありません。
Post Windowにずらずら出てきてdoneと出たら完了。

Score.play(~ar);

ちゃんと出来てるみたいですね!

o = ServerOptions.new.numOutputBusChannels = 2;
Score.recordNRT(~ar, "hoge.osc", "hoge.wav",
headerFormat: "WAV", sampleFormat: "int16",
options: o);

はいできた。
最後がぶち切れてしまいましたが、c_setの手前でtを増やしてマージンを作ってあげればオッケーでしょう。

Routineで作曲?

作曲と言ってもどうやるんだよという話ですが、せっかくプログラム言語でやってるので、メロディー書くとか心を込めて歌い上げるとか、そうした方向ではないものを目指したい。
うまく行くかどうかはともかく、どんな方法が可能なのかだけを血も涙もなしに考えてみる。

(
Tempo.tempo = 1;

SynthDef("hoge", {
arg nn;
var
e = EnvGen.ar(Env.perc(0, 1), doneAction:2);
o = SinOsc.ar(nn.midicps, 0, e); 
Out.ar(0, Pan2.ar(o, 0, 0.5));
}).store;

r = Routine {
	loop {
		n = 12.rand;
		n.postln;
		Synth(\hoge, [\nn, n+72]); // シンセを起動
		0.2.wait;
	}
}.play;
)

r.stop;

Routineの中にSynthを入れてやると音の並びができますね。
半音階をランダムに鳴らしているだけですが、一応これだけでも作曲と言えないことはない(本当か)
こういうところから始めて、どんどん自分の考えを入れていってやると、自分の曲って感じになっていくのかも知れません。
今度はいろんな音色を使ってみたいです。

(
SynthDef("hoge", {
arg ds, fq, dur, pan;
var
d = Dust.ar(ds);
e = EnvGen.ar(Env.linen(0, 0.05, 0), d);
f = if(ds>10, 1, e);
o = LFPulse.ar(fq, 0, 0.5, f); 
Out.ar(0, Pan2.ar(o, pan));
}).store;

SynthDef("moge", {
arg ds, fq, dur, pan;
var
d = Dust.ar(ds);
e = EnvGen.ar(Env.linen(0, 0.05, 0), d);
f = if(ds>10, 1, e);
o = LFNoise0.ar(fq, f); 
Out.ar(0, Pan2.ar(o, pan));
}).store;

SynthDef("mohe", {
arg ds, fq, dur, pan;
var
e = EnvGen.ar(Env.perc(0, dur));
m = SinOsc.ar(ds, 0, fq/ds, fq/2);
o = SinOsc.ar(m, 0, e); 
Out.ar(0, Pan2.ar(o, pan));
}).store;

r = Routine {
var inst;
inst = ["hoge","moge","mohe"];	// シンセ名
	loop {
	a = 15.0.rand;
	b = 10000.rand;
	c = 5.0.rand;
	d = 2.0.rand-1;
	e = 3.rand;
	inst[e].postln;
	a = Synth(inst[e], [\ds, a, \fq, b, \dur, c, \pan, d]);
	c.wait;
	a.free;
	}
}.play;
)

r.stop; a.free;

これまた適当シンセですが。ちなみにシンセ名を配列から呼んでいる。
パラメータを変えつつ音色が切り替わって、何らかの展開らしきが、あるようなないような、それにしてもサイコロばかりでは芸がないのう。

(
SynthDef("hoge", {
arg nn;
var
e = EnvGen.ar(Env.perc(0, 1), doneAction:2);
o = SinOsc.ar(nn.midicps, 0, e); 
Out.ar(0, Pan2.ar(o, 0, 0.5));
}).store;

r = Routine {
24.do { |i|
	if( coin(0.5), {	// 確率50%
	("---"++i).postln;
	while( {n!=i}, {	// iが出たら終わる
		n = 24.rand;
		n.postln;
		Synth(\hoge, [\nn, n/2+72]);
		0.1.wait;
		})
	}, {
	("==="++i).postln;
	while( {n!=i}, {	// iが出たら終わる
		n = 24.rand;
		m = 5.rand+1;
		(""++n++" "++m).postln;
		Synth(\hoge, [\nn, n/2+72]);
		Synth(\hoge, [\nn, n/2+m+72]);
		(1/m).wait;
		})
	});
};
"done".postln;
}.play;
)

制御文を無理矢理かませてみました。
またしてもデタラメに音を選んでますが、iと同じ数が出たら次のフレーズに行く。24周すると終わり。
2種類のフレーズをサイコロで選んでますが、このcoinというのも初めて見た。これは与えた確率に従って、真(True)か偽(False)をBooleanで返す。便利ですね。

coin(0.5).postln;

自分が知らないだけで、他にも大量に便利なものが隠れているのではという気がします。ううむ

Task

上の「while曲」はひとつのRoutineの中に処理内容まとめて書いてますが、さらに入り組んだ構造が作りたいとなると、やはり分割して書けたほうが後々楽だと思うのですよ常識的に考えて。
で、Routineをネストできるかとかいろいろ試してみたが、結果としては、

(
SynthDef("hoge", {
arg nn;
var
e = EnvGen.ar(Env.perc(0, 1), doneAction:2);
o = SinOsc.ar(nn.midicps, 0, e); 
Out.ar(0, Pan2.ar(o, 0, 0.5));
}).store;

~cnd = 0;			// グローバル変数
~wt = 0.00001; 		// 謎の待ち時間

a = Task {
c.stop;			// cを一時停止
("---" ++ ~cnd).postln;
while( {n != ~cnd}, {
	n = 24.rand;
	n.postln;
	Synth(\hoge, [\nn, n/2+72]);
	(0.1 - ~wt).wait;
	});
c.resume;			// cを再開
};

b = Task {
c.stop;			// cを一時停止
("===" ++ ~cnd).postln;
while( {n != ~cnd}, {
	n = 24.rand;
	m = 5.rand+1;
	(""++n++" "++m).postln;
	Synth(\hoge, [\nn, n/2+72]);
	Synth(\hoge, [\nn, n/2+m+72]);
	(1/m - ~wt).wait;
	});
c.resume;			// cを再開
};

c = Task {
24.do { 
	if( coin(0.5), { a.start }, { b.start });	// aまたはbを実行
	~wt.wait;					// 謎の待ち
	~cnd = ~cnd + 1;
};
"done".postln;
}.start;
)

Taskというのを使いまして、こんな感じになりました。
TaskはRoutineと微妙に違うようで、途中で止めたりできます。
あと変数の頭に~をつけてにょろりとさせるとグローバル変数というものになり、どこからでも見られて大変便利です。
さて、サブのTaskを呼び出した時に、メインTaskを止めなきゃならんとか、メインTaskに謎の待ち時間を入れる必要があるなど、少々不便な気もしますが、こうしないとwait無視で突進してしまいalready playingとかalready playingとかalready playingとか言われてぐちゃぐちゃになり爆発炎上して悲惨です。他にうまい方法があれば教えて下さい。
またどのくらいタイミングが合ってるのかも疑問ですが、コード的にきもすけわりーので謎待ち時間を減じている。
次回もう少しRoutine/Taskについて考えます。

制御文

一般にプログラムというものは、通常上から順に処理するところを、制御文を使って、処理をループさせたり、条件によって処理を変えたりできる。
かつそこに時間が挟まると、制御構造=楽曲構造、みたいな面白げなことができるのではー
と妄想するのだが、いよいよ難儀なことになりつつあるので、ひとまずどんな制御文があるのかを見てみる。

if

条件に当たった場合と外れた場合で処理を分ける。

if(
	10.rand < 5,	// 条件
	{"boo"},	// 真の場合
	{"foo"}		// 偽の場合
).postln;

答えが二つ返ってきますが、あくまで処理は一回であるようです。
ううむ。何度も見てますが、どういう意味なのか未だにわからない。

switch

複数の条件のどれに当たったかで処理を分ける。
条件はいくつでも立てられるのと、どれでもない場合も立てられる。

switch(
	4.rand,		// 条件
	0, {"boo"},	// 0の場合
	1, {"foo"},	// 1の場合
	{"orz"}		// どれでもない場合
).postln;

do

指定した回数繰り返す。

10.do({		// 10回繰り返す
	|i|		// arg i;と同じ
	("do "++i).postln
});

argを置くとインクリメントします。妙な書き換えを覚えた。
実はinf.doとすると無限に繰り返すのだが、後述する遅延処理がないとハングして強制終了せざるを得なくなり、HDが爆砕とかPCが炎上とかしがちなのでやめとこう。

(3..7).do({		// 3から7まで
	|i|
	("do "++i).postln
});

こんな書き方もできるんだって。forの代替みたいな感じ?

while

条件が当たってる時だけ繰り返します。

q = 0;
while(
	{q != 9},			// 条件(9ではない場合)
	{q = 10.rand; q.postln}	// 真の場合
);

上の例だとサイコロ振って9が出た瞬間終了なので、いつ終わるかは毎回違う。
制御文は他にもあるが、詳しくはControl-Structuresヘルプなどを見よ(ぶん投げ)。

Routine

Routineは、要するに処理の間に待ち時間が作れるようです。
ヘルプ読んだけどyieldとか何に使うのかよくわかりませんでしたので、相変わらず皮相的な理解です。

Routine {
	"boo".postln;
	1.0.wait;		// 1拍待つ
	"foo".postln;
}.play;

waitというのを間において、続く処理を遅らせておる。
わざわざ遅れを作って何がありがたいのかというと、これで音楽の時間構造が作れるのでは、という。

loop

放って置けば無限に繰り返します。

r = Routine {
loop {
	"boo".postln;
	1.0.wait;
}
}.play;

wait入れないとHDが液状化とかPCが反物質化とかしますので忘れぬよー。
止めたくなったら

r.stop;

1拍待ちがたりーなーと思ったら、数を0.5とかにするとか

Tempo.tempo = 4;	// 1秒あたり4拍

などと予め実行しておくがいいらしい。
なお、Tempoはいじると次にいじるまでそのままなので注意。

wait

waitはいくつでも置ける。

r = Routine {
a = 0;
loop {
	a.postln; a = a+3; 1.wait;
	a.postln; a = a-2; 1.wait;
}
}.play;

r.stop;

さっきのifの例文を自動化してみる。

r = Routine {
loop {
	if(
	10.rand < 5, {"boo"}, {"foo"}
	).postln;
1.wait;
}
}.play;

r.stop;

loopだけじゃなくてdoとかwhileでもwaitできます。

r = Routine {
10.do({|i|
	i.postln;
	i.wait;	// 意味なくだんだん遅くなる
});
"bang".postln;
}.play;
r = Routine {
q = 0;
while({q != 9}, {
	q = 10.rand;
	q.postln;
	1.wait;
});
"bang".postln;
}.play;

では次はRoutineを使った怪しい作曲法について考える。

バスなど

バスというのはチャンネルとかシールドみたいな理解でいいのか。
通常の左右2チャンネルの環境だと、0番と1番は音が出ます。
ところが2番以降は音が出ない。これを使ってシンセからシンセに音(とかコントロールレート)を渡してやることが出来るという。
例えば音源のSynthDefとエフェクトのSynthDefを別々に作っておいて、適宜つなげて使うとか、複数のシンセをミックスするとか、系統を分けるとか、そうなるといろいろ便利っぽいですね。
便利っぽいのだが、とりあえずどうやったら使えるのか、という最低限のところを実験。

Buffer.read(s, "hoge.wav", bufnum:10);

SynthDef("hoge", {
var len;
len = BufFrames.ir(10);
p = LFNoise2.ar(10, len/10);
o = BufRd.ar(1, 10, p);
Out.ar(10, o); // バス10番に出力
}).store;

SynthDef("moge", {
var
o = SinOsc.ar(SinOsc.ar(2, 0, 500), 0, 0.5);
Out.ar(10, o); // バス10番に出力
}).store;

SynthDef("mixx", {
var in;
in = In.ar(10, 1); // バス10番を入力
Out.ar(0, Pan2.ar(in, 0));
}).store;

二つのシンセの出力を一つのシンセにまとめてみる。

// 上から順にひとつずつ実行
x = Synth.new(\hoge);		// hogeを立ち上げ
y = Synth.after(x, \moge);	// xの後にmogeを立ち上げ
z = Synth.after(y, \mixx);	// yの後にmixxを立ち上げ

最後のzを実行したときに初めて音が出ます。
つまり10番を通ってる音というのは聴こえない。
こういう書き方をしないと鳴らない、というのはつなぎ順をはっきりさせないと、Inが受け取ってくれないのだろうか。
単にplayでだららーと鳴らすには

SynthDef("hoge", {
var len;
len = BufFrames.ir(10);
p = LFNoise2.ar(10, len/10);
o = BufRd.ar(1, 10, p);
Out.ar(10, o);
}).play;

SynthDef("moge", {
var
o = SinOsc.ar(SinOsc.ar(2, 0, 500), 0, 0.5);
Out.ar(10, o);
}).play;

SynthDef("mixx", {
var in;
in = InFeedback.ar(10, 1); // バス10番をフィードバック入力
Out.ar(0, Pan2.ar(in, 0));
}).play;

InFeedbackを使うとできるみたい。つなぎ順とか関係なしに入ってくるから注意が必要、ということか?

フィードバック

ところでフィードバックとは、出口と入口をくっつける、というようなやり方で、うまくすると面白い効果が作れる。
のだが、フィードバックの音量が1を超えると無限に増殖して音が爆発するので、フィードバック係数と言って1未満の数を掛けてやるのが一般的。
ところでシンセ内部でフィードバックさせるにはローカルというのが使える。

SynthDef("hoge", {
var len;
i = LocalIn.ar(1);		// ローカルから入力
len = BufFrames.ir(10);
p = LFNoise2.ar(10, len/10);
o = BufRd.ar(1, 10, p);
d = DelayN.ar(o+i, 0.5, 0.5);	// ディレイ
LocalOut.ar(d*0.9);		// ローカルに出力
Out.ar(0, Pan2.ar(d, 0));
}).play;

わけわからん出音ですが。これは0.5秒遅らせたのをフィードバックすることで1.0秒、1.5秒、2.0秒…の遅れのかたまりを作っていると。
DelayNのところでフィードバック入力を混ぜてますが、これがないとフィードバックになりませぬ。
あとLocalOutのところの0.9がフィードバック係数。フィードバックごとに音量が0.9、0.81、0.729、0.6561…と減っていくから、係数が小さいとより短命である、ということかと。

ファイル再生

PlayBuf

もうひとつ憶えといたらよさそうなのがファイル再生関連のUGen。
なのだが、これを使うには最初にファイルの読み込みをしなければならんらしい。事前にこれやれと。

Buffer.read(s, "hoge.wav", bufnum:10); // バッファ10番に読み込む

bufnumというパラメータでバッファ番号を当ててますが、たぶん番号は何でもいい。
早速鳴らしてみます。

SynthDef("hoge", {
var
o = PlayBuf.ar(1, 10); // バッファ10番を再生
Out.ar(0, Pan2.ar(o, 0));
}).play;

PlayBufはこの手のやつでは最も単純で、主に端から鳴らすだけ。

SynthDef("hoge", {
var
t = Impulse.ar(5); // トリガ
o = PlayBuf.ar(1, 10, trigger:t);
Out.ar(0, Pan2.ar(o, 0));
}).play;

トリガしてやることもできる。

SynthDef("hoge", {
var
o = PlayBuf.ar(1, 10, loop:1); // ループあり
Out.ar(0, Pan2.ar(o, 0));
}).play;

ループも可能。というかサンプラーみたいな感じ。

SynthDef("hoge", {
var
r = 4.0.rand-2; // いろんな速度
o = PlayBuf.ar(1, 10, rate:r, loop:1);
Out.ar(0, Pan2.ar(o, 0));
}).play;

実行するたびにいろんな速度で鳴ります。負の速度はいわゆる逆再生。
この速度とかのパラメータって、再生中はいじれないのか?

BufRd

もう少し面白おかしくしたい。

SynthDef("hoge", {
var len;
len = BufFrames.ir(10);		// バッファのフレーム数
p = Phasor.ar(0, 1, 0, len);		// 0からlenまで漸次増
o = BufRd.ar(1, 10, p);		// バッファ10番を読み取り
Out.ar(0, Pan2.ar(o, 0));
}).play;

いろいろできるぶん、少し設定が込み入ってます。
まずこれはほっとけば鳴るのではなく、波形でバッファを読む必要がある。テープの上をヘッドが動くイメージか。
この場合Phasorを使うのがおすすめらしい。Phasorというのは一直線に数が増えていくような波形。
他のオシレータみたいに周波数を与えるのではなく、サンプルごとの増加分と上限を与えてやるという不思議な感じ。

SynthDef("hoge", {
var len;
len = BufFrames.ir(10);
p = Phasor.ar(0, 1, 0, len);
q = LFNoise0.ar(10)*len; // ランダム
o = BufRd.ar(1, 10, p+q);
Out.ar(0, Pan2.ar(o, 0));
}).play;

間に低周波ノイズをかまして読み取り位置がランダムにぶっ飛ぶようにしてます。わけがわかりませんね。

SynthDef("hoge", {
var len;
len = BufFrames.ir(10);
p = LFNoise2.ar(10, len/10); // なめらかランダム
o = BufRd.ar(1, 10, p);
Out.ar(0, Pan2.ar(o, 0));
}).play;

LFNoise2ですと乱数の間がなめらかにつながり、するとなんだかスクラッチみたく聴こえてきます。面白いですね。
このように、読み取り波形をあれこれすることであれこれできるみたいですねえ。
再生関連は他にもありますけど、とりあえずこんなもんで。

リズムマシンヌ

へぼいドラム

エンベロープと基本波形の組み合わせでドラム音が作れるのでは。

SynthDef("kk", {
var
e = EnvGen.ar(Env.perc(0, 0.2)); 
o = SinOsc.ar(e**10*1000+60, 0, e);
Out.ar(0, Pan2.ar(o, 0));
}).play;

キックです。アタックのノイズっぽい感じはエンベロープの10乗。底を60Hzにすると胴鳴りのような。

SynthDef("sn", {
var
e = EnvGen.ar(Env.perc(0, 0.1)); 
o = LFNoise0.ar(4000, e);
Out.ar(0, Pan2.ar(o, 0));
}).play;

低周波ノイズにエンベロープかけただけのスネア的な何か。

SynthDef("hh", {
var
e = EnvGen.ar(Env.perc(0, 0.1)); 
o = LFNoise0.ar(4000, e) * SinOsc.ar(5000);
Out.ar(0, Pan2.ar(o, 0));
}).play;

上のスネアにサイン波を掛けてリング変調しただけのハット的な何か。
超手抜きー

へぼいパターン

ドラム音ができたので、次は簡単なドラムパターンを組めないかしら。
ここでスコアをガリガリ書く手もなくはないが、Patternというものでパターンを作ることもできる。(変な文章)
まず音源の方を若干手直し

SynthDef("kk", {
var
e = EnvGen.ar(Env.perc(0, 0.2), doneAction:2); 
o = SinOsc.ar(e**10*1000+60, 0, e);
Out.ar(0, Pan2.ar(o, 0));
}).store;

SynthDef("sn", {
arg amp;
var
e = EnvGen.ar(Env.perc(0, 0.1), doneAction:2); 
o = LFNoise0.ar(4000, e) * amp;
Out.ar(0, Pan2.ar(o, 0));
}).store;

SynthDef("hh", {
var
e = EnvGen.ar(Env.perc(0, 0.1), doneAction:2); 
o = LFNoise0.ar(4000, e) * SinOsc.ar(5000);
Out.ar(0, Pan2.ar(o, 0));
}).store;

EnvGenにdoneAction:2というのが混ざりましたが、これは音が鳴り終わるたびにシンセを消すというような意味。
これを入れないと、音出しするたびに楽器が増えるというえらいことになって、お後がよろしくないようだ。
あとスネアにampという引数を足しましたが、これは自分が現状、バックビートというか、オフセットの書き方がよくわからぬゆえ、音量を上げ下げしてバックビートぽく鳴らすというイカサマです。
イカサマでも鳴りゃいいんだよという考えもある。(乱暴)
然る後に下をまとめて実行。

(
Tempo.bpm = 180;	// BPMは180

a = Pbind(
	\instrument, \kk,
	\dur, Pseq([4,1,3],inf)
);			// キックのパターン

b = Pbind(
	\instrument, \sn,
	\dur, Pseq([2,2,2,2],inf),
	\amp, Pseq([0,1,0,1],inf)
);			// スネアのパターン

c = Pbind(
	\instrument, \hh,
	\dur, 1
);			// ハイハットのパターン

p = Ppar([a,b,c]);	// パターンを重ねる

p.play;		// パターンを演奏
)

おー、いきなり見たいことないもんだらけでわかんねえよ。
詳しくはStreamsのヘルプを見よ(超丸投げ)。
ともあれたどたどしい8ビートらしきが鳴りはじめ、非常に感動的です。
たどたどしい…というかヨレヨレである。何だこれは。(注:当方の環境では)
到底これではリアルタイムに演奏するなんて無理な気がしますがー
レイテンシーをいじると良くなるとか諸説ありますが、一向に良くならないので先送り。
このへんwin版ならではかもです。

パターンのレンダリング

レンダリングの方法を考えます。さきほどの

p = Ppar([a,b,c]);

まで実行した後で

q = p.asScore(16);			// 16拍分をスコアに変換
q = q.add([16, [\c_set, 0, 0]]);	// 末尾にc_setを追加
o = ServerOptions.new.numOutputBusChannels = 1;
q.recordNRT("hoge.osc", "hoge.wav",
headerFormat: "WAV", sampleFormat: "int16",
options: o);

とやると書き出せた。わお。
今度はもはやサンプルレベルで縦が揃っていると思われマーベラス
ついでにやや音割れ気味。
試しに

Tempo.bpm = 1000;

狂ったようにテンポ速くしても大丈夫です。非実時間処理は素晴らしいなあ。
さてこんな感じで、ドラムにベース足すとかいろいろすれば妙なテクノのごときも作れそうですね。