再生
再生部分の流れは次のとおりです。
- 読み込む
- 加工
- 書き込む
バッファのサイズ分を読み込んで再生し、再生が終わったら、また読み込んで再生することを繰り返します。
int readsize = mmioRead(hmmio,(HPSTR)buf,bufferSize); //加工 whdr.dwBufferLength =readsize; mmioWrite(hWave, &whdr , sizeof(WAVEHDR));
whdr
構造体には、buf
やその他の情報が入っています。
mmioRead
関数は読み込んだサイズを返すので、そのサイズをセットしておきます。そうしないと、前回読み込んだデータを再生してしまう可能性があるからです。
それと、注意すべき事があります。mmioRead
はチャンクに関係なく、指定された分を読んできます。ですから、読み込んだ分を記録して、オーバーしないように気を付けなければなりません。そうしないと、もし「data」の後に何か別のチャンクがあったとしても構わず再生してしまうことになり、雑音が入ってしまいます。
データ加工
いよいよ本題です。
16ビットの音データは0を基準に上下に振れています。型は2バイトの符号付き整数です(大抵の環境ではshort
だと思います)。つまり、符号付き2バイト整数が単純に並んでいるわけです。
ですが、これはモノラルの場合であり、ステレオの場合は「左右左右………」というように交互にデータが並んでいます。つまり添え字で考えると、偶数番目が左、奇数番目が右になります。
というわけで、次のようにして、モノラル化やボーカルキャンセル(真ん中の音を消す)の加工を行うことができます。
short *buf;//データが入っている int i; //2ずつインクリメント for(i=0;i<bufferLenght;i+=2){ //モノラル化 //short t1 = (short)((((int)buf[i]) +((int)buf[i+1]))/2); //真ん中の音を消す short t1 = (short)(((int)buf[i]) -((int)buf[i+1])); buf[i] = buf[i+1] =t1; }
好きな方を使ってください。他にも、特定の周波数を足すといった処理もできると思います。
マルチバッファリング
例えば、WAVEデータを全部メモリに展開すれば、バッファ切れに注意する必要はなくなります。しかし、それは現実的な方法とは言い難いです。例えば、サンプリング周波数 44.1kHzで16ビットのステレオデータの場合、1分だと約10Mバイトほどになってしまいます。
44.1k(周波数) * 2(チャンネル)* 2(バイト⇒16ビット) = 176400 [B/s] = 172.3 [KB/s]
そこで、大きなデータを一括ではなく、小さなデータを必要な分だけ書き込むようにします(1~3秒くらいずつ)。
waveOutWrite
で書き込んだデータは、再生が終わると標準でMM_WOM_DONE
というメッセージを送ってくるので、単純に考えると、そのメッセージが送られてきた段階で、また新たにwaveOutWrite
でデータを書き込めばよいことになります。
しかし、音はリアルタイムで流れているため、MM_WOM_DONE
メッセージを受け取った後、ファイルからデータを読み込んで処理し書き込んで、ということを行っていると、一時的にバッファが切れた状態になってしまいます。そのため、変なノイズが混じってしまいます(例えば、「ブツッ」という音が入ったりします)。
そこで、次の手順に従い「マルチバッファリング」を行います。
- 2つのバッファ「A」「B」を用意します。
- A、Bの順に
waveOutWrite
します。 - Aの再生が終わると
MM_WOM_DONE
が送られてきます。その間、裏では自動的にBの再生が始まります。 - Aを書き込みます。
- Bの再生が終わると
MM_WOM_DONE
が送られてきます。その間、裏では自動的にAの再生が始まります。
イメージとしては、次のソースコードのようになります。
//データの長さ whdr[0].dwBufferLength = 0; whdr[1].dwBufferLength = 0; //マルチバッファリングをするために空データを書き込み waveOutWrite(hWave , &(whdr[0]) , sizeof(WAVEHDR)); waveOutWrite(hWave , &(whdr[1]) , sizeof(WAVEHDR)); //セレクタの設定 int bufferSelect = 2%BUFFER_COUNT; { case MM_WOM_DONE: //バッファが切れたときに呼び出される。 { //データを持ってくる int readsize = readWaveData( hmmio,wWave[bufferSelect],readsize,bufferLenght); //再生バッファに書き込み whdr[bufferSelect].dwBufferLength =readsize; waveOutWrite((HWAVEOUT)wParam , &(whdr[bufferSelect]) , sizeof(WAVEHDR)); //バッファセレクタのインクリメント bufferSelect = (bufferSelect+1)%BUFFER_COUNT; } }
この方法は、3つや4つなどのバッファリングにも対応しています。
最初にwaveOutWrite
で長さ0のデータを書き込んでいますが、こうすることでreadWaveData
をあちこちで呼ばなくて済むようになります。
連続再生
WAVEデータの再生が終了したら、WAVEをデータの開始点までシークする必要があります。
それにはmmioSeek
を使います。ただし、ファイルの先頭には戻らないようにしてください。そんなことをしたら、フォーマットチャンクまで再生されてしまいます。
//waveDataSizeはデータサイズ。その分戻るので「-」を付けている。
mmioSeek(hmmio , -waveDataSize , SEEK_CUR);
恐らく、現在の地点からの移動であるSEEK_CUR
しか使えないでしょう。
その他補足事項
ここまで読んで大体の感じをつかめた方は、ソースコードを読んでください。コメントは大量に入れてあります。ソースコードは350行くらいです。ちょっとごちゃごちゃしていますが、大丈夫でしょう。
分からない点がある方は参考資料で学ぶと良いと思います。私も1週間ほどで0からここまでできるようになりました。それでも分からない方は質問していただければ答えます。
まとめ
私は、マルチバッファリングのやり方が分からずに苦労しました。それと、この記事書きの途中まではフォーマットチャンクも再生していました。危なかったです。
今後はmp3なども扱えるようにすると良いと思います。ただし、その場合にはバッファが複雑なことになると思います。また、エコーなどをかけてみるのも面白いと思います。ですが、エコーなどはDirectShowで簡単に実装できそうなので、その方が楽かもしれません。
寄せ集めの知識ですが、最後まで読んでいただきありがとうございました。そして、参考にさせて頂いた資料と、それを作った方々にも感謝します。
参考資料
- Windowsプログラミング研究所 『マルチメディアプログラミング』
- Yggdrasill 『Visual C++ & DirectX - Text 09』
- MSDN Library 『waveOutWrite』
- WisdomSoft 『マルチメディア API』
- 創作プログラミングの街 『Wave音源と信号処理実験室』