音楽のテンポを変えて演奏する

2004/06/24 放送のトリビア(trivai) の泉でこんな興味深い放送をしていました。
一青ヨウの「もらいなき」の再生速度を遅くすると(80%)、平井堅が歌っているように聞こえる

インターネットでは放送以前から知られていた話題で、トリビアの泉に投稿した人に対して非難轟々のメッセージが 駆け巡っていましたが、これをプログラムで検証してみましょう。

プログラムの世界では「実現できるか? できないか?」と言う情報も非常に重要で、もしできないとなれば幾ら苦労しても 徒労に終わります。
逆にできると言う確信があれば、意気込みも違うと言うものです。

多くの例があるのですが、最近では二足歩行のロボットが代表的な例でしょう。
ホンダのアシモが二足歩行を始めるまでは「二足歩行は技術的に不可能」と思われていて、多くの大学や研究所が開発にしのぎを 削っていたのですが、目覚しい成果はありませんでした。
テレビ局の先端技術を紹介する放送で、ある大学が「両側に手すりを設けて、これを伝いながら歩くロボット」を放送して いたぐらいです。
所が「アシモが二足歩行」を始めたとたんに、あちこちの研究所や会社から次々と二足歩行のロボットが発表されました。
これは、歩く姿が公開されたこともありますが「やればできる」と言う確信があって開発に打ち込んだ成果でしょう。

今まで教えてきたことは事前に私がプログラムを作成して検証してきた事ばかりですが、今回の「再生速度を遅くする」は 私の経験ではたぶんできると思いますが、今回はあえてその具体的なプログラミング手法をあなた方に委ねます。
できないかも知れないと不安を持って取り組むのと、できると言う確信を持って取り組むのではその結果は大きく異なります。
トリビアの泉でコンピュータを使って操作していたのですからできないはずはありません。
すごく簡単にできるのか?、非常に苦労するのか見ものですね。
私は「すごく簡単にできる」方に賭けますが、どうなるでしょう。
ゲームプログラマの世界では、誰かが開発した技術の後を追うのでは無く、他社に先駆けて開発する能力が問われています。


Performance を使って簡単にテンポを設定することができそうです。
MIDI のテンポは設定できたのですが WAVE はうまく行きません。
どうすれば良いのでしょう?
float   fTempo= 1.0f;
lpPerformance->SetGlobalParam(GUID_PerfMasterTempo,&fTempo,sizeof(float));

Performance で音量を設定したときのコードです。
long    Vol1= 600;
    Vol1+= 200;
    pPerform1->SetGlobalParam(GUID_PerfMasterVolume,(void*)&Vol1,sizeof(long));

オーディオパスごとに音量を設定するコードです。
範囲は 2000~-20000 で設定します。
g_pAudioPath->SetVolume(vol,0);


ネットで調べた所、音楽のテンポを遅くするには WAVE ファイルのヘッダ部に記述されているサンプリングレートと転送速度を 直接書き換えて演奏する方法が見つかりました。
WAVE ファイルの次の記述を書き換えて演奏して下さい。いずれも80%に落とした値です。
Binary Editor で直接ファイルを修正したところ DirectX で演奏することができました。
また Media Player でも「平井堅が歌っているように」演奏することができました。
演奏が終われば WAVE ファイルのヘッダ部を元の値に戻しておいて下さい。
修正する項目 バイト位置 元の値 変更する値
サンプリングレート 24~4 Byte 44AC0000 D08D0000
データ速度 28~4 Byte 10B10200 40270200


