WAVEファイルの作成

前回の記事の続きです。
アプリケーション上で保持しているPCMデータをWAVEファイルとして保存するプログラムについての解説です。

WAVEファイルの構造につきましてはこちらをご覧ください。
WAVEファイルはRIFF形式で保存され、フォーマットチャンクとデータチャンクが最低限必要になります。

まず、RIFFチャンクのヘッダ情報を書き込みます。

    // RIFF文字列の書き込み
    out.write("RIFF", 4);

    // RIFFチャンクサイズスキップ(後で書き込む)
    out.seekp(4, std::ios::cur);

    // WAVE文字列の書き込み
    out.write("WAVE", 4);

RIFFチャンクサイズの書き込みはスキップしていますが、これは作成後のWAVEファイルサイズを知る必要があるためです。
ヘッダを書き込んだら、フォーマットチャンクとデータチャンクを順に書き込みます。

    // フォーマットチャンクの書き込み
    WriteChunk(out, "fmt ", sizeof(m_format), &m_format);

    // データチャンクの書き込み
    WriteChunk(out, "data", m_buffer.size(), &m_buffer[0]);
// チャンクを書き込む
void PcmData::WriteChunk(std::ostream& out, const char type[4], U32 size, const void* data) {
    // チャンクタイプの書き込み
    out.write(type, 4);

    // チャンクサイズの書き込み
    out.write(reinterpret_cast<char*>(&size), sizeof(U32));

    // データの書き込み
    out.write(static_cast<const char*>(data), size);
}

フォーマットチャンクにはWAVEFORMATEX構造体のデータ、データチャンクにはPCMの生データとなります。

最後に、RIFFチャンクサイズを書き込みます。

    // RIFFチャンクサイズの書き込み
    U32 riffSize = out.tellp();
    riffSize -= 8;
    out.seekp(4, std::ios::beg);
    out.write(reinterpret_cast<char*>(&riffSize), sizeof(U32));

RIFFチャンクサイズはファイル全体サイズ – 8となるため、out.tellp()で末尾のストリームポインタ位置を取得してこれをファイル全体サイズとし、これから8を減じた値をRIFFサイズとして書き込んでいます。

上記の流れを記したサンプルプログラムを以下に記します。

PcmData.h

#ifndef _PCM_DATA_H_INCLUDED_
#define _PCM_DATA_H_INCLUDED_

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

typedef DWORD U32;

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

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

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

private:
    static void ReadChunk(std::istream& in, const char type[4], std::vector<char>& result);
    static void WriteChunk(std::ostream& out, const char type[4], U32 size, const void* data);

    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チャンクサイズの読み込み
    U32 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;
}

// PCMデータをWAVEファイルに保存する
void PcmData::Save(const std::string& fileName) {
    // WAVEファイルを開く
    std::ofstream out(fileName, std::ios::binary);
    if ( !out )
        throw std::runtime_error("WAVEファイルのオープンに失敗しました。");

    // 保存処理
    Save(out);
}

// PCMデータをWAVEファイルに保存する
void PcmData::Save(std::ostream& out) {
    // PCMが読み込まれていなければ失敗
    if ( !m_isLoaded )
        throw std::runtime_error("PCMデータが存在しません。");

    // RIFF文字列の書き込み
    out.write("RIFF", 4);

    // RIFFチャンクサイズスキップ(後で書き込む)
    out.seekp(4, std::ios::cur);

    // WAVE文字列の書き込み
    out.write("WAVE", 4);

    // フォーマットチャンクの書き込み
    WriteChunk(out, "fmt ", sizeof(m_format), &m_format);

    // データチャンクの書き込み
    WriteChunk(out, "data", m_buffer.size(), &m_buffer[0]);

    // RIFFチャンクサイズの書き込み
    U32 riffSize = out.tellp();
    riffSize -= 8;
    out.seekp(4, std::ios::beg);
    out.write(reinterpret_cast<char*>(&riffSize), sizeof(U32));
}

// データクリア
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];
    U32 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);
}

// チャンクを書き込む
void PcmData::WriteChunk(std::ostream& out, const char type[4], U32 size, const void* data) {
    // チャンクタイプの書き込み
    out.write(type, 4);

    // チャンクサイズの書き込み
    out.write(reinterpret_cast<char*>(&size), sizeof(U32));

    // データの書き込み
    out.write(static_cast<const char*>(data), size);
}

Main.cpp

#include "PcmData.h"

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

    // WAVEファイル作成
    pcm.Save("out.wav");

    MessageBox(NULL, TEXT("WAVEファイルの作成に成功しました。"), TEXT("OK"), MB_OK | MB_ICONINFORMATION);
    return 0;

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

サンプルは、test.wavファイルを読み込んでPcmDataオブジェクトでPCMデータを保持したあと、out.wavファイルに保持したPCMデータを書き込んでいます。
結果として、test.wavと同じオーディオデータのout.wavファイルが作成されます。

指定されたWAVEファイルによっては、out.wavのファイルサイズがtest.wavより小さくなることがあります。
これは、test.wavを読み込む時点でLISTチャンクなどの付加情報を無視しているためです。
この付加情報もout.wavに書き出せるようにするためには、PcmDataクラス自体を改良する必要があります。

WAVEファイルの作成自体は、読み込み処理よりも簡単に行うことが可能です。
アプリケーション上でWAVEファイルの読み込みと書き込みが行えるようになれば、WAVEファイルのエディタソフトも作れそうです。