WAVEファイルからストリーム経由でPCMを読み込む

WAVEファイルからPCMデータを読み込む方法についてのメモ書きです。
前回の記事で解説したWAVEファイルの構造を元に、実際に読み込む処理を書いてみました。

PcmData.h

#ifndef _PCM_DATA_H_INCLUDED_
#define _PCM_DATA_H_INCLUDED_

#include <windows.h>
#include <istream>
#include <vector>

typedef DWORD U8;

// PCMデータを管理するクラス
class PcmData {
public:
    PcmData();
    virtual ~PcmData();

    void Load(const std::string& fileName);
    void Load(std::istream& in);
    void Clear();

    WAVEFORMATEX& GetFormat();
    std::vector<char>& GetData();

private:
    static void ReadChunk(std::istream& in, const char type[4], std::vector<char>& result);

    WAVEFORMATEX m_format;      // WAVEフォーマット
    std::vector<char> m_buffer; // PCMデータを格納するバッファ
    bool m_isLoaded;            // PCMデータが既にロードされているかどうか
};

#endif  // _PCM_DATA_H_INCLUDED_

PcmData.cpp

#include "PcmData.h"
#include <stdexcept>
#include <fstream>

// コンストラクタ
PcmData::PcmData() :
    m_isLoaded(false)
{
}

// デストラクタ
PcmData::~PcmData() {
    Clear();
}

// WAVEファイルからPCMデータを読み込む
void PcmData::Load(const std::string& fileName) {
    // WAVEファイルを開く
    std::ifstream in(fileName, std::ios::binary);
    if ( !in )
        throw std::runtime_error("WAVEファイルのオープンに失敗しました。");

    // 読み込み処理
    Load(in);
}

// WAVEファイルからPCMデータを読み込む
void PcmData::Load(std::istream& in) {
    // 既に読み込まれていたら失敗
    if ( m_isLoaded )
        throw std::runtime_error("PCMデータは既に読み込まれてします。");

    // RIFFチャンクの読み込み
    char riff[4];
    in.read(riff, sizeof(riff));
    if ( memcmp(riff, "RIFF", sizeof(riff)) )
        throw std::runtime_error("RIFF/WAVEファイルではありません。");

    // RIFFチャンクサイズの読み込み
    U8 riffSize;
    in.read(reinterpret_cast<char*>(&riffSize), sizeof(riffSize));

    // WAVE文字列チェック
    char wave[4];
    in.read(wave, sizeof(wave));
    if ( memcmp(wave, "WAVE", sizeof(wave)) )
        throw std::runtime_error("RIFF/WAVEファイルではありません。");

    // フォーマットチャンクの読み込み
    std::vector<char> fmtChunk;
    ReadChunk(in, "fmt ", fmtChunk);

    // フォーマットチェック
    if ( fmtChunk.size() != sizeof(WAVEFORMATEX) )
        throw std::runtime_error("認識できないフォーマットです。");

    m_format = reinterpret_cast<waveformatex&>(fmtChunk[0]);

    if ( m_format.wFormatTag != WAVE_FORMAT_PCM )
        throw std::runtime_error("認識できないフォーマットです。");

    // データチャンクの読み込み
    ReadChunk(in, "data", m_buffer);

    m_isLoaded = true;
}

// データクリア
void PcmData::Clear() {
    if ( m_isLoaded ) {
        m_buffer.clear();
        m_isLoaded = false;
    }
}

// フォーマットを取得する
WAVEFORMATEX& PcmData::GetFormat() {
    return m_format;
}

// PCMデータを取得する
std::vector<char>& PcmData::GetData() {
    return m_buffer;
}

