音楽ファイルと使用されるWaveファイルを解析しました。Visual Studio Express 2013を使用してC#言語によりWave情報から振幅値データを取得しました。
Waveファイルフォーマットの仕様
Waveファイルフォーマットの仕様を次に示します。
| 名称 | サイズ | 機能 |
|---|---|---|
| RIFFヘッダ | 4バイト | RIFFファイル形式のファイルであることを表す文字列。 固定文字列「RIFF」 |
| ファイルサイズ | 4バイト | ファイルサイズ(バイト)。リトルエンディアン |
| WAVEヘッダ | 4バイト | RIFF形式のWAVEファイルであることを表す文字列。 固定文字列「WAVE」 |
| フォーマットチャンクヘッダ | 4バイト | 以下が”fmt ”(フォーマット)チャンクの定義を表す |
| フォーマット定義サイズ | 4バイト | フォーマットの定義関連のデータサイズ |
| フォーマットID | 2バイト | 圧縮のフォーマットID 0x0001:リニアPCM 0x0002:MS ADPCM 0x0005:IBM CSVD 0x0006:A-Law 0x0007:μ-Law |
| チャンネル数 | 2バイト | モノラルの場合:0x0001 ステレオの場合:0x0002 |
| サンプリングレート | 4バイト | 単位:Hz |
| データ転送速度[byte/sec] | 4バイト | 44.1kHz 16bitステレオの場合:44100×2×2 =176400 44.1KHz 16bitモノラルの場合:44100×2×1=88200 |
| バイト/サンプル | 2バイト | サンプルあたりのバイト数 16bitステレオ:4 16bitモノラル:2 8bitモノラル:1 |
| 量子化ビット数 | 2バイト | 16/8 |
| データチャンクヘッダ | 4バイト | 以下が”data”(データ)チャンクの定義を表す |
| データ定義サイズ | 4バイト | データの定義関連のデータサイズ |
| サンプリングデータ | nバイト | 音楽サンプルデータ。リトルエンディアン |
アドレス「0」から4バイトに「RIFF」が見えます。アドレス「c」から4バイトに「fmt」チャンクが見えます。アドレス「24」から4バイトに「data」が見えます。
アドレス「59c2c」から4バイトに「LIST」チャンクが見えます。
様々なフォーマットを持つWaveファイルのサンプルが「WAV」にあります。「RIFF Inspector」を利用して表示させると、次のように分かりやすい形式で表示できます。
Waveデータの振幅値データの取得プログラムの作成
Waveファイルフォーマットの仕様に基づいて、C#で作成したWaveデータの振幅値データの取得メソッド「ReadWave」を次に示します。
private bool ReadWave(string waveFilePath)
{
//Debug.WriteLine("ReadWave : " + waveFilePath);
maxValue = 0;
// ファイルの存在を確認する
if (!File.Exists(waveFilePath))
{
return false;
}
using (FileStream fs = new FileStream(waveFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
try
{
BinaryReader br = new BinaryReader(fs);
waveHeader.RiffHeader = Encoding.GetEncoding(20127).GetString(br.ReadBytes(4));
waveHeader.FileSize = BitConverter.ToInt32(br.ReadBytes(4), 0);
waveHeader.WaveHeader = Encoding.GetEncoding(20127).GetString(br.ReadBytes(4));
bool readFmtChunk = false;
bool readDataChunk = false;
while (!readFmtChunk || !readDataChunk)
{
// ChunkIDを取得する
string chunk = Encoding.GetEncoding(20127).GetString(br.ReadBytes(4));
if (chunk.ToLower().CompareTo("fmt ") == 0)
{
// Debug.WriteLine("fmt : " + waveFilePath);
// fmtチャンクの読み込み
waveHeader.FormatChunk = chunk;
waveHeader.FormatChunkSize = BitConverter.ToInt32(br.ReadBytes(4), 0);
waveHeader.FormatID = BitConverter.ToInt16(br.ReadBytes(2), 0);
waveHeader.Channel = BitConverter.ToInt16(br.ReadBytes(2), 0);
waveHeader.SampleRate = BitConverter.ToInt32(br.ReadBytes(4), 0);
waveHeader.BytePerSec = BitConverter.ToInt32(br.ReadBytes(4), 0);
waveHeader.BlockSize = BitConverter.ToInt16(br.ReadBytes(2), 0);
waveHeader.BitPerSample = BitConverter.ToInt16(br.ReadBytes(2), 0);
readFmtChunk = true;
}
else if (chunk.ToLower().CompareTo("data") == 0)
{
// Debug.WriteLine("data : ");
// dataチャンクの読み込み
waveHeader.DataChunk = chunk;
waveHeader.DataChunkSize = BitConverter.ToInt32(br.ReadBytes(4), 0);
waveData = br.ReadBytes(waveHeader.DataChunkSize);
// Debug.WriteLine(string.Format("waveData : {0:X} {1:X}", waveData[0], waveData[1]));
// 再生時間を算出する
int bytesPerSec = waveHeader.SampleRate * waveHeader.BlockSize;
waveHeader.PlayTimeMsec = (int)(((double)waveHeader.DataChunkSize / (double)bytesPerSec) * 1000);
Debug.WriteLine(string.Format("データサイズ:{0} 再生時間 : {1}", waveHeader.DataChunkSize, waveHeader.PlayTimeMsec));
tbxPlay.Text = waveHeader.PlayTimeMsec.ToString() + "ms秒";
convertWaveData();
readDataChunk = true;
}
else
{
Debug.WriteLine("chunk : " + chunk);
// 不要なチャンクの読み捨て
Int32 size = BitConverter.ToInt32(br.ReadBytes(4), 0);
if (0 < size)
{
br.ReadBytes(size);
}
}
}
}
catch
{
fs.Close();
return false;
}
}
return true;
}
- 27行目でチャンクを呼び出し、29行目でフォーマット、44行目でデータのそれぞれチャンクを判断しています。
- 33行目から40行目までで。fmtチャンクに含まれるフォーマットID等を各メソッドに保存しています。
- 51行目でdataチャンクに保存されているサンプリングデータをバイト単位で保存し、55行目から56行目で再生時間を算出しています。
- 59行目のメソッド「convertWaveData」で取得したサンプリングデータを符号付きint型に変換します。
符号付きint型変換メソッド「convertWaveData」は、チャンネル数と量子化ビット数に従って、取得したサンプリングデータを符号付きint型に変換します。8ビット/16ビットのモノラルとステレオのWeveファイルに対応しています。
private void convertWaveData()
{
try
{
// 音声データの取得
valuesR = new int[(waveHeader.DataChunkSize / waveHeader.Channel) / (waveHeader.BitPerSample / 8)];
valuesL = new int[(waveHeader.DataChunkSize / waveHeader.Channel) / (waveHeader.BitPerSample / 8)];
Debug.WriteLine(string.Format("valuesR.Length : {0} ", valuesR.Length));
// 1標本分の値を取得
int frameIndex = 0;
int chanelIndex = 0;
for (int i = 0; i < waveHeader.DataChunkSize / (waveHeader.BitPerSample / 8); i++)
{
byte[] data = new byte[2];
int work = 0;
switch (waveHeader.BitPerSample)
{
case 8:
work = (int)waveData[frameIndex];
frameIndex += 1;
break;
case 16:
Array.Copy(waveData, frameIndex, data, 0, 2);
work = (int)BitConverter.ToInt16(data, 0);
frameIndex += 2;
break;
default:
MessageBox.Show("波形解析できません", "エラー",
MessageBoxButtons.OK, MessageBoxIcon.Error);
break;
}
if (waveHeader.Channel == 1)
{
valuesR[i] = work;
}
else
{
if (chanelIndex == 0)
{
chanelIndex = 1;
valuesR[i/2] = work;
Debug.WriteLine(string.Format("valuesR :{0} {1:X} ", i,valuesR[i/2]));
}
else
{
chanelIndex = 0;
valuesL[i/2] = work;
// Debug.WriteLine(string.Format("valuesL : {0:X}", valuesL[i/2]));
}
}
}
Debug.WriteLine(string.Format("maxValue : {0}", maxValue));
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
}
- 6行目と7行目でそれぞれR/Lの音声データの保存領域を確保します。
- 19行目で量子化ビット数に従い、1バイトのデータか2バイトのデータかを判断します。
- 35行目でチャンネル数がモノラルの場合はR側のバッファに、ステレオの場合は、R/Lのバッファにそれぞれ保存します。
Waveデータの振幅値データの取得プログラムの実行
実際の16ビットステレオのWaveファイルの「fmt 」チャンクをダンプした結果を次に示します。
RIFF ヘッダ: RIFF ファイルサイズ: 31828 WAVE ヘッダ: WAVE フォーマットチャンク: fmt フォーマットチャンクサイズ: 16 フォーマット ID: 1 チャンネル数: 2 サンプリングレート: 44100 1秒あたりのデータ数: 176400 ブロックサイズ: 4 1サンプルあたりのビット数: 16
16ビットステレオのWaveファイルの取得したサンプリングデータを符号付きint型に変換します。
valuesR : FFFFFF93 valuesR : FFFFFFAE valuesR : FFFFFFB8 valuesR : FFFFFFB7 valuesR : FFFFFFAF valuesR : FFFFFFAD valuesR : FFFFFFBE valuesR : FFFFFFD3 valuesR : FFFFFFCA valuesR : FFFFFFB7 valuesR : FFFFFFAE valuesR : FFFFFFB0 ・・・ ・・・ valuesR : FFFFFFD5 valuesR : FFFFFFE7


