見出し画像

ピッチ検出(Pitch)を巡って

いわゆるライブエレクトロニック音楽など、楽器とSuperColliderを組み合わせて使いたい時がある。音声入力に対してエフェクトを施すのが一般的だろうが、ピッチ検出アルゴリズムを使えば、さらに込み入ったことが出来るかもしれない。市販品ではギターシンセサイザーやボーカルのピッチ補正にピッチ検出アルゴリズムが用いられている。

SuperColliderではピッチ検出にPitchを用いる。まずPitchに聴かせる音源を作成する。Example 0では、Pitchに音声を送るバスを立てる。ランダムに音が鳴るので、音色の感じがわかったら「Check the sound」の行をコメントアウトしても構わない。

// Example 0 : SynthDef
(
~src_bus = Bus.new(\audio, 10);

SynthDef(\source, {|pch, dur|
	var env, frq, sig;
	env = Env([0, 1, 0.3, 0.3, 0], [0.01, 0.01, dur-0.05, 0.02]).ar(2);
	frq = pch.midicps;
	sig = PMOsc.ar(frq, frq, 1, 0, env);
	Out.ar(~src_bus, sig);
	Out.ar(0, sig!2 * dbamp(-12));	// Check the sound
}).add;
)

次いでTdefでランダムな音高を演奏する。

// Example 1 : Tdef
(
Tdef(\source, {
	loop {
		var dur = rrand(0.1, 0.5);
		var pch = rrand(36, 72);
		Synth(\source, [pch:pch, dur:dur]);
		dur.wait;
	};
}).play;
)

Tdef(\source).playの状態でExample 2を実行すると、Pitchの生データがずらずらと表示される。Pitchの引数execFreqで指定した周波数(但しminFreqとmaxFreqの範囲内)で、推定したピッチをひたすら列挙している。例えばPitchに「execFreq:5, minFreq:5」と引数を与えると1/5秒ごとのデータになる。

// Example 2 : Changed
(
Ndef(\pitch, {
	var sig, frq, trg;
	sig = InFeedback.ar(~src_bus);
	frq = Pitch.kr(sig)[0];
	trg = Changed.kr(frq);
	Poll.kr(trg, frq, \pch);
	DC.ar;	// proxy out
}).play;
)

このままでは使い辛いと思い、Changedを用いてデータを間引くことを考える。Example 3では、Changedは周波数(frq)をピッチクラスに変換したもの(pch = frq.cpsmidi)を検査している。Changedの第2引数(threshold)を1にすると、1半音以内の変化を無視する。

// Example 3 : Changed
(
Ndef(\pitch, {
	var sig, frq, pch, trg;
	sig = InFeedback.ar(~src_bus);
	frq = Pitch.kr(sig)[0];
	pch = frq.cpsmidi;
	trg = Changed.kr(pch, 1);	// Ignore changes within a semitone
	Poll.kr(trg, pch, \pch);
	DC.ar;	// proxy out
}).play;
)

時折反応し損ねているとはいえ、通常であればこれで十分使い物になりそうだ。ところがシンセを複雑な音色に置き換えると異変が起こる。Example 4ではモジュレータの周波数比と変調量がランダムに変動する。

// Example 4 : SynthDef
(
SynthDef(\source, {|pch, dur|
	var env, frq, mod, amt, sig;
	env = Env([0, 1, 0.3, 0.3, 0], [0.01, 0.01, dur-0.05, 0.02]).ar(2);
	frq = pch.midicps;
	mod = Rand(0.5, 3);
	amt = Rand(1.5, 5);
	sig = PMOsc.ar(frq, mod * frq, amt, 0, env);	// unstable
	Out.ar(~src_bus, sig);
	Out.ar(0, sig!2 * dbamp(-12));	// Check the sound
}).add;
)

Pitchの挙動をわかりやすくするため、2秒毎に発音させる。

// Example 5
(
Tdef(\source, {
	loop {
		var dur = 2;
		var pch = rrand(36, 72);
		Synth(\source, [pch:pch, dur:dur]);
		dur.wait;
	};
}).play;
)

この場合、Ndef(\pitch)は一つの音に対して時々複数の周波数を反復的に出力する。Changedはあくまで連続する値の差を見ているので、1半音以上離れた複数のピッチが繰り返し流れ込むと、この方法では対応できなくなる。

