C#によるWaveファイルの解析

音楽ファイルと使用されるWaveファイルを解析しました。Visual Studio Express 2013を使用してC#言語によりWave情報から振幅値データを取得しました。

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バイト 音楽サンプルデータ。リトルエンディアン

waveファイルの「fmt」チャンクと「data」チャンク
アドレス「0」から4バイトに「RIFF」が見えます。アドレス「c」から4バイトに「fmt」チャンクが見えます。アドレス「24」から4バイトに「data」が見えます。
waveファイルの「LIST」チャンク
アドレス「59c2c」から4バイトに「LIST」チャンクが見えます。

様々なフォーマットを持つWaveファイルのサンプルが「WAV」にあります。

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