前に説明したとおり、WASM-4のサウンド関係のAPIは、波形や周波数、音の長さなどを指示する関数がひとつあるだけです。MP3ファイルを再生する関数みたいなのは一切ありません。つまり、ゲームにBGMをつけるには、楽譜にあたるデータを自分で用意して、それを読み取りながら再生する機構も自分で作らなければなりません。
BGMを作る
筆者は音楽については、ピアノやギターなら少し弾けるので多少の素養はあるといえると思うのですが、作曲の類はまったくやったことがありません。クラシックなどの自由に使える曲を使うのもいいですが、今回は絵も自分で描いてるし、効果音も自分で作っているわけで、ここまできたらせっかくなのでBGMも自力でどうにかしたいと思います。
ミュージックシーケンサーは、ググって見つけた以下のものを使ってみました。
作曲のことはよくわからないので、あんまり語ることがないです。なんか適当に音を並べて適当に作りました。BGMは何度も繰り返し聞くことになるので、あまりエモい感じの旋律だと頭に残りすぎてしまいます。ちょっとあっさりめのフレーズを狙いたいところです。実際にどんなBGMになったのかはデモのゲームを動かして聞いてみてください。
MIDIのノートナンバーを周波数に変換する
それで、音楽をどういうふうに表現するか考えなくてはいけません。WASM-4作者のブルーノ兄貴は、MIDIやMMLのようなフォーマットにするかみたいなことを考えているようですが、今のところWASM-4では特に標準的といえる方法はないようです。
いろいろ考えたすえ、ここではMIDIのノートナンバーと音の長さを組みにしてソースコードに直接書く方法でいくことにします。MIDIファイルに似ているけどそれをもっと単純化した感じです。
MIDIでは 0~127までの数に音階を割り当てます。ピアノの鍵盤に低い方から番号がひとつづつ割り当てられているイメージです。MIDIのノートナンバーを周波数に変換する式はウィキペディアに書いてあったので、これをそのまま実装しました。
fn note_to_frequency(d: u32) -> u32 {
(2.0f32.powf((d as f32 - 69.0) / 12.0) * 440.0) as u32
}
音楽を表現する
音楽は、ノートナンバーと音の長さを組みにして、それを連続させることで表すことにします。音を鳴らしたくない場合は、休符としてノートナンバー 0 を割り当てることにしました。 そのノートの集まりを1トラックとして、WAM-4のチャンネルは4つなので、トラックも4つでひとつのミュージックとします。
// (Pitch, Release)
type Note = (u32, u32);
type Track = &'static [Note];
type Music = [Track; 4];
これで適当なジングルを表すと、次のようになります。
pub static TITLE_BGM_SCORE: &Music = &[
&[],
&[(00, 60), (60, 10), (62, 10), (64, 10), ... ],
&[(00, 100), (60, 20), (00, 10), (60, 10), ...],
&[],
];
MIDIだと音の鳴らす位置も情報として持つのですが、WASM-4では1チャンネルに同時に1音しかならせないので、これで十分表現できそうです。
ちなみに、トラック(チャンネル)は4つなのですが、ゲーム本編のプレイ中は第1トラックと第4トラックをジャンプなどの効果音に割り当てるため、自由に使えるのは残りの2トラックだけです。厳しい……。(べつに4トラックすべて使ってもいいのですが、その場合はジャンプなどをするたびに音楽が途切れるようになります)
音楽を鳴らす
あとは、毎フレームごとに1づつ増えていくカウンターを用意して、そのカウンターの数字とノートの位置が一致した瞬間に音を鳴らすようにコーディングします。音楽をループさせたいときは、そのカウンターの数字を音楽の長さで割ることでループさせられます。
pub fn music(music: &Music, music_count: &mut u32, pitch_offset: i32, loop_music: bool) {
let current_position = if loop_music {
*music_count % music_length(&music)
} else {
*music_count
};
for (channel, notes) in music.iter().enumerate() {
let mut position = 0;
for note in *notes {
let (note_number, release) = *note;
if position == current_position && note_number != 0 {
let freq = note_to_frequency((note_number as i32 + pitch_offset) as u32);
play(Sound {
freq1: freq,
freq2: freq,
attack: 0,
decay: 0,
sustain: release / 2,
release: release / 2,
volume: 50,
channel: channel as u32,
mode: 0,
})
}
position += release;
}
}
*music_count = *music_count + 1;
}
サウンドのスケジュール
それで実装してみて気になったのは、WASM-4ではサウンドを事前にスケジュールすることができない点です。人間は視覚的なタイミングについてはわりとアバウトで、アニメーションが多少コマ落ちしたり間延びしたりしても、そこまで違和感は生じません。しかし、聴覚的なタイミングについては人間は妙に敏感で、音が流れるタイミングがずれるとすぐに気付いてしまいます。これを防いで音を正確なタイミングで流すためには、事前のスケジューリングを行います。
WASM-4は60FPSで動くことにはなっていますが、たまたまCPUが忙しかったりして60FPSから下がるとこのサイクルが乱れて、BGMがヨタヨタと乱れることがあります。まあそれはそれでレトロ感があっていいのかもしれませんが……。そのような問題を防ぐため、WebAudioなんかだと、予め数百ミリ秒とか先まで音のタイミングをスケジュールします。そうすれば、たまたまCPUが忙しくてFPSが乱れたとしても、サウンドのハードウェアはCPUに関係なく事前にスケジュールしたタイミングに合わせて音を鳴らしてくれるので、よほどのことがない限り正確にリズムを刻むことができるわけです。WASM-4にもそういうサウンドのスケジュールの機能がほしいですね。
次回予告