第 4 章: オーディオの再生


 

再生は、「プレゼンテーション」または「レンダリング」と呼ばれる場合があります。これらは、サウンド以外の種類のメディアにも使用される一般的な用語です。基本的な機能は、連続するデータを最終的にユーザが知覚できる場所に配信することです。サウンドのように、データが時間ベースの場合は、正しい速度で配信されなければなりません。サウンド再生が中断されると大きなクリックノイズやもどかしい歪みが生じることが多いので、サウンドではデータフローの速度を維持することが、ビデオの場合よりも重要です。JavaTM Sound API は、非常に長いサウンドの場合にも、アプリケーションプログラムがサウンドを円滑に再生するために役立つよう設計されています。

前章では、オーディオシステムまたはミキサーからラインを取得する方法について説明しました。この章では、ラインを介してサウンドを再生する方法について説明します。

サウンドの再生に使用できるラインには、ClipSourceDataLine の 2 種類があります。この 2 つのインタフェースについては、第 2 章「Sampled パッケージの概要」「Line インタフェースの階層」に簡単な説明があります。2 つの主な相違点は、Clip では再生の前に一度にすべてのサウンドデータを指定するのに対し、SourceDataLine では再生中にデータをバッファに継続的に書き込むことです。多くの場合、ClipSourceDataLine のどちらも使用できますが、特定の状況でどちらのラインの方が適しているのかを判断するには、次の条件を参考にしてください。

クリップの使用

Clip は、第 3 章「オーディオシステムリソースへのアクセス」「目的の種類のラインの取得」で説明した方法で取得します。最初の引数を Clip.class として DataLine.Info オブジェクトを構築し、AudioSystem または MixergetLine メソッドに引数としてこの DataLine.Info を渡します。

再生用のクリップのセットアップ

ラインを取得することは、単にそのラインを参照するための方法を得ることであり、getLine がそのラインを取得者のために実際に予約するわけではありません。ミキサーで利用できる、目的の種類のライン数は限られているので、getLine を呼び出してクリップを取得したあと、再生を始める前に、ほかのアプリケーションプログラムが飛び入りし、クリップを横取りする可能性があります。実際にクリップを使用するには、次の Clip メソッドのいずれかを呼び出すことにより、自分のプログラムが排他的に使用するためにそのクリップを予約する必要があります。

   void open(AudioInputStream stream) 
   void open(AudioFormat format, byte[] data, int offset,
   int bufferSize)
上記の 2 番目の open メソッドには bufferSize 引数がありますが、SourceDataLine とは異なり、Clip には新しいデータをバッファに書き込むメソッドがありません。ここでは、bufferSize 引数は、クリップにロードするバイト配列のバイト数を指定するためのものです。SourceDataLine のバッファのような、さらに続けてデータをロードできるバッファではありません。

クリップをオープンしたら、ClipsetFramePosition または setMicroSecondPosition メソッドを使って、データ内のどの位置から再生を開始するかを指定できます。指定しない場合は、先頭から再生が開始します。setLoopPoints メソッドを使って、再生が循環するように設定することもできます。

再生の開始と停止

再生の準備ができたら、start メソッドを呼び出します。クリップを停止または一時停止するには stop メソッドを呼び出し、再生を再開するには再び start を呼び出します。クリップは再生を停止したメディア位置を記憶しているので、一時停止と再開の明示的なメソッドは必要ありません。停止した位置からの再開を行わない場合は、すでに説明したフレームまたはマイクロ秒の位置決めメソッドを使って、クリップを先頭または希望する位置まで「巻き戻す」ことができます。

Clip のボリュームレベルと活動ステータスがアクティブかアクティブでないかは、DataLinegetLevel メソッド、isActive メソッドをそれぞれ呼び出すことにより監視できます。アクティブな Clip とは、現在サウンドを再生しているクリップです。

SourceDataLine の使用

SourceDataLine を取得することは、Clip を取得することに似ています。第 3 章「オーディオシステムリソースへのアクセス」「目的の種類のラインの取得」を参照してください。

再生用 SourceDataLine のセットアップ

