diff options
Diffstat (limited to 'engines/kyra/graphics/vqa.cpp')
-rw-r--r-- | engines/kyra/graphics/vqa.cpp | 667 |
1 files changed, 667 insertions, 0 deletions
diff --git a/engines/kyra/graphics/vqa.cpp b/engines/kyra/graphics/vqa.cpp new file mode 100644 index 0000000000..5a4e250b42 --- /dev/null +++ b/engines/kyra/graphics/vqa.cpp @@ -0,0 +1,667 @@ +/* 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. + * + */ + +// 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 "kyra/kyra_v1.h" +#include "kyra/graphics/vqa.h" +#include "kyra/graphics/screen.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" + +#include "common/system.h" +#include "common/events.h" + +#include "graphics/palette.h" +#include "graphics/surface.h" + +namespace Kyra { + +static uint32 readTag(Common::SeekableReadStream *stream) { + // Some tags have to be on an even offset, so they are padded with a + // zero byte. Skip that. + + uint32 tag = stream->readUint32BE(); + + if (stream->eos()) + return 0; + + if (!(tag & 0xFF000000)) { + tag = (tag << 8) | stream->readByte(); + } + + return tag; +} + +VQADecoder::VQADecoder() { + memset(&_header, 0, sizeof(_header)); +} + +VQADecoder::~VQADecoder() { + close(); + delete[] _frameInfo; +} + +bool VQADecoder::loadStream(Common::SeekableReadStream *stream) { + close(); + _fileStream = stream; + + if (_fileStream->readUint32BE() != MKTAG('F','O','R','M')) { + warning("VQADecoder::loadStream(): Cannot find `FORM' tag"); + return false; + } + + // Ignore the size of the FORM chunk. We're only interested in its + // children. + _fileStream->readUint32BE(); + + if (_fileStream->readUint32BE() != MKTAG('W','V','Q','A')) { + warning("VQADecoder::loadStream(): Cannot find `WVQA' tag"); + return false; + } + + // We want to find both a VQHD chunk containing the header, and a FINF + // chunk containing the frame offsets. + + bool foundVQHD = false; + bool foundFINF = false; + + VQAAudioTrack *audioTrack = NULL; + + // 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 (!foundVQHD || !foundFINF) { + uint32 tag = readTag(stream); + uint32 size = _fileStream->readUint32BE(); + + switch (tag) { + case MKTAG('V','Q','H','D'): + handleVQHD(_fileStream); + if (_header.flags & 1) { + audioTrack = new VQAAudioTrack(&_header, getSoundType()); + addTrack(audioTrack); + } + foundVQHD = true; + break; + case MKTAG('F','I','N','F'): + if (!foundVQHD) { + warning("VQADecoder::loadStream(): Found `FINF' before `VQHD'"); + return false; + } + if (size != 4 * getFrameCount()) { + warning("VQADecoder::loadStream(): Expected size %d for `FINF' chunk, but got %u", 4 * getFrameCount(), size); + return false; + } + handleFINF(_fileStream); + foundFINF = true; + break; + default: + warning("VQADecoder::loadStream(): Unknown tag `%s'", tag2str(tag)); + _fileStream->seek(size, SEEK_CUR); + break; + } + } + + return true; +} + +void VQADecoder::handleVQHD(Common::SeekableReadStream *stream) { + _header.version = stream->readUint16LE(); + _header.flags = stream->readUint16LE(); + _header.numFrames = stream->readUint16LE(); + _header.width = stream->readUint16LE(); + _header.height = stream->readUint16LE(); + _header.blockW = stream->readByte(); + _header.blockH = stream->readByte(); + _header.frameRate = stream->readByte(); + _header.cbParts = stream->readByte(); + _header.colors = stream->readUint16LE(); + _header.maxBlocks = stream->readUint16LE(); + _header.unk1 = stream->readUint32LE(); + _header.unk2 = stream->readUint16LE(); + _header.freq = stream->readUint16LE(); + _header.channels = stream->readByte(); + _header.bits = stream->readByte(); + _header.unk3 = stream->readUint32LE(); + _header.unk4 = stream->readUint16LE(); + _header.maxCBFZSize = stream->readUint32LE(); + _header.unk5 = stream->readUint32LE(); + + _frameInfo = new uint32[_header.numFrames + 1]; + + VQAVideoTrack *videoTrack = new VQAVideoTrack(&_header); + addTrack(videoTrack); + + // 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; + } + + if (_header.flags & 1) { + // 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); + } +} + +void VQADecoder::handleFINF(Common::SeekableReadStream *stream) { + for (int i = 0; i < _header.numFrames; i++) { + _frameInfo[i] = 2 * stream->readUint32LE(); + } + + // HACK: This flag is set in jung2.vqa, and its purpose - if it has + // one - is currently unknown. It can't be a general purpose flag, + // because in large movies the frame offset 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 = stream->pos(); + + while (1) { + uint32 scanTag = readTag(stream); + uint32 scanSize = stream->readUint32BE(); + + if (stream->eos()) + break; + + if (scanTag == MKTAG('V','Q','F','R')) { + _frameInfo[0] = (stream->pos() - 8) | 0x80000000; + break; + } + + stream->seek(scanSize, SEEK_CUR); + } + + stream->seek(oldPos); + } + + _frameInfo[_header.numFrames] = 0x7FFFFFFF; +} + +void VQADecoder::readNextPacket() { + VQAVideoTrack *videoTrack = (VQAVideoTrack *)getTrack(0); + VQAAudioTrack *audioTrack = (VQAAudioTrack *)getTrack(1); + + assert(videoTrack); + + int curFrame = videoTrack->getCurFrame(); + + // Stop if reading the tag is enough to put us ahead of the next frame + int32 end = (_frameInfo[curFrame + 1] & 0x7FFFFFFF) - 7; + + // At this point, we probably only need to adjust for the offset in the + // stream to be even. But we may as well do this to really make sure + // we have the correct offset. + if (curFrame >= 0) { + _fileStream->seek(_frameInfo[curFrame] & 0x7FFFFFFF); + if (_frameInfo[curFrame] & 0x80000000) { + videoTrack->setHasDirtyPalette(); + } + } + + while (!_fileStream->eos() && _fileStream->pos() < end) { + uint32 tag = readTag(_fileStream); + uint32 size; + + switch (tag) { + case MKTAG('S','N','D','0'): // Uncompressed sound + assert(audioTrack); + audioTrack->handleSND0(_fileStream); + break; + case MKTAG('S','N','D','1'): // Compressed sound, almost like AUD + assert(audioTrack); + audioTrack->handleSND1(_fileStream); + break; + case MKTAG('S','N','D','2'): // Compressed sound + assert(audioTrack); + audioTrack->handleSND2(_fileStream); + break; + case MKTAG('V','Q','F','R'): + videoTrack->handleVQFR(_fileStream); + break; + case MKTAG('C','M','D','S'): + // The purpose of this is unknown, but it's known to + // exist so don't warn about it. + size = _fileStream->readUint32BE(); + _fileStream->seek(size, SEEK_CUR); + break; + default: + warning("VQADecoder::readNextPacket(): Unknown tag `%s'", tag2str(tag)); + size = _fileStream->readUint32BE(); + _fileStream->seek(size, SEEK_CUR); + break; + } + } +} + +// ----------------------------------------------------------------------- + +VQADecoder::VQAAudioTrack::VQAAudioTrack(const VQAHeader *header, Audio::Mixer::SoundType soundType) : + AudioTrack(soundType) { + _audioStream = Audio::makeQueuingAudioStream(header->freq, false); +} + +VQADecoder::VQAAudioTrack::~VQAAudioTrack() { + delete _audioStream; +} + +Audio::AudioStream *VQADecoder::VQAAudioTrack::getAudioStream() const { + return _audioStream; +} + +void VQADecoder::VQAAudioTrack::handleSND0(Common::SeekableReadStream *stream) { + uint32 size = stream->readUint32BE(); + byte *buf = (byte *)malloc(size); + stream->read(buf, size); + _audioStream->queueBuffer(buf, size, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED); +} + +void VQADecoder::VQAAudioTrack::handleSND1(Common::SeekableReadStream *stream) { + stream->readUint32BE(); + uint16 outsize = stream->readUint16LE(); + uint16 insize = stream->readUint16LE(); + byte *inbuf = (byte *)malloc(insize); + + stream->read(inbuf, insize); + + if (insize == outsize) { + _audioStream->queueBuffer(inbuf, insize, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED); + } else { + 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 *outbuf = (byte *)malloc(outsize); + byte *in = inbuf; + byte *out = outbuf; + int16 curSample = 0x80; + uint16 bytesLeft = outsize; + + while (bytesLeft > 0) { + uint16 input = *in++ << 2; + byte code = (input >> 8) & 0xFF; + int8 count = (input & 0xFF) >> 2; + int i; + + switch (code) { + case 2: + if (count & 0x20) { + /* NOTE: count is signed! */ + count <<= 3; + curSample += (count >> 3); + *out++ = curSample; + bytesLeft--; + } else { + for (; count >= 0; count--) { + *out++ = *in++; + bytesLeft--; + } + curSample = *(out - 1); + } + break; + case 1: + for (; count >= 0; count--) { + code = *in++; + + for (i = 0; i < 2; i++) { + curSample += WSTable4Bit[code & 0x0F]; + curSample = CLIP<int16>(curSample, 0, 255); + code >>= 4; + *out++ = curSample; + } + + bytesLeft -= 2; + } + break; + case 0: + for (; count >= 0; count--) { + code = *in++; + + for (i = 0; i < 4; i++) { + curSample += WSTable2Bit[code & 0x03]; + curSample = CLIP<int16>(curSample, 0, 255); + code >>= 2; + *out++ = curSample; + } + + bytesLeft -= 4; + } + break; + default: + for (; count >= 0; count--) { + *out++ = curSample; + bytesLeft--; + } + break; + } + } + _audioStream->queueBuffer(outbuf, outsize, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED); + free(inbuf); + } +} + +void VQADecoder::VQAAudioTrack::handleSND2(Common::SeekableReadStream *stream) { + uint32 size = stream->readUint32BE(); + warning("VQADecoder::VQAAudioTrack::handleSND2(): `SND2' is not implemented"); + stream->seek(size, SEEK_CUR); +} + +// ----------------------------------------------------------------------- + +VQADecoder::VQAVideoTrack::VQAVideoTrack(const VQAHeader *header) { + memset(_palette, 0, sizeof(_palette)); + _dirtyPalette = false; + + _width = header->width; + _height = header->height; + _blockW = header->blockW; + _blockH = header->blockH; + _cbParts = header->cbParts; + + _newFrame = false; + + _curFrame = -1; + _frameCount = header->numFrames; + _frameRate = header->frameRate; + + _codeBookSize = 0xF00 * header->blockW * header->blockH; + _compressedCodeBook = false; + _codeBook = new byte[_codeBookSize]; + _partialCodeBookSize = 0; + _numPartialCodeBooks = 0; + _partialCodeBook = new byte[_codeBookSize]; + _numVectorPointers = (header->width / header->blockW) * (header->height * header->blockH); + _vectorPointers = new uint16[_numVectorPointers]; + + memset(_codeBook, 0, _codeBookSize); + memset(_partialCodeBook, 0, _codeBookSize); + memset(_vectorPointers, 0, _numVectorPointers); + + _surface = new Graphics::Surface(); + _surface->create(header->width, header->height, Graphics::PixelFormat::createFormatCLUT8()); +} + +VQADecoder::VQAVideoTrack::~VQAVideoTrack() { + _surface->free(); + delete _surface; + delete[] _codeBook; + delete[] _partialCodeBook; + delete[] _vectorPointers; +} + +uint16 VQADecoder::VQAVideoTrack::getWidth() const { + return _width; +} + +uint16 VQADecoder::VQAVideoTrack::getHeight() const { + return _height; +} + +Graphics::PixelFormat VQADecoder::VQAVideoTrack::getPixelFormat() const { + return _surface->format; +} + +int VQADecoder::VQAVideoTrack::getCurFrame() const { + return _curFrame; +} + +int VQADecoder::VQAVideoTrack::getFrameCount() const { + return _frameCount; +} + +Common::Rational VQADecoder::VQAVideoTrack::getFrameRate() const { + return _frameRate; +} + +void VQADecoder::VQAVideoTrack::setHasDirtyPalette() { + _dirtyPalette = true; +} + +bool VQADecoder::VQAVideoTrack::hasDirtyPalette() const { + return _dirtyPalette; +} + +const byte *VQADecoder::VQAVideoTrack::getPalette() const { + _dirtyPalette = false; + return _palette; +} + +const Graphics::Surface *VQADecoder::VQAVideoTrack::decodeNextFrame() { + if (_newFrame) { + _newFrame = false; + + int blockPitch = _width / _blockW; + + for (int by = 0; by < _height / _blockH; by++) { + for (int bx = 0; bx < blockPitch; bx++) { + byte *dst = (byte *)_surface->getBasePtr(bx * _blockW, by * _blockH); + int val = _vectorPointers[by * blockPitch + bx]; + int i; + + if ((val & 0xFF00) == 0xFF00) { + // Solid color + for (i = 0; i < _blockH; i++) { + memset(dst, 255 - (val & 0xFF), _blockW); + dst += _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) * _blockW * _blockH]; + + for (i = 0; i < _blockH; i++) { + memcpy(dst, src, _blockW); + src += _blockW; + dst += _width; + } + } + } + } + + if (_numPartialCodeBooks == _cbParts) { + if (_compressedCodeBook) { + Screen::decodeFrame4(_partialCodeBook, _codeBook, _codeBookSize); + } else { + memcpy(_codeBook, _partialCodeBook, _partialCodeBookSize); + } + _numPartialCodeBooks = 0; + _partialCodeBookSize = 0; + } + } + + _curFrame++; + return _surface; +} + +void VQADecoder::VQAVideoTrack::handleVQFR(Common::SeekableReadStream *stream) { + uint32 size = stream->readUint32BE(); + int32 end = stream->pos() + size - 8; + byte *inbuf; + + _newFrame = true; + + while (stream->pos() < end) { + uint32 tag = readTag(stream); + uint32 i; + size = stream->readUint32BE(); + + switch (tag) { + case MKTAG('C','B','F','0'): // Full codebook + stream->read(_codeBook, size); + break; + case MKTAG('C','B','F','Z'): // Full codebook + inbuf = (byte *)malloc(size); + stream->read(inbuf, size); + Screen::decodeFrame4(inbuf, _codeBook, _codeBookSize); + free(inbuf); + break; + case MKTAG('C','B','P','0'): // Partial codebook + _compressedCodeBook = false; + stream->read(_partialCodeBook + _partialCodeBookSize, size); + _partialCodeBookSize += size; + _numPartialCodeBooks++; + break; + case MKTAG('C','B','P','Z'): // Partial codebook + _compressedCodeBook = true; + stream->read(_partialCodeBook + _partialCodeBookSize, size); + _partialCodeBookSize += size; + _numPartialCodeBooks++; + break; + case MKTAG('C','P','L','0'): // Palette + assert(size <= 3 * 256); + stream->read(_palette, size); + break; + case MKTAG('C','P','L','Z'): // Palette + inbuf = (byte *)malloc(size); + stream->read(inbuf, size); + Screen::decodeFrame4(inbuf, _palette, 3 * 256); + free(inbuf); + break; + case MKTAG('V','P','T','0'): // Frame data + assert(size / 2 <= _numVectorPointers); + for (i = 0; i < size / 2; i++) + _vectorPointers[i] = stream->readUint16LE(); + break; + case MKTAG('V','P','T','Z'): // Frame data + inbuf = (byte *)malloc(size); + stream->read(inbuf, size); + size = Screen::decodeFrame4(inbuf, (uint8 *)_vectorPointers, 2 * _numVectorPointers); + for (i = 0; i < size / 2; i++) + _vectorPointers[i] = TO_LE_16(_vectorPointers[i]); + free(inbuf); + break; + default: + warning("VQADecoder::VQAVideoTrack::handleVQFR(): Unknown `VQFR' sub-tag `%s'", tag2str(tag)); + stream->seek(size, SEEK_CUR); + break; + } + } +} + +// ----------------------------------------------------------------------- + +VQAMovie::VQAMovie(KyraEngine_v1 *vm, OSystem *system) { + _system = system; + _vm = vm; + _screen = _vm->screen(); + _decoder = new VQADecoder(); +} + +VQAMovie::~VQAMovie() { + close(); + delete _decoder; +} + +bool VQAMovie::open(const char *filename) { + if (_file.open(filename)) { + return true; + } + return false; +} + +void VQAMovie::close() { + if (_file.isOpen()) { + _file.close(); + } +} + +void VQAMovie::play() { + if (_decoder->loadStream(&_file)) { + Common::EventManager *eventMan = _vm->getEventManager(); + int width = _decoder->getWidth(); + int height = _decoder->getHeight(); + int x = (Screen::SCREEN_W - width) / 2; + int y = (Screen::SCREEN_H - height) / 2; + + _decoder->start(); + + // Note that decoding starts at frame -1. That's because there + // is usually sound data before the first frame, probably to + // avoid sound underflow. + + while (!_decoder->endOfVideo()) { + Common::Event event; + while (eventMan->pollEvent(event)) { + switch (event.type) { + case Common::EVENT_KEYDOWN: + if (event.kbd.keycode == Common::KEYCODE_ESCAPE) + return; + break; + case Common::EVENT_RTL: + case Common::EVENT_QUIT: + return; + default: + break; + } + } + + if (_decoder->needsUpdate()) { + const Graphics::Surface *surface = _decoder->decodeNextFrame(); + if (_decoder->hasDirtyPalette()) { + const byte *decoderPalette = _decoder->getPalette(); + byte systemPalette[256 * 3]; + for (int i = 0; i < ARRAYSIZE(systemPalette); i++) { + systemPalette[i] = (decoderPalette[i] * 0xFF) / 0x3F; + } + _system->getPaletteManager()->setPalette(systemPalette, 0, 256); + } + + _system->copyRectToScreen((const byte *)surface->getBasePtr(0, 0), surface->pitch, x, y, width, height); + } + + _system->updateScreen(); + _system->delayMillis(10); + } + } +} + +} // End of namespace Kyra |