SoundFont 2.04 仕様の概要まとめ

  • 12
    Like
  • 0
    Comment

サウンドフォントの仕様の概要と、見落としがちな注意点を紹介します。 :musical_note:
本書はバージョン 2.01 のリファレンスとしてもお読みいただけます(相違点は24ビットサンプルのサポート有無のみ)。

サウンドフォントの詳細な仕様は SoundFont Technical Specification にあります。
構造体の具体的な定義はこちらでご確認ください。

本書の内容は RIFFPadPolyphone で実際の SoundFont ファイルとあわせてご覧になることをおすすめします。
また、010 Editor という有償バイナリエディタの Binary Template Repository ページに SF2.bt というサウンドフォント向けのテンプレートを投稿しました。データの可読性が飛躍的に向上するので、ユーザーの方はぜひご利用ください。

SF2Template.png

用語の定義・説明

バンク(Bank)
プリセットの集合を指す。
MIDI バンク(MIDI Bank)
MIDI バンクチェンジメッセージで選択されるプリセット集合のこと。
サウンドフォントの「バンク」と区別する際に使用。
プリセット(Preset)
MIDI のプログラムチェンジで指定するプリセットに相当。
1つ以上のインストゥルメントから構成される。
プリセットゾーン(Preset Zone)
特定のキーナンバーとレイヤーについて定義されたプリセットのサブセット。
かつてのバージョンでは「レイヤー(Layer)」と呼んでいたもの。
インストゥルメント(Instrument)
プリセットの構成単位。1つ以上のゾーンから構成される。
プリセットと異なりユーザーには公開されない単位。
ゾーン(Zone)
インストゥルメントを構成する単位。
特定のキーナンバーとベロシティのもとで鳴るように定義されたアーティキュレーションデータに関連付けられるオブジェクト。複数のゾーンを定義することで、キーレンジごとに異なるサンプルを鳴らしたり、複数のサンプルを重ねたりすることができる。
サンプル(Sample)
インストゥルメントを構成する波形データ(無圧縮PCM)のこと。
ゾーンごとに1つのサンプルを割り当て可能。
Generator
ゾーンにおける各オペレーターの値(音量、パン、ADSR、etc.)を定数的に定義するためのオブジェクト。
pgen/igen チャンクに出現する。
Modulator
ゾーンにおけるコントローラーとオペレーターの間の連続的な変化を定義するオブジェクト。
(たとえば、ベロシティに応じてカットオフフィルタがかかるとか、そういうのです。)
pmod/imod チャンクに出現する。
ヒドラ、ヒュドラー(Hydra)
9つの頭を持つギリシア神話の怪物。
サウンドフォントでは pdta チャンクが持つ9つのサブチャンクを意味する。

フォーマット概要

サウンドフォントは RIFF で構成されています。RIFF は「チャンク」と呼ばれるデータグループの組み合わせで構成されるシンプルな汎用フォーマットです。

RIFF の詳細は割愛しますが、チャンクサイズが奇数の場合、実データサイズは2の倍数になるように、チャンクの末尾にパディングを付加しなければならないことに注意が必要です。

チャンクの一覧と依存関係

ユーザー視点では、サウンドフォントにはおおまかに下記の階層関係があります。

  • Presets
    • Instruments
      • Samples

これに対して、ファイル上のチャンク構造はどうなっているのでしょうか。

RIFF チャンクの階層

  • RIFF - sbnk
    • LIST - INFO
      • [必須] ifil (SoundFont のバージョン)
      • [必須] isng (対象サウンドエンジン名)
      • [必須] INAM (SoundFont バンク名)
      • irom (Sound ROM の名前)
      • iver (Sound ROM のバージョン)
      • ICRD (作成日時)
      • IENG (サウンドデザイナーおよびエンジニア名)
      • IPRD (想定する製品名)
      • ICOP (Copyright メッセージ)
      • ICMT (あらゆるコメント)
      • ISFT (作成・変更に使用したツール名)
    • LIST - sdta
      • [必須] smpl (波形サンプル)
      • sm24 (24ビットサンプルの下位8ビット; v2.04)
    • LIST - pdta
      • [必須] phdr (プリセットヘッダー)
      • [必須] pbag (プリセットのインデックスのリスト)
      • [必須] pmod (プリセットの Modulator のリスト)
      • [必須] pgen (プリセットの Generator のリスト)
      • [必須] inst (インストゥルメントの名前とインデックス)
      • [必須] ibag (インストゥルメントのインデックスのリスト)
      • [必須] imod (インストゥルメントの Modulator のリスト)
      • [必須] igen (インストゥルメントの Generator のリスト)
      • [必須] shdr (サンプルヘッダー)

