2007年4月25日水曜日

JavaでWAVファイルを再生する(2)

前回のJavaでWAVファイルを再生するでは、あまりにもとりあえず過ぎたので、ちょっとはましなコードにしてみました。今回は、再生を一時停止しても読み込んだデータをスキップせずにちゃんと出力するようにしました。なお、LineListenerは今回ははずしました。必要があれば、前回のソースを参照してください。

public class RenderAudioFile implements Runnable {
protected AudioInputStream fileStream = null;
protected SourceDataLine sourceDataLine = null;
private byte sampleBuffer[]; //データ受け渡し用のバッファ
private ByteBuffer sampleByteBuffer; //バッファ参照用のByteBuffer

public void openFile(File file) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
 //出力するファイルを受け取るメソッド
 fileStream = AudioSystem.getAudioInputStream(file);
 AudioFormat format = fileStream.getFormat();
 sourceDataLine = AudioSystem.getSourceDataLine(format);
 sourceDataLine.addLineListener(this);
 sourceDataLine.open(format);
 sampleBuffer = new byte[sourceDataLine.getBufferSize()];
 sampleByteBuffer.wrap(sampleBuffer);
}
public void close() {
 //ファイルを閉じるメソッド
 fileStream.close();
 fileStream = null;
}
public void play() {
 // 再生開始、再開用メソッド
 if(sourceDataLine == null || fileStream == null) {
  return;
 }
 sourceDataLine.start();
 new Thread(this).start();
}
public void pause() {
 // 一時停止メソッド
 if(sourceDataLine == null || fileStream == null) {
  return;
 }
 sourceDataLine.stop();
}
public void run() {
 //再生スレッドのメソッド
 do {
  if (sampleByteBuffer.position() == 0) {
   try {
    int readSize = inputStream.read(sampleBuffer);
    if (readSize <= 0) {       sourceDataLine.stop();       break;      }      sampleByteBuffer.position(readSize);     } catch (IOException e) {      sourceDataLine.stop();      break;     }    }    soundByteBuffer.flip();    int writeSize = dataLine.write(sampleBuffer, 0, sampleByteBuffer.remaining());    if (writeSize >0) {
   putSampleData(sampleBuffer, writeSize);
   sampleByteBuffer.position(writeSize);
  }
  sampleByteBuffer.compact();
 } while (sourceDataLine.isRunning());
}
}

とりあえず、普通に再生するには問題ないのですが、まだ、手を抜いてあるところがあります。

  • ファイルをすべて再生しきったときの処理
  • 再生位置の変更

いずれも、自由に再生を開始する位置を変更できればよいのですが、これらを実現するために使えそうなメソッドとして、AudioInputStream.mark()とAudioInputStream.reset()というのがあります。これは、mark()で再生位置を保存しておいて、reset()でその位置に移動するというものです。 ただし、これらのメソッドはInputStreamによって、使えたり使えなかったりしますが、それを調べるためにAudioInputStream.markSupported()というメソッドがあります。再生しようとするファイルや環境に依存するかもしれませんが作ったプログラムでは、markSupported()はfalseを返してきます。すなわち、mark(),reset()が使えないということです。

とすると、どうすればいいかというと、再生位置を先頭に戻すには、一度close()してから再度open()するという処理しかなさそうです。さらに任意の位置に移動させるには、skip()を使うという方法しかないようです。

2007年4月18日水曜日

EclipseでJNIをデバッグする

前回、EclipseでJNIの開発について書いたが、その後デバッグをするにはどうするのかというのをゴリゴリとやっていたが、例のごとくはまってしまってので、忘れないように書いておくことにする。 まずは、JNIモジュールのデバッグは次のような基本手順を踏むということする。

  1. JNIのソースにブレイクポイントを設定
  2. 呼び出すJavaソースにブレイクポイントを設定
  3. Javaをデバッグ実行
  4. JNIモジュールをデバッガでアタッチ

まずはJavaのブレイクポイントは、System.LoadLibrary();を呼び出した後でかつJNIのブレイクポイントが呼び出される前に設定する。Javaのデバッグ実行でこの設定したブレイクポイントで止まった状態で、JNIモジュールをアタッチする。

JNIモジュールのデバッグ設定は、Eclipseの「構成およびデバッグ」のダイアログで、「ローカルアプリケーションへの接続」という構成で設定できる。このとき、アタッチの対象となるモジュール(ここではWin32なのでDLL)を「メイン」のC/C++アプリケーションというところに書くのだが、ここにはビルドしたモジュールを設定する(デフォルトだと「デバッグ/<DLLのファイル名>」)。

あと、「デバッガー」の項目のところは、「gdb/mi」を選択して、gdbのコマンドのところは、実行できる適切なパスが設定されていればよい。あと、このときに「詳細コンソールモード」というチェックをONにしておくと、デバッガーが何をしているのかわかるので最初のうちはONにしておいたほうがよいだろう。

この設定で指定したモジュール(DLL)がJavaから呼び出されないとアタッチできないので、Javaのデバッグ構成の管理で、引数のところでプログラムの引数かVMの引数のところで、「-Djava.library.path=<デバッグするモジュールのパス>」を設定をする。

