バッファ切り替えによる音階演奏

 今回は複数の波形バッファを作成し、そのバッファを切り替えながら音声を出力してみます。一つのバッファのみを書き換えながら使用すると処理のタイミングによって不自然に音が途切れたりしますが、別のバッファで処理してバッファ自体を切り替えればそのようなこともないですね。
音声処理でも、画像処理同様スムーズな出力にはダブル(というか複数)バッファが有効のようです。

再生バッファの切り替え

 Wave音源の出力は、デバイスを開いてから再生バッファのポインタを設定したWAVEHDR構造体をwaveOutPrepareHeader()に渡してバッファ再生の用意を行い、そのバッファをwaveOutWrite()で再生、という手順で行いました。複数のバッファを切り替える場合も全く同じ手順で再生できます。ただし、バッファを切り替える時には前のバッファをwaveOutUnprepareHeader()する必要があります。例えば、バッファlpBuf1の再生後にバッファlpBuf2を再生するなら

  WAVEHDR wh;

  wh.lpData=lpBuf1;

  lpBuf1を再生
      ・
      ・
  再生終了

  waveOutUnprepareHeader(hWOut,&wh,sizeof(WAVEHDR));

  wh.lpData=lpBu2;
  waveOutPrepareHeader(hWOut,&wh,sizeof(WAVEHDR));
  waveOutWrite(hWOut,&wh,sizeof(WAVEHDR));

 という流れで処理します。バッファの指定はWAVEHDR構造体のlpDataで行う、という点に注意してください。

 今回は、バッファ切り替えの実験としてドレミファソラシドの「音階」を演奏してみましょう。演奏するのは、オクターブ4のドからオクターブ5のドまでの8音です。今回も出力は矩形波なので、問題になるのは周波数だけ。さっそく、オクターブ4から5の音階の周波数を計算してみましょう。

 音階上で1オクターブ上がると音の周波数は2倍になり、1オクターブの間は等比数列状の12半音で分けられています。つまり、半音一つ上がると周波数は21/12倍、大体1.06倍になるわけです。ある音の周波数をaとすると半音n上の音の周波数bは2の12乗根をn回かけることになるので

 b=a×(21/12n

 という式で求まる事になります。オクターブ4の「ラ」の周波数は440Hz、ドは1オクターブ下のラから見て半音3つ上(ラ−シ♭−シ−ド)ですので、オクターブ4の「ド」を求めるにはオクターブ3の「ラ」(周波数は半分の220Hz)に21/12を3回かければ良いでしょう。

  /* 半音一つの倍率 */
  lfRate=pow(2,1.0/12.0);

  /* オクターブ4のドの周波数 */
  lfFreq[0]=220*lfRate*lfRate*lfRate;

 これでオクターブ4のドが求まりました。今回は、音階の周波数を半音毎に配列lfFreq[]に求めておきます。

  for (i=1;i<13;i++)
      lfFreq[i]=lfFreq[i-1]*lfRate;

 ただ、この13個の音階のうち、実際に使うのはドレミファソラシドの8個のみですね。この8個がドから見て半音いくつかの位置にあるかを示す情報もついでに配列pl[]に作っておきます。

  pl[]={0,2,4,5,7,9,11,12};

 これで、オクターブ4のド〜オクターブ5までの各音階の周波数がわかったので、これをもとに今回使う8つの音階の波形をバッファに作っていきましょう。サンプリング形式は前回同様8000Hz・8ビットとし、各音の波形バッファは2次元のBYTE型配列lpBuf[][]に作成します。

  for (i=0;i<8;i++) {

      /* 音の波長計算 */
      l=(int)(8000.0/lfFreq[pl[i]]);

      for (j=0;j<8000;j++) { /* 波形データ作成 */

          if ((j % l)<l/2)
              lpBuf[i][j]=192;
          else
              lpBuf[i][j]=64;

      }

  }

 波形が出来たら、その波形を出力します。まず最初にオクターブ4のド(lpBuf[0])を出力しましょう。

  wf.wFormatTag=WAVE_FORMAT_PCM ;
  wf.nChannels=1;
  wf.nSamplesPerSec=8000;
  wf.nAvgBytesPerSec=8000;
  wf.nBlockAlign=1 ;
  wf.wBitsPerSample=8 ;
  wf.cbSize=0 ;

  /* デバイスオープン */
  waveOutOpen(&hWOut,WAVE_MAPPER,&wf,(DWORD)hwnd,0,CALLBACK_WINDOW);

  wh.lpData=lpBuf[0];
  wh.dwBufferLength=8000;
  wh.dwBytesRecorded=0;
  wh.dwUser=0;
  wh.dwFlags=0;
  wh.dwLoops=1;
  wh.lpNext=NULL ;
  wh.reserved=0 ;

  /* オクターブ4のドを出力 */
  waveOutPrepareHeader(hWOut,&wh,sizeof(WAVEHDR));
  waveOutWrite(hWOut,&wh,sizeof(WAVEHDR));

 ドの出力が終了したらMM_WOM_DONEメッセージが来るので、その応答で次のレ(lpBuf[1])を、またレの出力が終わったらミを....という順番で出力します。これは、波形バッファlpBuf[]のインデックスを切り替えれば良いので、インデックスを保持するstatic変数nで管理することにしましょう。

  case MM_WOM_DONE: /* 再生終了 */

      if (n==7) { /* 音階をすべて再生したらデバイスクローズ */

          waveOutClose(hWOut);

          return 0;

      }

      n++; /* 再生バッファのインデックス更新 */

      waveOutUnprepareHeader(hWOut,&wh,sizeof(WAVEHDR));

      /* 再生バッファのアドレス更新 */
      wh.lpData=lpBuf[n];

      /* 新しいバッファのアドレスで再生開始 */
      waveOutPrepareHeader(hWOut,&wh,sizeof(WAVEHDR));
      waveOutWrite(hWOut,&wh,sizeof(WAVEHDR));

      return 0 ;

プログラム

 実行すると、オクターブ4のドからオクターブ5のドまでの音階を出力します。音階を出力している間はプログラムを終了しないでください。また、このプログラムのビルドにはwinmm.libのリンクが必要です。

プログラムソース表示


画像と音声処理実験室 > プログラミング資料庫