前回の記事でJUCEでFM音源をAudioUnit化してGarageBandで音が出ることを確認しましたが、やっぱり2オペレータじゃそれっぽい音が出ないよなー、ということで、今回は4オペレータにしてアルゴリズムも切り替えられるようにしていきます。
FM音源でいうところのアルゴリズムとは
アルゴリズム…というとソフトウェアエンジニア勢はちょっと身構えてしまうかもしれませんが、FM音源の世界では異なる意味合いで使われています。端的に言ってしまえば、オペレータ間の接続方法です。
FM音源の基本は、2つのオペレータ(モジュレータとキャリア)を直列に接続することで、出力する波形を複雑にすることです。オペレータが2つの場合には基本それだけですが、オペレータが増えてくると、これを数珠繋ぎにしてみたり、並列で繋いでみたりと様々なパターンが生まれます。この繋ぎ方がアルゴリズムです。
PC-98で使われていた4オペレータのFM音源OPN(A)では、以下の図にある8つのアルゴリズムが規定されています。
素人にはどの組み合わせがどんな音になるのか想像つきませんが、この8つの組み合わせをFaustで表現できれば音が出せそう、ということで実装していきます。
Faustにおけるブロックの並列接続とアルゴリズムの表現
前々回の投稿で、Faustでは直列の接続を「:」で表すことができると述べましたが、並列などもあります。早速ドキュメントにあたってみましょう。
Five binary composition operations are available to combine block-diagrams:
– recursion (~)
– parallel ( , )
– sequential ( : )
– split ( <: )
– merge ( :> )
Faust Syntax
なるほど。「,」を使えとのこと。では、ちょっとそれぞれのアルゴリズムで見ていきましょう。
アルゴリズム0
これは直列ですので、
op1:op2:op3:op4
でOKですね。
アルゴリズム1
OP1とOP2が並列になったものがOP3に突っ込まれていますので、
(op1,op2):op3:op4
かしら?と思って書いてみると、エラーが出ます。曰く、
ERROR in sequential composition A:B The number of outputs [2] of A must be equal to the number of inputs [1] of B
とのこと。(op1, op2):op3との部分で入出力の個数があってないというクレームのようです。確かに、OP1とOP2はそれぞれ出力の口を一個ずつ持っていて、OP3は一つの入力を持っているので、それでNGということになるわけですね。
そこで使うのが、集約に使う「:>」となります。これで、2本の出力を1本にしてやりましょう。
op1,op2:>op3:op4
実際にFaust IDEでダイアグラムを見てみるとこの通り。
なんかうまくいってるっぽいですね。オペレータ2への入力が余ってるので髭が出てますが…、まぁ気にしないことにしましょう。
アルゴリズム2
こちらもアルゴリズム1同様にやれば大丈夫そうです。
op1,(op2:op3):>op4
はい。
アルゴリズム3
どんどんいきましょう。
(op1:op2),op3:>op4
アルゴリズム4
この調子で…。
(op1:op2),(op3:op4):>
とやったらsyntax errorが出てしまいました。最後に接続の記号で終わらせる、というのはNGのようです。
明示的にお尻に1種類の出力がある、ということを伝えればOKのようなので、
(op1:op2),(op3:op4):>_
とやってやればsyntax errorも消えます。
うむ、めでたい。
まぁ、残りは同じようにやっていけばOKです。
アルゴリズムを選択できるようにする
ここまでできてくると、Audio UnitやVSTプラグインで出力したら、そこで8つのアルゴリズムから1つを選択して音色を変更する、とかやってみたいところです。
何かいい例はないものかとFaustのサンプルコードを眺めていたら、parを使って実現するのが定石のようです。
parって何?ということで調べてみると、
The
par
iteration can be used to duplicate an expression in parallel. Just like other types of iterations in Faust:its first argument is a variable name containing the number of the current iteration (a bit like the variable that is usually named
i
in a for loop) starting at 0,its second argument is the number of iterations,
its third argument is the expression to be duplicated.
Faust Syntax
- 最初の引数は変数名を突っ込む。その数字は0から始まってインクリメントされる
- 二番目の引数は繰り返しの回数
- 三番目の引数は実際に繰り返される式
なるほど…? 式を入れられるので、こういう表現ができるようです。
par(i,8,select2(スライダーとかの値 == i,0,algorithm(i))):>_
解説中に新出単語を使ってしまいました。select2について説明します。
select2(式, 式がFALSEだった時の値,式がTRUEだった時の値)
という感じになります。三項演算子っぽいですが、trueとfalseの位置が逆っつー紛らわしい感じですが。いわゆる条件分岐っぽく使えるようです(ドキュメントには厳密には違うと書かれています)。
つまり、
- parを使ってアルゴリズムを並列に展開
- それを最後に「:>_」で集約
- 普通に並列に展開しただけだと全部音が鳴ってしまうので、select2を使ってスライダの値と同じアルゴリズムだけ音を出す(それ以外は0を出力)
という処理になります。何だかややこしいですが、DSPでのループ処理を確実に回すためにこのような形になっているのではないかと思います。
これらを組み合わせてみる
アルゴリズムの接続と、一つのアルゴリズムを選択できるようにする工夫を組み合わせると、以下のような形になります。
//-----------------------------------------------------
// Simple 4-operator FM synthesizer
//-----------------------------------------------------
declare nvoices "16";
import("stdfaust.lib");
//operator function
operator(freq, index, adsr, amp, phase) = os.oscp(freq * index, phase*ma.PI) * amp * adsr;
// UI elements
freq = hslider("/[2]freq",200,40,2000,0.01);
gain = hslider("/[3]gain",0.5,0,1,0.01);
gate = button("/[1]gate");
feedback = hslider("/[4]op1 feedback", 0, 0, 7, 1);
algorithm = hslider("/[5]algorithm",0,0,7,1);
eg(g) = vgroup("[9]EG", en.adsr(a, d, s, r, g))
with {
a = hslider("[1]attack", 0.1, 0, 10, 0.01);
d = hslider("[2]decay", 0.1, 0, 10, 0.01);
s = hslider("[3]sustain", 0.9, 0, 1, 0.01);
r = hslider("[4]release", 0.5, 0, 10, 0.01);
};
operator_control(ch, phase) = hgroup("[9]Operator #%ch", operator(freq, _index, _eg, _amp, phase))
with {
_eg = eg(gate);
_index = vslider("[1]freq index",1,1,10,1);
_amp = vslider("[0]amp", 0.5,0,1,0.01);
};
//0, PI/16, PI/8, PI/4, PI/2, PI, PI x 2, PI x 4
feedbacktable = waveform{0,0.1963495,0.392699,0.785398,1.570796,3.141592,6.2831,12.566};
fbvalue = feedbacktable,int(feedback):rdtable;
op1 = operator_control(1) ~ * (fbvalue);
op2 = operator_control(2);
op3 = operator_control(3);
op4 = operator_control(4);
alg(0) = op1:op2:op3:op4;
alg(1) = op1,op2:>op3:op4;
alg(2) = op1,(op2:op3):>op4;
alg(3) = (op1:op2),op3:>op4;
alg(4) = (op1:op2),(op3:op4):>_;
alg(5) = op1<:op2,op3,op4:>_;
alg(6) = (op1:op2),op3,op4:>_;
alg(7) = op1,op2,op3,op4:>_;
synths = par(i,8,select2(algorithm == i,0,alg(i))):>_;
process = tgroup("[6]Operator control", synths)*gain;
キモはそれぞれのアルゴリズムを関数のように定義して、parとselectで選択できるようにしていることです。
それにしても、FM音源のアルゴリズムがこんなに簡単に表現・実装できるってのはFaust凄いと思います(他を知らないでの感想ですが)。UI入れて50行ちょっとですし、各アルゴリズムで1行ですからねぇ。
Chrome上でみるとOperator 4のタブが変な感じになってしまっているのですが、JUCE経由で書き出すと問題ないようです。
まとめ
前回までに作成したコードを拡張して、4オペレータのFM音源を実装しました。だいぶ音作りの幅が広がりましたので、そこそこ楽しめそうです。
今後の展開としては、
- UIのカスタマイズに手を出す。opsixっぽいのが作れないか…とか
- LFOなど、音作りの細部にこだわる
- FM音源のパラメータを時間的に変化させて新しい音を作ってみる。YAMAHA MONTAGEのようにはいかないでしょうが
- シンプルに6-opを実装する。まぁ、できるはできるでしょうが、6-op作ると今度はDX-7のパラメータと揃えたくなってくる…
などありそうですので、時間を見つけていじってみることにします。
蛇足
FM音源の音作りってやはり難しいですよね。最近以下のような本が出版されたようなので購入して読み始めています。
FM音源の原理の話は説明のアプローチが本ブログの過去記事と全然違っていて、それはそれで興味深いです。興味のある方はどうぞ。