これでEclipseからの操作でデバッグができる。DLLのデバッグで、gdbのアタッチ後に中断状態になるが、そのまま再開すればモジュールは実行される。

あとは、MinGWの固有だと思われるが、gdbのバージョンが古いとEclipseから投げるMIのコマンドの不一致か、ブレイクポイントがうまく設定されないようだった。そのため、MinGWからgdb6.xをダウンロードしてインストールすればよいようだ。

もうひとつ、これはうちの固有の問題かもしれないが、gdbでデバッグしようとすると「LdrAccessResource」のところでセグメンテイションエラーが発生して、gdbが落ちてしまう。これについては、いろいろ調べているものの解決方法がわからず、現状デバッグ不能である。どうやら、ntdllとgdbの相性の問題のようで詳細はわかりませんでした。

2007年4月12日木曜日

EclipseでJNIモジュールの開発をする

プログラミングTIPSではないのですが、EclipseでJNIのモジュールを開発を試みたときにプロジェクトをどう設定すればよいかという話です。要するにちょっと設定にはまったので、忘れないようにメモっておこうということです。ただし、以下の方法はベストな方法とは思ってません。とりあえず、こうやったらできましたっていう程度のものだと思ってください。ちなみにですが、Win32でEclipseとMinGWを使って開発するというお話しです(他のプラットホームでは違うところがあるかもしれません)。

蛇足かもしれませんが、JNIの開発そのものは、JDKのドキュメント(Java5のJDKのリンクですが)に載っていて、MinGWでJNIのモジュールをコンパイルするにはどうするかというのは、MinGWのFAQに書いてあります。

まずは、JNIのモジュールを開発するには、nativeの宣言を含むJavaのソースをまず書いてコンパイルする。classファイルからjavahでC(++)のヘッダーを出力して、C(++)のコードを書いてコンパイルする。出来上がったモジュールをJavaから実行させる、という手順であると思っていることが前提となります。

何が話題かというと、JavaのモジュールはJavaのプロジェクトで開発して、JNIのモジュールはCDTを使って、C(++)のプロジェクトで開発するのですが、javahで作られるヘッダーをどうやって、C(++)のプロジェクトへもってくるか、できたモジュールをどこにコピーして、実行するかということです。

C(++)のプロジェクトのほうがビルドの設定の自由度が高いと踏んで、C(++)のプロジェクト側でがんばるということにします。依存関係としては、C(++)のプロジェクトがJavaのプロジェクトを参照しているというふうにします(お互いに参照していてもよいとは思います)。

C(++)のプロジェクトの設定で「C/C++ビルド」という項目の中に、ビルドステップというのあって、ここにビルドの前後に何かコマンドが実行できるので、これを利用することにします。 まずは、javahをビルド前に実行するためにビルド前のステップのところに以下のように入力します。

<JDKのフルパス>\bin\javah -jni -d .. -classpath ../../<Javaのプロジェクト名> <パッケージ名>.<クラス名>

※もし、「\」がバックスラッシュに見えたら、「¥」の半角だと思ってください このとき、 「<パッケージ名>.<クラス名>」は、複数記述することができます。

これで、JavaのプロジェクトからJNIのモジュールを読み込んで実行できるわけです(もちろん、何かバグってたらだめですが…)。デバッグや実行もEclipseからやりたいので、「構成および実行」の設定をして、Javaを実行させればよいのですが、JNIのモジュールがjava.library.pathにないと実行できません。 そのときにどうするかというと、JNIモジュールを読み込むjavahを実行したJavaプロジェクトとは別に、これらを使うJavaプロジェクトを作成します。 この新しいほうのプロジェクトのビルドの構成の「プロジェクト(P)」でJNIのJavaプロジェクトを参照するように設定すれば、実行できます。この設定に「ネイティブ・ライブラリーのロケーション」というサブツリーがあると思いますが、ここでJNIのネイティブモジュール(Win32だとDLL)のパスを設定すればよいです。

リリースしたときにjava.library.pathの設定をどうするのかというところは気になるところですが、VMの実行時にパラメタで渡さなくても、いわゆるpath変数で見つけられるところにモジュールがあれば実行できるようです。

2007年4月10日火曜日

JavaでWAVファイルを再生する

javax.sound.sampledというパッケージを使って、とりあえずWAVファイル(AIFFなども可能)をサウンドデバイスに出力する。とりあえずということで、前提と条件は以下の通り

  • デフォルトのデバイスに出力する
  • 制御は、開始、一時停止、再開だけ

とりあえず、いきなりソースコード。

public class RenderAudioFile implements LineListener, Runnable {
 protected AudioInputStream fileStream = null;
 protected SourceDataLine sourceDataLine = null;
 private byte sampleBuffer[];//データ受け渡し用のバッファ

