C++
VisualStudio
PortAudio
オーディオ
音声処理

PortAudioでWAVEファイルを再生してみる(WAVEファイル読み込み編)

More than 5 years have passed since last update.


3編に分かれてます

この記事は前後編に分かれてます。

PortAudioで~って書いてありますがこの前編ではPortAudioの話はほぼ出てきません、

そっちが知りたい方は後編の方参照してください。

あとライブラリのビルドについて書いた準備編もあるのでそちらも参考に。


今回からコード書くよ

前回の記事(準備編)に引き続きPortAudioの記事です。

今回も自分用備忘録めいた記事なので、他の人が読むことはあまり考慮してませんすみません。

(公開設定ではありますけど)

前回はライブラリのビルド~サンプル実行時の注意点を書きましたが、

今回はPortAudioを使ってWAVEファイルを再生してみます。 今回はストリーミングは考慮してません。

相も変わらず実行環境はVS2012,Windows8,64bitです。

PortAudioのライブラリは前回ビルドしたやつ(portaudio_x86.dllとlib)を使用します。


WAVEファイルは自分で取り扱おう

さて、「再生してみる」とか書きましたが、

その実PortAudioはドライバへ働きかけ、波形をアウトプットする最低限のAPIしかサポートしてません。

なので、 WAVEファイルを読み込むクラスは自前で用意する必要があります。

OGGとかMP3でやる場合も基本は同じですんで注意してください。

なんでわざわざそんなめっちゃ手間なことやってんの?って言われると完全な自己満足です、暇なので

で、今回の参考サイト様なんですけど以下の2つになります。

上のサイトはWAVEファイルのデータ構造が載ってるサイトです。

下はWAVEファイル読み込みをクラス化してみましたみたいな記事です。

…なんですが、私の理解不足もあってなんか波形の出力がうまくできなかったので、

似たようなフォーマットで新しくクラス作ってしまいました/(^o^)\

(下のサイトの人見てたらほんとすみません…)

とりあえずWAVEファイルのフォーマット格納する構造体から載せます。


wave.h

//WAVEファイル読み込み

//---------------------------------------------------------------------------
#define _CRT_SECURE_NO_WARNINGS
#include <vector>
#include <stdio.h>
#include <string>

//フォーマット格納用
//---------------------------------------------------------------------------
struct WAVE_FORMAT
{
unsigned short format_id; //フォーマットID
unsigned short channel; //チャンネル数 monaural=1 , stereo=2
unsigned long sampling_rate;//1秒間のサンプル数,サンプリングレート(Hz),だいたいは44.1kHz
unsigned long bytes_per_sec;//1秒間のデータサイズ 44.1kHz 16bit ステレオ ならば44100×2×2 = 176400
unsigned short block_size; //1ブロックのサイズ.8bit:nomaural=1byte , 16bit:stereo=4byte
unsigned short bits_per_sample;//1サンプルのビット数 8bit or 16bit
};
//---------------------------------------------------------------------------

この構造体には、上の参考サイトで言うところの「データ1」の部分が格納されます。

まぁ、だいたいは 44100Hz,16bit,stereo,リニアPCMフォーマット なんじゃないかって気がします。

で、次に波形データと、上の構造体を持ってるクラスを載せます。

とりあえずヘッダのみ。


wave.h

//---------------------------------------------------------------------------

class WAVE
{
private:
WAVE(const WAVE&); //コピーコンストラクタ
WAVE& operator=(const WAVE&); //代入演算子定義

WAVE_FORMAT fmt; //フォーマット情報
std::vector<unsigned char> data_8bit; //音データ(8bitの場合)
std::vector<short> data_16bit; //音データ(16bitの場合)

int RIFFFileSize; //RIFFヘッダから読んだファイルサイズ
int PCMDataSize; //実際のデータサイズ
int BufPos; //バッファポジション
bool LoopFlag; //ループフラグ

public:
WAVE(bool _LoopFlag = false);
WAVE(std::string _name,bool _LoopFlag = false);
~WAVE(){}
bool LoadFile(std::string _name);
template <class T> T WAVE::Read(){
//PCMデータを読んで波形を返す
if(LoopFlag && Get_End()){
//ループフラグが立ってる場合、巻き戻し
BufPos = 0;
}
if(fmt.bits_per_sample == 8){
//8bit
return data_8bit[BufPos++];
}else if(fmt.bits_per_sample == 16){
//16bit
return data_16bit[BufPos++];
}
return 0;
}

bool Get_End();

WAVE_FORMAT Get_Format(){ return fmt; }
unsigned long Get_DataSize(){ return PCMDataSize; }
double Get_Sec(){ return PCMDataSize/(double)fmt.sampling_rate; }
void Set_LoopFlag(bool _LoopFlag){ LoopFlag = _LoopFlag; }
bool Get_LoopFlag(){return LoopFlag;}
};
//---------------------------------------------------------------------------


Read関数だけヘッダ内に書いてあるのは、

メンバ関数テンプレートはヘッダ内に書かないとコンパイルエラーになるため です。