たとえ演奏ができたとしても「WAVE ファイルを直接書き換える」と言うのはいただけません。(^_^;
何とか WAVE ファイルを直接変更しないで演奏したいのですが?。
WAVE を生成した CMusicSegment にはヘッダ情報が格納されているはずで、この値を変更すれば演奏できるのでは? と考えました。

directx9_c.chm を調べて行って WAVEFORMATEX 構造体を見つけました。
WAVEFORMATEX 構造体
WAVEFORMATEX 構造体は、波形オーディオ データのフォーマットを定義する。
すべての波形オーディオ データのフォーマットに共通のフォーマット情報だけがこの構造体に含まれる。
追加情報が必要なフォーマットの場合、この構造体は、追加情報と共に別の構造体の先頭メンバとして含まれる。

この構造体は Platform SDK の一部で、Mmreg.h で宣言されているが、便宜上このドキュメントにも記載している。

構文

typedef struct { 
  WORD  wFormatTag; 
  WORD  nChannels; 
  DWORD nSamplesPerSec; 
  DWORD nAvgBytesPerSec; 
  WORD  nBlockAlign; 
  WORD  wBitsPerSample; 
  WORD  cbSize; 
} WAVEFORMATEX; 

メンバ

wFormatTag 

波形オーディオのフォーマット タイプ。
フォーマット タグは、多くの圧縮アルゴリズム用に Microsoft Corporation に登録されている。
フォーマット タグの完全なリストは、Mmreg.h ヘッダー ファイルにある。
1 チャンネルか 2 チャンネルの PCM (Pulse Code Modulation) データの場合、この値は WAVE_FORMAT_PCM にする必要がある。

nChannels 

波形オーディオ データに含まれるチャンネル数。
モノラル データは 1 チャンネルを使い、ステレオ データは 2 チャンネルを使う。

nSamplesPerSec 

サンプル/秒で表すサンプル レート (単位 Hz)。
wFormatTag が WAVE_FORMAT_PCM の場合、nSamplesPerSec の一般的な値は 8.0 kHz、11.025 kHz、22.05 kHz、44.1 kHz となる。
非 PCM フォーマットの場合、このメンバは製造業者のフォーマット タグ仕様に従って算出する必要がある。 

nAvgBytesPerSec 

フォーマット タグに必要な平均データ転送レート (単位 バイト/秒)。
wFormatTag が WAVE_FORMAT_PCM の場合、nAvgBytesPerSec は nSamplesPerSec と nBlockAlign の積と等しくなければならない。
非 PCM フォーマットの場合、このメンバは製造業者のフォーマット タグ仕様に従って算出する必要がある。 

nBlockAlign 

ブロック アラインメント (単位 バイト)。
ブロック アラインメントとは、wFormatTag フォーマット タイプのデータの最小構成単位である。
wFormatTag が WAVE_FORMAT_PCM または WAVE_FORMAT_EXTENSIBLE の場合、nBlockAlign はnChannels と wBitsPerSample の積を 8 (1 バイトあたりのビット数) で割った値と等しくなければならない。
非 PCM フォーマットの場合、このメンバは製造業者のフォーマット タグ仕様に従って算出する必要がある。 

ソフトウェアは、一度に複数の nBlockAlign バイトのデータを処理する必要がある。
デバイスに対するデータの書き込みと読み取りは常に、ブロックの先頭から開始しなければならない。
たとえば、PCM データの再生をサンプルの途中 (すなわち、非ブロック アライン境界上) から開始することは不正である。 

wBitsPerSample 

wFormatTag フォーマット タイプの 1 サンプルあたりのビット数。
wFormatTag が WAVE_FORMAT_PCM の場合、wBitsPerSample は 8 または 16 でなければならない。
wFormatTag が WAVE_FORMAT_EXTENSIBLE の場合、この値は、任意の 8 の倍数を指定できる。
一部の圧縮スキームは wBitsPerSample の値を定義できないので、このメンバには 0 を指定してもかまわない。 

cbSize 

WAVEFORMATEX 構造体の最後に追加される追加フォーマット情報のサイズ (単位バイト)。
非 PCM フォーマットは、この情報を使って wFormatTag の追加属性を格納できる。
wFormatTag に追加情報が必要ない場合は、このメンバはゼロに設定しなければならない。
WAVE_FORMAT_PCM フォーマットしかない場合、このメンバは無視される。 

参照 

DirectShow 構造体 
WAVEFORMATEXTENSIBLE 構造体 


WAVEFORMATEX 構造体がその定義なのですが「どこで定義されていて、どのようにしてアクセスする」のでしょう?
さらに directx9_c.chm を調べて行くと WAVEFORMATEX 構造体を取得する関数と設定する関数を見つけました。
IDirectMusicPort8::GetFormat
IDirectMusic8::CreatePort に渡す DMUS_PORTPARAMS8 構造体で指定されている WAV フォーマットに関する情報、ウェーブフォーム出力で使うバッファの推奨サイズに関する情報を取得する。
これらの情報を使って、ポート用に互換性のある DirectSound バッファを作成できる。

構文

HRESULT GetFormat(
  LPWAVEFORMATEX pWaveFormatEx,
  LPDWORD pdwWaveFormatExSize
  LPDWORD pdwBufferSize
);

パラメータ

pWaveFormatEx

フォーマットに関する情報を受け取る WAVEFORMATEX 構造体のアドレス。
この値は NULL でもよい。「注意」を参照すること。

pdwWaveFormatExSize

構造体のサイズを指定または取得する変数のアドレス。「注意」を参照すること。

pdwBufferSize

DirectSound バッファの推奨サイズを受け取る変数のアドレス。

戻り値

戻り値は実装によって決まる。成功した場合は S_OK を返す。

失敗した場合は E_POINTER を返す。

注意

WAVEFORMATEX 構造体は、フォーマットの詳細に応じて可変長にもなる。
フォーマットの記述を取得する前に、アプリケーションは、pWaveFormatEx パラメータに NULL を指定してこのメソッドを呼び出すことにより、シンセサイザ オブジェクトにフォーマットのサイズを問い合わせなければならない。
構造体のサイズは、pdwWaveFormatExSize が指す変数に返される。
その後、アプリケーションは、十分なメモリを割り当ててから GetFormat を再度呼び出し、フォーマットの記述を取得できる。

pWaveFormatEx が NULL でない場合、DirectMusic が構造体に書き込む情報は多くても pdwWaveFormatExSize バイトである。

要件

  ヘッダー:dmusicc.h で宣言。

参照 

IDirectMusicPort8 インターフェイス 
IDirectMusicPort8::SetDirectSound 

IDirectSoundBuffer8::SetFormat
プライマリ バッファのフォーマットを設定する。
このアプリケーションが入力フォーカスを得るたびに、DirectSound はプライマリ バッファを指定されたフォーマットに設定する。

プライマリ サウンド バッファは IDirectSoundBuffer8 インターフェイスをサポートしていないので、IDirectSoundBuffer でこのメソッドを呼び出す必要がある。

WDM ドライバに対しては、プライマリ バッファのフォーマットを設定しても何も影響がない。
出力フォーマットは、カーネル ミキサによって決定される。
詳細については、「DirectSound ドライバ モデル」を参照すること。

構文

HRESULT SetFormat(
  LPCWAVEFORMATEX pcfxFormat 
);

パラメータ

pcfxFormat

プライマリ サウンド バッファの新しいフォーマットが記述されている WAVEFORMATEX 構造体のアドレス。

戻り値

成功した場合は DS_OK を返す。

失敗した場合は、次のいずれかのエラー値を返す。

リターン コード 
DSERR_BADFORMAT 
DSERR_INVALIDCALL 
DSERR_INVALIDPARAM 
DSERR_OUTOFMEMORY 
DSERR_PRIOLEVELNEEDED 
DSERR_UNSUPPORTED 


注意

プライマリ バッファのフォーマットは、セカンダリ バッファを作成する前に設定する必要がある。

アプリケーションに DSSCL_NORMAL 協調レベルが設定されている場合、メソッドは失敗する。

アプリケーションが DSSCL_WRITEPRIMARY 協調レベルで DirectSound を使っている場合は、SetFormat を呼び出す前にバッファを停止しなければならない。
フォーマットがサポートされていない場合、メソッドは失敗する。

協調レベルが DSSCL_PRIORITY である場合、DirectSound はプライマリ バッファを停止し、フォーマットを変更して、バッファを再開する。
要求されたフォーマットをハードウェアがサポートしていない場合でも、メソッドは成功する。
DirectSound は、要求に最も近いフォーマットにバッファを設定する。
このような状況が発生しているかどうかをアプリケーションで判断するには、プライマリ バッファに対して IDirectSoundBuffer8::GetFormat メソッドを呼び出し、その結果を SetFormat メソッドで要求したフォーマットと比較する。

このメソッドは、セカンダリ サウンド バッファでは利用できない。
新しいフォーマットが必要な場合は、アプリケーションは新しい DirectSoundBuffer オブジェクトを作成しなければならない。

要件

  ヘッダー:dsound.h で宣言。

参照 

IDirectSoundBuffer8 インターフェイス 
IDirectSoundBuffer8::GetFormat 


さらに IDirectSoundBuffer8 を検索すると次の記述を見つけました。
IDirectSoundBuffer8 インターフェイス
IDirectSoundBuffer8 インターフェイスは、サウンド バッファを管理するために使う。 

IDirectSoundBuffer に代わって IDirectSoundBuffer8 インターフェイスが提供され、新しいメソッドが追加されている。

このインターフェイスを取得するには、IDirectSound8::CreateSoundBuffer メソッドを使って IDirectSoundBuffer を取得し、IID_IDirectSoundBuffer8 を IDirectSoundBuffer::QueryInterface に渡す。 

プライマリ バッファの場合は、IDirectSoundBuffer インターフェイスを使う必要がある。
IDirectSoundBuffer8 は利用できない。
IDirectSoundBuffer の独立した説明項目はない。
説明は、対応する IDirectSoundBuffer8 メソッドを参照すること。 

IDirectSoundBuffer のすべてのメソッドが、プライマリ バッファに対して有効であるわけではない。
たとえば、SetCurrentPosition は失敗する。
各メソッドについては、リファレンスのトピックを参照すること。

IUnknown から継承するメソッドのほかに、IDirectSoundBuffer8 インターフェイスでは、カテゴリ別に示す以下のメソッドが公開されている。

エフェクト
メソッド 説明 
GetObjectInPath バッファに関連付けられたエフェクト オブジェクトのインターフェイスを取得する。 
SetFX バッファのエフェクトを有効にする。  


リソース管理
メソッド 説明 
AcquireResources DSBCAPS_LOCDEFER フラグで作成されたバッファにリソースを割り当てる。 
Restore 失われたサウンド バッファへのメモリ割り当てを復元する。 


再生管理
メソッド 説明 
GetCurrentPosition サウンド バッファの再生カーソルと書き込みカーソルの位置を取得する。 
Lock バッファの全部または一部をデータ書き込み用に準備し、データを書き込むことのできるポインタを返す。 
Play サウンド バッファを再生カーソルの位置から再生する。 
SetCurrentPosition 再生カーソルの位置を設定する。再生カーソルは、バッファから次のデータ バイトが読み込まれる位置を表す。 
Stop サウンド バッファの再生を停止する。 
Unlock サウンド バッファをアンロックする。 


サウンドのパラメータ
メソッド 説明 
GetFrequency バッファが再生されている周波数 (1 秒あたりのサンプリング数) を取得する。 
GetPan 左右のオーディオ チャンネルの相対ボリュームを取得する。 
GetVolume サウンドの減衰を取得する。 
SetFrequency サンプリング オーディオの再生周波数を設定する。 
SetPan 左右のチャンネルの相対ボリュームを設定する。 
SetVolume サウンドの減衰を設定する。 


その他
メソッド 説明 
GetCaps バッファ オブジェクトの能力を取得する。 
GetFormat バッファ内のサウンド データのフォーマットに関する情報、またはフォーマットの記述を取得するために必要なバッファ サイズを取得する。 
GetStatus サウンド バッファのステータスを取得する。 
Initialize 初期化されていない サウンド バッファ オブジェクトを初期化する。 
SetFormat プライマリ バッファのフォーマットを設定する。 


次の表は、DirectMusic オーディオパスから取得されるバッファ オブジェクトでサポートされているメソッドをまとめたものである。
ミックスイン バッファは、他のバッファからの送信を受け入れ、DirectMusic Producer で作成されたオーディオパス構成にのみ存在する。
オーディオパスの他のバッファはすべて、シンクイン バッファである。
シンクイン バッファは、シンセサイザ シンクだけからデータを受け入れる。

IDirectSoundBuffer8 のメソッド ミックスイン シンクイン
AcquireResources サポートされない サポートされない
GetCaps サポートされる サポートされる
GetCurrentPosition サポートされない サポートされない
GetFormat サポートされる サポートされる
GetFrequency サポートされない サポートされない
GetObjectInPath サポートされる サポートされる
GetPan サポートされる サポートされる
GetStatus サポートされる サポートされる
GetVolume サポートされる サポートされる
Initialize サポートされない サポートされない
Lock サポートされない サポートされない
Play サポートされる サポートされない
Restore サポートされない サポートされない
SetCurrentPosition サポートされない サポートされない
SetFormat サポートされない サポートされない
SetFrequency サポートされない サポートされない
SetFX サポートされる サポートされる
SetPan サポートされる サポートされる
SetVolume サポートされる サポートされる
Stop サポートされる サポートされない
Unlock サポートされない サポートされない

LPDIRECTSOUNDBUFFER8 型は、IDirectSoundBuffer インターフェイスへのポインタとして定義されている。 typedef struct IDirectSoundBuffer8 *LPDIRECTSOUNDBUFFER8; 要件 ヘッダー:dsound.h で宣言。 参照 DirectSound のインターフェイス


再生速度の設定で次の記述を見つける。
IMediaSeeking::SetRate
SetRate メソッドは、再生レートを設定する。

構文

HRESULT SetRate(
  double dRate
);

パラメータ

dRate

[in] 再生レート。ゼロであってはならない。

戻り値

HRESULT 値を返す。可能な値は次のとおりである。

値 説明 
S_OK  成功。 
E_INVALIDARG 指定したレートは、0 または負の値だった。「注意」を参照。 
E_NOTIMPL 実装されていない。 
E_POINTER NULL ポインタ引数。 
VFW_E_UNSUPPORTED_AUDIO オーディオ デバイスあるいはフィルタがこのレートをサポートしていない。 


注意

再生レートは、通常速度との比率で表現される。
したがって、1.0 は通常の再生速度、0.5 は半分の速度、2.0 は 2 倍の速度を示す。
オーディオ ストリームについては、レートを変えるとそのピッチも変わる。

負の値は逆方向の再生を示す。
大部分のフィルタは逆再生をサポートせず、代わりに dRate 引数が負の場合はエラー コードを返す。

アプリケーションがこのメソッドをフィルタ グラフ マネージャで呼び出すと、フィルタ グラフ マネージャは以下のことを行う。

IMediaSeeking::GetCurrentPosition メソッドを呼び出す。
この呼び出しによってフィルタ グラフ マネージャが計算した現在位置を返す。 
(グラフがポーズあるいは実行中の場合) フィルタ グラフを停止する。 
現在位置を開始タイムとして、フィルタで IMediaSeeking::SetPositions メソッドを呼び出す。
これはストリーム タイムをゼロにリセットする結果となる。 
新しいレートで、フィルタの SetRate メソッドを呼び出す。 
ポーズあるいは実行中だった場合、フィルタ グラフを再開する。 
ステップ 4 でエラーが起きると、フィルタ グラフ マネージャは以前のレートを再び使おうとする。

フィルタは以下のようにレート変更に対応する必要がある。

パーサー フィルタとソース フィルタ : タイム スタンプを発生させているフィルタは SetRate 呼び出しに対応しなければならない。
通常これは AVI スプリッタ フィルタのようなパーサー フィルタだが、ソース フィルタの場合もある。
シークやレート変更の後、フィルタは IPin::NewSegment メソッドを新しい設定で呼び出す必要がある。
レート変更の後、そのタイム スタンプを必要に応じて調整すべきである。
レート変更はシークに優先するので、タイム スタンプはゼロから再スタートとなる。
したがって、フィルタはそのレートで割った新しいタイム スタンプを計算できる。

デコーダ フィルタ :デコーダは SetRate 呼び出しに対して、それらをアップストリームに渡す以外のことを行ってはならない。
代わりに、デコーダはアップストリーム パーサーが発行する NewSegment 呼び出しに対応すべきである。
デコーダ フィルタが新しいセグメント情報を受け取るとき、そのフィルタはその値を保存して、NewSegment 呼び出しをダウンストリームに渡す。
一部のデコーダはその入力を補間することによって追加タイム スタンプを生成する必要がある。
その場合、レート変更を考慮する必要がある。

レンダラ : 受信フレームは既に正しいタイム スタンプを持っているので、ビデオ レンダラは通常レート変更を無視できる。
オーディオ デコーダは通常レート変換を行わないので、オーディオ レンダラがその再生レートを変更する必要がある。 

参照 

エラー コードと成功コード 
IMediaSeeking インターフェイス 

あとは、今後の課題として後輩に残しておきましょう。(^_^;