SourceDataLine をオープンする目的は、Clip の場合と同様に、ラインを予約するためです。ただし、DataLine から継承した別のメソッドを使用します。

    void open(AudioFormat format) 
Clip の場合とは異なり、SourceDataLine をオープンするときは、そのラインにサウンドデータを関連付けません。その代わり、再生するオーディオデータの形式を指定します。デフォルトのバッファ長がシステムにより選択されます。

また、次の変数を使って特定のバッファ長をバイト単位で指定することもできます。

    void open(AudioFormat format, int bufferSize) 
類似メソッドとの整合性を維持するために、buffersize 引数はバイト数で表しますが、フレームの整数値に対応していなければなりません。

バッファサイズを決める方法について考えます。バッファサイズはプログラムの条件により異なります。

まず、バッファサイズは小さい方が待ち時間が少なくなります。新しいデータを送ったときに、より早く再生されます。高度に対話的なプログラムなど、アプリケーションプログラムによっては、このような応答の速さが重視されます。たとえば、ゲームでは再生の開始は視覚的なイベントと正確に同期する必要があります。このようなプログラムでは、待ち時間を 0.1 秒未満にすることが要求されます。別の例として、会議アプリケーションでは再生と取り込みの両方で遅れを防ぐ必要があります。ただし、多くのアプリケーションプログラムでは、サウンドがいつ再生を開始するかはあまり問題にならないので、ユーザが困惑するほどの遅れでないかぎり、1 秒程度の遅れがあっても構いません。このことは、1 秒分のバッファを使って大きなオーディオファイルをストリーミングするアプリケーションプログラムにも当てはまります。サウンドそのものは長時間途切れずに再生され、高度に対話的な操作を伴わないため、再生の開始に数秒間かかってもユーザは気にしないでしょう。

反面、バッファサイズが小さい場合は、バッファに十分な速度でデータを書き込めず、書き込みに失敗する可能性が大きくなります。書き込みに失敗するとデータに不連続部が発生します。不連続部は、クリックノイズや付随音を生じます。また、バッファサイズが小さい場合は、バッファを常に満たしておくためにプログラムの動作が激しくなり、CPU を集中的に使用することになります。このため、ほかのプログラムはもとより、そのプログラムのほかのスレッドの実行も遅くなります。

したがって、最適なバッファサイズとは、待ち時間がアプリケーションプログラムで許容できる程度に短く、かつ、バッファのアンダーフローの危険が少なく、CPU リソースの不必要な消費が避けられるようなサイズです。会議アプリケーションのようなプログラムでは、音の再現性の低さよりも遅れの方が問題視されるので、バッファサイズは小さい方が適しています。音楽のストリーミングでは、初期の遅れは許容されますが付随音は許容されません。このため、音楽のストリーミングでは 1 秒程度の大きなバッファサイズが望まれます。ただし、サンプリングレートが高い場合は、DataLine API のバッファサイズの計量単位であるバイト数に換算すると、バッファサイズは大きくなります。

上記の open メソッドを使う代わりに、Lineopen() メソッドを使って、引数を指定しないで SourceDataLine をオープンすることもできます。この場合、ラインはデフォルトのオーディオ形式とバッファサイズでオープンされます。ただし、このオーディオ形式とバッファサイズはあとから変更できません。ラインのデフォルトのオーディオ形式とバッファサイズは、ラインがオープンされていなくても、DataLinegetFormat メソッドと getBufferSize メソッドを呼び出して知ることができます。

再生の開始と停止

SourceDataLine をオープンすると、サウンドの再生を開始できます。再生を行うには、DataLine の start メソッドを呼び出してから、ラインの再生バッファにデータを繰り返し書き込みます。

start メソッドにより、ラインはバッファにデータが入り次第、ただちに再生を開始できるようになります。バッファには、次のメソッドを使ってデータを書き込みます。

    int write(byte[] b, int offset, int length) 
配列に対するオフセットは配列の長さと同様に、バイト単位で表されます。

