LoginSignup
7
7

More than 5 years have passed since last update.

PortAudioでWAVEファイルを再生してみる(鳴らす編)

Last updated at Posted at 2014-02-17

3編に分かれてます

この記事は前後編に分かれてます。連動してるので前編も読むといいとおもいます
ライブラリのビルドについて書いた準備編もあるのでそちらも参考に。

実際に鳴らすよ

前々回(準備編)、前回に引き続きPortAudioの記事です。
今回は実際に音が出ます。長かった…!

さて、今回はコードを細かく区切って見ていきます。
といってもmain関数しかありませんが。

Source.cppその1
#pragma once
#pragma comment(lib, "portaudio_x86.lib")

#include "wave.h"
#include "portaudio.h"

typedef float SAMPLE;
#define PA_SAMPLE_TYPE paFloat32

int main(){
//WAVファイルの読み込み----------------------------
    WAVE wav1("test.wav",false);
    WAVE_FORMAT fmt = wav1.Get_Format();    //フォーマット取得
//-------------------------------------------------
//PortAudioの初期化--------------------------------
    auto err = Pa_Initialize(); //エラー処理
    if( err != paNoError){
        Pa_Terminate();
        return 1;
    }

    //アウトプットデバイスの構築(マイク使わない場合はインプットデバイス使わない)
    PaStreamParameters outputParameters; 
    outputParameters.device = Pa_GetDefaultOutputDevice();  //デフォデバイスを使う
    outputParameters.channelCount = fmt.channel;    //フォーマット情報を使う
    outputParameters.sampleFormat = PA_SAMPLE_TYPE; //paFloat32(PortAudio組み込み)型
    outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputParameters.device )->defaultLowOutputLatency;   //レイテンシの設定
    outputParameters.hostApiSpecificStreamInfo = NULL;  //よくわからん
//-------------------------------------------------

まずはここまで。
WAVEファイルの読み込みとPorAudioの初期化です。
WAVEファイルはともかく初期化の方はいきなりわけのわからん設定が並んでますが、
私もよくわからないので音を出すだけなら問題ないと思います。

詳しくはリファレンスを見ましょう。

といってもやってること自体は、 Pa_Initialize(); でPortAudio自体を初期化し、
outputParameters にデバイスの情報を書いてやってるだけです。

あと、コメントでも書いてありますが、マイクを使う場合のみ PaStreamParameters をもう一つ、Input用に
構築してやる必要があります。
その辺の処理はこちらに詳しいので、ありがたく参考にさせてもらってます。

さて、次見てみましょう。

Source.cppその2
//ラムダ式でコールバック作ってみる(関数ポインタ)
//なんかstd::functionで受けたりautoで受けたりしたのを渡すとコンパイルエラーになる・・・
    int (*Callback)(const void *inputBuffer, void *outputBuffer,
        unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo* timeInfo,
        PaStreamCallbackFlags statusFlags, void *userData);

    Callback = [](
        const void *inputBuffer,
        void *outputBuffer,
        unsigned long framesPerBuffer,
        const PaStreamCallbackTimeInfo* timeInfo,
        PaStreamCallbackFlags statusFlags,
        void *userData )->int
    {
        WAVE* data = (WAVE*)userData;   //userDataをWAVE*型へキャストしておく
        SAMPLE *out = (SAMPLE*)outputBuffer;    //同じくoutputもキャスト
        (void) timeInfo;    
        (void) statusFlags; //この辺はよく分からんけど使わないらしい(未調査) 

        //フレーム単位で回す
        for(int i=0; i < (int)framesPerBuffer; i++ ){
        //インターリーブ方式でL,R,L,R,...と記録されてる(WAVも同じ)ので、
        //outputに同じ感じで受け渡してやる
            for(int c=0 ; c < (int)data->Get_Format().channel ; ++c){
            //チャンネル数分だけ回す
                //残りの音声データがなく、ループフラグが立ってない場合終了
                if(data->Get_End() && !data->Get_LoopFlag()){
                    return (int)paComplete;
                }
                *out++ = (data->Read<SAMPLE>())/32767.0f; //-1.0~1.0に正規化(内部ではshortで保持してるので,floatに変換)
            }
        }

        return paContinue;
    };

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

 
うわあ、読みたくない…

