Since I get a new page claim, I thought I'd occupy this with one of JJ2's most elusive file formats -- the Jazz2 Cinematic Files (or J2Vs).
The J2V File Format
Like all other file formats, the file begins with a J2V Header, followed by 4 ZLIB streams. It's worth noting that the ZLIB streams are sync flushed every few frames, which explains the 00 00 FF FF at the end of every stream. So essentially the 4 streams are interleaved: Data1, Data2, Data3, Data4, back to Data1 etc. With the compressed size preceding each stream.
Another thing to note is that it only contains video data. I will talk about where the audio data comes from towards the end of the post.
The Header
Code:
struct J2V_Header {
char Magic[8] = "CineFeed";
int FileSize;
int CRC32; // of the lowercase filename, e.g. "intro" or "endinglq"
int Width;
int Height;
short BitsPerPixel;
short DelayBetweenFrames; // in milliseconds
int TotalFrames;
int MaxUSize1;
int MaxUSize2;
int MaxUSize3;
int MaxUSize4;
int MaxCSize;
}
The header is followed by 4 interleaved sync-flushed zlib streams. How JJ2 does it is to allocate 5 memory buffers of the 5 sizes given at the end of the header. Each "compressed string of bytes" is read and then uncompressed into the corresponding uncompressed buffer. But since we're in an age with unlimited memory, our best approach would be simply be to concatenate everything into one long buffer.
Data1 contains frame data given as instructions per row, Data2 contains absolute x offsets, Data3 contains relative y offsets, and Data4 contains raw palette and pixel data.
Data1 - Frame Data
This starts with a codebyte of either 0x00 (which means no palette), or 0x01 (which means read 1024 bytes from Data4 and update palette), followed by Height (i.e. 480 or 200) sets of instructions.
Here are the possible instructions:
0x00: Read the next two bytes, then copy that many bytes from Data4.
0x01-0x7F: Copy this many bytes from Data4.
0x80: End of line.
0x81: Read the next two bytes, then copy that many bytes from some offset given by Data2 and Data3.
0x82-0xFF: Read the next two bytes, then copy that many bytes minus 106 from some offset as above.
Data2 - Absolute X Offset
Just a bunch of offsets, in shorts.
Data3 - Relative Y Offset
Another bunch of offsets, in bytes.
Data4 - Raw Palette and Pixel Data
Palette data is pretty much 256 colours in RGBA format, for a total of 1024 bytes. Pixels are 1 byte each.
How the copying from Data2/Data3 is done
If you're copying to (x, y) on the current frame, you want to copy from (read(data2), y + read(data1) - 127) from the previous frame.
Sound Data
This is obtained from the corresponding SoundFXList.Name from Data.J2D, where it can be read as the following struct:
Code:
struct SoundFXList {
int Frame;
int Sample;
int Volume;
int Panning;
}
which gives a list of which frames sounds are played.
Finally, here's some C# code which needs nothing but the .NET framework and compilation under unsafe code:
Code:
using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
namespace jj2video
{
class Program
{
static void Main()
{
var d = new BinaryReader[4];
int i, frame = 0, frames, width, height, delay;
using (var br = new BinaryReader(File.OpenRead(@"D:\Games\Jazz2\Intro.j2v")))
{
br.ReadBytes(16); width = br.ReadInt32(); height = br.ReadInt32();
br.ReadInt16(); delay = br.ReadInt16(); frames = br.ReadInt32();
br.ReadBytes(20);
var strs = new MemoryStream[4]; for (i = 0; i < 4; i++) strs[i] = new MemoryStream();
for (i = 0; br.BaseStream.Position != br.BaseStream.Length; i++)
{
foreach (var str in strs)
{
int len = br.ReadInt32();
str.Write(br.ReadBytes(len), 0, len);
}
}
for (i = 0; i < 4; i++)
{
strs[i].Position = 2;
d[i] = new BinaryReader(new System.IO.Compression.DeflateStream(strs[i], 0));
}
}
var bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
var pb = new PictureBox() { Size = new Size(width, height) };
var f = new Form() { ClientSize = pb.Size }; f.Controls.Add(pb);
var t = new Timer() { Enabled = true, Interval = delay };
t.Tick += new EventHandler((s, e) =>
{
if (frame++ >= frames) return;
var data = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, bmp.PixelFormat);
unsafe
{
var pixels = (byte*)data.Scan0;
var copy = new byte[data.Stride * height];
for (i = 0; i < copy.Length; i++) copy[i] = pixels[i];
if (d[0].ReadByte() == 1)
{
var p = bmp.Palette;
for (i = 0; i < 256; i++) p.Entries[i] = Color.FromArgb(d[3].ReadByte(), d[3].ReadByte(), d[3].ReadInt16());
bmp.Palette = p;
}
for (int y = 0; y < height; y++)
{
byte c;
int x = 0;
while ((c = d[0].ReadByte()) != 128)
{
if (c < 128)
{
int u = c == 0 ? d[0].ReadInt16() : c;
for (i = 0; i < u; i++) pixels[y * data.Stride + x++] = d[3].ReadByte();
}
else
{
int u = c == 0x81 ? d[0].ReadInt16() : c - 106;
int n = d[1].ReadInt16() + (d[2].ReadByte() + y - 127) * data.Stride;
for (i = 0; i < u; i++) pixels[y * data.Stride + x++] = copy[n++];
}
}
}
}
bmp.UnlockBits(data);
pb.Image = bmp;
});
Application.Run(f);
}
}
}