波形を描画する

概要:現在の再生位置周辺の波形を描画します

現在の再生位置がわかれば、その周辺の波形データにアクセスする事は簡単です。
後はどのように描画するか、というデザインの問題ですね。

■バッファの準備まで

WAVEデータを作成して、再生可能な状態にします。
サンプルではステレオで作成しますが、
モノラルでも試してみたい方の為に必要な部分をコメントアウトしておきました。

#define SRATE    8000    //標本化周波数(1秒間のサンプル数)
#define F        400     //周波数(1秒間の波形数)

WAVEFORMATEX wfe;
static HWAVEOUT hWaveOut;
static WAVEHDR whdr;
static LPBYTE lpWave;
int i,len;

wfe.wFormatTag=WAVE_FORMAT_PCM;
wfe.nChannels=2;    //ステレオ
wfe.wBitsPerSample=8;    //量子化ビット数
wfe.nBlockAlign=wfe.nChannels * wfe.wBitsPerSample/8;
wfe.nSamplesPerSec=SRATE;    //標本化周波数
wfe.nAvgBytesPerSec=wfe.nSamplesPerSec * wfe.nBlockAlign;

waveOutOpen(&hWaveOut,WAVE_MAPPER,&wfe,(DWORD)hWnd,0,CALLBACK_WINDOW);

lpWave=(LPBYTE)calloc(wfe.nAvgBytesPerSec,2);    //2秒分

len=SRATE/F;    //波長
for(i=0;i<SRATE*2;i++){    //波形データ作成
    if(i%len < len/2){
        lpWave[2*i  ]=128+64;    //左
        lpWave[2*i+1]=128+32;    //右
    }else{
        lpWave[2*i  ]=128-64;    //左
        lpWave[2*i+1]=128-32;    //右
    }
}
/*for(i=0;i<SRATE*2;i++){    //波形データ作成
    if(i%len < len/2)    lpWave[i]=128+64;
    else                 lpWave[i]=128-64;
}*/

whdr.lpData=(LPSTR)lpWave;
whdr.dwBufferLength=wfe.nAvgBytesPerSec * 2;
whdr.dwFlags=WHDR_BEGINLOOP | WHDR_ENDLOOP;
whdr.dwLoops=1;

waveOutPrepareHeader(hWaveOut,&whdr,sizeof(WAVEHDR));

■必要な情報を構造体に設定する

波形を描画する関数に必要な情報を GCW という構造体を定義して設定します。

typedef struct _GCW{
  WORD nChannels;
  WORD wBitsPerSample;
  void *lpWave;
  DWORD sample_sum;
  DWORD win_r,win_b;
  POINT *left,*right;
  DWORD point_sum;
}GCW;


nChannels にはチャンネル数を指定します。
wBitsPerSample には量子化ビット数を指定します。
lpWave には波形データの先頭を指すアドレスを指定します。
sample_sum には lpWave が指す波形データの合計サンプル数を指定します。
 標本化周波数に秒数を掛けたものになります。
win_r , win_b には波形を描画する領域の右下の座標を指定します。
 (0,0)〜(win_r,win_b) の領域に波形を描画します。
 現在の振幅値は中心に位置するよう描画します。
left , right が指すバッファに左右の波形が格納されます。
point_sum には left と right の個数を指定します(バイト数ではない)。

関数は x 座標について関与しないので、適切に初期化する必要があります。
また、y 方向の中心は win_b/2 になるよう関数を設計したので、初期値として win_b/2 を設定しましょう。

#define WIN_W    640    //横幅
#define WIN_H    480    //高さ

typedef struct _GCW{
    WORD nChannels;
    WORD wBitsPerSample;
    void *lpWave;
    DWORD sample_sum;
    DWORD win_r,win_b;
    POINT *left,*right;
    DWORD point_sum;
}GCW;

static GCW gcw;

gcw.nChannels=wfe.nChannels;
gcw.wBitsPerSample=wfe.wBitsPerSample;
gcw.lpWave=whdr.lpData;
gcw.sample_sum=SRATE*2;
gcw.win_r=WIN_W-1;
gcw.win_b=WIN_H-1;
gcw.left=(POINT*)calloc(WIN_W+1,sizeof(POINT));
gcw.right=(POINT*)calloc(WIN_W+1,sizeof(POINT));
gcw.point_sum=WIN_W+1;

for(i=0;i<gcw.point_sum;i++){
    gcw.left[i].x=i; gcw.left[i].y=gcw.win_b/2;
    gcw.right[i].x=i; gcw.right[i].y=gcw.win_b/2;
}