読みたくないですけど、このコールバック関数が今回の肝です。
この関数内で音声処理します。
この関数をコールバックとして、後で出てくる関数に投げてやって
ストリームを再生すると音が出るという寸法です。
ちょっとずつ細分化して見ていこうと思います。

    int (*Callback)(const void *inputBuffer, void *outputBuffer,
        unsigned long framesPerBuffer,const PaStreamCallbackTimeInfo* timeInfo,
        PaStreamCallbackFlags statusFlags, void *userData);

ここの部分ですが、関数ポインタの宣言です。
やたらめったらパラメータが多いですけど、今回の記事では半分ぐらいしかパラメータ使わないんで大丈夫です。

この関数ポインタにラムダ式を入れます。(関数ポインタへの暗黙の変換)

    Callback = [](
        const void *inputBuffer,
        void *outputBuffer,
        unsigned long framesPerBuffer,
        const PaStreamCallbackTimeInfo* timeInfo,
        PaStreamCallbackFlags statusFlags,
        void *userData )->int
    {
              //(略)
         }

同じシグネチャを持っててクロージャを伴わないラムダ式ならば関数ポインタへ変換できるんで、
先に宣言したやつにぶち込んでるわけです。
ここらへんの記事が詳しいです。

余談ですが、最初はラムダ式をautoで受けたのをコールバックにできるかな?とか思ったり、
std::functionで受ければtargetで関数ポインタにできるのでは?とか試しましたが、なんかめんどくさそうなのでやめました

 
さて、コールバックの引数について解説します。
const void *inputBuffer ですが、 今回は使いません。
  マイクとかからの入力信号を受け取りますがWAVEファイルの処理なので全く使いません。
次に void *outputBuffer です。こいつに波形情報を乗せてやると音が出ます。重要です。
unsigned long framesPerBuffer は1回の処理で載せられるバッファの数です。
  1フレームあたりこのバッファ分だけforループしてやります(後述)
const PaStreamCallbackTimeInfo* timeInfo,PaStreamCallbackFlags statusFlags の2つは、
 今回は使いません。
void *userData にはWAVEファイルが渡されます。

基本的に単純な再生で使うのはoutputBuffer,framePerBuffer,userDataだけです。
 ストリーミング再生になるとちょっと変わってきます。

コールバック関数の中身を見ていきます。

コールバック最初
WAVE* data = (WAVE*)userData;   //userDataをWAVE*型へキャストしておく
SAMPLE *out = (SAMPLE*)outputBuffer;    //同じくoutputもキャスト
(void) timeInfo;    
(void) statusFlags; //この辺はよく分からんけど使わないらしい(未調査)

void*で受け取ったデータは適切な型にキャストしておきます。
userDataは前回作ったWAVEクラスに、outputBufferはSAMPLE(float)にキャストします。
ポインタなので当然ポインタで受けます。

コールバック音声処理部分
        //フレーム単位で回す
        for(int i=0; i < (int)framesPerBuffer; i++ ){
        //インターリーブ方式でL,R,L,R,...と記録されてる(WAVも同じ)ので、
        //outputに同じ感じで受け渡してやる
            for(int c=0 ; c < (int)data->Get_Format().channel ; ++c){
            //チャンネル数分だけ回す
                //残りの音声データがなく、ループフラグが立ってない場合終了
                if(data->Get_End() && !data->Get_LoopFlag()){
                    return (int)paComplete;
                }
                *out++ = (data->Read<SAMPLE>())/32767.0f; //-1.0~1.0に正規化(内部ではshortで保持してるので,floatに変換)
            }
        }

ちょっと説明しづらいですが、ここの肝は

*out++ = (data->Read<SAMPLE>())/32767.0f; //-1.0~1.0に正規化(内部ではshortで保持してるので,floatに変換)

