mirror of
https://github.com/collinsmith/riiablo.git
synced 2025-07-08 14:57:30 +07:00
Created BIK coded along with BinkAudio and BinkVideo (see #59)
This commit is contained in:
172
core/src/com/riiablo/video/BIK.java
Normal file
172
core/src/com/riiablo/video/BIK.java
Normal file
@ -0,0 +1,172 @@
|
||||
package com.riiablo.video;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import com.riiablo.io.BitUtils;
|
||||
import com.riiablo.io.ByteInput;
|
||||
import com.riiablo.io.InvalidFormat;
|
||||
import com.riiablo.logger.LogManager;
|
||||
import com.riiablo.logger.Logger;
|
||||
import com.riiablo.logger.MDC;
|
||||
|
||||
public class BIK {
|
||||
private static final Logger log = LogManager.getLogger(BIK.class);
|
||||
|
||||
static final int EXTRADATA_SIZE = 1;
|
||||
static final int MAX_WIDTH = 7680;
|
||||
static final int MAX_HEIGHT = 4800;
|
||||
static final int MAX_TRACKS = 256;
|
||||
static final int SMUSH_BLOCK_SIZE = 512;
|
||||
|
||||
private static final byte[] SIGNATURE = new byte[] { 0x42, 0x49, 0x4B };
|
||||
|
||||
private static final int FLAG_VIDEO_ALPHA = 0x00100000;
|
||||
private static final int FLAG_VIDEO_GRAYSCALE = 0x00020000;
|
||||
|
||||
final ByteBuf buffer;
|
||||
final short version;
|
||||
final int size;
|
||||
final int numFrames;
|
||||
final int largestFrame;
|
||||
final int width;
|
||||
final int height;
|
||||
final float fps;
|
||||
final int flags;
|
||||
final int numTracks;
|
||||
final BinkAudio[] tracks;
|
||||
final int[] offsets;
|
||||
final BinkVideo video;
|
||||
|
||||
public static BIK loadFromByteBuf(ByteBuf buffer) {
|
||||
return new BIK(buffer);
|
||||
}
|
||||
|
||||
BIK(ByteBuf buffer) {
|
||||
log.trace("Reading bik...");
|
||||
this.buffer = buffer;
|
||||
|
||||
ByteInput in = ByteInput.wrap(buffer);
|
||||
|
||||
log.trace("Validating bik signature");
|
||||
in.readSignature(SIGNATURE);
|
||||
|
||||
version = in.read8u();
|
||||
log.tracef("version: %c (0x%1$02x)", version);
|
||||
if (version != 0x69) {
|
||||
throw new InvalidFormat(in, "version(" + String.format("0x%02x", version) + ") is not supported!");
|
||||
}
|
||||
|
||||
size = in.readSafe32u() + 8; // include {SIGNATURE, version, size}
|
||||
log.trace("size: {} ({} bytes)", FileUtils.byteCountToDisplaySize(size), size);
|
||||
|
||||
numFrames = in.readSafe32u();
|
||||
log.trace("numFrames: {}", numFrames);
|
||||
|
||||
largestFrame = in.readSafe32u();
|
||||
log.trace("largestFrame: {} bytes", largestFrame);
|
||||
|
||||
in.skipBytes(4); // skip duplicate of numFrames
|
||||
|
||||
width = in.readSafe32u();
|
||||
log.trace("width: {}", width);
|
||||
if (width < 1 || width > MAX_WIDTH) {
|
||||
throw new InvalidFormat(in, "width(" + width + ") not in [" + 1 + ".." + MAX_WIDTH + "]");
|
||||
}
|
||||
|
||||
height = in.readSafe32u();
|
||||
log.trace("height: {}", height);
|
||||
if (height < 1 || height > MAX_HEIGHT) {
|
||||
throw new InvalidFormat(in, "height(" + height + ") not in [" + 1 + ".." + MAX_HEIGHT + "]");
|
||||
}
|
||||
|
||||
final int fpsDividend = in.readSafe32u();
|
||||
log.trace("fpsDividend: {}", fpsDividend);
|
||||
if (fpsDividend <= 0) {
|
||||
throw new InvalidFormat(in, "fpsDividend(" + fpsDividend + ") <= 0");
|
||||
}
|
||||
|
||||
final int fpsDivisor = in.readSafe32u();
|
||||
log.trace("fpsDivisor: {}", fpsDivisor);
|
||||
if (fpsDivisor <= 0) {
|
||||
throw new InvalidFormat(in, "fpsDivisor(" + fpsDivisor + ") <= 0");
|
||||
}
|
||||
|
||||
fps = (float) fpsDividend / fpsDivisor;
|
||||
log.trace("fps: {} fps", fps);
|
||||
|
||||
flags = in.read32();
|
||||
if (log.traceEnabled()) log.tracef("flags: 0x%08x (%s)", flags, getFlagsString());
|
||||
|
||||
numTracks = in.readSafe32u();
|
||||
log.trace("numTracks: {}", numTracks);
|
||||
if (numTracks < 0 || numTracks > MAX_TRACKS) {
|
||||
throw new InvalidFormat(in, "numTracks(" + numTracks + ") not in [" + 0 + ".." + MAX_TRACKS + "]");
|
||||
}
|
||||
|
||||
tracks = new BinkAudio[numTracks];
|
||||
for (int i = 0, s = numTracks; i < s; i++) {
|
||||
try {
|
||||
MDC.put("track", i);
|
||||
tracks[i] = new BinkAudio(in);
|
||||
} finally {
|
||||
MDC.remove("track");
|
||||
}
|
||||
}
|
||||
|
||||
video = new BinkVideo();
|
||||
|
||||
offsets = BitUtils.readSafe32u(in, numFrames + 1);
|
||||
if (log.traceEnabled()) {
|
||||
StringBuilder builder = new StringBuilder(256);
|
||||
builder.append('[');
|
||||
for (int offset : offsets) {
|
||||
builder.append(Integer.toHexString(offset)).append(',');
|
||||
}
|
||||
builder.setCharAt(builder.length() - 1, ']');
|
||||
log.trace("offsets: {}", builder);
|
||||
}
|
||||
}
|
||||
|
||||
public String getFlagsString() {
|
||||
if (flags == 0) return "0";
|
||||
StringBuilder builder = new StringBuilder(64);
|
||||
if ((flags & FLAG_VIDEO_ALPHA) == FLAG_VIDEO_ALPHA) builder.append("ALPHA|");
|
||||
if ((flags & FLAG_VIDEO_GRAYSCALE) == FLAG_VIDEO_GRAYSCALE) builder.append("GRAYSCALE|");
|
||||
if (builder.length() > 0) builder.setLength(builder.length() - 1);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
BinkAudio track(int track) {
|
||||
return tracks[track];
|
||||
}
|
||||
|
||||
int numTracks() {
|
||||
return numTracks;
|
||||
}
|
||||
|
||||
void decode(int frame) {
|
||||
final int offset = offsets[frame];
|
||||
log.tracef("offset: +%x", offset);
|
||||
final ByteBuf slice = buffer.slice(offset, offsets[frame + 1] - offset);
|
||||
final ByteInput in = ByteInput.wrap(slice);
|
||||
|
||||
for (int i = 0, s = numTracks; i < s; i++) {
|
||||
try {
|
||||
MDC.put("track", i);
|
||||
final int packetSize = in.readSafe32u();
|
||||
log.trace("packetSize: {} bytes", packetSize);
|
||||
|
||||
final ByteInput audioPacket = in.readSlice(packetSize);
|
||||
final int numSamples = audioPacket.readSafe32u();
|
||||
log.trace("numSamples: {}", numSamples);
|
||||
|
||||
log.trace("bytesRemaining: {} bytes", audioPacket.bytesRemaining());
|
||||
} finally {
|
||||
MDC.remove("track");
|
||||
}
|
||||
}
|
||||
|
||||
log.trace("videoPacket.bytesRemaining: {} bytes", in.bytesRemaining());
|
||||
}
|
||||
}
|
86
core/src/com/riiablo/video/BinkAudio.java
Normal file
86
core/src/com/riiablo/video/BinkAudio.java
Normal file
@ -0,0 +1,86 @@
|
||||
package com.riiablo.video;
|
||||
|
||||
import com.riiablo.io.ByteInput;
|
||||
import com.riiablo.io.InvalidFormat;
|
||||
import com.riiablo.logger.LogManager;
|
||||
import com.riiablo.logger.Logger;
|
||||
|
||||
public class BinkAudio {
|
||||
private static final Logger log = LogManager.getLogger(BinkAudio.class);
|
||||
|
||||
static final int SIZE = 0x0C;
|
||||
static final int MAX_CHANNELS = 2;
|
||||
|
||||
static final int FLAG_AUDIO_16BITS = 0x4000;
|
||||
static final int FLAG_AUDIO_STEREO = 0x2000;
|
||||
static final int FLAG_AUDIO_DCT = 0x1000;
|
||||
|
||||
private static final int[] BANDS = {
|
||||
100, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720, 2000,
|
||||
2320, 2700, 3150, 3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500,
|
||||
24500
|
||||
};
|
||||
|
||||
private static final int[] RLE = {
|
||||
2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 32, 64
|
||||
};
|
||||
|
||||
final short numChannels;
|
||||
final int sampleRate;
|
||||
final int flags;
|
||||
final int id;
|
||||
|
||||
final int frameLen;
|
||||
final int overlapLen;
|
||||
final int blockSize;
|
||||
final int halfSampleRate;
|
||||
|
||||
BinkAudio(ByteInput in) {
|
||||
log.trace("slicing {} bytes", SIZE);
|
||||
in = in.readSlice(SIZE);
|
||||
|
||||
in.skipBytes(2); // unknown -- I've seen { 0x00, 0x1C }
|
||||
|
||||
numChannels = in.readSafe16u();
|
||||
log.trace("numChannels: {}", numChannels);
|
||||
if (numChannels < 1 || numChannels > MAX_CHANNELS) {
|
||||
throw new InvalidFormat(in, "numChannels(" + numChannels + ") not in [" + 1 + ".." + MAX_CHANNELS + "]");
|
||||
}
|
||||
|
||||
sampleRate = in.read16u();
|
||||
log.trace("sampleRate: {} Hz", sampleRate);
|
||||
|
||||
flags = in.read16u();
|
||||
log.tracef("flags: 0x%04x (%s)", flags, getFlagsString());
|
||||
|
||||
id = in.readSafe32u();
|
||||
log.trace("id: {}", id);
|
||||
|
||||
final int frameLenBits;
|
||||
if (sampleRate < 22050) {
|
||||
frameLenBits = 9;
|
||||
} else if (sampleRate < 44100) {
|
||||
frameLenBits = 10;
|
||||
} else {
|
||||
frameLenBits = 11;
|
||||
}
|
||||
|
||||
frameLen = 1 << frameLenBits;
|
||||
overlapLen = frameLen >> 4;
|
||||
blockSize = (frameLen - overlapLen) * numChannels;
|
||||
halfSampleRate = (sampleRate + 1) / 2;
|
||||
}
|
||||
|
||||
public String getFlagsString() {
|
||||
if (flags == 0) return "0";
|
||||
StringBuilder builder = new StringBuilder(64);
|
||||
if ((flags & FLAG_AUDIO_16BITS) == FLAG_AUDIO_16BITS) builder.append("16BITS|");
|
||||
if ((flags & FLAG_AUDIO_STEREO) == FLAG_AUDIO_STEREO) builder.append("STEREO|");
|
||||
if (builder.length() > 0) builder.setLength(builder.length() - 1);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public boolean isMono() {
|
||||
return (flags & FLAG_AUDIO_STEREO) == 0;
|
||||
}
|
||||
}
|
7
core/src/com/riiablo/video/BinkVideo.java
Normal file
7
core/src/com/riiablo/video/BinkVideo.java
Normal file
@ -0,0 +1,7 @@
|
||||
package com.riiablo.video;
|
||||
|
||||
public class BinkVideo {
|
||||
|
||||
BinkVideo() {}
|
||||
|
||||
}
|
49
core/test/com/riiablo/video/BIKTest.java
Normal file
49
core/test/com/riiablo/video/BIKTest.java
Normal file
@ -0,0 +1,49 @@
|
||||
package com.riiablo.video;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.audio.AudioDevice;
|
||||
import com.badlogic.gdx.files.FileHandle;
|
||||
|
||||
import com.riiablo.RiiabloTest;
|
||||
import com.riiablo.logger.Level;
|
||||
|
||||
public class BIKTest extends RiiabloTest {
|
||||
@BeforeClass
|
||||
public static void before() {
|
||||
com.riiablo.logger.LogManager.setLevel("com.riiablo.video", Level.TRACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void load_bik240() {
|
||||
FileHandle handle = Gdx.files.internal("test\\New_Bliz640x240.bik");
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(handle.readBytes());
|
||||
BIK.loadFromByteBuf(buffer);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void load_bik480() {
|
||||
FileHandle handle = Gdx.files.internal("test\\New_Bliz640x480.bik");
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(handle.readBytes());
|
||||
BIK.loadFromByteBuf(buffer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void read_bik480() {
|
||||
FileHandle handle = Gdx.files.internal("test\\New_Bliz640x480.bik");
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(handle.readBytes());
|
||||
BIK bik = BIK.loadFromByteBuf(buffer);
|
||||
bik.decode(0);
|
||||
|
||||
AudioDevice audio = Gdx.audio.newAudioDevice(44100, false);
|
||||
// TODO: create video tool
|
||||
// TODO: test audio playback
|
||||
// TODO: test injecting bik audio into AudioDevice stream
|
||||
// TODO: test AudioDevice android support
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user