// チャンクを読み込む
void PcmData::ReadChunk(std::istream& in, const char type[4], std::vector<char>& result) {
    size_t initPos = in.tellg();

    char readType[4];
    U8 size = 0;

    // チャンクの探索
    do {
        // データチャンクサイズ分をスキップ
        in.seekg(size, std::ios::cur);

        // チャンクタイプ読み込み
        in.read(readType, sizeof(readType));
        if ( !in )
            throw std::runtime_error("チャンクの探索に失敗しました。");

        // チャンクサイズ読み込み
        in.read(reinterpret_cast<char*>(&size), sizeof(size));

    } while ( memcmp(type, readType, sizeof(readType)) );

    // データの読み込み
    result.assign(size, char());
    in.read(&result[0], size);

    in.seekg(initPos, std::ios::beg);
}

Main.cpp

#include "PcmData.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
try {
    // WAVEファイル読み込み
    PcmData pcm;
    pcm.Load("test.wav");

    MessageBox(NULL, TEXT("PCMの読み込みに成功しました。"), TEXT("OK"), MB_OK | MB_ICONINFORMATION);
    return 0;

    return 0;
} catch ( std::exception& e ) {
    MessageBoxA(NULL, e.what(), NULL, MB_OK | MB_ICONSTOP);
}

PcmData::LoadメソッドでWAVEファイルの中身を解析し、PCM形式ならデータを読み込むようにしています。
それ以外の場合はエラーとしています。

// WAVEファイルからPCMデータを読み込む
void PcmData::Load(std::istream& in) {
    // 既に読み込まれていたら失敗
    if ( m_isLoaded )
        throw std::runtime_error("PCMデータは既に読み込まれてします。");

    // RIFFチャンクの読み込み
    char riff[4];
    in.read(riff, sizeof(riff));
    if ( memcmp(riff, "RIFF", sizeof(riff)) )
        throw std::runtime_error("RIFF/WAVEファイルではありません。");

    // RIFFチャンクサイズの読み込み
    U8 riffSize;
    in.read(reinterpret_cast<char*>(&riffSize), sizeof(riffSize));

    // WAVE文字列チェック
    char wave[4];
    in.read(wave, sizeof(wave));
    if ( memcmp(wave, "WAVE", sizeof(wave)) )
        throw std::runtime_error("RIFF/WAVEファイルではありません。");

    // フォーマットチャンクの読み込み
    std::vector<char> fmtChunk;
    ReadChunk(in, "fmt ", fmtChunk);

    // フォーマットチェック
    if ( fmtChunk.size() != sizeof(WAVEFORMATEX) )
        throw std::runtime_error("認識できないフォーマットです。");

    m_format = reinterpret_cast<waveformatex&>(fmtChunk[0]);

    if ( m_format.wFormatTag != WAVE_FORMAT_PCM )
        throw std::runtime_error("認識できないフォーマットです。");

    // データチャンクの読み込み
    ReadChunk(in, "data", m_buffer);

    m_isLoaded = true;
}

ReadChunk()メソッドでフォーマットチャンクとデータチャンクを読み込むようにしています。

// チャンクを読み込む
void PcmData::ReadChunk(std::istream& in, const char type[4], std::vector<char>& result) {
    size_t initPos = in.tellg();

    char readType[4];
    U8 size = 0;

    // チャンクの探索
    do {
        // データチャンクサイズ分をスキップ
        in.seekg(size, std::ios::cur);

        // チャンクタイプ読み込み
        in.read(readType, sizeof(readType));
        if ( !in )
            throw std::runtime_error("チャンクの探索に失敗しました。");

        // チャンクサイズ読み込み
        in.read(reinterpret_cast<char*>(&size), sizeof(size));

    } while ( memcmp(type, readType, sizeof(readType)) );

    // データの読み込み
    result.assign(size, char());
    in.read(&result[0], size);

    in.seekg(initPos, std::ios::beg);
}

チャンクを1個ずつ読み込み、一致するチャンクタイプのチャンクを発見したらそこで探索を終了し、チャンクデータを読み込むようにしています。

今回はあくまでPCMデータの読み込みしか出来ませんが、MP3データの読み込みに対応させることも可能です。