/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * $URL$ * $Id$ * */ // Player for Kyrandia 3 VQA movies, based on the information found at // http://multimedia.cx/VQA_INFO.TXT // // The benchl.vqa movie (or whatever it is) is not supported. It does not have // a FINF chunk. // // The jung2.vqa movie does work, but only thanks to a grotesque hack. #include "common/events.h" #include "common/system.h" #include "sound/audiostream.h" #include "sound/mixer.h" #include "kyra/sound.h" #include "kyra/screen.h" #include "kyra/vqa.h" namespace Kyra { VQAMovie::VQAMovie(KyraEngine_v1 *vm, OSystem *system) { _system = system; _vm = vm; _opened = false; _x = _y = _drawPage = -1; } VQAMovie::~VQAMovie() { close(); } void VQAMovie::initBuffers() { for (int i = 0; i < ARRAYSIZE(_buffers); i++) { _buffers[i].data = 0; _buffers[i].size = 0; } } void *VQAMovie::allocBuffer(int num, uint32 size) { assert(num >= 0 && num < ARRAYSIZE(_buffers)); assert(size > 0); if (size > _buffers[num].size) { /* * We could use realloc() here, but we don't actually need the * old contents of the buffer. */ delete[] _buffers[num].data; _buffers[num].data = new uint8[size]; _buffers[num].size = size; } assert(_buffers[num].data); return _buffers[num].data; } void VQAMovie::freeBuffers() { for (int i = 0; i < ARRAYSIZE(_buffers); i++) { delete[] _buffers[i].data; _buffers[i].data = NULL; _buffers[i].size = 0; } } uint32 VQAMovie::readTag() { // Some tags have to be on an even offset, so they are padded with a // zero byte. Skip that. uint32 tag = _file.readUint32BE(); if (!(tag & 0xFF000000)) { tag = (tag << 8) | _file.readByte(); } return tag; } void VQAMovie::decodeSND1(byte *inbuf, uint32 insize, byte *outbuf, uint32 outsize) { const int8 WSTable2Bit[] = { -2, -1, 0, 1 }; const int8 WSTable4Bit[] = { -9, -8, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 8 }; byte code; int8 count; uint16 input; int16 curSample = 0x80; while (outsize > 0) { input = *inbuf++ << 2; code = (input >> 8) & 0xff; count = (input & 0xff) >> 2; switch (code) { case 2: if (count & 0x20) { /* NOTE: count is signed! */ count <<= 3; curSample += (count >> 3); *outbuf++ = curSample; outsize--; } else { for (; count >= 0; count--) { *outbuf++ = *inbuf++; outsize--; } curSample = *(outbuf - 1); } break; case 1: for (; count >= 0; count--) { code = *inbuf++; curSample += WSTable4Bit[code & 0x0f]; curSample = CLIP(curSample, 0, 255); *outbuf++ = curSample; curSample += WSTable4Bit[code >> 4]; curSample = CLIP(curSample, 0, 255); *outbuf++ = curSample; outsize -= 2; } break; case 0: for (; count >= 0; count--) { code = *inbuf++; curSample += WSTable2Bit[code & 0x03]; curSample = CLIP(curSample, 0, 255); *outbuf++ = curSample; curSample += WSTable2Bit[(code >> 2) & 0x03]; curSample = CLIP(curSample, 0, 255); *outbuf++ = curSample; curSample += WSTable2Bit[(code >> 4) & 0x03]; curSample = CLIP(curSample, 0, 255); *outbuf++ = curSample; curSample += WSTable2Bit[(code >> 6) & 0x03]; curSample = CLIP(curSample, 0, 255); *outbuf++ = curSample; outsize -= 4; } break; default: for (; count >= 0; count--) { *outbuf++ = curSample; outsize--; } } } } bool VQAMovie::open(const char *filename) { debugC(9, kDebugLevelMovie, "VQAMovie::open('%s')", filename); close(); if (!_file.open(filename)) return false; if (_file.readUint32BE() != MKID_BE('FORM')) { warning("VQAMovie::open: Cannot find `FORM' tag"); return false; } // For now, we ignore the size of the FORM chunk. _file.readUint32BE(); if (_file.readUint32BE() != MKID_BE('WVQA')) { warning("WQAMovie::open: Cannot find `WVQA' tag"); return false; } bool foundHeader = false; bool foundFrameInfo = false; // The information we need is stored in two chunks: VQHD and FINF. We // need both of them before we can begin decoding the movie. while (!foundHeader || !foundFrameInfo) { uint32 tag = readTag(); uint32 size = _file.readUint32BE(); switch (tag) { case MKID_BE('VQHD'): // VQA header _header.version = _file.readUint16LE(); _header.flags = _file.readUint16LE(); _header.numFrames = _file.readUint16LE(); _header.width = _file.readUint16LE(); _header.height = _file.readUint16LE(); _header.blockW = _file.readByte(); _header.blockH = _file.readByte(); _header.frameRate = _file.readByte(); _header.cbParts = _file.readByte(); _header.colors = _file.readUint16LE(); _header.maxBlocks = _file.readUint16LE(); _header.unk1 = _file.readUint32LE(); _header.unk2 = _file.readUint16LE(); _header.freq = _file.readUint16LE(); _header.channels = _file.readByte(); _header.bits = _file.readByte(); _header.unk3 = _file.readUint32LE(); _header.unk4 = _file.readUint16LE(); _header.maxCBFZSize = _file.readUint32LE(); _header.unk5 = _file.readUint32LE(); // Kyrandia 3 uses version 1 VQA files, and is the only // known game to do so. This version of the format has // some implicit default values. if (_header.version == 1) { if (_header.freq == 0) _header.freq = 22050; if (_header.channels == 0) _header.channels = 1; if (_header.bits == 0) _header.bits = 8; } setX((Screen::SCREEN_W - _header.width) / 2); setY((Screen::SCREEN_H - _header.height) / 2); _frameInfo = new uint32[_header.numFrames]; _frame = new byte[_header.width * _header.height]; _codeBookSize = 0xf00 * _header.blockW * _header.blockH; _codeBook = new byte[_codeBookSize]; _partialCodeBook = new byte[_codeBookSize]; memset(_codeBook, 0, _codeBookSize); memset(_partialCodeBook, 0, _codeBookSize); _numVectorPointers = (_header.width / _header.blockW) * (_header.height * _header.blockH); _vectorPointers = new uint16[_numVectorPointers]; memset(_vectorPointers, 0, _numVectorPointers * sizeof(uint16)); _partialCodeBookSize = 0; _numPartialCodeBooks = 0; if (_header.flags & 1) { // This VQA movie has sound. Kyrandia 3 uses // 8-bit sound, and so far testing indicates // that it's all mono. // // This is good, because it means we won't have // to worry about the confusing parts of the // VQA spec, where 8- and 16-bit data have // different signedness and stereo sample // layout varies between different games. assert(_header.bits == 8); assert(_header.channels == 1); _stream = Audio::makeAppendableAudioStream(_header.freq, Audio::Mixer::FLAG_UNSIGNED); } else { _stream = NULL; } foundHeader = true; break; case MKID_BE('FINF'): // Frame info if (!foundHeader) { warning("VQAMovie::open: Found `FINF' before `VQHD'"); return false; } if (size != 4 * (uint32)_header.numFrames) { warning("VQAMovie::open: Expected size %d for `FINF' chunk, but got %u", 4 * _header.numFrames, size); return false; } foundFrameInfo = true; for (int i = 0; i < _header.numFrames; i++) { _frameInfo[i] = 2 * _file.readUint32LE(); } // HACK: This flag is set in jung2.vqa, and its // purpose, if it has one, is unknown. It can't be a // general purpose flag, because in large movies the // frame offsets can be large enough to set this flag, // though of course never for the first frame. // // At least in my copy of Kyrandia 3, _frameInfo[0] is // 0x81000098, and the desired index is 0x4716. So the // value should be 0x80004716, but I don't want to // hard-code it. Instead, scan the file for the offset // to the first VQFR chunk. if (_frameInfo[0] & 0x01000000) { uint32 oldPos = _file.pos(); while (1) { uint32 scanTag = readTag(); uint32 scanSize = _file.readUint32BE(); if (_file.eof()) break; if (scanTag == MKID_BE('VQFR')) { _frameInfo[0] = (_file.pos() - 8) | 0x80000000; break; } _file.seek(scanSize, SEEK_CUR); } _file.seek(oldPos); } break; default: warning("VQAMovie::open: Unknown tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF)); _file.seek(size, SEEK_CUR); break; } } initBuffers(); _opened = true; return true; } void VQAMovie::close() { debugC(9, kDebugLevelMovie, "VQAMovie::close()"); if (_opened) { delete[] _frameInfo; delete[] _frame; delete[] _codeBook; delete[] _partialCodeBook; delete[] _vectorPointers; if (_vm->_mixer->isSoundHandleActive(_sound)) { _vm->_mixer->stopHandle(_sound); } _frameInfo = NULL; _frame = NULL; _codeBookSize = 0; _codeBook = NULL; _partialCodeBook = NULL; _vectorPointers = NULL; _stream = NULL; if (_file.isOpen()) _file.close(); freeBuffers(); _opened = false; } } void VQAMovie::displayFrame(uint frameNum) { debugC(9, kDebugLevelMovie, "VQAMovie::displayFrame(%d)", frameNum); if (frameNum >= _header.numFrames || !_opened) return; bool foundSound = _stream ? false : true; bool foundFrame = false; uint i; _file.seek(_frameInfo[frameNum] & 0x7FFFFFFF); while (!foundSound || !foundFrame) { uint32 tag = readTag(); uint32 size = _file.readUint32BE(); if (_file.eof()) { // This happens at the last frame. Apparently it has // no sound? break; } byte *inbuf, *outbuf; uint32 insize, outsize; uint32 end; switch (tag) { case MKID_BE('SND0'): // Uncompressed sound foundSound = true; inbuf = new byte[size]; _file.read(inbuf, size); assert(_stream); _stream->queueBuffer(inbuf, size); break; case MKID_BE('SND1'): // Compressed sound, almost like AUD foundSound = true; outsize = _file.readUint16LE(); insize = _file.readUint16LE(); inbuf = new byte[insize]; _file.read(inbuf, insize); if (insize == outsize) { assert(_stream); _stream->queueBuffer(inbuf, insize); } else { outbuf = new byte[outsize]; decodeSND1(inbuf, insize, outbuf, outsize); assert(_stream); _stream->queueBuffer(outbuf, outsize); delete[] inbuf; } break; case MKID_BE('SND2'): // Compressed sound foundSound = true; warning("VQAMovie::displayFrame: `SND2' is not implemented"); _file.seek(size, SEEK_CUR); break; case MKID_BE('VQFR'): foundFrame = true; end = _file.pos() + size - 8; while (_file.pos() < end) { tag = readTag(); size = _file.readUint32BE(); switch (tag) { case MKID_BE('CBF0'): // Full codebook _file.read(_codeBook, size); break; case MKID_BE('CBFZ'): // Full codebook inbuf = (byte *)allocBuffer(0, size); _file.read(inbuf, size); Screen::decodeFrame4(inbuf, _codeBook, _codeBookSize); break; case MKID_BE('CBP0'): // Partial codebook _compressedCodeBook = false; _file.read(_partialCodeBook + _partialCodeBookSize, size); _partialCodeBookSize += size; _numPartialCodeBooks++; break; case MKID_BE('CBPZ'): // Partial codebook _compressedCodeBook = true; _file.read(_partialCodeBook + _partialCodeBookSize, size); _partialCodeBookSize += size; _numPartialCodeBooks++; break; case MKID_BE('CPL0'): // Palette assert(size <= 3 * 256); _file.read(_vm->screen()->_currentPalette, size); break; case MKID_BE('CPLZ'): // Palette inbuf = (byte *)allocBuffer(0, size); _file.read(inbuf, size); Screen::decodeFrame4(inbuf, _vm->screen()->_currentPalette, 768); break; case MKID_BE('VPT0'): // Frame data assert(size / 2 <= _numVectorPointers); for (i = 0; i < size / 2; i++) _vectorPointers[i] = _file.readUint16LE(); break; case MKID_BE('VPTZ'): // Frame data inbuf = (byte *)allocBuffer(0, size); outbuf = (byte *)allocBuffer(1, 2 * _numVectorPointers); _file.read(inbuf, size); size = Screen::decodeFrame4(inbuf, outbuf, 2 * _numVectorPointers); assert(size / 2 <= _numVectorPointers); for (i = 0; i < size / 2; i++) _vectorPointers[i] = READ_LE_UINT16(outbuf + 2 * i); break; default: warning("VQAMovie::displayFrame: Unknown `VQFR' sub-tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF)); _file.seek(size, SEEK_CUR); break; } } break; default: warning("VQAMovie::displayFrame: Unknown tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF)); _file.seek(size, SEEK_CUR); break; } } // The frame has been decoded if (_frameInfo[frameNum] & 0x80000000) { _vm->screen()->setScreenPalette(_vm->screen()->_currentPalette); } int blockPitch = _header.width / _header.blockW; for (int by = 0; by < _header.height / _header.blockH; by++) { for (int bx = 0; bx < blockPitch; bx++) { byte *dst = _frame + by * _header.width * _header.blockH + bx * _header.blockW; int val = _vectorPointers[by * blockPitch + bx]; if ((val & 0xFF00) == 0xFF00) { // Solid color for (i = 0; i < _header.blockH; i++) { memset(dst, 255 - (val & 0xFF), _header.blockW); dst += _header.width; } } else { // Copy data from _vectorPointers. I'm not sure // why we don't use the three least significant // bits of 'val'. byte *src = &_codeBook[(val >> 3) * _header.blockW * _header.blockH]; for (i = 0; i < _header.blockH; i++) { memcpy(dst, src, _header.blockW); src += _header.blockW; dst += _header.width; } } } } if (_numPartialCodeBooks == _header.cbParts) { if (_compressedCodeBook) { Screen::decodeFrame4(_partialCodeBook, _codeBook, _codeBookSize); } else { memcpy(_codeBook, _partialCodeBook, _partialCodeBookSize); } _numPartialCodeBooks = 0; _partialCodeBookSize = 0; } _vm->screen()->copyBlockToPage(_drawPage, _x, _y, _header.width, _header.height, _frame); } void VQAMovie::play() { uint32 startTick; if (!_opened) return; startTick = _system->getMillis(); // First, handle any sound chunk that appears before the first frame. // At least in some versions, it will contain half a second of audio, // presumably to lessen the risk of audio underflow. // // In most movies, we will find a CMDS tag. The purpose of this is // currently unknown. // // In cow1_0.vqa, cow1_1.vqa, jung0.vqa, and jung1.vqa we will find a // VQFR tag. A frame before the first frame? Weird. It doesn't seem to // be needed, though. byte *inbuf, *outbuf; uint32 insize, outsize; if (_stream) { while (_file.pos() < (_frameInfo[0] & 0x7FFFFFFF)) { uint32 tag = readTag(); uint32 size = _file.readUint32BE(); if (_file.eof()) { warning("VQAMovie::play: Unexpected EOF"); break; } switch (tag) { case MKID_BE('SND0'): // Uncompressed sound inbuf = new byte[size]; _file.read(inbuf, size); _stream->queueBuffer(inbuf, size); break; case MKID_BE('SND1'): // Compressed sound outsize = _file.readUint16LE(); insize = _file.readUint16LE(); inbuf = new byte[insize]; _file.read(inbuf, insize); if (insize == outsize) { _stream->queueBuffer(inbuf, insize); } else { outbuf = new byte[outsize]; decodeSND1(inbuf, insize, outbuf, outsize); _stream->queueBuffer(outbuf, outsize); delete[] inbuf; } break; case MKID_BE('SND2'): // Compressed sound warning("VQAMovie::play: `SND2' is not implemented"); _file.seek(size, SEEK_CUR); break; case MKID_BE('CMDS'): // Unused tag, always empty in kyra3 debugC(9, kDebugLevelMovie, "VQAMovie::play: skipping CMDS tag"); _file.seek(size, SEEK_CUR); break; default: warning("VQAMovie::play: Unknown tag `%c%c%c%c'", char((tag >> 24) & 0xFF), char((tag >> 16) & 0xFF), char((tag >> 8) & 0xFF), char(tag & 0xFF)); _file.seek(size, SEEK_CUR); break; } } } _vm->_mixer->playInputStream(Audio::Mixer::kSFXSoundType, &_sound, _stream); for (uint i = 0; i < _header.numFrames; i++) { displayFrame(i); // TODO: Implement frame skipping? while (1) { uint32 elapsedTime; if (_vm->_mixer->isSoundHandleActive(_sound)) { elapsedTime = _vm->_mixer->getSoundElapsedTime(_sound); } else { elapsedTime = _system->getMillis() - startTick; } if (elapsedTime >= (i * 1000) / _header.frameRate) break; Common::Event event; Common::EventManager *eventMan = _system->getEventManager(); while (eventMan->pollEvent(event)) { switch (event.type) { case Common::EVENT_KEYDOWN: if (event.kbd.ascii == 27) return; break; case Common::EVENT_QUIT: _vm->quitGame(); return; default: break; } } _system->delayMillis(10); } _vm->screen()->updateScreen(); } // TODO: Wait for the sound to finish? } } // end of namespace Kyra