また、pdta チャンクについては、サブチャンク(親)のフィールドが他のサブチャンク(子)のインデックス(0 から開始)を保持するものがあります。下記がその階層構造です。

  • LIST - pdta
    • phdr (プリセットヘッダー)
      • pbag (インデックスのリスト)
        • pmod (Modulator のリスト)
        • pgen (Generator のリスト)
    • inst (インストゥルメントの名前とインデックス)
      • ibag (インデックスのリスト)
        • imod (Modulator のリスト)
        • igen (Generator のリスト)
    • shdr (サンプルヘッダー)
      • smpl チャンクを参照する

Index Generator による依存関係の定義

pgen/igen チャンクは Generator と呼ばれる単純な構造の属性値を多数持ちます。

struct sfGenList {
    SFGenerator sfGenOper;      // 16-bit enum value
    genAmountType genAmount;    // 16-bit integer
};

Generator の中でも重要なのが、チャンク間の参照関係を定義する Index Generator と呼ばれる属性値です。
下記の種類の Index Generator が存在します。

  • [pgen] sfGenOper = instrument (41); Preset-Instrument の参照関係を示します。
  • [igen] sfGenOper = sampleID (53); Instrument-Sample の参照関係を示します。

チャンク早見表

下記はあらためて各チャンクの名前、型、値の例などを示した表です。

チャンク名 必須 説明 値の例
INFO ifil SoundFont のバージョン struct sfVersionTag 2.01
INFO isng 対象サウンドエンジン名 ZSTR "EMU8000"
INFO INAM SoundFont バンク名 ZSTR "Fluid R3 GM"
INFO irom Sound ROM の名前 ZSTR "1MGM"
INFO iver Sound ROM のバージョン struct sfVersionTag 2.08
INFO ICRD 作成日時 ZSTR "July 15, 1997"
INFO IENG サウンドデザイナーおよびエンジニア名 ZSTR "John Q. Sounddesigner"
INFO IPRD バンクが想定する製品名 ZSTR "SBAWE64 Gold"
INFO ICOP Copyright メッセージ ZSTR "Copyright (c) 1997 E-mu Systems, Inc."
INFO ICMT あらゆるコメント ZSTR "This is a comment"
INFO ISFT 作成・変更に使用したツール名 ZSTR ":Preditor 2.00a:Vienna SF Studio 2.0:"
sdta smpl 波形サンプル(16 bit signed PCM) SHORT[n >= 1] -
sdta sm24 波形サンプル(8 bit part of 24 bit PCM) BYTE[n >= 1] -
pdta phdr プリセットヘッダー struct sfPresetHeader[n >= 1] -
pdta pbag プリセットのインデックスのリスト struct sfPresetBag[n >= 1] -
pdta pmod プリセットの Modulator のリスト struct sfModList[n >= 1] -
pdta pgen プリセットの Generator のリスト struct sfGenList[n >= 1] -
pdta inst インストゥルメントの名前とインデックス struct sfInst[n >= 1] -
pdta ibag インストゥルメントのインデックスのリスト struct sfInstBag[n >= 1] -
pdta imod インストゥルメントの Modulator のリスト struct sfInstModList[n >= 1] -
pdta igen インストゥルメントの Generator のリスト struct sfInstGenList[n >= 1] -
pdta shdr サンプルヘッダー(波形のオフセットなど) struct sfSample[numOfSamples >= 1] -

サンプルデータはすべてモノラルサンプルです。データ上はステレオサンプルという概念がほぼ存在しませんが、左と右にサンプルを配置するようなゾーンを定義することで、ステレオサンプルを適切に鳴らすことができます。

