この記事は音楽ツール・ライブラリ・技術 Advent Calendar 2019 12/24の記事です。
(今回は急いで用意したので若干趣旨に合ってないかもです。来年は自作音源とか音声圧縮とかをやりたいところです。
はじめに
DTMを初めて早10年、10年前に導入したMusic Studio Producer と8年前に導入したTimidity++とSoundFontの組み合わせで長々と使ってきましたが、更新されていないTimidityのMIDIドライバーとWindows10の相性問題を踏み、ついにTimidityを廃止することに...
10年前は手動でマッピングファイルを作っていたのですが、この10年でプログラミングに対応したため今回はプログラムの力で楽をして快適なDTM環境を手に入れようという、そういうお話です。
サウンドフォントのマッピング
MIDIしか扱えないDAWでサウンドフォントを使う場合、TimidityのようなサウンドフォントをMIDI音源として使える仮想音源を利用します。サウンドフォントの音色は バンク番号とプリセット番号の二つ を組み合わせることで指定でき、MIDIの バンクセレクト、プログラムチェンジと対応 するためMIDIメッセージでサウンドフォント中の任意の音色を選択することができます。
ただし、サウンドフォントを複数同時に使用するとバンクやプリセット番号が衝突したり、サウンドフォントによってはMIDIで規定されている音色の配置と大幅に異なる場合があり、不便なためマッピングを行います。
幸いTimidityにはこの機能があり、使いやすいように配置しました。
(なお、当時はユーザーとして使っていたので、サウンドフォントのファイルフォーマットはもちろん知らないですし、MIDIについても詳しくありませんでした。)
256*256の空間にExcel上で音色を配置し、対応するようにTimidityの設定ファイルとDAWの音色ファイル(楽器名とプログラム番号、バンク番号の対応表です。設定するとDAWのGUIで楽器名が表示されるので作成していました。)を手で書いていました。
追加したいサウンドフォントはたくさんありましたが、Excelで割り当てて、設定ファイルを手で追加する...めんどくさいですね
TimidityからVirtualMIDISynth
今年の夏くらいにWin10をメジャーアップデートしたらついにTimidity++が動かなくなりました。そもそもTimidity++は署名がないドライバーで動いており、更新もされていないのでそろそろ限界かと思い乗り換えへ
乗り換え先の仮想MIDI音源としてVirtualMIDISynthを導入しました。
ところがサウンドフォントのマッピング機能がないのです。複数ファイルのサウンドフォントを読み込むことはできますが、衝突した場合優先度の高いサウンドフォントファイルの音色が利用されるようです。これは困りました。
ということでサウンドフォントファイルを書き換えて衝突が起きないようにし、ついでに音色マップまで自動で生成してしまおうという話です<前置きが長い
仕様
- サウンドフォントファイルのバンクとプリセットを衝突しないように再配置する
- サウンドフォントに対応するMusic Studio Producer用の音色マップを生成する
- VirtualMIDISynth用の設定ファイルを生成する
- 削除する音色・音色名の省略などの指定ができるようにする
音色の削除はサウンドフォントファイルにゴミ音色データが入っているものが見られたため、音色名の省略は音色名が長すぎてDAWの表示がおかしくなるため追加しました。
なお、Music Studio ProducerとVirtualMIDISynthの設定ファイルの説明はここでは行いません。
サウンドフォントのファイル構造
仕様書:http://freepats.zenvoid.org/sf2/sfspec24.pdf
サウンドフォントはRIFF形式で格納されています。RIFFはチャンクと呼ばれる単位でデータが格納されており、チャンクはIDとサイズとデータで構成されています。
RIFFの構造
チャンクの基本構造
項目 | サイズ | 備考 |
---|---|---|
チャンクID | 4byte | チャンクの識別子(RIFF/LISTなど) |
データサイズ | 4byte | データのサイズ(リトルエンディアン) |
データ | Nbyte |
また、先頭のチャンクであるRIFFチャンク、複数のチャンクをまとめるLISTチャンクの2つは特別なチャンクとして用意されています。(なお、RIFFとLIST以外のチャンクにはチャンクを含めることはできません。)
RIFFチャンクの構造
項目 | サイズ | 備考 |
---|---|---|
チャンクID | 4byte | RIFF |
データサイズ | 4byte | N+4 |
ファイル識別子 | 4byte | RIFFファイルに格納しているデータの識別子 (サウンドフォントの場合sfbk) |
データ | Nbyte | チャンクやLISTチャンクが入る |
LISTチャンクの構造
項目 | サイズ | 備考 |
---|---|---|
チャンクID | 4byte | LIST |
データサイズ | 4byte | N+4 |
リスト識別子 | 4byte | リストに格納しているデータの識別子 (INFO/dataなど) |
データ | Nbyte | チャンクやLISTチャンクが入る |
RIFFファイルはこれらのチャンクを利用し入れ子構造データを表記できます。
すべてのチャンクはデータ部のサイズが先頭に書かれているため、不要なチャンクは読み飛ばしてしまうこともできます。そのため、サウンドフォントのチャンクのうちバンク番号とプリセット番号に関するチャンクだけを処理し後のチャンクはそのまま読み飛ばすだけという実装で目的は達成できます。
サウンドファイルのRIFF構造
サウンドフォントのRIFF構造は以下のようになります。このうちpdta以下に入っているチャンクに楽器名やプリセット番号などが入っています。
pdtaにはプリセット、インストゥルメント、サンプルに関するサブチャンクが含まれています。このうちインストゥルメントは複数のサンプルをまとめた単位でサウンドフォントの内部で利用される単位となり、プリセットは複数のインストゥルメントをまとめユーザーが利用できる単位となります。
そのため、今回はプリセット関係のサブチャンクのみ利用します。
なお、サブチャンクは構造体の配列として格納されており、末尾の値は終端を示す特別な値となります。また、サイズはsizeof(構造体)の整数倍となります。
phdrサブチャンク
phdrサブチャンクはヘッダー情報(プリセットの楽器名やバンク、プリセット番号など)が格納されています。
struct phdr {
char achPresetName[20]; // プリセット名 null終端ascii
WORD wPreset; // プリセット番号
WORD wBank; // バンク番号 0~127は楽器用 128はパーカッション用
WORD wPresetBagNdx; // pbagの先頭のindex
DWORD dwLibrary; // 予約 0
DWORD dwGenre; // 予約 0
DWORD dwMorphology; // 予約 0
}
なお、wPresetBagNdxはphdrの先頭から順に増加する必要があります。
当初この仕様を見落としており、phdrだけ書き換えて不要なものは消せばOKだと思い、実装した結果音が違う音が鳴るようになってしまいました。
この仕様を満たすため、pbag, pmod, pgenサブチャンクも編集する必要があります。
phdrの末尾(EOP)の値は以下のようにします。
変数名 | 値 |
---|---|
achPresetName | EOP |
wPreset | 0 |
wBank | 0 |
wPresetBagNdx | pbagの末尾のindex |
dwLibrary | 0 |
dwGenre | 0 |
dwMorphology | 0 |
pbagサブチャンク
pbagサブチャンクはどのモジュレーション(pmod)とジェネレーター(pgen)をプリセットで利用するかを示す情報が格納されています。
プリセットとpbagの関連付けはあるプリセットのwPresetBagNdxが指すpbagから次のプリセットのwPresetBagNdx-1のpbagまでが関連付けられます。
(そのため1つのプリセットに複数のpbagを関連付けることも可能です)
struct pbag {
WORD wGenNdx; // pgenの先頭のindex
WORD wModNdx; // pmodの先頭のindex
}
phdr同様wGenNdxとwModNdxはpbagの先頭から順に増加する必要があります。
pbagの末尾の値は以下のようにします。
変数名 | 値 |
---|---|
wGenNdx | pgenの末尾のindex |
wModNdx | pmodの末尾のindex |
pgenサブチャンク
pgenサブチャンクはプリセットと関連付けるインストゥルメントや音量、フィルターといったパラメータ情報(ジェネレーター)が格納されます。
中身はパラメーターの種類と値というキーバリュー形式となっています。
struct pgen {
WORD sfGenOper; // パラメーターの種類
WORD genAmount; // パラメーターの値
}
なおgenAmountはパラメーターの種類に応じて2つのbyte、short、もしくはword型の値が入ます。(サイズはword固定です。)
pbagの末尾の値は以下のようにします。
変数名 | 値 |
---|---|
sfGenOper | 0 |
genAmount | 0 |
pmodサブチャンク
pmodサブチャンクはMIDIのコントロールチェンジやベロシティといった動的なパラメーターから音をどのように変化させるか(音量を変えたりフィルターを掛けたり)を対応付ける情報が格納されています。
struct pmod {
WORD sfModSrcOper; // モジュレーション元のパラメーターの種類(CCやベロシティなど
WORD sfModDestOper; // 操作するパラメーターの種類(音量やフィルターの強さなど)
SHORT modAmount; // 操作量
WORD sfModAmtSrcOper; // モジュレーションの操作量を変化させるモジュレーション元のパラメーターの種類
WORD sfModTransOper; // 入力された操作量を変換する(線形、曲線)
}
pmodの末尾の値は以下のようにします。
変数名 | 値 |
---|---|
sfModSrcOper | 0 |
sfModDestOper | 0 |
modAmount | 0 |
sfModAmtSrcOper | 0 |
sfModTransOper | 0 |
サブチャンクの関係
各サブチャンクの関係を見るとこのようになります。
例えばこの図の例だとプリセット0はbag0とbag1が関連付けれれており、bag0はgen0,gen1とmod0が、bag1はgen2とmod1, mod2が関連付けられているのでプリセットで利用されるジェネレーターはgen0~gen2、モジュレーションはmod0~mod2というイメージになります。
(仕様書を読み込んでないので正しくないかもしれませんが、bag単位でジェネレーターとモジュレーションは関連付けられて音が鳴ると思われますが、ファイルを触る範囲ではあまり気にしなくていいと思います。)
サウンドフォントパーサー
ソースコード:https://github.com/mmitti/sf2conv/blob/master/riff.py
RIFFとサウンドフォントの構造(の一部)をパースできるスクリプトをPythonとstructモジュールを利用して作成しました。
RIFFチャンク、LISTチャンク、phdr、pbag、pmod、pgenサブチャンクの読み書きに対応しています。また、それ以外のチャンクは編集しないため読み出したものをそのまま書き込むようにしています。
phdrを削除する際にpbag, pmod, pgenも対応するものを削除する必要がありますので、書き込み時に更新処理をしています。
(夏休み中自動車学校の待ち時間に実装していたのですが、今見ると汚いですね。RiffRootとかElementとか何という
サウンドフォントのマッピングを行うプログラム
ソースコード:https://github.com/mmitti/sf2conv/blob/master/main.py
上のRiffパーサー(というかサウンドフォントパーサー)を利用してサウンドフォントを変換しMusicStudioProducerの音色マップとVirtualMIDISynthの設定ファイルを吐くスクリプトを作りました。
jsonファイルに入力するサウンドフォントや除外する音色、音色名の置換ルールなどを書いておき、これを利用してサウンドフォントファイルを書き換え、音色名とバンク、プログラム番号を音色マップに吐くだけの簡単なスクリプトとなっています。
地味に変な名前の音色(------とか)や管楽器系がピアノのプログラム番号に割り当てられているサウンドフォントがあったりして、設定できるルールを増やしていた結果設定項目は増えてしまいましたが、設定ファイルを書いてしまえばあとは空いている部分に音色が割り当てられてDAWの音色リストに追加されるので新しいサウンドフォントの追加は楽になりました。
おわりに
今回はDTM環境が破損したことからサウンドフォントの中身をのぞくこととなり、多少は理解が深まりました。
スクリプトでサウンドフォントの追加作業が簡単になりましたので、さっそく使ってみたかったsinfonを導入して1曲打ち込んでみました。
今作っているFPGA USB MIDIデバイスが完成した後はサウンドフォントを読み込む方式のMIDI音源も作ってみたいと思っているのでまた、そのうちサウンドフォントについて調べて書くかもしれません。
それではまた