右下の座標は(横幅-1 , 高さ-1)です。
left , right バッファを横幅より一つ多くとっているのは、Polyline 関数が終点を描画しないからです。
従って、余分に思える最後のバッファにダミーである終点座標が格納されます。

■色分け

描画は Polyline 関数を使って行います。
その時、左右の波形を見て区別できるようにペンの色を変える事にします。
ペンはあらかじめ作成しておき、必要な時に選択します。

static HPEN leftPen,rightPen;

leftPen=CreatePen(PS_SOLID,1,RGB(0,255,0));
rightPen=CreatePen(PS_SOLID,1,RGB(0,0,255));

■再生

マウスの左ボタンをクリックしたら再生開始します。
同時にタイマーを起動させて、定期的に波形を描画させます。

case WM_LBUTTONDOWN:
    waveOutWrite(hWaveOut,&whdr,sizeof(WAVEHDR));
    SetTimer(hWnd,1,50,NULL);
    return 0;

■タイマー

サンプル単位で現在の再生位置を取得して、これを波形描画関数に渡します。
関数はサンプル単位で渡す事を要求しているので、
サンプル単位で取得できなかった場合は変換する必要があります。
今回は変換処理を実装していませんが、簡単なので必要であれば自分で実装してみて下さい。

MMTIME mmt;
DWORD sample;

case WM_TIMER:
    mmt.wType=TIME_SAMPLES;
    waveOutGetPosition(hWaveOut,&mmt,sizeof(MMTIME));
    if(mmt.wType!=TIME_SAMPLES) return 0;
    sample=mmt.u.sample;

    GetCurrentWave(hWaveOut,sample,&gcw);
    InvalidateRect(hWnd,NULL,TRUE);
    return 0;

■振幅値取得関数

現在の再生位置周辺の振幅値を取得する関数 GetCurrentWave を作成しました。
描画は WM_PAINT メッセージ処理で Polyline 関数を呼び出して行います。
従って、この関数は Polyline 関数に必要な情報を取得&格納していく事になります。

この関数は指定領域の中心に現在の振幅値を格納します。
また、周辺の振幅値も1サンプル毎に取得&格納します。

void GetCurrentWave(HWAVEOUT hWaveOut,DWORD sample,GCW *gcw);

hWaveOut にはウェーブフォームオーディオ出力デバイスのハンドルを指定します。
sample には現在の再生位置をサンプル単位で指定します。
gcw にはGCW構造体のアドレスを指定します。

void GetCurrentWave(HWAVEOUT hWaveOut,DWORD sample,GCW *gcw)
{
    DWORD center_x=gcw->win_r/2;
    int left,right;
    for(DWORD i=0;i<gcw->point_sum;i++){
        if(gcw->nChannels==1){
            if(sample+i<center_x || (sample+i >= gcw->sample_sum+center_x)){
                gcw->left[i].y=gcw->win_b/2;
            }else{
                if(gcw->wBitsPerSample==8){
                    left=((BYTE*)(gcw->lpWave))[(sample-center_x)+i];
                    gcw->left[i].y=gcw->win_b*left/255;
                }else if(gcw->wBitsPerSample==16){
                    left=((short*)(gcw->lpWave))[(sample-center_x)+i];
                    gcw->left[i].y=gcw->win_b*(left+32768)/65536;
                }
            }
            gcw->left[i].y = gcw->win_b - gcw->left[i].y;      //上下反転
        }else if(gcw->nChannels==2){
            if(sample+i<center_x || (sample+i >= gcw->sample_sum+center_x)){
                gcw->left[i].y=gcw->win_b/2; gcw->right[i].y=gcw->win_b/2;
            }else{
                if(gcw->wBitsPerSample==8){
                    left=((BYTE*)(gcw->lpWave))[(sample-center_x)*2 + 2*i];
                    right=((BYTE*)(gcw->lpWave))[(sample-center_x)*2 + 2*i+1];
                    gcw->left[i].y=gcw->win_b*left/255;
                    gcw->right[i].y=gcw->win_b*right/255;
                }else if(gcw->wBitsPerSample==16){
                    left=((short*)(gcw->lpWave))[(sample-center_x)*2 + 2*i];
                    right=((short*)(gcw->lpWave))[(sample-center_x)*2 + 2*i+1];
                    gcw->left[i].y=gcw->win_b*(left+32768)/65536;
                    gcw->right[i].y=gcw->win_b*(right+32768)/65536;
                }
            }
            gcw->left[i].y = gcw->win_b - gcw->left[i].y;      //上下反転
            gcw->right[i].y = gcw->win_b - gcw->right[i].y;    //上下反転
        }
    }
}