sm24 チャンクはバージョン 2.04 の追加仕様です。24ビットサンプルの下位8ビットを保持します。

制約・注意事項

サウンドフォントは単純なフォーマットですが、細かな注意点が多々あります。

チャンクの出現順序

INFO チャンクは RIFF の先頭 に出現しなければなりません(これは RIFF のルールです)。INFO 子チャンクの順序は任意ですが、仕様書上の順序が推奨されます。

その他、すなわち sdta, pdta チャンクおよび子チャンクは、仕様書上の順序どおりに出現しなければなりません

文字列(ZSTR)は終端文字を保持する、長さは256バイト以下

ZSTR 文字列形式のフィールド値は終端文字(値 0)を含まなければなりません

また、終端文字はデータサイズが偶数になるように1~2バイトを挿入します。これは RIFF のパディングバイトとは異なる概念で、データ自身を伸長する ZSTR チャンクの仕様です。

「Hello.」という文字列を格納する場合、格納値は「Hello.\0\0」で、チャンクサイズは8バイトです。
データサイズを偶数に調整している関係上、RIFF のパディングバイトは発生しません。これにより、ヘッダ上のデータサイズも、ファイル上の実データサイズも、どちらも8バイトになります。

|+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F |0123456789ABCDEF|
|------------------------------------------------|----------------|
|49 43 4D 54 08 00 00 00 48 65 6C 6C 6F 2E 00 00 |ICMT....Hello...|

また、ZSTR チャンクの長さは256バイト以下 とするように規定されています。

リストの要素の一部は次の要素とあわせて解釈しなければならない

チャンクが保持するインデックス番号は「開始インデックス」であることがほとんどで、次の要素の「開始インデックス」とあわせて「インデックス範囲」を表現する仕様となっています。
たとえば、ある要素のインデックスが 2 で、次の要素のインデックスが 5 の場合、ある要素は 2, 3, 4 のインデックス範囲を参照するということです。

具体的には下記のフィールドがこれに該当します。

  • [phdr] sfPresetHeader.wPresetBagNdx
  • [pbag] sfPresetBag.wGenNdx
  • [pbag] sfPresetBag.wModNdx
  • [inst] sfInst.wInstBagNdx
  • [ibag] sfInstBag.wInstGenNdx
  • [ibag] sfInstBag.wInstModNdx

上記の構造上、複数のゾーンが同一の Generator 要素あるいは Modulator 要素を指すことは絶対にありません。

チャンクによっては終端(terminator)データを挿入しなければならない

上記のようにリスト要素をペアで解釈する都合もあり、チャンクによっては末尾にダミーデータを挿入する必要があります。

下記は必要な終端の一覧です。終端要素は特定のフィールドを除いて値 0 を保持します。

チャンク
phdr achPresetName: "EOP"
wPresetBagNdx: インデックス(sfPresetBag 総数と同数)
pbag wGenNdx: インデックス(sfGenList 総数と同数)
wModNdx: インデックス(sfModList 総数と同数)
pgen 0
pmod 0
inst achInstName: "EOI"
wInstBagNdx: インデックス(sfInstBag 総数と同数)
ibag wInstGenNdx: インデックス(sfInstGenList 総数と同数)
wInstModNdx: インデックス(sfInstModList 総数と同数)
igen 0
imod 0
shdr achSampleName: "EOS"

サンプルのデータポイント数の制約

さまざまなハードウェア環境における再現性を考慮して、サンプル(波形)は下記の制約に従わなければならない(must)とされています。

  • 48 以上のデータポイントを持つ。
  • ループ区間は 32 以上のデータポイントを持つ。
    • ループが短すぎる場合はコピペで倍にしたりすると良い?
  • ループの前後はそれぞれ有効な 8 以上のデータポイントを持つ。
    • ループ終了後の8サンプルは、ループ先頭の8サンプルと同じでなければならない。したがって、ループありのサンプルを出力する際は、ループ開始点から8サンプルをコピーして末尾に出力すると良い。
    • ループが8サンプル目よりも前にあるサンプルはどうすべき? 先頭に無音サンプルを足す? あるいは後ろにサンプルを追加して、ループポイントを本来の位置よりも8サンプル後ろにずらす?(嫌ですね……)