 public void openFile(File file) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
  //出力するファイルを受け取るメソッド
  fileStream = AudioSystem.getAudioInputStream(file);
  AudioFormat format = fileStream.getFormat();
  sourceDataLine = AudioSystem.getSourceDataLine(format);
  sourceDataLine.addLineListener(this);
  sourceDataLine.open(format);
  sampleBuffer = new byte[sourceDataLine.getBufferSize()];
 }
 public void play() {
  // 再生開始、再開用メソッド
  if(sourceDataLine == null) {
   return;
  }
  sourceDataLine.start();
  new Thread(this).start();
 }
 public void pause() {
  // 一時停止メソッド
  if(sourceDataLine == null) {
   return;
  }
  sourceDataLine.stop();
 }
 public void update(LineEvent ev) {
  // LineEvent用ハンドラ UI制御用に使うとよい
  LineEvent.Type type = ev.getType();
  if( type == LineEvent.Type.OPEN) {
  }
  else if( type == LineEvent.Type.START) {
  }
  else if( type == LineEvent.Type.STOP) {
  }
 }
 public void run() {
  //再生用のスレッド用メソッド
  int readSize = 0;
  do {
   try {
    readSize = fileStream.read(sampleBuffer);
   } catch (IOException e) {
    sourceDataLine.stop();
    break;
   }
   if (readSize < 0) {
    sourceDataLine.stop();
    break;
   }
   }
   sourceDataLine.write(sampleBuffer, 0, readSize);
  } while (sourceDataLine.isRunning());
 }
}

とりあえず、こんな感じです。大したコードでなくて申し訳ないですが、いくつかポイントがあるので、説明させていただきますと。

public void update(LineEvent ev);ですが、いらないときはメソッドごと消してもらって、implementsからLineListenerを消して、、sourceDataLine.addLineListener(this);も消してください。あと、このイベントハンドラーで「START」がくるタイミングですが、sourceDataLine.start();を呼び出した時には呼ばれずに、sourceDataLine.write();でデーターを書き込んだときに呼ばれるようです。UIの制御にこれを使ったほうがよいかというと、startやstopの処理が完了するとこれらのイベントが呼ばれてくるので、連打されたりしたときにおかしなことにならないようにこのタイミングで制御したほうがよいかと思われるからです。

あと、とりあえずで手を抜いてしまってあるところがあるので、白状しておきますが。sourceDataLine.write();はstopが呼ばれると書き込みのブロッキングを解除して復帰してくるのですが、このときの戻り値に書き込みされたデーターのサイズとなります。この戻り値が渡したバッファーのサイズより小さいときには、その差分のデーターは書き込みされなかったわけで、本当なら再開されたときにその分のデーターを書き込んでから、ファイルから読み込むという処理をしないと、そのデーターはスキップされて再生されるということになります。

これを回避するには、この戻り値をメンバー変数にとっておいて、再度スレッドが呼ばれたときにファイルから読み込まず、残りのデーターを渡すという処理を入れればよいでしょう。

2007年4月8日日曜日

Java3Dをやってみる

Java3Dは、SunからJREと同じように配布されているライブラリであるが、JREやJDKには含まれてはいない。どうやら、大きすぎていっしょにいれなかったようだ。 まずプログラムするには、Java 3D APIへいって、APIをダウンロードしてインストールするわけだが、JDKのバージョンもあわしておく必要があるようなので、ついでに最新のJDKを入れておくのがよいかもしれない。APIドキュメントもダウンロードできるが、オンラインでも見れるので、お好みで入れればいいかも。

APIドキュメントだけでは、途方にくれるだけなのでjava.netのJava3DのWikiにあるJava3DBooksにチュートリアル(英語)があるのでそれを読むか、日本語の書籍も出ているようなのでそれを読めばよいのだが…。

とりあえず、何か動かしたいのであれば、チュートリアルのChapter1に載っているソース(HelloJava3D)を打つか、Java3DにおいてあるExampleを動かしてみるとかでよいかもしれない。

とりあえず、おそらくこの段階でわかっておくことは、Java3Dのすべてのオブジェクトはツリー構造で定義される。3次元オブジェクトをツリー構造で記述することは、普通なことなのであえて説明は不要であるとおもうが。 ツリー構造の根っこにあるのが、VirtualUniverseというものであるのだが、そこに3次元のオブジェクト(ドキュメントではSceneGraphといってる)やそれらを表示するためのCanvas3Dも含まれている。

3次元で表示するためには視線(投射法の概念も含むのでカメラというようだが)を定義するためのViewやViewPlatformなどが必要なのだが、VirtualUniverseやViewとかをひっくるめてSimpleUniverseがやってくれるようだ。 すなわち、表示するためのCanvas3Dを作って、それを渡してSimpleUniverseを作成する。あとは、3次元オブジェクトの類をSimpleUniverseに渡すとCanvas3Dに表示される(もちろん、視線に入るものだけだが)ということである。

オブジェクトの定義は、ドキュメントのHelloJava3DだとColorCubeというものを表示しているが、com.sun.j3d.utils.geometryにあるConeやCylinder、Sphereなどを使えば、とりあえず3Dっぽいものは表示できる。今回はとりあえずなので、Geometryについてはいずれまた…。