これです。
Read()関数は前回書いたので、それを参考にしてください。PCM波形を読み出す関数です。
float型としてキャストされて波形が出てくるのですが、
元々内部ではshort型(-32767~32767の範囲)なので32767で割ってやって、
-1.0~1.0に正規化します。
なんでそんなことが必要かといいますと、 paFloat32型(PortAudio組み込み)の仕様です。
ちなみに他にもサンプル形式はpaInt32とかいろいろあるみたいなんですが、
ちょっとよく分からなかったので理解できたらまた解説したいと思います。
「おい!8bitの時を考慮してねーぞ!」と思われた方、その通りです。
今回は書くのめんどくさいのでオミットしました。まあ、似たような感じで行けると思います。

*out++ の表記ですが、参照先に代入した後にポインタをインクリメントしてます。
参照先をインクリメントしてるわけじゃないので紛らわしいですねこれ…

前回の記事でも書きましたが、波形データはL,R,L,R,...というように格納されているので
チャンネルごとにRead()してやる必要があります。
for(int c=0 ; c < (int)data->Get_Format().channel ; ++c) ←これですね
まあぶっちゃけWAVEデータがステレオだというのが分かっているなら
*out++ = (data->Read<SAMPLE>())/32767.0f; と2回書いてもいいんじゃないでしょうか。

んでこれを1回の処理で処理できるバッファ分だけ回してやります。
for(int i=0; i < (int)framesPerBuffer; i++ ) ←これです

基本はこれだけです。
paContinueとかpaConmpleteとかなんかよくわかんないのを返してますが、
実質intなんで直接0とか打ってもいいです。
あとはまあループ/終了時の処理を適当に書いてるだけですね。
 
 
さて、ようやくコールバックが終わったところで次行きましょう。

Souce.cppその3
//------------------------------------------------- 
//ストリームを開く(コールバックの登録)-------------

    PaStream *stream;
    err = Pa_OpenStream(
        &stream,            //なんか再生するべきストリーム情報が帰ってくる
        NULL,               //マイクとかのインプットないのでNULL
        &outputParameters,  //アウトプット設定
        fmt.sampling_rate,  //44100Hz
        paFramesPerBufferUnspecified,//1サンプルあたりのバッファ長(自動)
        paNoFlag,           //ストリーミングの設定らしい とりあえず0
        Callback,           //コールバック関数(上のラムダ)
        &wav1 );            //wavファイルデータを渡す(ちなみにどんなデータでも渡せる)
    if( err != paNoError ) { //エラー処理
        Pa_Terminate();
        return 1;
    }
//-------------------------------------------------

Pa_OpenStreamという関数でストリームを開きます。
これにWAVEデータとかコールバックの関数ポインタを渡してやります。
paFramesPerBufferUnspecified ですが、
これはコールバックの unsigned long framesPerBuffer として渡されます。
なんか適切な数字を自動で計算してくれるっぽいです。

マイクを使う場合には2番目の引数のNULLにinputParameterを登録します。
(今回は作ってませんが)
6番目のpaNoFlagについてはよくわかってません。ストリーミングの設定らしいですが…

さて、いよいよラスト、再生と停止です。

Source.cppその4
//再生---------------------------------------------
    err = Pa_StartStream(stream);
    if( err != paNoError ) { //エラー処理
        Pa_Terminate();
        return 1;
    }

    printf("Hit ENTER to stop program.\n"); //なんか押すまでストップ
    getchar();
//-------------------------------------------------
//再生終了 ストリームを閉じる---------------------
    err = Pa_CloseStream( stream );
    if( err != paNoError ) { //エラー処理
        Pa_Terminate();
        return 1;
    }
    Pa_Terminate();
//-------------------------------------------------

}

再生した後getchar()で、ブロッキングしてます。
まあここは特に解説の必要もない気がするのでいいでしょう…。
 

おわり

おつかれさまでした。というわけでWAVEファイルの再生ができました。
コールバックの中身書き換えればDSP処理も簡単にできますね!ヤッターバンザーイ!!
というわけで次があればOGG再生とか、ストリーミングとか、DSP処理とかについて書こうと思います。

あと、結構不明というか有耶無耶にした部分が多いのでそこら辺の解説もできればと思います。
いやむしろ誰か解説してくださいおねがいします。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7