言い換えると、下記の条件式を満たすとされています。

// valid sample?
if (sfSample.dwStart < sfSample.dwStartloop - 7 &&
    sfSample.dwStartloop < sfSample.dwEndloop - 31 &&
    sfSample.dwEndloop < sfSample.dwEnd - 7)

世間のツールやライブラリがどのくらい上記に準拠しているかはよくわかりません。

サンプルの終端データポイントについて

サンプルの終端(dwEnd が指す位置の後ろ)には、値 0 のデータポイントを46個記録しなければなりません。

サンプルの dwEnd より後、次のサンプルの dwStart より前にくるデータなので、この値がサンプルの範囲に含まれることはありません。

The DWORD dwEnd contains the index, in sample data points, from the beginning of the sample data field to the first of the set of 46 zero valued data points following this sample.

グローバルゾーン(global zone)について

グローバルゾーンとは、同インストゥルメントの各ゾーンに共通する Generator/Modulator の値を格納する場所です。

下記のすべての条件に合致するゾーンがグローバルゾーンです。

  • 最初のゾーンであること。
  • プリセットゾーンの場合:最後の Generator が instrument ではないこと。
  • インストゥルメントゾーンの場合:最後の Generator が sampleID ではないこと。

逆に言えば、最初に出現するゾーンであっても、必ずしもグローバルゾーンではないと言えそうです。

Generator/Modulator 列挙子の制約(出現順序など)

各チャンクの章や「10.7 Illegal enumerator」章にいくつかの制約が記載されています。

Generator と Modulator のユニーク制約は下記のとおりです。

  • Generator は自身の sfGenOper によって定義される。同一ゾーン内のすべての Generator はユニークな sfGenOper を持たなければならない。もし複数回出現した場合、以前の内容は無視される。
  • Modulator は自身の sfModSrcOper, sfModDestOper, sfModSrcAmtOper によって定義される。同一ゾーン内のすべての Modulator はこれらの3つの列挙子のユニークな組を持たなければならない。もし複数回出現した場合、以前の内容は無視される。

また、出現順序に関しても制約があります。

  • keyRange および velRange はゾーンの最初の Generator でなくてはならない(keyRange, velRange の順に出現)。
  • instrument 列挙子は pgen チャンクに出現すべきであり、インストゥルメントゾーンに出現することは許可されない。
  • instrument 列挙子はゾーンの最後の Generator でなくてはならない(グローバルゾーン以外は必ず出現)。それ以降の Generator は無視される(will be)。
  • sampleID 列挙子は igen チャンクに出現すべきであり、プリセットゾーンに出現することは許可されない。
  • sampleID 列挙子はゾーンの最後の Generator でなくてはならない(グローバルゾーン以外は必ず出現)。それ以降の Generator は無視される(will be)。
  • ゾーンは必ず1つ以上の Generator を有する(例外:Modulator のみを持つグローバルゾーン)。

本筋からは少し外れた事項のQ&A

本筋からは少し外れた事柄に対するQ&Aです。個人的疑問リストです。

Q. メロディー楽器とドラムセットの区別はないの?

サウンドフォントの規格上は両者に区別はありません。

ただ、General MIDI との親和性のために、パーカッションバンクはバンク番号 128 に配置することになっています。

余談ですが、General MIDI でリズムチャンネルのマッピングを避けるには、チャンネル 10 以外を使用するか、SysEx で割り当てを変更することになると思います。

# GS: USE FOR RHYTHM PART OFF
F0 41 10 42 12 40 10 15 00 1B F7

Q. バンク変更の MIDI CC って CC0/CC32 どっちなの?

結論としては、CC0 または CC0 & CC32 の組み合わせでバンク変更すると記されており、明確な決まりはありません。(「9.2 MIDI Functions」章)

実質的には CC0 で 0~127 のバンクを選択できる実装が多いように思います。

Q. マルチバイト文字の扱いについて

文字エンコーディングについては ASCII と書かれているので、マルチバイト文字は使うべきではありません。

関連ライブラリ