FM音源って知ってますか? 今じゃ表立って耳にする機会も少ないですが、つい最近まで携帯電話の着メロなんかはこのFM音源を使ってました。
「機械っぽい音」なんていわれるとまぁその通りではあるんですが、往年のNEC製PCなどで使われていたこともあり、独特な音にファンも少なからずいたりします。
このFM音源、「FM (Frequency Modulation)」と名のつく通り「周波数変調」を利用して音を作っているのですが、これがイマイチよく分かりません。いや、Wikipediaの当該ページを見ると波形を導き出す式も載っていますし、それはそれで分かるんですが、その式をいくら見たところで「どんな音がするか」がよく分からない…。
様々なサイトで「周波数変調は、扇風機の前で声を出すとあ”あ”あ”あ”あ”〜〜〜と音が変わるのと同じ原理です」なんて説明されていたりするのですが、この説明は周波数変調がわかっている人にしか理解できないものだと思うのです。
…というわけで、式を見ても分からないなら手を動かせ、ということで、単純なFM音源を鳴らすプログラムを組んでみました。 各オペレータの波形をリアルタイムに表示させて、パラメータをいじるとどのように波形が変わるのかが分かるようにしてみました。
とりあえずお披露目
Cocoaアプリを作りました。何も考えずに作ったのですが、もしかしたらSnow Leopardでしか動かないかもしれません。 ソースも置いておきます。他人に見せるために書いてないので汚くてコメントないですが…。
ま、適当にいじってみてください。大したことはできませんが…。 頭では原理がわかっていても、パラメータをいじると実際に波形が変わるので、見てるだけで(個人的には)楽しいです。 また、パラメータだけだと想像つかなかった音色も、波形を見ればだいぶ想像しやすくなる気がします。
FM音源のキホン
Wikipediaの説明を読むと、FM音源の基本は周波数を変調することだそうですが、どう変調するか、そこがキモのようです。 昔の数学やら物理やらの授業の記憶を掘り返し、頑張って正弦波(sin波)の基本公式を思い出してください(笑)
y = A sin (2 π f t + φ)
ってやつですね(Aは振幅、fが周波数、φが初期位相、tが時間)。
この中でA, f, φが一定であれば普通の正弦波になるわけですが(tだけ時間に合わせて値が変わる)、FM音源は、ここのφの部分も時間に応じて変化させて複雑な音を作り出します。
φをどうやって時間変化させるか
基本的なFM音源の場合、φを、さらに別の正弦波を使って時間変化させます。 これが、キャリアとモジュレータの関係みたいですね。キャリアがメインとなる正弦波を作るオシレータ、モジュレータがキャリアのφを時間変化させるオシレータです。
キャリアとモジュレータの周波数は同じでなくても構いません。 ただ、モジュレータの周波数がキャリアの周波数の整数倍でないと、音がキャリア周波数の高さに聴こえません。 これは、整数倍でないときに
y = A sin (2 π f t + φ)
のsinの中身が、キャリアの1周期ごとに0にならなくなってしまうからです(ちょっと計算してみるとわかります)。
エンベロープジェネレータを使ってさらに変化させる
エンベロープジェネレータの説明はWikipediaなどを読んでいただくとして。 楽器から音が出始めてから終わるまで、音量は一定ではありません。オルガンなどは鍵盤を押した瞬間に最大音量となり、離すと音がプツリと消えますが、ピアノであれば鍵盤を話しても弦が揺れて少しのあいだ音が鳴り続けますし、バイオリンなどは音の出始めがゆっくりです。
FM音源の場合、キャリアに対してエンベロープ変化をつけると、上記のように「オルガンっぽい音の出だし」から「バイオリンっぽい音の出だし」といったように音量を変化させることができます。
しかしさらに複雑なことに、モジュレータの出力に対してもエンベロープジェネレータを使って変化させることができます。 これによって、キャリアから出力される波形が最初は普通の正弦波なんだけどだんだんφが大きくなって違う音になります。つまり、エンベロープジェネレータをモジュレータの出力にかけることによって、音量だけでなく音色まで変化させることができるようになるわけです。
どうやってコーディングしてるか
FM音源のオペレータは、構造体で以下のように定義されています。
struct _FMInfo{
double isFB;
int isEnabled;
double carrierFreq;
double freqIndex;
double modIndex;
double attack;
double decay;
double sustain;
double release;
int numOfParents;
FMInfo** parents;
};
そして、ある時間の波形の高さは、以下の関数で計算しています。ほんとに、上記の公式を愚直に計算してます(笑
double fmGetValue(FMInfo* fm, double sec){
double parentPhase = 0.0;
for (int i = 0; i < fm->numOfParents; i++){
parentPhase += fmGetValue(fm->parents[i], sec);
}
if (fm->isFB > 0){
double value = 0.0;
double beta = M_PI * fm->isFB / 8.0;
for (int i = 0; i < 20; i++){
value = sin(2.0 * M_PI * (fm->carrierFreq * fm->freqIndex) * sec + beta * value);
}
return fm->modIndex * value * fmGetPower(fm, sec);
}
return fm->modIndex * sin(2 * M_PI * (fm->carrierFreq * fm->freqIndex) * sec + parentPhase) * fmGetPower(fm, sec);
}
fmGetPowerという関数はエンベロープジェネレータでの高さを計算する部分です。 今回は、「鍵盤を離した」部分は「音が鳴り始めたら4秒」と決め打ちにしているので、以下のようなコードになっています。
double fmGetPower(FMInfo* fm, double sec){
double power = 1.0;
if (sec < fm->attack){
power = sec / fm->attack;
}else if (sec < fm->decay + fm->attack) {
double t = sec - fm->attack;
double w = (1.0 - fm->sustain) * t / fm->decay;
power = 1.0 - w;
}else if( sec < 4){
power = fm->sustain;
}else if (sec > 4 && sec < (4 + fm->release)) {
double t = sec - 4.0;
power = fm->sustain * (1.0 - t / fm->release);
}else if (sec > + fm->release){
power = 0;
}
return power;
}
興味のある方はソース中にあるOperator.hおよびOperator.cをご覧下さい。綺麗ではありませんが、どうなっているかは理解できるのではないかと思います。
オペレータの並列接続
今回のアプリは、キャリアとモジュレータ2つの3オペレータを直列につないだものしか音が出せないのですが、実際には並列接続のものもあります。 並列接続は、いわゆる波形の合成です。周波数をいじるのではなくて、単純に足し算ですね。
上記のコードを読んだ方はお気づきかもしれませんが、コード内では既に並列接続の計算もできるようになっています。ただアプリのUIが複雑になるため、今回はパスしました。暇があったら作るかもしれません。…作らないかもしれません。
まとめ
FM音源の原理はわかっていてもイマイチどういう音が出てくるのか想像できなかったため、簡単なFM音源もどきを実際に作りました。 パラメータをいじって音色を確かめられるだけではなく、波形がリアルタイムに変わることで、パラメータを意図して変化させることも容易になるかもしれません。
今回のアプリではオペレータの並列接続はサポートしていませんが、いつか暇ができたら(そして興味がつづいていたら)実装するかもしれません。 AudioUnitで動く音源を作るのも面白そうですが、そこまでできるかなー。
蛇足
エントリと直接関係ありませんが、右にある本はiPhoneでCoreAudioをいじくるにはとても良い本です。少々値段が高いのですが、iPhone / iPad / iPod touchで音楽をいじるにはこの本は必須だと思います。どうもCoreAudio系はAppleの技術文書が少ないイメージがあり、このように系統だって(それも日本語で!)まとめられた本があるのは助かります。
実はこの本の中にシンプルなFM音源ドラムマシンの実装があり、最初はこれを参考にiPadアプリを作ろうとしていました(オペレータ間の接続をタッチで行ったりできると楽しいなぁ、と)。
ただiPadよりもまずは慣れたMacでやろうということで、ひとまず上記のアプリになりました。また、波形生成のアルゴリズム部分に関しても、オペレータの並列・直列やフィードバックなどを盛り込んだため、異なった実装になっています。
補足
次のエントリに4オペレータ版で並列配置も可能なものを公開しました。よろしければご覧になってください。