ラインは、ミキサーへのデータ送信を可能なかぎり早期に開始します。ミキサー自体がターゲットにデータを送るとき、SourceDataLineSTART イベントを生成します。(Java Sound API の一般的な実装では、ソースラインがミキサーにデータを送るまぎわとミキサーがターゲットにデータを送るまぎわとの間の遅延はサンプル 1 個の長さよりはるかに短いので、無視できる)。この START イベントは、この後の「ラインのステータスの監視」で説明するように、そのラインのリスナーオブジェクトに送られます。この時点でこのラインはアクティブ状態とみなされるので、DataLineisActive メソッドは true を返します。これらはすべて、バッファに再生データが入ってはじめて行われる動作であり、start メソッドが呼び出された直後に行われるとは限りません。新しい SourceDataLine に対して start メソッドを呼び出してもバッファにデータを書き込まなければ、そのラインはアクティブにならず、START イベントも送られません。ただし、この場合、DataLineisRunning メソッドは true を返します。

ここで、バッファに書き込むデータの量と 2 回目のバッチデータを送る時期を決める方法を考えます。幸い、2 回目の書き込みの呼び出し時期を記録して最初のバッファの終わりと同期させる必要はなくなりました。その代わりに、write メソッドのブロックを利用することができるようになりました。

次に、ストリームから読み込んだデータのチャンクを反復する例を示します。ここでは、チャンクを一度に 1 つずつ SourceDataLine に書き込んで再生します。

// read chunks from a stream and write them to a source data 
line 
line.start();
while (total < totalToRead && !stopped)}
    numBytesRead = stream.read(myData, 0, numBytesToRead);
    if (numBytesRead == -1) break;
    total += numBytesRead; 
    line.write(myData, 0, numBytesRead);

}
write メソッドがブロックされないようにするには、まず available メソッドをループ内で呼び出して、ブロックされずに書き込めるバイト数を調べ、numBytesToRead 変数をその数値に制限してから、ストリームからの読み込みを行います。この例では、write メソッドがループの中で呼び出されており、ループの最後の繰り返し時に、最後のバッファが書き込まれるまでループは終了しないので、ブロックは問題になりません。ブロッキング技法を使うか使わないかにかかわらず、長いサウンドの再生中にプログラムがフリーズしているように見えることを防ぐために、独立したスレッドの再生ループをアプリケーションのほかの部分から呼び出す方法があります。ループの繰り返しごとに、ユーザが再生の停止を要求したかどうかを調べることができます。停止が要求されたら、上のコードでは stopped ブール変数を true に設定する必要があります。

write はすべてのデータの再生が終了する前に戻るので、再生が実際に終わった時期を知る方法が必要です。1 つの方法は、最後の 1 バッファ分のデータを書き込んだあとに DataLinedrain メソッドを呼び出すことです。このメソッドは、すべてのデータの再生が終わるまでブロックされます。プログラムに制御が戻った時点で、必要に応じてラインを解放できます。その際にオーディオサンプルの再生が途中で打ち切られる心配はありません。

line.write(b, offset, numBytesToWrite); 
//this is the final invocation of write
line.drain();
line.stop();
line.close();
line = null;
もちろん、意図的に再生を途中で停止させることもできます。たとえば、アプリケーションプログラムにユーザ用の [停止] ボタンがある場合があります。バッファの途中であっても再生をただちに停止するには DataLinestop メソッドを呼び出します。未再生のデータはそのままバッファに残るので、続いて start を呼び出すと、停止した位置から再生が再開されます。再生を再開しない場合は、flush を呼び出して、バッファ内に残っているデータを破棄します。

データの流れが停止すると、SourceDataLine は常に STOP イベントを生成します。これは、データの流れが drain、stop、flush のいずれかのメソッドにより停止した場合にも、アプリケーションプログラムが次のデータを書き込むために write を呼び出す前に再生バッファの最後に到達したためにデータの流れが停止した場合にも当てはまります。STOP イベントが生成されても、必ずしも stop メソッドが呼び出されたとは限らず、続いて isRunning を呼び出しても false が返されるとは限りません。ただし、isActive メソッドは false を返しますstart メソッドがすでに呼び出されている場合は、STOP イベントが生成されても isRunning メソッドはtrue を返し、stop メソッドが呼び出されたあとに false を返すようになります。重要なことは、START イベントと STOP イベントは、isRunning ではなく isActive に対応しているということです。

