はじめに
- Github Pageによるデモ
-
githubリポジトリ
このリポジトリの目的はWebAudioAPIを使ってMIDIシンセサイザーを作ることにより、MIDIファイル(Standard MIDI File)とSoundFontの仕様を理解することである。
この記事ではこれらを読み込むための構造を解説する。
SoundFont
まず、SoundFontについて解説する、SoundFontのパースはsrc/sf/以下のディレクトリに置かれている。
SoundFontはRIFF形式という汎用形式に則っている、これはリポジトリのsrc/sf/read_riff.tsでパースしている。
RIFFは子要素を持てるLIST(RIFF)とデータ本体があり、これらをチャンクと呼ぶ。RIFFはファイル先頭に置かれる特殊なLISTである。
リストは以下でTYPEで識別子を置ける。
- ID (4バイト、LIST/RIFF)
- SIZE (4バイト)
- TYPE (4バイト、識別子)
- DATA (SIZEバイト、子チャンク)
データチャンクはこのような形になる。
- ID (4バイト、識別子)
- SIZE (4バイト)
- DATA (SIZEバイト、データ本体)
これらのチャンクに以下のように各項目がアサインされている。
- RIFF - sfbk
- 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 (サンプルヘッダー)
- LIST - INFO
INFOは基本的には音を作るためには使わないメタデータであり
sdtaが音を作るためのデータ本体であり、それを読み込むための情報がヘッダー(shdr)にある。
これらを加工、結合したのがinstであり、そのためのデータがigen、imodに入っている、それを読み込むためのデータがibagに入っている。
更にinstは同様にpresetに纏められる。
つまり、Preset-(Zone)→Instrument-(Zone)→Sampleという順番で参照する形式にする。
ここで、間に挟まっているゾーンが使うInstrument/Sampleの選択や音色の加工などのデータが入っており、Zone: { Generators[], Modulators[] }のようにジェネレーターとモジュレーターの配列が入っている。
より詳しい解説は、この記事(SoundFont 2.04 仕様の概要まとめ)を参照してください。
SoundFont系で使われる型は以下でSFFileがファイル本体、SFInfoがInfoメタファイル、 { major: number, minor: number }はバージョン番号として使っている。
export type SFFile = {
info: SFInfo;
presets: Preset[];
}
export type SFInfo = Record<string, string | { major: number, minor: number }>;
export type Sample = {
name: string;
sample: Float32Array;
loop_start: number;
loop_end: number;
sampling_rate: number;
origin_pitch: number;
correction: number;
sample_link: number;
sample_type: number;
}
export type Modulator = {
mod_src_oper: number;
gen_name: string;
amount: number;
mod_amt_src_oper: number;
mod_trans_oper: number;
}
export type Instrument = {
name: string;
zones: InstZone[];
global_zone?: InstZone;
}
export type Preset = {
name: string;
MIDInumber: number;
bank: number;
zones: PresetZone[];
global_zone?: PresetZone;
library: number;
genre: number;
morphology: number;
};
export type InstZone = {
generators: Record<string, number | [ number, number ] | undefined>;
modulators: Modulator[];
sample?: Sample;
}
export type PresetZone = {
generators: Record<string, number | [ number, number ] | undefined>;
modulators: Modulator[];
instrument?: Instrument;
}
MIDIファイル
MIDIファイルのパースはsrc/midi以下に置かれている。
MIDIファイルは独自のバイナリ形式で
- 'MThd' (4バイト、固定文字列)
- SIZE (4バイト、全体のデータサイズ)
- FORMAT (2バイト、フォーマット)
- NTRACK (2バイト、トラック数)
- DIVISION (2バイト、基本時間分割単位)
- 'MTrk' (4バイト、トラックの先頭に現れる)
- Event列 (トラックの中に含まれるEvent、NTRACKだけ繰り返される)
となっている。ここでフォーマットは0、1、2があり0はトラック一つに、1は複数トラックがあるがテンポは全トラック共通、2はテンポが独立している、2はほぼ使われないのでこのリポジトリでは対応していない。
MIDIEventと可変長データ
データ本体はMIDIEventを並べる形で書かれている。
MIDIの時間はデータ圧縮のため可変長データという形式が多く出てくる、これは最上位7bitを次のデータを読むためのフラグとして利用しており、以下のコードで読みだしている。
読むデータも可変なので読んだデータ(position)を返している。
export default function readVLQ(view: DataView, offset: number) {
let value = 0;
for( let i = 0; i < 4; i++ ){
const byte = view.getUint8(offset++);
value = (value << 7) | (byte & 0x7f);
if( (byte & 0x80) === 0 ) return { value, offset };
}
throw new Error('可変長数値が4バイトを超えています');
}
これは、デルタタイム(MIDI内で使われる時間)やMetaEvent、SysExEventなどの可変なデータ長を読むときにも使われている。
MIDIEventにはMidiEventとMetaEvent、SysExEventがある。
MidiEventが演奏に使われるEventでありMetaEventは基本的には演奏にかかわらないものであるがTempoはテンポを変えて演奏時間を変えるので演奏に必要である。
SysExEventはシステムに係るものでシステムごとに定義されるものであるが全リセットだけはよく使われる。
MIDIの読み取りで使われる型は以下でMIDIFIleがファイル本体、
NoteOn/NoteOffのMidiEventはpost_parseでまとめている。また時間計算に必要なTempoに係るEventは抜き出してある。
export type MidiFile = {
format: number;
division: number;
tempos: Event[];
tracks: Array<Array<Event>>;
}
export type MidiEvent = {
event: string;
type: string;
tick: number;
channel: number;
data: number | [ number, number ];
};
export type Note = {
event: string;
type: string;
channel: number;
midi_note: number;
velocity: number;
tick: number;
endTick: number;
off_tick?: number;
release_scale?: number;
soft?: boolean;
};
export type MetaEvent = {
event: string;
type: string;
tick: number;
id: number;
data: number |
{ hr: number, mn: number, se: number, fr: number, ff: number } |
{ nn: number, dd: number, cc: number, bb: number } |
{ sf: number, minor: boolean } |
Uint8Array |
string | undefined
};
export type SysExEvent = {
event: string;
type: string;
tick: number;
data: Uint8Array;
}
export type Event = MidiEvent | MetaEvent | SysExEvent | Note;
最後に
これでMIDIとSFを読みだせたので次に実際に発音させる段階で詳細な意味などを解説していきたい。