- 締切済み
音声再生/SourceDataLineの遅延
軽いオーディオ編集ソフトを作ろうと思い、Javaで制作していたのですが… 再生にはSourceDataLineを使っているのですが、どうしても遅延が出てしまいます。 while (true) { if (play) { line.write(buf, x, 4); x ++; } } (play ...再生orストップ x ...再生箇所。自由に移動できる buf ...音声データが格納されている) 今回は16bitのステレオデータを取り扱っているので、1まとまりのデータにおそらく4byte必要だと思ってそう設定しています。 これをループさせているのですが、遅延が発生してしまいます。 SourceDataLineを使う時点で不可能なのでしょうか..? ご教授願います。
- みんなの回答 (9)
- 専門家の回答
みんなの回答
- KSOH
- ベストアンサー率93% (29/31)
なんども同じ回答つけてしまいすみません。「回答順」に表示させるとある回答以降が見えないんでうまく投稿できなかったと勘違いしました。「新着順」でみたら見えました。なんで全部のコメントが表示されないのだろう・・・ >実際はこのようになっています そのコードだと問題ないように見えますね。 自分はvolume/panが整数型に見えるといいましたが勘違いでした。+=演算子の仕様をうっかりしてました。 int型の変数varに対しては var += expression; は var =(int) (var + (expression)); と同じ意味になります。普段この仕様に依存したコーディングしないので気づきませんでした。 >こちらにプツプツの問題は移動しました。 コードが一切のっていないのでコメントがつきずらいと思います。 しかるべき範囲のコードを追記したほうがいいでしょう。
- KSOH
- ベストアンサー率93% (29/31)
ノイズの原因は振幅データの加工論理の問題のようです。 次を参考にしてみてください。 int byteToInt(byte lsb, byte msb) { return (lsb & 0xff) + (msb << 8); //元のコードでは 0xff とマスクをとってないのでlsbが負数のとき値が不正です } // volumeRatio, panRatio は0.0から1.0までの値です。 int cookVolumeAndPan(byte lsb, byte msb, double volumeRatio, double panRadio) { //元のコードではmsbだけにvolue/panを乗算しているように見えました。 //またvolumeRatio, panRatioはint型であるように見えました。 return (int) byteToInt(lsb, msb) * volueRatio * panRatio; }
- KSOH
- ベストアンサー率93% (29/31)
>ダブルバッファリングのようなものですか? おっしゃるとおりダブルバッファリングです。JComponentが備えているダブルバッファリング機能と考え方は同じですが混乱を避けるためにちょっと用語を変えてイメージバッファと言うことにします。 イメージバッファなしでも充分な速度で画面が再描画できるようでしたら現状の方式で充分なのでしょう。自分が試したときはスクロールバーを時間軸方向にぐりぐりと高速に動かした際に反応がもたつくように感じたのでイメージバッファを使ったのですが、なんといいますか自分のアプリケーションの描画処理自体に性能問題があるような気がしてきました...orz >ノイズ コードに問題がありそうです。 (1)振幅値を2byte->intへ変換する際、低位バイトをunsignedとして扱っていない (2)volumeRatio, panRatioを振幅の上位バイトだけに乗算している (3)volumeRatio, panRatioがint型に見える。0.0~1.0の範囲のfloat/double型の方が適切と思えます。 具体的な正誤は以下の通りです。 int j = (x - wav.getX()) * 4 + i * 2; と仮定します。 誤: bufInt[i] += (data[j]) + (data[j + 1] << 8) * getVolumeRatio(略) * getPanRatio(略); // getVolumeRatio(), getPanRatio()はint型(値の範囲:0 ~ ?) 正: bufInt[i] += (int) (((data[j] & 0xff) + (data[j + 1] << 8)) * getVolumeRatio(略) * getPanRatio(略)); // getVolumeRatio(), getPanRatio()はfloatまたはdouble型(値の範囲:0.0 ~ 1.0) なお、問題の内容がタイトルとずれてきているので改めて質問を起こしたほうがよかったかもですね。
補足
http://oshiete.goo.ne.jp/qa/8773735.html こちらにプツプツの問題は移動しました。この問題についてはこちらでご意見いただけると嬉しい限りです。 ダブルバッファリングは、処理の重さが感じるようになったらまた実装してみます。 現状ではグリグリしようがぬるぬる動きます。 実際はこのようになっています for (int i = 0; i < 2; i++) bufInt[i] += (double)((data[(x - wav.getX()) * 4 + i * 2] & 0xff) + (data[((x - wav.getX()) * 4 + i * 2 + 1)] << 8)) * getVolumeRatio(wav.getTrack()) * Math.abs(i - (1 - getPanRatio(wav.getTrack()))) +=演算子の場合自動的に右辺がintにキャストされるんでしょうか。エラーが出ていないのでそのままにしています。 volumeRatio/panRatioは浮動少数(0.0 ~ 1.0)となっています。
- KSOH
- ベストアンサー率93% (29/31)
>ダブルバッファリングのようなもの? はい。ダブルバッファリングそのものです。 なのですが・・・よく考えると自分は波形を時間軸方向で圧縮して画面上に何十秒分もの波形を表示するとき描画の間引きをせずに(ちょうど質問者さんがやっていたのと同様に)すごい量のGraphics.drawLine()を呼び出していたのでした。それを間引きする対処もやったのですが「描画が遅くても反応が鈍くならないように」みたいなのりでダブルバッファリングしたのでした。別スレッドでダブルバッファリングすると、バッファへの描画中に新たなイベントが到着した際にスレッドの描画処理に割り込んで最新の描画のみをやるといった工夫ができます。そのため例え描画が遅い実装のままでもスクロールバーをグリグリ動かしたようなときに画面が遅延なく追従するようになります。それで「ダブルバッファリング有効だ」というのが頭に残っていてコメントしてしまいました。 適切に描画している場合はUIの反応が鈍いと感じるほど描画が遅くなることはないかも知れません。そういう問題がなければダブルバッファリングをわざわざするまでもありません。 >ノイズ (A)byte->16bit整数への変換 javaではbyteは符号付きですので下位のバイトが負数の場合に結果の値がおかしくなります。 誤:bufInt[i] = data[j] + (data[j+1] << 8); 正:bufInt[i] = (data[j] & 0xff) + (data[j+1] << 8); (B1)volume/panの乗算 上位バイトだけに掛け算しているように見えますが... 誤:bufInt[i] = (data[j] & 0xff) + (data[j+1] << 8) * volumeRatio * panRatio; 正:bufInt[i] = ((data[j] & 0xff) + (data[j+1] << 8)) * volumeRatio * panRatio; (B1)volume/panは整数?浮動少数? volumeRatio/panRatioはコード上は整数に見えます。0/1ではmuteスイッチみたいなものになってしまいあまり意味がないので、ある程度の大きさの値例えば(min=0,max=100とか)を想定するのだと思いますが、そうしてしまうとoverflow/underflowが頻繁に起きてしまうと思います。 volumeRatio/panRatioは浮動小数(0.0 ~ 1.0)として以下のようにすべきかと思いますが、ひょっとしたら既に浮動小数になっていてコードがそう見えないのは単にコピペミスなのでしょうか。(なんとなくそんな気がします) bufInt[i] = (int) (((data[...] & 0xff) + (data[...] << 8)) * volumeRatio * panRatio); >新しい質問として投稿したほうが良いですか? 今のタイトルが「遅延」なのでタイトルから話題がずれてきたときに他のリーダーの方に配慮して新たに質問をおこしたほうがよかったかと思います。
補足
http://oshiete.goo.ne.jp/qa/8773735.html こちらにプツプツの問題は移動しました。この問題についてはこちらでご意見いただけると嬉しい限りです。 ダブルバッファリングは、処理の重さが感じるようになったらまた実装してみます。 実際はこのようになっています for (int i = 0; i < 2; i++) { bufInt[i] += (double)((data[(x - wav.getX()) * 4 + i * 2] & 0xff) + (data[((x - wav.getX()) * 4 + i * 2 + 1)] << 8)) * getVolumeRatio(wav.getTrack()) * Math.abs(i - (1 - getPanRatio(wav.getTrack()))) +=演算子の場合自動的に右辺がintにキャストされるんでしょうか。エラーが出ていないのでそのままにしています。 volumeRatio/panRatioは浮動少数(0.0 ~ 1.0)となっています。
- KSOH
- ベストアンサー率93% (29/31)
EDT上での描画処理が重かったために、ボタンをクリックしても描画が終わるまでボタンのハンドラーの起動が待たされてしまったというこですね。なるほど。 デバッグお疲れ様でした。 ちなみに、音声を再生しながらのスクロールですとか時間軸に従って波形描画領域をスクロールするとかいったときに反応が鈍く感じたら波形の描画そのものを別スレッドに持っていくという方法も検討されるとよいかも知れません。 (1)波形の表示位置などが変化したとき、別スレッドで波形をBufferedImage上に生成。イメージができたらrepain() ご存じと思いますがBufferedImage.getGraphics()でGraphicsオブジェクトが取得できるのでBufferedImage上への描画は普通の描画と同じよう簡単にできます。 (2)paintComponent()ではdrawImageのみ実行 一定以上複雑な図形を描画する場合、Graphicsに対して多数の描画メソッドを呼び出すより、drawImage()一発の方がずっと早いためEDT上での1回のイベント処理時間が短縮できます。そのためUIイベントの発生からハンドラーが呼ばれるまでのタイムラグが常に短く保て、結果としてアプリケーションの反応がよくなるのですね。
補足
バッファ上に描いてからGraphicsに描く、ダブルバッファリングのようなものですか? 波形をそのままgに書き込まず、作成したBufferedImageに書き込んでみてそれをdrawImageで表示する。 この方法で試してみたところ、40トラックに23秒程度の音声を設置してみたときにgに直接書き込んだほうが速いように感じました。そんなことはないかもしれませんが、少なくとも変わってませんでした。BufferedImageのインスタンス化に時間がかかるんでしょうか。 さて、遅延は解決したのですが、まだ2つほど問題が・・。 (1)再生時にたまにプツプツとノイズが入る。 (2)再生時に小さいホワイトノイズのようなものが常に入っている。 (2)に関してはbufの足し合わせがうまくいってない可能性があります。 buf[0]~[4]に既にデータが入ったとして。 for (int i = 0; i < 2; i++) { bufInt[i] += (data[(x - wav.getX()) * 4 + i * 2]) + (data[((x - wav.getX()) * 4 + i * 2 + 1)] << 8) * getVolumeRatio(略) * getPanRatio(略); これでデータを足し合わせています。bufInt[0]がL、bufInt[1]がRです。 最後に buf[0] = (byte) (bufInt[0]); buf[1] = (byte) (bufInt[0] >> 8); buf[2] = (byte) (bufInt[1]); buf[3] = (byte) (bufInt[1] >> 8); これを行ってからline.write(buf, 0, 4)をしています。 どこか問題がありますか? ビットシフトを逆にすると通常の再生でノイズはなくなるのですが、getVolumeRatio等で1以外をかけると良く分からない値をとるのかノイズだけになりザーってなります。 これらの問題は、新しい質問として投稿したほうが良いですか?
- KSOH
- ベストアンサー率93% (29/31)
あまり長いものでなければコードを拝見するのはかまわないのですがKWが不明だったのでDLできませんでした >好きなところからいつでも再生したいので、 AudioInputStream.skip()が利用できます。ただしskip()が利用できるストリームとできないストリームがあることに注意が必要です。 FileInputStream fin = new FileInputStream("xxx.wav"); float sampleRate = 44100; // サンプリングレート int sampleSizeInBits = 16; // サンプルビット数 int channels = 2; // チャンネル数=2(ステレオ) boolean signed = true; // 符号付き整数を仮定 boolean bigEndian = false; // リトルエンディアンを仮定 AudioFormat af = new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian); AudioInputStream ain = new AudioInputStream(fin, af); のようにAudioInputStreamのコンストラクターにFileInputStreamを指定してやればain.skip()が使えたと思います。 >バッファに書き込まれているデータ量を取得する方法が分かりませんでした。 DataLine.available()とDataLine.getBufferSize()です。 int remainSize = line.getBufferSize() - line.available(); とすると、バッファに残っている再生待ちのバイト数がわかります。remainSizeがレイテンシー以上の量になっていればsleep(1)などとしてやればいいわけです。ひょっとしてAPI Documentを調べる際にSourceDataLineに直接定義されているメソッドしかご覧になっていないのではないでしょうか。基底クラスにあるメソッドも全て調べてみてください。 その他: 複数のWAVファイルの波形合成の間違いに気づきました。遅延とかとは関係ないと思いますが。サンプルビット数が16の場合振幅データは2バイトになるので合成する場合は各バイトを加算してはダメですね。エンディアンを意識しつつ16ビットの値として加算しないといけません。 もう一つ、複数の波形データを単純に加算すると場合によっては16ビットの範囲から飛び出た値になってしまう(いわゆるオーバーフロー・アンダーフローが発生する)ため結果として波形が歪みノイズ(音割れ)となって聞こえます。対処としては・・・ (1)なにもしない。音割れがたまにあっても気にしない。 (2)単純に合成する波形の数Nで振幅データを割ってオーバーフロー・アンダーフローを防ぐ 音割れは防げますが音量が全体的に小さくなってしまいます。 (3)コンプレッサー的加工をする これは音が歪みそうなときにあるアルゴリズムによってスムーズに波形データの振幅を小さくしてやるというものです。音声のコンプレッサーに関する方式を検索するとなにがしか情報が得られます。 (3)はかなり面倒そうです。せいぜい(2)にして音量が小さく感じたらサウンドカードのボリュームコントロールで調整するという妥協でいいような気がします。
お礼
原因が分かり解決もしました。 原因は"描画"でした。 シンプルにrepaint()のたびに波形を全て描画していたため処理が極端に重かったのです。 全て、というのは波形の長さの分、どれだけ縮小しようが描画をするということで、たった10秒の波形も表示のために44100 * 10 * 2回のdrawLineを呼んでいたのです。また、画面外だろうと書き込んでいました。 それを同じx座標でdrawLineをする回数を最高10回までに制限し、画面外での描画を避けたところ、遅延が全く気にならなくなりました。 複数のWAVファイルの波形合成については修正しました。 音割れについては、とりあえずはなにもしないでいこうと思います。 色々考えてくださってありがとうございました。 原因は案外単純なところで、見落としていたのに、すみませんでした。
補足
書き忘れていました。 音が再生したあとのストップ状態のときにプツッ プツッ などと永遠に繰り返される現象の原因は分からずじまいでしたが、ストップさせた時点でflush()させてバッファの中身をクリアすることで音はならなくなりました。
- KSOH
- ベストアンサー率93% (29/31)
>Array.fill(buf,0) すみません。クラス名と引数を間違えました!正しくは以下です。 Arrays.fill(buf, (byte)0); // java.util.Arraysのメソッド forループで初期化するのと同じことです。配列の中身を初期化したいとき便利というだけの話でした。 >EDT上で全ての処理をやらなきゃいけないって思ってたので... なるほどそうでしたか。ある程度以上CPUを占有してしまう処理はEDTでやるとUIの阻害要因になるのでむしろ積極的に別スレッドで行うべきと思います。 >遅延というのは、再生ボタンを押してから再生されるまで... 再生開始時にWAVファイルの数が多い程遅延が出るということならWAVファイルの読み込みに時間がかかっているのだと思えます。 今のプログラムですと一旦音声データを全てメモリーへ読み込んでいるのでWAVファイルのデータ量全体が多いとI/Oの時間やGCの時間がかかっているのではないでしょうか。もしそうならデータ全部を読み込むのではなくWAVファイルをAudioInputStreamとしてopenし、SourceDataLineへ書き込むのに必要なだけ随時readしてやれば再生開始時の遅延を軽減できると思います。 >ストップボタンを押してからストップするまで 停止時にもWAVファイルが多いほど遅延が大きくなるのでしょうか?WAVファイルの多さに従って遅延が大きくなる理由は思い当たりませんが、遅延がでること自体はレイテンシーにからむことのように思います。 今のプログラムについて考えるとSourceDataLineの内部バッファにある再生待ちのデータの量を気にせずにwriteしているため内部バッファは常に満杯に近い状態になっていると思います。再生停止ボタンを押した際にwriteを即座に止めたとしてもラインの内部バッファに残っているデータはサウンドカードへ送信されつづけるのでバッファ上のデータがなくなるまで音声は止まりません。デフォルトでは内部バッファのサイズは1秒程度あったかと思います。故に常に再生停止時に1秒程度の遅延が出ると思います。 対処はレイテンシーを決め(例えば100ミリなど)SourceDataLineの内部バッファにレイテンシー分以上のデータを書き込まないようにすればよいと思います。 >短い音が繰り返し無限になる現象 一般的に停止時にノイズが出るというのはサウンドカードにつきものの現象のようですがコンピューターの設定やサウンドカードの特徴などで状況が違うのかも知れません。ソフトウェア的に解決するなら停止ボタンを押したときSourceDataLine.stop()をする前に、ボリュームをゆるやかに絞ったり再生波形をゆるやかに0に絞るような加工をするといった方法が考えられますが自分がWAV再生のプログラムを試したときは停止時にwrite()を止めてdrain()し、その後にstop()しただけですがノイズは気になりませんでした・・・ もっとも継続的にノイズが出るというのはこのような原因ではなく何かSourceDataLineの制御処理自体に問題があると思えます。しかし残念ながら提示されたコードの断片だけではなんともいえません。
補足
http://www1.axfc.net/u/3329067 こちらにプログラムをアップロードしました。 あまり他人に見せるように作っていないので汚いですが、ご確認いただけると嬉しいです。 (Eclipseで開発) >>EDTについて 今後は描画系以外のものにおいては処理の重さを考えて別スレッドで設計をするということも考えるようにします。貴重な情報ありがとうございます。 >>WAVファイルをAudioInputStreamとしてopen... 好きなところからいつでも再生したいので、それだとスタートから最後まで順番に鳴らすことしかできないのでは? >>レイテンシーを決め(例えば100ミリなど)SourceDataLineの内部バッファにレイテンシー分以上のデータを書き込まないように なるほど・・。 メソッドを確認してみましたが、バッファに書き込まれているデータ量を取得する方法が分かりませんでした。 >>停止時のノイズ 停止時に起こる「プツッ」といったもの、これはあまり気にしていないので大丈夫です。 丁寧にありがとうございます。 問題としているのは継続的に続く音の方です。 プログラミング初心者で申し訳ないですが、もう少しお付き合い願います・・。
- KSOH
- ベストアンサー率93% (29/31)
1フレーム処理する度にrepaint()していますね。質問者さんは「遅延」とおっしゃっていますが、 この処理だと遅延というより音飛びしそうな気がします。 音声出力のようなリアルタイム性が求められるような処理は以下のように考えるとよいと思います。 ・音声出力処理 音声出力処理はSourceDataLineへの出力に専念し、それに関係ない処理 (特にスレッドをブロックするようなメソッドとか時間がかかる処理)は やらないようにします。 こういった関係ない処理をしてしまうと音切れなどの原因になります。 最初の回答で1バイトあたり数マイクロ秒のレートでいいので処理能力的に余裕と 書きましたがそれは専用スレッドで他の関係ない処理をはさまない場合の話です。 1フレーム毎にrepaintするとさすがに処理が間に合わないと思います。 万一出力処理をEDT(イベントディスパッチスレッド)でやっておられるなら、 専用スレッドを起こしてそこで出力処理をするようにすべきと思います。 ・画面更新処理 再生状況を画面上に描画したい場合は、EDTで一定時間ごとに現在の再生位置を 調べて描画します。現在の位置はSourceDataLine.getMicrosecondPosition()なりで 求められます。EDT上で一定時間ごとに何かする際には例えばjavax.swing.Timerなどを 使います。 今のプログラムでは1フレーム毎に画面更新してますがフレームレートが44.1Kzだとすれば 1秒間に44100回画面の再描画を要求することになります。一般のディスプレー装置の リフレッシュレートはたかだか100Hz程度ですからこの更新レートは頻繁過ぎると思います。 10~100ミリ秒おき程度の画面更新で充分だと思います。 ・その他 プログラムでは1フレーム毎にiteratorやバイト配列のnewをしています。 してもかまわないとは思いますがVMの処理を少しでも軽くするならオブジェクトを 生成しないですむ代替手段を使うように意識するのも悪くないと思います。 なにせ1秒間に数万回オーダーで動く処理なので一回あたり十数バイトの メモリー量だとしても1秒間に数メガバイトオーダーの消費になってしまいますので。 例えばバイト配列については使う度にnewするのではなく、メンバー変数にでもして、 Array.fill(buf, 0) などとして0クリアしてから使うとよいと思います。
補足
先ほど貼ったプログラムをメインループを呼称。 byte配列は、毎回for文で0で満たすようにしました(Array.fill(buf,0)はなんのことか分かりませんでした) >>万一出力処理をEDT(イベントディスパッチスレッド)でやっておられるなら、 専用スレッドを起こしてそこで出力処理をするようにすべきと思います。 Swingを使っているので、EDT上で全ての処理をやらなきゃいけないって思ってたのでSwingWorker上でメインループを処理させていましたが、よくよく考えれば、描画系(repaint()は除く)以外ならばEDT上ではなくても問題なかったですね。メインループはThreadのrun()内で、repaint()はTimerで行うようにしました。するとぐんと処理が早くなりました、ありがとうございます。 遅延というのは、再生ボタンを押してから再生されるまで、ストップボタンを押してからストップするまでのことをさしていました。分かりづらくてすみません。 読み込むWavファイルが多くなると結局その遅延は顕著になってきますが、どうしようもないんでしょうか。 またもう一点さらに追加で申し訳ないのですが、再生をストップしたあとに「プツッ ・・・ プツッ ・・・ プツッ」など決まった短い音が繰り返し無限になる現象が起きてしまいます。何が原因なのでしょう・・ お願いします。
- KSOH
- ベストアンサー率93% (29/31)
もしSourceDataLineへ44.1Khz ステレオ 16bit 程度の音声データの再生をするのであれば、バイトあたり数マイクロ秒程度の処理レートが確保できれば充分であり今時のコンピューターならかなり余裕があると思います。自分でもやってみたことがありますが特に問題なく再生できました。 それはともかく、質問文にあるコードにひとつ問題があるように見えます。 line.write(buf, x, 4); x++; 4バイト出力しているのにオフセットを1バイト分しかずらしていないように見えます。正しくは line.write(buf, x, 4); x += 4; だと思います。もし実際にこのようなコーディングになっているなら遅延というより音程が低い歪んだ音が聞こえると思います。
補足
うーん・・なんででしょうか。 見てくださるならソースプログラムも提示します。 すみません、簡略化しすぎて逆に間違えてました。 ワンループをコピペしますね。 while (true) { if (play) { Iterator<Wav> it = wavList.iterator(); byte[] buf = new byte[4]; while (it.hasNext()) { Wav wav = it.next(); if (x - wav.x < 0 || (x - wav.x) * 4 + 4 >= wav.getData().length) continue; for (int i = 0; i < 4; i++) { buf[i] += wav.getData()[(x - wav.x) * 4 + i]; } } line.write(buf, 0, 4); x += 1; if (playmode == FOLLOW) { offsetX += (x - 1)/getWidthZoomingRatio() - x/getWidthZoomingRatio(); } } else { //line.drain(); } repaint(); } Wavオブジェクトは、オーディオファイルのデータ(data)と位置(x)を保持したものです。 xは時間を表します。 同じ時間で複数のWavオブジェクトが重なっていた場合、2回書き込むと処理が重たくなるので、2つの音声データを加算してそれをSourceDataLineに書き込んでいます。 メインループはこんな感じで組んであるのですが、遅延が起きるのはなぜなんでしょうか・・
お礼
なるほど、やはりそういう仕様になっているんですね。 わかりました。また解りやすくしておきます。 丁寧にお返事してくださりとても助かってます、ありがとうございます!