一番外側のループはバッファの個数を越えないよう見張っています。
バッファの個数は gcw->point_sum です。

次にチャンネル数で分岐させます。
そして、参照しようとしているバッファのサンプル数が有効なものであるかチェックします。
0 未満であったり、最大値以上であったりしてはいけません。
 if( (sample-center_x)+i<0 || ((sample-center_x)+i >= gcw->sample_sum) ){
  //NG
 }

center_x は現在の振幅値が領域の中心になるようにする仕掛けです。
gcw->win_r/2 だけ前から参照する事で、現在の振幅値が領域の中心になります。
不正なサンプル数である場合は、上下の中心座標を設定しておきましょう。
上記プログラムでは、sample が符号無し型であることに注意して条件式を工夫しています。

有効なサンプル数である事がわかったら、今度はビット数で分岐させます。
ここでややこしいのがアドレス計算式です。
サンプル数はチャンネル数を考慮していないので、モノラルとステレオではアドレスが異なります。
ここで、アドレスの有効を調べる方がプログラムとしては直接的ですが、
サンプル数の有効性を調べる方が簡単ですし、同じ事なのでサンプル数でチェックしています。
さて、モノラルの場合は特に迷う事はないと思います。
ステレオの場合はチャンネル数を考慮しなければなりません。
(sample-center_x)*2 で基本となるアドレスを求めて、2*i または 2*i+1 を加算します。
2*i もチャンネル数を考慮して偶数に左、奇数に右の振幅値がある事を利用しています。

振幅値を取得したら最小値が 0 、最大値が win_b になるように調節します。
最小値の調節は負値も取り得る16ビットの場合だけ必要です。

そして最後に、上下を反転させます。
この処理は場合によっては必要ありません。
不要なら削除して下さい。

■表示

左右でペンの色を変えて表示します。
モノラルの場合は右チャンネルを表示しません。

HDC hdc;
PAINTSTRUCT ps;

case WM_PAINT:
    hdc=BeginPaint(hWnd,&ps);
    SelectObject(hdc,leftPen);
    Polyline(hdc,gcw.left,gcw.point_sum);
    if(gcw.nChannels==2){
        SelectObject(hdc,rightPen);
        Polyline(hdc,gcw.right,gcw.point_sum);
    }
    EndPaint(hWnd,&ps);
    return 0;

表示したい領域を移動させたい場合は、GetCurrentWave 関数を呼び出した後の
WM_TIMER メッセージ処理でシフトさせて下さい。
結果を上書きするのであれば、WM_PAINT メッセージ処理で何度も実行されては困ります。

■停止

停止したら現在の再生位置を 0 に戻す為に waveOutReset 関数を呼び出します。
放っておくと現在の再生位置はどんどん加算されてしまいます。

タイマーも止めてしまいましょう。

case MM_WOM_DONE:
    waveOutReset((HWAVEOUT)wParam);
    KillTimer(hWnd,1);
    return 0;

■終了処理

左右チャンネルの振幅値を格納しているバッファを解放する事も忘れないで下さい。

case WM_DESTROY:
    waveOutReset(hWaveOut);
    waveOutUnprepareHeader(hWaveOut,&whdr,sizeof(WAVEHDR));
    waveOutClose(hWaveOut);
    free(lpWave);

    free(gcw.left);
    free(gcw.right);

    DeleteObject(leftPen);
    DeleteObject(rightPen);
    PostQuitMessage(0);
    return 0;

■ウィンドウ

クライアント領域の大きさが希望のサイズになるようウィンドウの大きさを調節します。
また、背景を黒にしてみましょう。

int WINAPI WinMain(
    HINSTANCE hInstance,HINSTANCE hPrevInstance,PSTR lpCmdLine,int nCmdShow)
{
    WNDCLASS wc;
    MSG msg;

    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WindowProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL,IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL,IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = __FILE__;
    if(!RegisterClass(&wc)) return 0;

    RECT rc={0,0,WIN_W,WIN_H};
    AdjustWindowRect(&rc,WS_OVERLAPPEDWINDOW,FALSE);

    HWND hWnd=CreateWindow(
        __FILE__,"波形を描画する",
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        CW_USEDEFAULT,CW_USEDEFAULT,
        rc.right-rc.left,rc.bottom-rc.top,
        NULL,NULL,hInstance,NULL);
    if(hWnd==NULL) return 0;

    BOOL bRet;
    while((bRet=GetMessage(&msg,NULL,0,0))!=0){
        if(bRet==-1) break;
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

★☆ ソースファイル表示 ☆★


戻る / ホーム