はじめに
ここではRIFFファイルとフォーマットの概要と読み込むためのクラスの実装について説明します。
今回の作成するクラスのソースファイルはこちらにあります。 → riffsample_20231210
※WavファイルやSoundfontファイル読込サンプルも同梱されています。
RIFFファイル形式をもとにして作られたファイルの取り扱についても下記にまとめています。
RIFFファイルとは
RIFFファイルとは「Resource Interchange File Format」の略になります。
MicrosoftとIBMが提案したマルチメディア用のファイルフォーマットで、音声や動画などのデータ(=Resource)をアプリケーション間でやり取り(Interchange)するためのファイルフォーマットになります。
アプリケーション固有の情報をファイルに付与しても、他のアプリケーションでの読み込みに影響をなくすように設計されたファイルフォーマットです。
有名なファイルとして音声ファイルの「.wav」や動画ファイルの「.avi」などがある他、DTMでサンプラー等の音色ファイルとして使われるSoundFontファイル(.sf2)もこのRIFFファイルフォーマットとなっています。
RIFFファイルフォーマット概要
RIFFファイルではデータに4Byteの識別子(ID)を付けてデータを管理します。
識別子(ID)はFourCC(4 character codeの略)と呼ばれる形式で、1文字1Byteの4文字(4Byte)であらわす形式になります。
(NULL文字で終端されておらず4Byte固定ですので文字列ではありません。)
識別子(ID)を付けてまとめたデータのかたまりをチャンク(Chunk)と呼びます。識別子(ID)はチャンクIDとも呼ばれます。
チャンクは階層構造にして管理することもできます。
RIFFファイルフォーマットのイメージは下記になります。
チャンクについて
1つ1つのチャンクはすべて下記の構造になっています。
構造 | サイズ | 概要 |
---|---|---|
チャンクID | 4Byte | データにつけられた識別子(ID)。 【例】「WAVE」「fmt 」「smpl」など。(「fmt 」の最後は空白文字がある。) |
データサイズ | 4Byte | データ部のサイズ。単位はByte。 【例】データ部のサイズが100byteなら100になる。 |
データ部 | – | 実際のデータ内容。サイズは上記の「データサイズ」になる。 【例】wavファイルの識別子「data」のチャンクなら音声波形データが保存されている |
チャンクやデータサイズ、データ部のエンディアンは基本的にリトルエンディアンになります。(MIDIのSMFファイルなどはビッグエンディアンのようです。)
特殊なチャンクとしてRIFFチャンクとLISTチャンクがあります。それ以外は一般チャンクとなります。
RIFFチャンクについて
RIFFチャンクは必ずファイルの先頭にあるチャンクで、必ずRIFFファイルのトップ階層になります。
(他のチャンクはすべてRIFFチャンク配下となります。)
RIFFチャンクのチャンクIDは「RIFF」固定で、データ部の先頭4Byteはファイル識別子と呼ばれるFourCC形式のデータが入ります。
ファイル識別子はファイルの種類を示すためのコードになります。(【例】wavファイルの場合「WAVE」、SoundFontファイルの場合「sfbk」等)
構造 | サイズ | 概要 |
---|---|---|
チャンクID | 4Byte | 「RIFF」固定 |
データサイズ | 4Byte | データ部のサイズ。必ず「ファイルサイズ – 8Byte」となる。 (ファイルサイズからチャンクID 4Byteとデータサイズ 4Byteを除いた値。) |
データ部 | – | 実際のデータ内容。 先頭4Byteはファイル識別子(FourCC形式)で、その後はLISTチャンクや一般チャンクが保存されている。 【ファイル識別子 例】wavファイルの場合「WAVE」、SoundFontファイルの場合「sfbk」等 |
LISTチャンクについて
LISTチャンクは一般チャンクをまとめるためのチャンクになります。(一般チャンク配下にはチャンクを入れることができません。)
LISTチャンクのチャンクIDは「LIST」固定で、データ部の先頭4ByteはLIST識別子と呼ばれるFourCC形式のデータが入ります。
LIST識別子はLISTチャンクの種類を示すためのコードになります。
構造 | サイズ | 概要 |
---|---|---|
チャンクID | 4Byte | 「LIST」固定 |
データサイズ | 4Byte | データ部のサイズ。配下の全チャンク合計サイズ + 4Byte(LIST識別子分)になる。 |
データ部 | – | 実際のデータ内容。 先頭4ByteはLIST識別子(FourCC形式)で、その後は一般チャンクが保存されている。 【LIST識別子 例】SoundFontファイルの場合「INFO」「sdta」「pdta」等 |
チャンク構造イメージ
各チャンクの構造をイメージした図が下記になります。
基本的にRIFFチャンクをトップ階層として、その配下にLISTチャンクや一般チャンクがある階層構造になっています。
RIFFファイル形式をもとにして作られたファイルには厳密に階層構造やチャンクの順番等が決められているものもあるようです。
チャンクIDの重複について
チャンクIDについては重複が許されているようで、スタンダードMIDIファイル(.smfファイル)などは同じチャンクIDのチャンクが複数存在することがあります。
RIFFファイル読み込みについて
RIFFファイルの読み込み方法として簡単な方法は、ファイルを開いてから下記を繰り返す形で読み込みます。
厳密には階層構造・チャンク順番等を考慮しなければならないのですが、一般チャンクを探してそのデータを読み込むだけでよい場合が多いためです。
- ファイル内からチャンクIDをもとに必要な一般チャンクを探す
- 探したチャンクのデータサイズ分メモリを確保
- 確保したメモリにチャンクのデータを読み込み
RIFFファイル読み込みクラス
今回作成するRIFFファイル読み込みクラスはCRiffLoaderとしています。
CRiffLoaderクラスの詳細は後述しますが、下記の機能を備えたクラスにしています。
- ファイルを開くためのコンストラクタ
- チャンクIDを指定するとそのチャンクのサイズを返すgetChunkSize()関数
- チャンクIDを指定するとそのチャンクのデータをバッファにコピーするgetChunkData()関数
CRiffLoaderクラスを使えば「RIFFファイル読み込みについて」の手順で読み込めるようにしています。
【使用方法】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 読み込みたいチャンクのIDを設定(FOURCC型はunsigned longの再定義) // 「FOURCC fccTEST = 'TEST';」のように代入すると、 // バイトオーダーの関係で中身が逆(上記の場合'TSET')になるので注意 FOURCC fccTEST = FourCC('T', 'E', 'S', 'T') ; CRiffLoader rl("test.wav"); // ファイルを開く // 指定したチャンクのデータを取得する // チャンクのサイズを取得 → サイズ分のバッファ確認 → データをバッファに取得 // チャンクサイズを取得。引数はチャンクID(FOURCC型)。 // 戻り値はチャンクサイズ(DWORD型はunsigned longの再定義) DWORD size = rl.getChunkSize(fccTEST); // バッファを確保 char* buffer = new [size]; // チャンクデータをバッファに取得。引数はチャンクID(FOURCC型)、バッファ、バッファサイズの順 rl.getChunkData(fccTEST, buffer, size); // 上記を必要なチャンクをすべて読み込むまで繰り返す ~~ 以下略 ~~ |
具体的にCRiffLoaderクラスは下記のように定義しています。
基本的に上記の「使用方法」以上のことはできませんので、説明については省略させていただきます。
流用する際は下記にご注意ください。
- 可読性を上げるためエラー処理は省いておりますので流用する際は必要なエラー処理を追記ください。
- 再定義している「BYTE」「WORD」「DWORD」「FOURCC」等はwindows.h等と重複しているので必要に応じて書き換えてください
- fopen()関数を利用していますが、Visual Studioではセキュリティ上の警告がでるので、fopen_s()関数などに書き換えてください。
ソースファイルはこちらになります。 → riffsample_20231210
※WavファイルやSoundfontファイル読込サンプルも同梱されています。
【criffloader.h】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
#pragma once #include <map> #include <vector> #include <string> using namespace std; // 型の再定義 // この再定義はwindows.hなどのヘッダファイルと重複するので注意 typedef unsigned char BYTE; // 1バイトデータ用 typedef unsigned short WORD; // 2バイトデータ用 typedef unsigned long DWORD; // 4バイトデータ用 // FourCC用に型を再定義 typedef DWORD FOURCC; // チャンクIDの定義 #define FourCC(c1, c2, c3, c4) (c1 + (c2 << 8) + (c3 << 16) + (c4 << 24)) // チャンクID作成用マクロ const FOURCC fccRIFF = FourCC('R', 'I', 'F', 'F'); const FOURCC fccLIST = FourCC('L', 'I', 'S', 'T'); // Standard MIDI Fileなどの一部のファイルはビッグエンディアンなので変換に必要 // (Windowsはリトルエンディアン) inline DWORD swap32bit(DWORD data) { return ((data & 0x000000FF) << 24) + ((data & 0x0000FF00) << 8) + ((data & 0x00FF0000) >> 8) + ((data & 0xFF000000) >> 24); } inline WORD swap16bit(WORD data) { return ((data & 0x00FF) << 8) + ((data & 0xFF00) >> 8); } // RIFFファイルを読み込むためのクラス // エラー処理は省いているため必要に応じて追加すること // 【使用方法】 // FOURCC fccTEST = FourCC('T', 'E', 'S', 'T') ; // CRiffLoader rl("test.wav"); // ファイルを開く // // // 指定したチャンクのデータを取得する // // チャンクのサイズを取得 → サイズ分のバッファ確認 → データをバッファに取得 // DWORD size = rl.getChunkSize(fccTEST); // チャンクサイズを取得 // // char* buffer = new [size]; // バッファを確保 // rl.getChunkData(fccTEST, buffer, size); // チャンクデータを取得 // class CRiffLoader { private: FILE* fp = nullptr; DWORD filesize = 0; bool isBigEndian = false; map<FOURCC, vector<DWORD>> chunkmap; // チャンクデータの位置(ファイルポインタ位置)の記録用 public: // コンストラクタ。開くファイルを指定することもできる。 CRiffLoader() {}; CRiffLoader(string filename, bool bigEndian = false) { open(filename, bigEndian); }; void open(string filename, bool bigEndian = false) { // ファイルを開く fopen_s(&fp, filename.c_str(), "rb"); if (fp == nullptr) { return; } isBigEndian = bigEndian; // ファイルサイズを取得 // fseek(SEEK_END) + ftell()を使ったファイルサイズ取得は // 環境によっては動作しないので環境に合わせて修正すること // (Windows 10、Visual Studio 2017 ver15.4.4はたぶん大丈夫) fseek(fp, 0, SEEK_END); filesize = ftell(fp); fseek(fp, 0, SEEK_SET); // ファイルの位置を最初に戻しておく // ファイルの終わりまで確認してチャンクの位置をchunkmapに保存する while ((DWORD)ftell(fp) < filesize) { // チャンクのID(FourCC)とサイズを読み込む DWORD chunkID; fread(&chunkID, sizeof(FOURCC), 1, fp); DWORD chunkSize; fread(&chunkSize, sizeof(DWORD), 1, fp); if (isBigEndian) { chunkSize = swap32bit(chunkSize); } // エンディアンの変換 switch (chunkID) { case fccRIFF: // RIFFチャンクとINFOチャンクの処理 case fccLIST: // データ部分の最初 4byte(チャンクタイプ)をスキップ // 残りのデータ部分はサブチャンクなので最初 4byteのみとする fseek(fp, sizeof(DWORD), SEEK_CUR); break; default: // 通常のチャンクの処理 // ファイルポインタの位置を保存しておく // (該当するチャンクのサイズの位置) chunkmap[chunkID].push_back((DWORD)ftell(fp) - sizeof(DWORD)); // データ部分はスキップする fseek(fp, chunkSize, SEEK_CUR); break; } } } ~CRiffLoader() { // ファイルを閉じる if (fp != nullptr) { fclose(fp); } } // ファイルサイズを取得する DWORD getFileSize() { return filesize; }; // 引数のchunkID(FourCC)で指定したチャンクの数を取得する // (MIDI SMFのように同じチャンクIDが複数ある場合があるため) DWORD getChunkNum(FOURCC chunkID) { // chunkmapにchunkIDがあるかのチェック if (chunkmap.count(chunkID) == 0) { return 0; } return chunkmap[chunkID].size(); } // 引数のchunkID(FourCC)で指定したチャンクのサイズを取得する // 失敗すると0、成功するとチャンクのサイズが返る DWORD getChunkSize(FOURCC chunkID, DWORD id = 0) { // chunkmapにchunkIDがあるかやidがサイズを超えていないかのチェック if (chunkmap.count(chunkID) == 0) { return 0; } if (chunkmap[chunkID].size() <= id) { return 0; } // ファイルポインタの位置をチャンクの場所まで進めて読み込み DWORD size = 0; fseek(fp, chunkmap[chunkID][id], SEEK_SET); fread(&size, sizeof(DWORD), 1, fp); if (isBigEndian) { size = swap32bit(size); } // エンディアンの変換 return size; } // 引数のchunkID(FourCC)で指定したチャンクのデータをdataに取得する // 失敗すると0、成功すると1が返る DWORD getChunkData(FOURCC chunkID, void* data, DWORD datasize, DWORD id = 0) { // chunkmapにchunkIDがあるかやidがサイズを超えていないかのチェック if (chunkmap.count(chunkID) == 0) { return 0; } if (chunkmap[chunkID].size() <= id) { return 0; } // ファイルポインタの位置をチャンクの場所まで進めて読み込み DWORD size = 0; fseek(fp, chunkmap[chunkID][id], SEEK_SET); fread(&size, sizeof(DWORD), 1, fp); if (isBigEndian) { size = swap32bit(size); } // エンディアンの変換 size = (datasize > size) ? (size) : (datasize); fread(data, size, 1, fp); return 1; } }; |
最後に
RIFFファイルの概要と読み込みは以上です。
RIFFファイル形式をもとにして作られたファイルの取り扱についても下記にまとめています。必要に応じてご参照ください。
今回の作成するクラスのソースファイルはこちらにあります。 → riffsample_20231210
※WavファイルやSoundfontファイル読込サンプルも同梱されています。
質問はコメント欄や掲示板、Twitterでいただけばとおもいます。
また、「この部分を詳しく」などの要望も掲示板やTwitterでいただければと思います。
■掲示板
■Twitterアカウント:@vstcpp URL:https://twitter.com/vstcpp