波形部分の読み出しですが、16bitの場合shortとしてvectorに読んで、

8bitの場合unsigned charとして読んで格納みたいな実装です。

どうでもいいですが個人的にC++のファイルストリームが嫌いなのでchar*型も嫌いなんですよ…。

普通はとりあえずunsigned char*で保持して、波形として読むときにキャストするみたいな感じだと思いますが、

あんまり好きくないので分けてしまいました。

一応説明すると8bitの場合1byteごとのデータなんでunsigned charで保存、

16bitの場合2byteデータなんでshortで保存してあります。

んでステレオの場合L,R,L,R,...と波形が保存されてるんで、

16bitデータの場合1ブロック4byteです。

int RIFFFileSize; は、上の参考サイトで言うところのファイルサイズ(byte単位)で、

int PCMDataSize; は、上の参考サイトで言うところの 「データの長さ 3」 に相当します。

int BufPos; は単なるカウンタです。

実際重要な関数部分は

    bool LoadFile(std::string _name);

template <class T> T Read();
bool Get_End();

これだけです。あとはGetter/Setterなので。

Read以外の実装は以下のようになります。


wave.cpp

#include "wave.h"


//---------------------------------------------------------------------------
WAVE::WAVE(bool _LoopFlag = false){
BufPos = 0;
LoopFlag = _LoopFlag;
}
//---------------------------------------------------------------------------
WAVE::WAVE(std::string _name,bool _LoopFlag = false){
BufPos = 0;
LoopFlag = _LoopFlag;
LoadFile(_name);
}
//---------------------------------------------------------------------------
bool WAVE::LoadFile(std::string _name){
FILE *fp;
fopen_s(&fp,_name.c_str(), "rb");

std::string readbuf; //適当
readbuf.resize(4);
int readbuf2; //適当なバッファ

//RIFFを読む
fread( (char*)readbuf.c_str(), 4,1,fp); //4byte読む "RIFF"がかえる
if(readbuf != "RIFF")return false;

fread( &RIFFFileSize, 4,1,fp); //4byte読む これ以降のファイルサイズ (ファイルサイズ - 8)が返る
//WAVEチャンク
fread( (char*)readbuf.c_str(), 4,1,fp); //4byte読む "WAVE"がかえる
if(readbuf != "WAVE")return false;

//フォーマット定義
fread( (char*)readbuf.c_str(), 4,1,fp); //4byte読む "fmt "がかえる
if(readbuf != "fmt ")return false;

//fmtチャンクのバイト数
fread( &readbuf2, 4,1,fp); //4byte読む リニアPCMならば16が返る

//データ1
fread( &fmt.format_id, 2,1,fp); //2byte読む リニアPCMならば1が返る
fread( &fmt.channel, 2,1,fp); //2byte読む モノラルならば1が、ステレオならば2が返る
fread( &fmt.sampling_rate, 4,1,fp); //4byte読む 44.1kHzならば44100が返る
fread( &fmt.bytes_per_sec, 4,1,fp); //4byte読む 44.1kHz 16bit ステレオならば44100×2×2 = 176400
fread( &fmt.block_size, 2,1,fp); //2byte読む 16bit ステレオならば2×2 = 4
fread( &fmt.bits_per_sample, 2,1,fp); //2byte読む 16bitならば16

//拡張部分は存在しないものとして扱う
if(fmt.format_id != 1)return false;

//dataチャンク
fread( (char*)readbuf.c_str(), 4,1,fp); //4byte読む "data"がかえる
if(readbuf != "data")return false;
fread( &PCMDataSize, 4,1,fp); //4byte読む 実効データのバイト数がかえる

//以下、上のデータサイズ分だけブロックサイズ単位でデータ読み出し
if(fmt.bits_per_sample == 8){
//8ビット
data_8bit.resize(PCMDataSize+1); //reserveだと範囲外エラー
fread( &data_8bit[0],1,(size_t)2*PCMDataSize/fmt.block_size,fp); //vectorに1byteずつ詰めていく
}else if(fmt.bits_per_sample == 16){
//16ビット
data_16bit.resize(PCMDataSize/2+2); //reserveだと範囲外エラー
fread( &data_16bit[0],2,(size_t)2*PCMDataSize/fmt.block_size,fp); //vectorに2byteずつ詰めていく
}

fclose( fp );
return true; //正常終了
}
//---------------------------------------------------------------------------
bool WAVE::Get_End(){
if(fmt.bits_per_sample == 8){
return BufPos >= (int)data_8bit.size();
}else if(fmt.bits_per_sample == 16){
return BufPos >= (int)data_16bit.size();
}
return true;
}
//---------------------------------------------------------------------------


LoadFile() がなんか長々と書いてありますが、

やってることはfread()で読んで適宜メンバに格納してるだけです。

書き捨てじみて書いたので例外なにそれおいしいのって感じですね…

まあ元々最低限の機能しかないので置いときましょう。

Get_End() は演奏終了を知らせるだけの関数です。

さて、WAVEファイルを読み込む必要最低限のクラスができたので、

後編ではこのクラスを利用して実際に音を鳴らしてみます。