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ファイルのエディタソフトも作れそうです。