ラインのステータスの監視

サウンドの再生を開始したら、それがいつ終了するかを知る手段が必要になります。1 つの方法として、最後のバッファのデータを書き込んでから drain メソッドを呼び出す方法をすでに説明しましたが、これは、SourceDataLine にしか使用できません。SourceDataLineClip の両方に使用できるもう 1 つの方法は、ラインがステータスを変更したときにそのラインから通知を受けるように登録することです。これらの通知は LineEvent オブジェクトの形で生成され、OPENCLOSESTARTSTOP の 4 種類があります。

LineListener インタフェースを実装するプログラムのどのオブジェクトも、この通知を受け取るように登録することができます。LineListener インタフェースの実装に必要なものは、LineEvent オブジェクトを引数に取る update メソッドだけです。このオブジェクトをそのラインのリスナーの 1 つとして登録するには、次の Line メソッドを呼び出します。

public void addLineListener(LineListener listener)

ラインのオープン時、クローズ時、開始時、および停止時に、必ずすべてのリスナーに update メッセージが送信されます。オブジェクトは、自分が受け取る LineEvent を問い合わせることができます。まず LineEvent.getLine を呼び出して、停止したラインが当該ラインであることを確認します。このケースではサウンドが終了したかどうかを知りたいので、LineEvent の種類が STOP かどうかを調べます。STOP の場合は、最後まで到達したのか、それとも別の理由 (ユーザが [停止] ボタンをクリックしたなど) で停止されたのかを調べるために、LineEvent オブジェクトに保存されているサウンドの正確な現在位置を調べ、サウンドの長さ (わかっている場合) と比べます。ただし、停止の理由はプログラムのほかの部分で調べることもできます。

同じラインについて、ラインがオープン、クローズ、または開始された時期を知る必要があるときも、同じ機構を使用します。LineEvents は、ClipsSourceDataLines 以外の種類のラインによっても生成されます。しかし、Port の場合は、イベントによってラインのオープンまたはクローズ状態を知ることはできません。たとえば、Port は作成された当初からオープンされているので、プログラムは open メソッドを呼び出さず、PortOPEN イベントを生成することはありません(第 3 章「オーディオシステムリソースへのアクセス」「入出力ポートの選択」を参照)。

複数ラインの再生の同期

オーディオの複数トラックを同時に再生する場合は、正確に同じ時刻にすべてのトラックの開始と停止を行うことが望まれます。一部のミキサーには synchronize メソッドによりこの機能が備わり、データラインのグループに対する openclosestartstop などの操作を単一のコマンドで行うことができるので、各ラインを個別に制御する必要はありません。さらに、ラインに対する操作の精度を調節することができます。

特定のミキサーが、指定されたグループデータラインに対してこの機能を使用できるかどうかを調べるには、Mixer インタフェースの isSynchronizationSupported メソッドを呼び出します。

boolean isSynchronizationSupported(Line[] lines, 
  boolean  maintainSync)
最初のパラメータは特定のデータラインのグループを指定し、2 番目のパラメータは同期を維持すべき精度を示します。2 番目のパラメータが true の場合、クエリーは、そのミキサーが指定されたラインを制御するときにサンプルどおりの精度を常に維持する機能があるかどうかを問い合わせます。その機能がない場合には、正確な同期は開始と停止の操作時にのみ必要で、再生全体を通じては必要ありません。

出力オーディオの処理

ソースデータラインには、ゲイン、パン、リバーブ、およびサンプリングレートのコントロールのような信号制御コントロールを備えているものがあります。同様な機能、特にゲインコントロールが出力ポートにも備わっていることがあります。ラインがそのようなコントロールを備えているかどうかを調べる方法、およびそれらの使用方法の詳細は、第 6 章「コントロールを使ったオーディオ処理」を参照してください。