こうしたケースについて、ここでは敢えてポリフォニックなピッチ検出を検討してみる。つまり同音連打を避けつつ、得られた周波数を全て利用したい。OnsetsはFFTによって入力音のアタックを推定する。極力アタックが正確になるように引数thresholdを調整する。

// Example 6 : Onsets
(
Ndef(\pitch, {
	var sig, fft, atk, trg;
	sig = InFeedback.ar(~src_bus);
	fft = FFT(LocalBuf(4096), sig);
	atk = Onsets.kr(fft, threshold: 0.05);
	trg = Changed.kr(atk);
	Poll.kr(trg, atk, \atk);
	DC.ar;
}).play;
)

Example 3と6のNdef(\pitch)を合わせて、SendReplyを使って双方の値をOSCFuncに投げる。かつ、それらの値をOSCFuncがどのように受けるのか見てみる。msgの4番目の値(msg[3])がそれぞれの値である。

// Example 7 : SendReply + OSCFunc
(
Ndef(\pitch, {
	var sig, frq, pch, trg, fft, atk;
	sig = InFeedback.ar(~src_bus);
	frq = Pitch.kr(sig)[0];
	pch = frq.cpsmidi;
	trg = Changed.kr(pch, 1);
	SendReply.kr(trg, '/freq', frq);
	fft = FFT(LocalBuf(4096), sig);
	atk = Onsets.kr(fft, threshold: 0.05);
	trg = Changed.kr(atk);
	SendReply.kr(trg, '/atk', atk);
	DC.ar;	// proxy out
}).play;

~func0.free;
~func0 = OSCFunc({|msg| msg.postln }, \freq);

~func1.free;
~func1 = OSCFunc({|msg| msg.postln }, \atk);
)

Example 8では、受け取った周波数を~arrayに格納する。この時、条件として半音単位で同じピッチクラスと見なされる周波数が既に~arrayに存在する場合は格納を行わない。そしてこれだけだと~arrayに周波数が溜まる一方なので、Onsetsがアタックを検出、即ちatkが1になったタイミングで~arrayを初期化する。

// Example 8 : Array
(
~array = [];

~func0.free;
~func0 = OSCFunc({|msg|
	var frq = msg[3];
	var ary = ~array.collect{|e| e.cpsmidi.round };

	// Is there no similar pitch class?
	if ( ary.includes(frq.cpsmidi.round) == false )
	{
		~array = ~array.add(frq);
		~array.collect{|e| e.cpsmidi.round.asInteger }.postln;
		// Synth(\pno, [frq:frq]);		// play synth
	};
}, \freq);

~func1.free;
~func1 = OSCFunc({|msg|
	if ( msg[3] == 1 )
	{ ~array = [] };
}, \atk);
)

結果を音にして聴いてみる。Example 9を実行したのち、Example 8の「play synth」の行をアンコメントする。~arrayに新規の周波数が追加されるタイミングで発音される。

// Example 9 : Synth
(
SynthDef(\pno, {|frq|
	var env, sig;
	env = Env.perc(0, 1).ar(2);
	sig = PMOsc.ar(frq, frq, 0.5, 0, env / 4);
	sig = Pan2.ar(sig, Rand(-1, 1));
	Out.ar(0, sig);
}).add;
)

ロジックとしてはこのような感じだろうが……「遠からずと雖も当たらず」といった様子である。時折反応しなかったり、とんでもない音が鳴ったりする。とはいえ、ここで再びExample 1を実行すると、ピッチやタイミングのバラつきは気にならなくなり、シンセどうしのセッションのようにも聴こえて来る。

再度、Example 0を実行すると、タイミングはさておきピッチの誤差が目立つ。ピッチやタイミングの誤差も包容する即興音楽等であれば、このようなアルゴリズムでも使えるかもしれない。尚、アルトサックスとコンピュータのための拙作「Hypersanity」では物理乱数的にピッチ検出を用いてドラム音をトリガーするなどしている。

(
~func0.free;
~func1.free;
Tdef(\source).stop;
Ndef(\pitch).clear;
)


いいなと思ったら応援しよう!