Files consist of a fixed-length header, followed by an index table, and then frame chunks. All numbers are little-endian.
File header (0x68 bytes):
u8 sig -- File signature, "JV" u8 palmode1 -- Initial palette mode. 'W' - all White, '0' (or any other value) - Black. (Un)used for fade-in effect u8 palmode2 -- Final palette mode. Same as above, but fade-out u8 copyright[0x4C]; -- "Compression by John M Phillips"... bla-bla s16 width; -- Frame width s16 height; -- Frame height s16 frames; -- Number of frames s16 rate; -- Frame rate. Delay between successive frames in ms s32 maxchunk; -- Maximal size of a single frame chunk s32 freq; -- Audio frequency u8 flags0:1; -- ? u8 volume:7; -- Default sound volume (0 - max) u8 unused; u8 flags1:3; -- Number of fade-in frames? u8 flags2:3; -- ? u8 flags3:1; -- ? u8 flags4:1; -- ?
File header is followed by frame index table, one entry per frame.
Table entry format (0x10 bytes):
s32 chunk_size; -- Size of the chunk data s32 audio_size; -- Size of the audio portion of the frame s32 video_size; -- Size of the video portion of the frame s8 has_palette; -- New palette is present s8 audio_type; -- Audio format. Always 0 - mono, 8-bit PCM s8 video_type; -- Video compression method (see below) s8 unused;
Finally, frame chunks follows.
Frama chunk format:
s8 audio[audio_size]; -- Audio data RGB palette; -- New palette (present only if has_palette flag is set) s8 video[video_size]; -- Video data s8 padding; -- sizeof(audio + palette + video + padding) == chunk_size
Depending on video_type value, frame can be encoded using the following way:
Type 0: Raw frame
Paint frame using the video data and continue with Type 1 decoding. Apparently, someone forgot to put the "break" keyword here because additional decoding of the raw data seems redundant. Or perhaps it was originally planned to (losely) compress this block data somehow.
Type 1: BTC frame
Initialize a bit-reader using video data. Fill it with 8-bit values, fetch bits starting from the highest. Multi-byte values are read as byte0 = GetByte(), byte1 = GetByte(), etc.
Frame is encoded as a series of 8x8 blocks. To decode single block:
Read 2-bit block type: 0: End. End of (sub)block decoding. 1: Fill. Read 8 bits and use them to fill the (sub)block. 2: BTC. Read color0 and color1, 8 bits each. Read mask. For each pixel of (sub)block, if mask bit is set - paint it using color1. Use color0 if bit is clear. Complete mask read first (4/16/64 bits) and processed from its highest bit to lowest. 3: Subblock. Split the block on 4 equal subblocks and process each one separately using the same scheme recursively. If the subblock size is already 2x2, paint it using the next 2 * 2 * 8 bits.
Type 2: Solid frame
Read 8-bit pixel value from video data and use it to fill the whole frame.