diff options
author | Martin Kiewitz | 2015-11-24 15:42:17 +0100 |
---|---|---|
committer | Martin Kiewitz | 2015-11-24 15:42:17 +0100 |
commit | d69ffaa0e4507c7c5761bd020dc3beac4d85a977 (patch) | |
tree | d1e9e60b93a448e670ff0271c83bf7e4c14d6c43 | |
parent | 5137d0d3f928d0a8951f34dbe0e0108cf06b2e51 (diff) | |
download | scummvm-rg350-d69ffaa0e4507c7c5761bd020dc3beac4d85a977.tar.gz scummvm-rg350-d69ffaa0e4507c7c5761bd020dc3beac4d85a977.tar.bz2 scummvm-rg350-d69ffaa0e4507c7c5761bd020dc3beac4d85a977.zip |
ACCESS: movie player for Noctropolis+Synnergist
accessible via debug command "playmovie"
-rw-r--r-- | engines/access/access.h | 2 | ||||
-rw-r--r-- | engines/access/asurface.cpp | 2 | ||||
-rw-r--r-- | engines/access/asurface.h | 2 | ||||
-rw-r--r-- | engines/access/debugger.cpp | 24 | ||||
-rw-r--r-- | engines/access/debugger.h | 3 | ||||
-rw-r--r-- | engines/access/module.mk | 1 | ||||
-rw-r--r-- | engines/access/screen.cpp | 2 | ||||
-rw-r--r-- | engines/access/screen.h | 2 | ||||
-rw-r--r-- | engines/access/video/movie_decoder.cpp | 739 | ||||
-rw-r--r-- | engines/access/video/movie_decoder.h | 158 |
10 files changed, 931 insertions, 4 deletions
diff --git a/engines/access/access.h b/engines/access/access.h index 37b9fec5a5..83e313083b 100644 --- a/engines/access/access.h +++ b/engines/access/access.h @@ -313,6 +313,8 @@ public: void SPRINTCHR(char c, int fontNum); void PRINTCHR(Common::String msg, int fontNum); + + bool playMovie(const Common::String &filename, const Common::Point &pos); }; } // End of namespace Access diff --git a/engines/access/asurface.cpp b/engines/access/asurface.cpp index 526690807a..abae6bf703 100644 --- a/engines/access/asurface.cpp +++ b/engines/access/asurface.cpp @@ -258,7 +258,7 @@ void ASurface::transBlitFrom(ASurface &src) { blitFrom(src); } -void ASurface::blitFrom(Graphics::Surface &src) { +void ASurface::blitFrom(const Graphics::Surface &src) { assert(w >= src.w && h >= src.h); for (int y = 0; y < src.h; ++y) { const byte *srcP = (const byte *)src.getBasePtr(0, y); diff --git a/engines/access/asurface.h b/engines/access/asurface.h index 022e2534c1..ce9928c00e 100644 --- a/engines/access/asurface.h +++ b/engines/access/asurface.h @@ -107,7 +107,7 @@ public: virtual void transBlitFrom(ASurface &src); - virtual void blitFrom(Graphics::Surface &src); + virtual void blitFrom(const Graphics::Surface &src); virtual void copyBuffer(Graphics::Surface *src); diff --git a/engines/access/debugger.cpp b/engines/access/debugger.cpp index 6cb2bb606c..601617034f 100644 --- a/engines/access/debugger.cpp +++ b/engines/access/debugger.cpp @@ -52,12 +52,23 @@ Debugger *Debugger::init(AccessEngine *vm) { } } +void Debugger::postEnter() { + if (!_playMovieFile.empty()) { + _vm->playMovie(_playMovieFile, Common::Point(0, 0)); + + _playMovieFile.clear(); + } + + _vm->pauseEngine(false); +} + /*------------------------------------------------------------------------*/ Debugger::Debugger(AccessEngine *vm) : GUI::Debugger(), _vm(vm) { registerCmd("continue", WRAP_METHOD(Debugger, cmdExit)); registerCmd("scene", WRAP_METHOD(Debugger, Cmd_LoadScene)); registerCmd("cheat", WRAP_METHOD(Debugger, Cmd_Cheat)); + registerCmd("playmovie", WRAP_METHOD(Debugger, Cmd_PlayMovie)); switch (vm->getGameID()) { case GType_Amazon: @@ -133,6 +144,19 @@ bool Debugger::Cmd_Cheat(int argc, const char **argv) { return true; } +bool Debugger::Cmd_PlayMovie(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Format: playmovie <movie-file>\n"); + return true; + } + + // play gets postboned until debugger is closed + Common::String filename = argv[1]; + _playMovieFile = filename; + + return cmdExit(0, 0); +} + /*------------------------------------------------------------------------*/ namespace Amazon { diff --git a/engines/access/debugger.h b/engines/access/debugger.h index f4d8df7634..1c1e00327c 100644 --- a/engines/access/debugger.h +++ b/engines/access/debugger.h @@ -35,13 +35,16 @@ class AccessEngine; class Debugger : public GUI::Debugger { protected: AccessEngine *_vm; + Common::String _playMovieFile; bool Cmd_LoadScene(int argc, const char **argv); bool Cmd_Cheat(int argc, const char **argv); + bool Cmd_PlayMovie(int argc, const char **argv); Common::String *_sceneDescr; int _sceneNumb; public: static Debugger *init(AccessEngine *vm); + void postEnter(); public: Debugger(AccessEngine *vm); virtual ~Debugger(); diff --git a/engines/access/module.mk b/engines/access/module.mk index f7cf7f2261..cccb603d31 100644 --- a/engines/access/module.mk +++ b/engines/access/module.mk @@ -21,6 +21,7 @@ MODULE_OBJS := \ scripts.o \ sound.o \ video.o \ + video/movie_decoder.o \ amazon/amazon_game.o \ amazon/amazon_logic.o \ amazon/amazon_player.o \ diff --git a/engines/access/screen.cpp b/engines/access/screen.cpp index 41f6194238..364b0a7eef 100644 --- a/engines/access/screen.cpp +++ b/engines/access/screen.cpp @@ -296,7 +296,7 @@ void Screen::transBlitFrom(ASurface *src, const Common::Rect &bounds) { ASurface::transBlitFrom(src, bounds); } -void Screen::blitFrom(Graphics::Surface &src) { +void Screen::blitFrom(const Graphics::Surface &src) { addDirtyRect(Common::Rect(0, 0, src.w, src.h)); ASurface::blitFrom(src); } diff --git a/engines/access/screen.h b/engines/access/screen.h index 52485e5c7c..5cb85471c6 100644 --- a/engines/access/screen.h +++ b/engines/access/screen.h @@ -98,7 +98,7 @@ public: virtual void transBlitFrom(ASurface *src, const Common::Rect &bounds); - virtual void blitFrom(Graphics::Surface &src); + virtual void blitFrom(const Graphics::Surface &src); virtual void copyBuffer(Graphics::Surface *src); diff --git a/engines/access/video/movie_decoder.cpp b/engines/access/video/movie_decoder.cpp new file mode 100644 index 0000000000..78b7a33fd0 --- /dev/null +++ b/engines/access/video/movie_decoder.cpp @@ -0,0 +1,739 @@ +/* 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. + * + */ + +#include "common/scummsys.h" +#include "common/stream.h" +#include "common/textconsole.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" + +#include "access/access.h" +#include "access/video/movie_decoder.h" + +// for Test-Code +#include "common/system.h" +#include "common/events.h" +#include "common/keyboard.h" +#include "engines/engine.h" +#include "engines/util.h" +#include "graphics/palette.h" +#include "graphics/pixelformat.h" +#include "graphics/surface.h" + +namespace Access { + +AccessVIDMovieDecoder::AccessVIDMovieDecoder() + : _stream(0), _videoTrack(0), _audioTrack(0) { + _streamSeekOffset = 0; + _streamVideoIndex = 0; + _streamAudioIndex = 0; +} + +AccessVIDMovieDecoder::~AccessVIDMovieDecoder() { + close(); +} + +bool AccessVIDMovieDecoder::loadStream(Common::SeekableReadStream *stream) { + uint32 videoCodecTag = 0; + uint32 videoHeight = 0; + uint32 videoWidth = 0; + uint16 regularDelay = 0; + uint32 audioSampleRate = 0; + + close(); + + _stream = stream; + _streamSeekOffset = 15; // offset of first chunk + _streamVideoIndex = 0; + _streamAudioIndex = 0; + + // read header + // ID [dword] "VID" + // ?? [byte] + // ?? [word] + // width [word] + // height [word] + // regular delay between frames (60 per second) [word] + // ?? [word] + + videoCodecTag = _stream->readUint32BE(); + if (videoCodecTag != MKTAG('V','I','D',0x00)) { + warning("AccessVIDMoviePlay: bad codec tag, not a video file?"); + close(); + return false; + } + _stream->skip(3); + videoWidth = _stream->readUint16LE(); + videoHeight = _stream->readUint16LE(); + regularDelay = _stream->readUint16LE(); + _stream->skip(2); + + if (!regularDelay) { + warning("AccessVIDMoviePlay: delay between frames is zero?"); + close(); + return false; + } + + // create video track + _videoTrack = new StreamVideoTrack(videoWidth, videoHeight, regularDelay); + addTrack(_videoTrack); + + //warning("width %d, height %d", videoWidth, videoHeight); + + // Look through the first few packets + static const int maxPacketCheckCount = 10; + + for (int i = 0; i < maxPacketCheckCount; i++) { + byte chunkId = _stream->readByte(); + + // Bail out if done + if (_stream->eos()) + break; + + // Bail also in case end of file chunk was found + if (chunkId == kVIDMovieChunkId_EndOfFile) + break; + + uint32 chunkStartOffset = _stream->pos(); + //warning("data chunk at %x", chunkStartOffset); + + switch (chunkId) { + case kVIDMovieChunkId_FullFrame: + case kVIDMovieChunkId_FullFrameCompressed: + case kVIDMovieChunkId_PartialFrameCompressed: + case kVIDMovieChunkId_FullFrameCompressedFill: { + if (!_videoTrack->skipOverFrame(_stream, chunkId)) { + close(); + return false; + } + break; + } + + case kVIDMovieChunkId_Palette: { + if (!_videoTrack->skipOverPalette(_stream)) { + close(); + return false; + } + break; + } + + case kVIDMovieChunkId_AudioFirstChunk: + case kVIDMovieChunkId_Audio: { + // sync [word] + // sampling rate [byte] + // size of audio data [word] + // sample data [] (mono, 8-bit, unsigned) + // + // Only first chunk has sync + sampling rate + if (chunkId == kVIDMovieChunkId_AudioFirstChunk) { + byte soundblasterRate; + + _stream->skip(2); // skip over sync + soundblasterRate = _stream->readByte(); + audioSampleRate = 1000000 / (256 - soundblasterRate); + + _audioTrack = new StreamAudioTrack(audioSampleRate); + addTrack(_audioTrack); + + _stream->seek(chunkStartOffset); // seek back + } + + if (!_audioTrack) { + warning("AccessVIDMoviePlay: regular audio chunk, before audio chunk w/ header"); + close(); + return false; + } + if (!_audioTrack->skipOverAudio(_stream, chunkId)) { + close(); + return false; + } + break; + } + + default: + warning("AccessVIDMoviePlay: Unknown chunk-id '%x' inside VID movie", chunkId); + close(); + return false; + } + + // Remember this chunk inside our cache + IndexCacheEntry indexCacheEntry; + + indexCacheEntry.chunkId = chunkId; + indexCacheEntry.offset = chunkStartOffset; + + _indexCacheTable.push_back(indexCacheEntry); + + // Got an audio chunk now? -> exit b/c we are done + if (audioSampleRate) + break; + } + + // Remember offset of latest not-indexed-yet chunk + _streamSeekOffset = _stream->pos(); + + // If sample rate was found, create an audio track + if (audioSampleRate) { + _audioTrack = new StreamAudioTrack(audioSampleRate); + addTrack(_audioTrack); + } + + // Rewind back to the beginning right to the first chunk + _stream->seek(15); + + return true; +} + +void AccessVIDMovieDecoder::close() { + Video::VideoDecoder::close(); + + delete _stream; _stream = 0; + _videoTrack = 0; + + _indexCacheTable.clear(); +} + +// We try to at least decode 1 frame +// and also try to get at least 0.5 seconds of audio queued up +void AccessVIDMovieDecoder::readNextPacket() { + uint32 currentMovieTime = getTime(); + uint32 wantedAudioQueued = currentMovieTime + 500; // always try to be 0.500 seconds in front of movie time + + uint32 streamIndex = 0; + IndexCacheEntry indexEntry; + bool currentlySeeking = false; + + bool videoDone = false; + bool audioDone = false; + + // Seek to smallest stream offset + if ((_streamVideoIndex <= _streamAudioIndex) || (!_audioTrack)) { + streamIndex = _streamVideoIndex; + } else { + streamIndex = _streamAudioIndex; + } + + if (_audioTrack) { + if (wantedAudioQueued <= _audioTrack->getTotalAudioQueued()) { + // already got enough audio queued up + audioDone = true; + } + } else { + // no audio track, audio is always done + audioDone = true; + } + + while (1) { + // Check, if stream-index is already cached + if (streamIndex < _indexCacheTable.size()) { + indexEntry.chunkId = _indexCacheTable[streamIndex].chunkId; + indexEntry.offset = _indexCacheTable[streamIndex].offset; + currentlySeeking = false; + + } else { + // read from file + _stream->seek(_streamSeekOffset); + indexEntry.chunkId = _stream->readByte(); + indexEntry.offset = _stream->pos(); + currentlySeeking = true; + + // and store that as well + _indexCacheTable.push_back(indexEntry); + } + + // end of stream -> exit + if (_stream->eos()) + break; + + // end of file chunk -> exit + if (indexEntry.chunkId == kVIDMovieChunkId_EndOfFile) + break; + +// warning("chunk %x", indexEntry.chunkId); + + switch (indexEntry.chunkId) { + case kVIDMovieChunkId_FullFrame: + case kVIDMovieChunkId_FullFrameCompressed: + case kVIDMovieChunkId_PartialFrameCompressed: + case kVIDMovieChunkId_FullFrameCompressedFill: { + if ((_streamVideoIndex <= streamIndex) && (!videoDone)) { + // We are at an index, that is still relevant for video decoding + // and we are not done with video yet + if (!currentlySeeking) { + // seek to stream position in case we used the cache + _stream->seek(indexEntry.offset); + } + //warning("video decode chunk %x at %lx", indexEntry.chunkId, _stream->pos()); + _videoTrack->decodeFrame(_stream, indexEntry.chunkId); + videoDone = true; + _streamVideoIndex = streamIndex + 1; + } else { + if (currentlySeeking) { + // currently seeking, so we have to skip the frame bytes manually + _videoTrack->skipOverFrame(_stream, indexEntry.chunkId); + } + } + break; + } + + case kVIDMovieChunkId_Palette: { + if ((_streamVideoIndex <= streamIndex) && (!videoDone)) { + // We are at an index, that is still relevant for video decoding + // and we are not done with video yet + if (!currentlySeeking) { + // seek to stream position in case we used the cache + _stream->seek(indexEntry.offset); + } + _videoTrack->decodePalette(_stream); + _streamVideoIndex = streamIndex + 1; + } else { + if (currentlySeeking) { + // currently seeking, so we have to skip the frame bytes manually + _videoTrack->skipOverPalette(_stream); + } + } + break; + } + + case kVIDMovieChunkId_AudioFirstChunk: + case kVIDMovieChunkId_Audio: { + if ((_streamAudioIndex <= streamIndex) && (!audioDone)) { + // We are at an index that is still relevant for audio decoding + if (!currentlySeeking) { + // seek to stream position in case we used the cache + _stream->seek(indexEntry.offset); + } + _audioTrack->queueAudio(_stream, indexEntry.chunkId); + _streamAudioIndex = streamIndex + 1; + + if (wantedAudioQueued <= _audioTrack->getTotalAudioQueued()) { + // Got enough audio + audioDone = true; + } + } else { + if (!_audioTrack) { + error("AccessVIDMoviePlay: audio chunks found without audio track active"); + } + if (currentlySeeking) { + // currently seeking, so we have to skip the audio bytes manually + _audioTrack->skipOverAudio(_stream, indexEntry.chunkId); + } + } + break; + } + + default: + error("AccessVIDMoviePlay: Unknown chunk-id '%x' inside VID movie", indexEntry.chunkId); + } + + if (currentlySeeking) { + // remember currently stream offset in case we are seeking + _streamSeekOffset = _stream->pos(); + } + + // go to next index + streamIndex++; + + if ((videoDone) && (audioDone)) { + return; + } + } + + if (!videoDone) { + // no more video frames? set end of video track + _videoTrack->setEndOfTrack(); + } +} + +AccessVIDMovieDecoder::StreamVideoTrack::StreamVideoTrack(uint32 width, uint32 height, uint16 regularFrameDelay) { + _width = width; + _height = height; + _regularFrameDelay = regularFrameDelay; + _curFrame = -1; + _nextFrameStartTime = 0; + _endOfTrack = false; + + memset(&_palette, 0, sizeof(_palette)); + + _surface = new Graphics::Surface(); + _surface->create(_width, _height, Graphics::PixelFormat::createFormatCLUT8()); +} + +AccessVIDMovieDecoder::StreamVideoTrack::~StreamVideoTrack() { + delete _surface; +} + +bool AccessVIDMovieDecoder::StreamVideoTrack::endOfTrack() const { + return _endOfTrack; +} + +Graphics::PixelFormat AccessVIDMovieDecoder::StreamVideoTrack::getPixelFormat() const { + return _surface->format; +} + +void AccessVIDMovieDecoder::StreamVideoTrack::decodeFrame(Common::SeekableReadStream *stream, byte chunkId) { + byte *framePixelsPtr = (byte *)_surface->getPixels(); + byte *pixelsPtr = framePixelsPtr; + byte rleByte = 0; + uint16 additionalDelay = 0; + int32 expectedPixels = 0; + + switch (chunkId) { + case kVIDMovieChunkId_FullFrame: { + // Full frame is: + // data [width * height] + additionalDelay = stream->readUint16LE(); + stream->read(framePixelsPtr, _width * _height); + break; + } + + case kVIDMovieChunkId_FullFrameCompressed: + case kVIDMovieChunkId_PartialFrameCompressed: { + // Skip manually over compressed data + // Full frame compressed is: + // additional delay [word] + // REPEAT: + // RLE [byte] + // RLE upper bit set: skip over RLE & 0x7F pixels + // RLE upper bit not set: draw RLE amount of pixels (those pixels follow right after RLE byte) + // + // Partial frame compressed is: + // sync [word] + // horizontal start position [word] + // REPEAT: + // see full frame compressed + uint16 horizontalStartPosition = 0; + + additionalDelay = stream->readUint16LE(); + + if (chunkId == kVIDMovieChunkId_PartialFrameCompressed) { + horizontalStartPosition = stream->readUint16LE(); + if (horizontalStartPosition >= _height) { + error("AccessVIDMoviePlay: starting position larger than height during partial frame compressed, data corrupt?"); + return; + } + } + + byte rleByte = 0; + expectedPixels = _width * (_height - horizontalStartPosition); + + // adjust frame destination pointer + pixelsPtr += (horizontalStartPosition * _width); + + while (expectedPixels >= 0) { + rleByte = stream->readByte(); + if (!rleByte) // NUL means end of stream + break; + + if (rleByte & 0x80) { + rleByte = rleByte & 0x7F; + expectedPixels -= rleByte; + } else { + // skip over pixels + expectedPixels -= rleByte; + stream->read(pixelsPtr, rleByte); // read pixel data into frame + } + pixelsPtr += rleByte; + } + // expectedPixels may be positive here in case stream got terminated with a NUL + if (expectedPixels < 0) { + error("AccessVIDMoviePlay: pixel count mismatch during full/partial frame compressed, data corrupt?"); + } + break; + } + + case kVIDMovieChunkId_FullFrameCompressedFill: { + // Full frame compressed fill is: + // additional delay [word] + // REPEAT: + // RLE [byte] + // RLE upper bit set: draw RLE amount (& 0x7F) of pixels with specified color (color byte follows after RLE byte) + // RLE upper bit not set: draw RLE amount of pixels (those pixels follow right after RLE byte) + additionalDelay = stream->readUint16LE(); + expectedPixels = _width * _height; + + while (expectedPixels > 0) { + rleByte = stream->readByte(); + + if (rleByte & 0x80) { + rleByte = rleByte & 0x7F; + expectedPixels -= rleByte; + + byte fillColor = stream->readByte(); + memset(pixelsPtr, fillColor, rleByte); + } else { + // skip over pixels + expectedPixels -= rleByte; + stream->read(pixelsPtr, rleByte); // read pixel data into frame + } + pixelsPtr += rleByte; + } + if (expectedPixels < 0) { + error("AccessVIDMoviePlay: pixel count mismatch during full frame compressed fill, data corrupt?"); + } + break; + } + default: + assert(0); + break; + } + + _curFrame++; + + // TODO: not sure, if additionalDelay is supposed to affect the follow-up frame or the current frame + // the videos, that I found, don't have it set + uint32 currentFrameStartTime = getNextFrameStartTime(); + uint32 nextFrameStartTime = (_regularFrameDelay * _curFrame) * 1000 / 60; + if (additionalDelay) { + nextFrameStartTime += additionalDelay * 1000 / 60; + } + assert(currentFrameStartTime <= nextFrameStartTime); + setNextFrameStartTime(nextFrameStartTime); +} + +bool AccessVIDMovieDecoder::StreamVideoTrack::skipOverFrame(Common::SeekableReadStream *stream, byte chunkId) { + byte rleByte = 0; + int32 expectedPixels = 0; + + switch (chunkId) { + case kVIDMovieChunkId_FullFrame: { + // Full frame is: + // additional delay [word] + // data [width * height] + stream->skip(2); + stream->skip(_width * _height); + break; + } + + case kVIDMovieChunkId_FullFrameCompressed: + case kVIDMovieChunkId_PartialFrameCompressed: { + // Skip manually over compressed data + // Full frame compressed is: + // additional delay [word] + // REPEAT: + // RLE [byte] + // RLE upper bit set: skip over RLE & 0x7F pixels + // RLE upper bit not set: draw RLE amount of pixels (those pixels follow right after RLE byte) + // + // Partial frame compressed is: + // sync [word] + // horizontal start position [word] + // REPEAT: + // see full frame compressed + uint16 horizontalStartPosition = 0; + + stream->skip(2); + + if (chunkId == kVIDMovieChunkId_PartialFrameCompressed) { + horizontalStartPosition = stream->readUint16LE(); + if (horizontalStartPosition >= _height) { + warning("AccessVIDMoviePlay: starting position larger than height during partial frame compressed, data corrupt?"); + return false; + } + } + + byte rleByte = 0; + expectedPixels = _width * (_height - horizontalStartPosition); + + while (expectedPixels >= 0) { + rleByte = stream->readByte(); + if (!rleByte) // NUL means end of stream + break; + + if (rleByte & 0x80) { + expectedPixels -= rleByte & 0x7F; + } else { + // skip over pixels + expectedPixels -= rleByte; + stream->skip(rleByte); // skip over pixel data + } + } + // expectedPixels may be positive here in case stream got terminated with a NUL + if (expectedPixels < 0) { + warning("AccessVIDMoviePlay: pixel count mismatch during full/partial frame compressed, data corrupt?"); + return false; + } + break; + } + + case kVIDMovieChunkId_FullFrameCompressedFill: { + // Full frame compressed fill is: + // additional delay [word] + // REPEAT: + // RLE [byte] + // RLE upper bit set: draw RLE amount (& 0x7F) of pixels with specified color (color byte follows after RLE byte) + // RLE upper bit not set: draw RLE amount of pixels (those pixels follow right after RLE byte) + stream->skip(2); + expectedPixels = _width * _height; + + while (expectedPixels > 0) { + rleByte = stream->readByte(); + + if (rleByte & 0x80) { + expectedPixels -= rleByte & 0x7F; + stream->skip(1); + } else { + // skip over pixels + expectedPixels -= rleByte; + stream->skip(rleByte); // skip over pixel data + } + } + if (expectedPixels < 0) { + warning("AccessVIDMoviePlay: pixel count mismatch during full frame compressed fill, data corrupt?"); + return false; + } + break; + } + default: + assert(0); + break; + } + return true; +} + +bool AccessVIDMovieDecoder::StreamVideoTrack::skipOverPalette(Common::SeekableReadStream *stream) { + stream->skip(0x300); // 3 bytes per color, 256 colors + return true; +} + +void AccessVIDMovieDecoder::StreamVideoTrack::decodePalette(Common::SeekableReadStream *stream) { + assert(stream); + + for (uint16 curColor = 0; curColor < 256; curColor++) { + _palette[curColor * 3] = stream->readByte(); + _palette[curColor * 3 + 1] = stream->readByte(); + _palette[curColor * 3 + 2] = stream->readByte(); + } + + _dirtyPalette = true; +} + +const byte *AccessVIDMovieDecoder::StreamVideoTrack::getPalette() const { + _dirtyPalette = false; + return _palette; +} + +bool AccessVIDMovieDecoder::StreamVideoTrack::hasDirtyPalette() const { + return _dirtyPalette; +} + +AccessVIDMovieDecoder::StreamAudioTrack::StreamAudioTrack(uint32 sampleRate) { + _totalAudioQueued = 0; // currently 0 milliseconds queued + + _sampleRate = sampleRate; + _stereo = false; // always mono + + _audioStream = Audio::makeQueuingAudioStream(sampleRate, _stereo); +} + +AccessVIDMovieDecoder::StreamAudioTrack::~StreamAudioTrack() { + delete _audioStream; +} + +void AccessVIDMovieDecoder::StreamAudioTrack::queueAudio(Common::SeekableReadStream *stream, byte chunkId) { + Common::SeekableReadStream *rawAudioStream = 0; + Audio::RewindableAudioStream *audioStream = 0; + uint32 audioLengthMSecs = 0; + + if (chunkId == kVIDMovieChunkId_AudioFirstChunk) { + stream->skip(3); // skip over additional delay + sample rate + } + + uint32 audioSize = stream->readUint16LE(); + + // Read the specified chunk into memory + rawAudioStream = stream->readStream(audioSize); + audioLengthMSecs = audioSize * 1000 / _sampleRate; // 1 byte == 1 8-bit sample + + audioStream = Audio::makeRawStream(rawAudioStream, _sampleRate, Audio::FLAG_UNSIGNED | Audio::FLAG_LITTLE_ENDIAN, DisposeAfterUse::YES); + if (audioStream) { + _totalAudioQueued += audioLengthMSecs; + _audioStream->queueAudioStream(audioStream, DisposeAfterUse::YES); + } else { + // in case there was an error + delete rawAudioStream; + } +} + +bool AccessVIDMovieDecoder::StreamAudioTrack::skipOverAudio(Common::SeekableReadStream *stream, byte chunkId) { + if (chunkId == kVIDMovieChunkId_AudioFirstChunk) { + stream->skip(3); // skip over additional delay + sample rate + } + uint32 audioSize = stream->readUint16LE(); + stream->skip(audioSize); + return true; +} + +Audio::AudioStream *AccessVIDMovieDecoder::StreamAudioTrack::getAudioStream() const { + return _audioStream; +} + +bool AccessEngine::playMovie(const Common::String &filename, const Common::Point &pos) { + AccessVIDMovieDecoder *videoDecoder = new AccessVIDMovieDecoder(); + + Common::Point framePos(pos.x, pos.y); + + if (!videoDecoder->loadFile(filename)) { + warning("AccessVIDMoviePlay: could not open '%s'", filename.c_str()); + return false; + } + + bool skipVideo = false; + uint16 width = videoDecoder->getWidth(); + uint16 height = videoDecoder->getHeight(); + + _events->clearEvents(); + videoDecoder->start(); + + while (!shouldQuit() && !videoDecoder->endOfVideo() && !skipVideo) { + if (videoDecoder->needsUpdate()) { + const Graphics::Surface *frame = videoDecoder->decodeNextFrame(); + + if (frame) { + _screen->blitFrom(*frame); + + if (videoDecoder->hasDirtyPalette()) { + const byte *palette = videoDecoder->getPalette(); + g_system->getPaletteManager()->setPalette(palette, 0, 256); + } + + _screen->updateScreen(); + } + } + + _events->pollEventsAndWait(); + + Common::KeyState keyState; + if (_events->getKey(keyState)) { + if (keyState.keycode == Common::KEYCODE_ESCAPE) + skipVideo = true; + } + } + + videoDecoder->close(); + delete videoDecoder; + + return !skipVideo; +} + +} // End of namespace Access diff --git a/engines/access/video/movie_decoder.h b/engines/access/video/movie_decoder.h new file mode 100644 index 0000000000..dc4a87720c --- /dev/null +++ b/engines/access/video/movie_decoder.h @@ -0,0 +1,158 @@ +/* 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. + * + */ + +#ifndef ACCESS_VIDEO_MOVIE_DECODER_H +#define ACCESS_VIDEO_MOVIE_DECODER_H + +#include "common/rect.h" +#include "video/video_decoder.h" +#include "audio/decoders/raw.h" + +namespace Audio { +class QueuingAudioStream; +} + +namespace Common { +class SeekableReadStream; +} + +namespace Image { +class Codec; +} + +namespace Access { + +enum kDebugLevels { + kVIDMovieChunkId_FullFrame = 0x00, + kVIDMovieChunkId_FullFrameCompressed = 0x01, + kVIDMovieChunkId_Palette = 0x02, + kVIDMovieChunkId_FullFrameCompressedFill = 0x03, + kVIDMovieChunkId_PartialFrameCompressed = 0x04, + kVIDMovieChunkId_EndOfFile = 0x14, + kVIDMovieChunkId_AudioFirstChunk = 0x7C, + kVIDMovieChunkId_Audio = 0x7D +}; + +// This video format is used in at least the following Access engine games: +// - Noctropolis +// - Synnergist + +class AccessVIDMovieDecoder : public Video::VideoDecoder { +public: + AccessVIDMovieDecoder(); + ~AccessVIDMovieDecoder(); + + bool loadStream(Common::SeekableReadStream *stream); + void close(); + +protected: + void readNextPacket(); + +private: + bool streamSkipFullFrameCompressedFill(); + +private: + int32 _streamSeekOffset; /* current stream offset, pointing to not-yet-indexed stream position */ + uint32 _streamVideoIndex; /* current stream index for video decoding */ + uint32 _streamAudioIndex; /* current stream index for audio decoding */ + + struct IndexCacheEntry { + byte chunkId; + int32 offset; + }; + + Common::Array<IndexCacheEntry> _indexCacheTable; + +private: + class StreamVideoTrack : public VideoTrack { + public: + StreamVideoTrack(uint32 width, uint32 height, uint16 regularFrameDelay); + ~StreamVideoTrack(); + + bool endOfTrack() const; + + uint16 getWidth() const { return _width; } + uint16 getHeight() const { return _height; } + Graphics::PixelFormat getPixelFormat() const; + int getCurFrame() const { return _curFrame; } + void setNextFrameStartTime(uint32 nextFrameStartTime) { _nextFrameStartTime = nextFrameStartTime; } + uint32 getNextFrameStartTime() const { return _nextFrameStartTime; } + const Graphics::Surface *decodeNextFrame() { return _surface; } + + const byte *getPalette() const; + bool hasDirtyPalette() const; + + void decodePalette(Common::SeekableReadStream *stream); + void decodeFrame(Common::SeekableReadStream *stream, byte chunkId); + bool skipOverFrame(Common::SeekableReadStream *stream, byte chunkId); + bool skipOverPalette(Common::SeekableReadStream *stream); + + void setEndOfTrack() { _endOfTrack = true; } + + private: + Graphics::Surface *_surface; + + int _curFrame; + uint32 _nextFrameStartTime; + + byte _palette[3 * 256]; + mutable bool _dirtyPalette; + uint16 _width, _height; + + uint16 _regularFrameDelay; // delay between frames (1 = 1/60 of a second) + bool _endOfTrack; + }; + + class StreamAudioTrack : public AudioTrack { + public: + StreamAudioTrack(uint32 sampleRate); + ~StreamAudioTrack(); + + void queueAudio(Common::SeekableReadStream *stream, byte chunkId); + bool skipOverAudio(Common::SeekableReadStream *stream, byte chunkId); + + protected: + Audio::AudioStream *getAudioStream() const; + + private: + Audio::QueuingAudioStream *_audioStream; + uint32 _totalAudioQueued; /* total amount of milliseconds of audio, that we queued up already */ + + public: + uint32 getTotalAudioQueued() const { return _totalAudioQueued; } + + private: + int16 decodeSample(uint8 dataNibble); + + uint32 _codecTag; + uint16 _sampleRate; + bool _stereo; + }; + + Common::SeekableReadStream *_stream; + StreamVideoTrack *_videoTrack; + StreamAudioTrack *_audioTrack; +}; + +} // End of namespace Access + +#endif |