Java で RIFF コンテナーを解析するためのコードをいくつか書いています (教育目的で、Java.wav
または他の RIFF ファイルをロードするライブラリがたくさんあることを知っています)。私のデザインの目標は、可能な限りフォーマットに固執することです。
ウィキペディアによると、基本的な RIFF 形式は非常に単純です。
- 4 バイト: このチャンクの ASCII 識別子 (例は "fmt " と "data" です。"fmt " 内のスペースに注意してください)。
- 4 バイト: このチャンクの長さを持つ符号なしのリトル エンディアン 32 ビット整数 (このフィールド自体とチャンク識別子を除く)。
- 可変サイズ フィールド: 前のフィールドで指定されたサイズのチャンク データ自体。
- チャンクの長さが偶数でない場合は、パッド バイト。
現在、可変サイズのフィールドの表現に苦労しています。これまでのところ、私が思いついた最高のものはそのように見えます。BaseFormat
1 つ目は、すべての形式解析クラスのベースとなる抽象クラスです。
abstract class BaseFormat {
BaseFormat(MyStream stream) {
this.stream = stream;
}
MyStream stream;
public MyStream stream() { return stream; }
abstract public void read() throws IOException;
}
ストリームへの参照を格納するだけで、実行read
されるメソッドとのインターフェースとして機能します。次に、2 つの主要な形式クラスがあります。
public class Riff extends BaseFormat {
public Riff(MyStream stream) {
super(stream);
}
@Override
public void read() throws IOException {
chunks = new ArrayList<Chunk>();
while (!stream.eof()) {
Chunk c = new Chunk(stream);
c.read();
chunks.add(c);
}
}
private ArrayList<Chunk> chunks;
public ArrayList<Chunk> chunks() { return chunks; }
}
public class Chunk extends BaseFormat {
Chunk(MyStream stream) {
super(stream);
}
public void read() throws IOException {
tag = stream.getAsciiString(4);
len = stream.getInt();
MyStream substream = stream.getBytesAsSubstream(len);
switch (tag) {
case "FMT1":
Fmt1Body b1 = new Fmt1Body(stream);
b1.read();
body = b1;
break;
case "FMT2":
Fmt2Body b2 = new Fmt2Body(stream);
b2.read();
body = b2;
break;
// etc, etc, many various subformats
}
if (len % 2 == 1)
stream.skipBytes(1);
}
private String tag;
public String tag() { return tag; }
private int len;
public int len() { return len; }
private Object body;
public Object body() { return body; }
}
そこには派手なことは何もありません。簡潔にするために、クラスを投稿していません。これは、、、、などの便利なユーティリティ関数を備えMyStream
た非常に単純なラッパーです。InputStream
getInt
getByte
getAsciiString
RIFF 形式のすべての部分は、独自のクラスを取得します。RIFF ファイル全体はRiff
であり、チャンクをクラスとして読み取ろうとし、Chunk
チャンク ヘッダー + ボディを読み取ろうとします。そして魔法が起こります。大きなswitch
内部Chunk.read()
が、チャンク内のどのフォーマット クラスを適用するかを選択します。つまりFmt1Body
、Fmt2Body
などです。
それは機能し、実際に RIFF ファイルを読み取ることができます。私が抱えている問題は、すべての異なるチャンク形式が、共通点のない異なるクラスによって表されることです。したがって、メモリ内で解析されたファイルを操作する場合、多くの型チェックとアップキャストを行うことになります。つまり、次のようなものです。
for (Chunk chunk : riff.chunks()) {
Object cbody = chunk.body();
if (cbody instanceof Fmt1Body) {
Fmt1Body body = (Fmt1Body) cbody;
// do something with FMT1 chunk body using `body`, i.e.
System.out.println("title = " + body.getTitle());
} else if (cbody instanceof Fmt2Body) {
Fmt2Body body = (Fmt2Body) cbody;
// do something with FMT2 chunk body using `body`, i.e.
System.out.println(body.getWidth() + "x" + body.getHeight());
}
}
などなど。これは多くの関数型言語では正常ですが、Java では悪い方法だと思います。Object
+型チェック+アップキャスト(または、BaseFormat
とにかくキャストが必要になるような同等に役に立たないインターフェース)に頼らずに、メモリ内のRIFFコンテナを表現するより良い方法はありますか?
サブフォーマット クラスのソース
stream
サブフォーマット クラスに関心のある方のために、メソッドと havingread()
メソッドを使用する以外に共通点がないことを示すサンプルをいくつか示します。
public class Fmt1Body extends BaseFormat {
Fmt1Body(MyStream stream) {
super(stream);
}
@Override
public void read() throws IOException {
title = stream.getAsciiString(64);
artist = stream.getAsciiString(64);
album = stream.getAsciiString(64);
}
private String title;
private String artist;
private String album;
public String getTitle() { return title; }
public String getArtist() { return artist; }
public String getAlbum() { return album; }
}
public class Fmt2Body extends BaseFormat {
Fmt2Body(MyStream stream) {
super(stream);
}
@Override
public void read() throws IOException {
width = stream.getInt();
height = stream.getInt();
bitmap = stream.getBytes(width * height);
}
private int width;
private int height;
private byte[] bitmap;
public int getWidth() { return width; }
public int getHeight() { return height; }
public byte[] getBitmap() { return bitmap; }
}