aboutsummaryrefslogtreecommitdiff
path: root/engines/kyra/graphics/vqa.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'engines/kyra/graphics/vqa.cpp')
-rw-r--r--engines/kyra/graphics/vqa.cpp667
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