SoundFont とサンプル音源
サンプル音源は、シンセサイザー とは正反対の方法で音を作ります。波形を生成するのではなく、実在の楽器の短い録音を再生します。ピアノをいくつかの音といくつかの強さで録音してクリップとして保存し、キーが押されたら最も近いクリップを目的のピッチで再生する。サンプリングの要点はこれだけです。録音が 1 つでは足りないのは、速く再生するとピッチが上がるだけでなく長さが縮み、フォルマント(声や楽器の音色を決める共鳴の山)も一緒に上にずれて「チップマンク」のような声になるからです。そのためライブラリは数半音ごとに録音し、各サンプルを少しだけ伸縮させて使います。実際の録音なので、結果は非常にリアルになります。
このページでは、SoundFont とは何か、どうアドレス指定されるか、そして libsonare が MIDI アレンジメントから必ず音を出す仕組みを説明します。概念だけで、コードは出てきません。
サンプルと合成
合成(シンセ)音源は音を計算で作るので、どんなピッチや音色にもなりますが、音を設計する必要があります。サンプル音源は録音を再生するので本物そのままに鳴りますが、録音した範囲から出られず、保存容量も格段に大きくなります。多くの環境では両方を併用し、リアルな楽器にはサンプルを、アコースティックには存在しない音にはシンセを使います。
SoundFont とは
SoundFont(.sf2 ファイル形式)は、録音された楽器サンプルのライブラリ全体を、再生のルール(どのサンプルがどの鍵域を担当するか、ループの扱い、基本的なエンベロープ)とともに 1 つにまとめたファイルです。1 つの .sf2 で、General MIDI の楽器セット全体を数メガバイトに収められます。
ファイル内では、音はバンクとプログラムに整理され、個々の楽器は (bank, program) の組でアドレス指定されます。
- プログラムは、「アコースティックグランドピアノ」「フィンガーベース」のような、演奏できる楽器 1 つです。
- バンクは、最大 128 のプログラムを並べた番号付きの棚です。バンク 0 がメインのセットで、上のバンクにはバリエーションが入ります。
MIDI がプログラムチェンジとバンクセレクトで使うアドレス方式もこれと同じです。MIDI と SoundFont が素直に噛み合うのはそのためです。
General MIDI のプログラム番号とドラムバンク
SoundFont は (bank, program) でアドレス指定されるので、General MIDI 準拠の .sf2 はプログラム番号を GM マップに揃えています。プログラム 0 がアコースティックグランドピアノ、24 がナイロン弦ギター、40 がバイオリンというように、128 の GM 楽器すべてが対応します。打楽器は別のドラムバンクにあり、ノート番号がピッチではなく別々のドラムやシンバルを選びます。これは MIDI のチャンネル 10 をドラムに使う慣習と一致します。
根底の原理は単純です。ノートのリストは楽器に依存せず、どの楽器で演奏するかはアドレスが決めます。下のピアノロールで確かめられます。音符はそのままで、楽器を切り替えると同じ MIDI が別の音色を指すだけです。
プリセットの中身:ゾーン、ジェネレーター、モジュレーター
プリセットは 1 つの録音ではありません。ゾーンの集まりで、各ゾーンが鍵域とベロシティ範囲を受け持ち、1 つのサンプルを指します(間にインストゥルメントという層があり、1 つのインストゥルメントを複数のプリセットで使い回せます)。各ゾーンはジェネレーターを持ちます。サンプルのルートキー、ループポイント、フィルターのカットオフ、エンベロープの時間といった固定の設定です。モジュレーターはライブの入力(ベロシティ、CC、ピッチベンド)をジェネレーターへつなぎます。強く弾くと減衰量が下がる、モジュレーションホイールでビブラートが深くなる、といった動きはこれで作られます。仕様には既定のモジュレーターセットがあり、ファイルに 1 つも書かれていなくても、どのプレイヤーもこれを適用します。
聴覚が対数的なので、ジェネレーターの値も対数の単位で保存されます。
| 単位 | 使われる場所 | 換算 |
|---|---|---|
| タイムセント | エンベロープの時間、LFO の遅延 | 秒 = 2^(タイムセント / 1200)。−12000 で約 1 ms、0 で 1 秒、1200 で 2 秒 |
| センチベル | 減衰量、サステインレベル | 1 cB = 0.1 dB。960 cB は 96 dB の減衰 |
| 絶対セント | フィルターのカットオフ、LFO の速さ | Hz = 8.176 × 2^(セント / 1200)。13500 セントで約 20 kHz |
この単位なら、時間が倍になることもレベルが同じ幅だけ動くことも、数直線上では同じ距離です。そのためファイルは、鍵盤の位置やベロシティに応じてエンベロープを伸縮させる指定を単純な足し算で書けます。
libsonare がノートを解決する仕組み
libsonare は SoundFont を読み込んで MIDI を鳴らせますが、ここに 1 つ重要な保証を加えています。.sf2 ファイルを与えると、使用された (channel, bank, program) の各グループを 1 つのバックエンド状態にまとめます。
| 状況 | バックエンド | 聞こえる音 |
|---|---|---|
(channel, bank, program) グループで演奏されるすべてのノートを、読み込んだ SoundFont のゾーンがカバーする | 'sf2' | それらのノートに使われる SF2 サンプル |
| カバーされないノートが 1 つでもある、または SoundFont 未読み込み | 'synth' | カバーされないノート向けの NativeSynth General MIDI フォールバック |
重要な帰結は、SoundFont にゾーンがないことだけを理由に MIDI ノートが無音にならないことです。libsonare はそのノートごとに内蔵の NativeSynth GM バンクへフォールバックします。マニフェストは保守的なグループ単位の情報なので、同じ (channel, bank, program) グループにあるノートの 1 つでも SF2 のゾーンから外れると、そのグループの状態は 'synth' になります。ほかのノートが SF2 を使っていても同じです。
フォールバックが重要な理由
SoundFont ごとにカバーする楽器は異なり、小さな .sf2 にはプログラムが数個しか入っていないこともあります。フォールバックがあるので、どんなアレンジメントとどんな SoundFont(なくてもかまいません)を渡しても曲全体を聴けます。あとから充実した SoundFont に差し替えれば、MIDI を変えずに音だけを良くできます。
何がどこに解決されたかは正確に確認できます。soundFontManifest() は、アレンジメントが鳴らす各 (channel, bank, program) グループについて 1 つの状態を返します。バックエンドはそのグループ内のすべてのノートをまとめて判定し、少なくとも 1 つのノートがフォールバックを使うと 'synth' になります。
libsonare での実装
Project 上で loadSoundFont(bytes) がバイトバッファから .sf2 ファイルを登録します。続いて soundFontManifest() が、アレンジメントの使う (channel, bank, program) の組ごとに 1 件の Sf2ProgramStatus を返します。各エントリは 'sf2' か 'synth' の backend と、解決された presetName を持ちます(ドラムチャンネルはバンク 128 を報告します)。マニフェストは各組のすべてのノートオンを集約し、SoundFont のゾーンから外れるノートが 1 つでもあれば、そのエントリを 'synth' に変えます。これはグループ内のすべてのノートが NativeSynth を使ったという意味ではありません。bounceWithSf2Instrument(...) はノートごとに同じ GM フォールバックを適用するため、カバーされていないノートもレンダリングされます。SoundFont を一切読み込んでいない場合も、すべてのノートが NativeSynth のボイスへ解決されます。