Created BIK coded along with BinkAudio and BinkVideo (see #59)

This commit is contained in:
Collin Smith
2020-10-04 16:58:22 -07:00
parent 58d31d0634
commit 2bbd3f0c9c
4 changed files with 314 additions and 0 deletions

View 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());
}
}

View 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;
}
}

View File

@ -0,0 +1,7 @@
package com.riiablo.video;
public class BinkVideo {
BinkVideo() {}
}

View 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
}
}