diff options
Diffstat (limited to 'engines/sci/video/robot_decoder.cpp')
-rw-r--r-- | engines/sci/video/robot_decoder.cpp | 1807 |
1 files changed, 1507 insertions, 300 deletions
diff --git a/engines/sci/video/robot_decoder.cpp b/engines/sci/video/robot_decoder.cpp index a2795d21f9..f3354f9e44 100644 --- a/engines/sci/video/robot_decoder.cpp +++ b/engines/sci/video/robot_decoder.cpp @@ -20,391 +20,1598 @@ * */ -#include "common/archive.h" -#include "common/stream.h" -#include "common/substream.h" -#include "common/system.h" -#include "common/textconsole.h" -#include "common/util.h" - -#include "graphics/surface.h" -#include "audio/audiostream.h" -#include "audio/decoders/raw.h" - -#include "sci/resource.h" -#include "sci/util.h" -#include "sci/sound/audio.h" #include "sci/video/robot_decoder.h" +#include "common/archive.h" // for SearchMan +#include "common/debug.h" // for debugC +#include "common/endian.h" // for MKTAG +#include "common/memstream.h" // for MemoryReadStream +#include "common/platform.h" // for Platform::kPlatformMacintosh +#include "common/rational.h" // for operator*, Rational +#include "common/str.h" // for String +#include "common/stream.h" // for SeekableReadStream +#include "common/substream.h" // for SeekableSubReadStreamEndian +#include "common/textconsole.h" // for error, warning +#include "common/types.h" // for Flag::NO, Flag::YES +#include "sci/engine/seg_manager.h" // for SegManager +#include "sci/graphics/celobj32.h" // for Ratio, ::kLowResX, ::kLowResY +#include "sci/graphics/text32.h" // for BitmapResource +#include "sci/sound/audio32.h" // for Audio32 +#include "sci/sci.h" // for kDebugLevels::kDebugLevelVideo +#include "sci/util.h" // for READ_SCI11ENDIAN_UINT16, READ_SC... namespace Sci { -// TODO: -// - Positioning -// - Proper handling of frame scaling - scaled frames look squashed -// (probably because both dimensions should be scaled) -// - Transparency support -// - Timing - the arbitrary 100ms delay between each frame is not quite right -// - Proper handling of sound chunks in some cases, so that the frame size -// table can be ignored (it's only used to determine the correct sound chunk -// size at the moment, cause it can be wrong in some cases) -// - Fix audio "hiccups" - probably data that shouldn't be in the audio frames - - -// Some non technical information on robot files, from an interview with -// Greg Tomko-Pavia of Sierra On-Line -// Taken from http://anthonylarme.tripod.com/phantas/phintgtp.html -// -// (...) What we needed was a way of playing video, but have it blend into -// normal room art instead of occupying its own rectangular area. Room art -// consists of a background pic overlaid with various animating cels -// (traditional lingo: sprites). The cels each have a priority that determines -// who is on top and who is behind in the drawing order. Cels are read from -// *.v56 files (another proprietary format). A Robot is video frames with -// transparent background including priority and x,y information. Thus, it is -// like a cel, except it comes from an RBT - not a v56. Because it blends into -// our graphics engine, it looks just like a part of the room. A RBT can move -// around the screen and go behind other objects. (...) - -enum RobotPalTypes { - kRobotPalVariable = 0, - kRobotPalConstant = 1 -}; - -RobotDecoder::RobotDecoder(bool isBigEndian) { - _fileStream = 0; - _pos = Common::Point(0, 0); - _isBigEndian = isBigEndian; - _frameTotalSize = 0; +#pragma mark RobotAudioStream + +extern void deDPCM16(int16 *out, const byte *in, const uint32 numBytes, int16 &sample); + +RobotAudioStream::RobotAudioStream(const int32 bufferSize) : + _loopBuffer((byte *)malloc(bufferSize)), + _loopBufferSize(bufferSize), + _decompressionBuffer(nullptr), + _decompressionBufferSize(0), + _decompressionBufferPosition(-1), + _waiting(true), + _finished(false), + _firstPacketPosition(-1) {} + +RobotAudioStream::~RobotAudioStream() { + free(_loopBuffer); + free(_decompressionBuffer); } -RobotDecoder::~RobotDecoder() { - close(); +static void interpolateChannel(int16 *buffer, int32 numSamples, const int8 bufferIndex) { + if (numSamples <= 0) { + return; + } + + if (bufferIndex) { + int16 lastSample = *buffer; + int sample = lastSample; + int16 *target = buffer + 1; + const int16 *source = buffer + 2; + --numSamples; + + while (numSamples--) { + sample = *source + lastSample; + lastSample = *source; + sample /= 2; + *target = sample; + source += 2; + target += 2; + } + + *target = sample; + } else { + int16 *target = buffer; + const int16 *source = buffer + 1; + int16 lastSample = *source; + + while (numSamples--) { + int sample = *source + lastSample; + lastSample = *source; + sample /= 2; + *target = sample; + source += 2; + target += 2; + } + } } -bool RobotDecoder::loadStream(Common::SeekableReadStream *stream) { - close(); +static void copyEveryOtherSample(int16 *out, const int16 *in, int numSamples) { + while (numSamples--) { + *out = *in++; + out += 2; + } +} + +bool RobotAudioStream::addPacket(const RobotAudioPacket &packet) { + Common::StackLock lock(_mutex); + + if (_finished) { + warning("Packet %d sent to finished robot audio stream", packet.position); + return false; + } + + // `packet.position` is the decompressed (doubled) position of the packet, + // so values of `position` will always be divisible either by 2 (even) or by + // 4 (odd). + const int8 bufferIndex = packet.position % 4 ? 1 : 0; + + // Packet 0 is the first primer, packet 2 is the second primer, + // packet 4+ are regular audio data + if (packet.position <= 2 && _firstPacketPosition == -1) { + _readHead = 0; + _readHeadAbs = 0; + _maxWriteAbs = _loopBufferSize; + _writeHeadAbs = 2; + _jointMin[0] = 0; + _jointMin[1] = 2; + _waiting = true; + _finished = false; + _firstPacketPosition = packet.position; + fillRobotBuffer(packet, bufferIndex); + return true; + } - _fileStream = new Common::SeekableSubReadStreamEndian(stream, 0, stream->size(), _isBigEndian, DisposeAfterUse::YES); + const int32 packetEndByte = packet.position + (packet.dataSize * sizeof(int16) * kEOSExpansion); - readHeaderChunk(); + // Already read all the way past this packet (or already wrote valid samples + // to this channel all the way past this packet), so discard it + if (packetEndByte <= MAX(_readHeadAbs, _jointMin[bufferIndex])) { + debugC(kDebugLevelVideo, "Rejecting packet %d, read past %d / %d", packet.position, _readHeadAbs, _jointMin[bufferIndex]); + return true; + } - // There are several versions of robot files, ranging from 3 to 6. - // v3: no known examples - // v4: PQ:SWAT demo - // v5: SCI2.1 and SCI3 games - // v6: SCI3 games - if (_header.version < 4 || _header.version > 6) - error("Unknown robot version: %d", _header.version); + // The loop buffer is full, so tell the caller to send the packet again + // later + if (_maxWriteAbs <= _jointMin[bufferIndex]) { + debugC(kDebugLevelVideo, "Rejecting packet %d, full buffer", packet.position); + return false; + } - RobotVideoTrack *videoTrack = new RobotVideoTrack(_header.frameCount); - addTrack(videoTrack); + fillRobotBuffer(packet, bufferIndex); - if (_header.hasSound) - addTrack(new RobotAudioTrack()); + // This packet is the second primer, so allow playback to begin + if (_firstPacketPosition != -1 && _firstPacketPosition != packet.position) { + debugC(kDebugLevelVideo, "Done waiting. Robot audio begins"); + _waiting = false; + _firstPacketPosition = -1; + } - videoTrack->readPaletteChunk(_fileStream, _header.paletteDataSize); - readFrameSizesChunk(); - videoTrack->calculateVideoDimensions(_fileStream, _frameTotalSize); + // Only part of the packet could be read into the loop buffer before it was + // full, so tell the caller to send the packet again later + if (packetEndByte > _maxWriteAbs) { + debugC(kDebugLevelVideo, "Partial read of packet %d (%d / %d)", packet.position, packetEndByte - _maxWriteAbs, packetEndByte - packet.position); + return false; + } + + // The entire packet was successfully read into the loop buffer return true; } -bool RobotDecoder::load(GuiResourceId id) { - // TODO: RAMA's robot 1003 cannot be played (shown at the menu screen) - - // its drawn at odd coordinates. SV can't play it either (along with some - // others), so it must be some new functionality added in RAMA's robot - // videos. Skip it for now. - if (g_sci->getGameId() == GID_RAMA && id == 1003) - return false; +void RobotAudioStream::fillRobotBuffer(const RobotAudioPacket &packet, const int8 bufferIndex) { + int32 sourceByte = 0; - // Robots for the options in the RAMA menu - if (g_sci->getGameId() == GID_RAMA && (id >= 1004 && id <= 1009)) - return false; + const int32 decompressedSize = packet.dataSize * sizeof(int16); + if (_decompressionBufferPosition != packet.position) { + if (decompressedSize != _decompressionBufferSize) { + _decompressionBuffer = (byte *)realloc(_decompressionBuffer, decompressedSize); + _decompressionBufferSize = decompressedSize; + } - // TODO: The robot video in the Lighthouse demo gets stuck - if (g_sci->getGameId() == GID_LIGHTHOUSE && id == 16) - return false; + int16 carry = 0; + deDPCM16((int16 *)_decompressionBuffer, packet.data, packet.dataSize, carry); + _decompressionBufferPosition = packet.position; + } + + int32 numBytes = decompressedSize; + int32 packetPosition = packet.position; + int32 endByte = packet.position + decompressedSize * kEOSExpansion; + int32 startByte = MAX(_readHeadAbs + bufferIndex * 2, _jointMin[bufferIndex]); + int32 maxWriteByte = _maxWriteAbs + bufferIndex * 2; + if (packetPosition < startByte) { + sourceByte = (startByte - packetPosition) / kEOSExpansion; + numBytes -= sourceByte; + packetPosition = startByte; + } + if (packetPosition > maxWriteByte) { + numBytes += (packetPosition - maxWriteByte) / kEOSExpansion; + packetPosition = maxWriteByte; + } + if (endByte > maxWriteByte) { + numBytes -= (endByte - maxWriteByte) / kEOSExpansion; + endByte = maxWriteByte; + } - Common::String fileName = Common::String::format("%d.rbt", id); + const int32 maxJointMin = MAX(_jointMin[0], _jointMin[1]); + if (endByte > maxJointMin) { + _writeHeadAbs += endByte - maxJointMin; + } + + if (packetPosition > _jointMin[bufferIndex]) { + int32 packetEndByte = packetPosition % _loopBufferSize; + int32 targetBytePosition; + int32 numBytesToEnd; + if ((packetPosition & ~3) > (_jointMin[1 - bufferIndex] & ~3)) { + targetBytePosition = _jointMin[1 - bufferIndex] % _loopBufferSize; + if (targetBytePosition >= packetEndByte) { + numBytesToEnd = _loopBufferSize - targetBytePosition; + memset(_loopBuffer + targetBytePosition, 0, numBytesToEnd); + targetBytePosition = (1 - bufferIndex) ? 2 : 0; + } + numBytesToEnd = packetEndByte - targetBytePosition; + if (numBytesToEnd > 0) { + memset(_loopBuffer + targetBytePosition, 0, numBytesToEnd); + } + } + targetBytePosition = _jointMin[bufferIndex] % _loopBufferSize; + if (targetBytePosition >= packetEndByte) { + numBytesToEnd = _loopBufferSize - targetBytePosition; + interpolateChannel((int16 *)(_loopBuffer + targetBytePosition), numBytesToEnd / sizeof(int16) / kEOSExpansion, 0); + targetBytePosition = bufferIndex ? 2 : 0; + } + numBytesToEnd = packetEndByte - targetBytePosition; + if (numBytesToEnd > 0) { + interpolateChannel((int16 *)(_loopBuffer + targetBytePosition), numBytesToEnd / sizeof(int16) / kEOSExpansion, 0); + } + } + + if (numBytes > 0) { + int32 targetBytePosition = packetPosition % _loopBufferSize; + int32 packetEndByte = endByte % _loopBufferSize; + int32 numBytesToEnd = 0; + if (targetBytePosition >= packetEndByte) { + numBytesToEnd = (_loopBufferSize - (targetBytePosition & ~3)) / kEOSExpansion; + copyEveryOtherSample((int16 *)(_loopBuffer + targetBytePosition), (int16 *)(_decompressionBuffer + sourceByte), numBytesToEnd / kEOSExpansion); + targetBytePosition = bufferIndex ? 2 : 0; + } + copyEveryOtherSample((int16 *)(_loopBuffer + targetBytePosition), (int16 *)(_decompressionBuffer + sourceByte + numBytesToEnd), (packetEndByte - targetBytePosition) / sizeof(int16) / kEOSExpansion); + } + _jointMin[bufferIndex] = endByte; +} + +void RobotAudioStream::interpolateMissingSamples(int32 numSamples) { + int32 numBytes = numSamples * sizeof(int16) * kEOSExpansion; + int32 targetPosition = _readHead; + + if (_readHeadAbs > _jointMin[1]) { + if (_readHeadAbs > _jointMin[0]) { + if (targetPosition + numBytes >= _loopBufferSize) { + const int32 numBytesToEdge = (_loopBufferSize - targetPosition); + memset(_loopBuffer + targetPosition, 0, numBytesToEdge); + numBytes -= numBytesToEdge; + targetPosition = 0; + } + memset(_loopBuffer + targetPosition, 0, numBytes); + _jointMin[0] += numBytes; + _jointMin[1] += numBytes; + } else { + if (targetPosition + numBytes >= _loopBufferSize) { + const int32 numSamplesToEdge = (_loopBufferSize - targetPosition) / sizeof(int16) / kEOSExpansion; + interpolateChannel((int16 *)(_loopBuffer + targetPosition), numSamplesToEdge, 1); + numSamples -= numSamplesToEdge; + targetPosition = 0; + } + interpolateChannel((int16 *)(_loopBuffer + targetPosition), numSamples, 1); + _jointMin[1] += numBytes; + } + } else if (_readHeadAbs > _jointMin[0]) { + if (targetPosition + numBytes >= _loopBufferSize) { + const int32 numSamplesToEdge = (_loopBufferSize - targetPosition) / sizeof(int16) / kEOSExpansion; + interpolateChannel((int16 *)(_loopBuffer + targetPosition), numSamplesToEdge, 0); + numSamples -= numSamplesToEdge; + targetPosition = 2; + } + interpolateChannel((int16 *)(_loopBuffer + targetPosition), numSamples, 0); + _jointMin[0] += numBytes; + } +} + +void RobotAudioStream::finish() { + Common::StackLock lock(_mutex); + _finished = true; +} + +RobotAudioStream::StreamState RobotAudioStream::getStatus() const { + Common::StackLock lock(_mutex); + StreamState status; + status.bytesPlaying = _readHeadAbs; + status.rate = getRate(); + status.bits = 8 * sizeof(int16); + return status; +} + +int RobotAudioStream::readBuffer(Audio::st_sample_t *outBuffer, int numSamples) { + Common::StackLock lock(_mutex); + + if (_waiting) { + return 0; + } + + assert(!((_writeHeadAbs - _readHeadAbs) & 1)); + const int maxNumSamples = (_writeHeadAbs - _readHeadAbs) / sizeof(Audio::st_sample_t); + numSamples = MIN(numSamples, maxNumSamples); + + if (!numSamples) { + return 0; + } + + interpolateMissingSamples(numSamples); + + Audio::st_sample_t *inBuffer = (Audio::st_sample_t *)(_loopBuffer + _readHead); + + assert(!((_loopBufferSize - _readHead) & 1)); + const int numSamplesToEnd = (_loopBufferSize - _readHead) / sizeof(Audio::st_sample_t); + + int numSamplesToRead = MIN(numSamples, numSamplesToEnd); + Common::copy(inBuffer, inBuffer + numSamplesToRead, outBuffer); + + if (numSamplesToRead < numSamples) { + inBuffer = (Audio::st_sample_t *)_loopBuffer; + outBuffer += numSamplesToRead; + numSamplesToRead = numSamples - numSamplesToRead; + Common::copy(inBuffer, inBuffer + numSamplesToRead, outBuffer); + } + + const int32 numBytes = numSamples * sizeof(Audio::st_sample_t); + + _readHead += numBytes; + if (_readHead > _loopBufferSize) { + _readHead -= _loopBufferSize; + } + _readHeadAbs += numBytes; + _maxWriteAbs += numBytes; + assert(!(_readHead & 1)); + assert(!(_readHeadAbs & 1)); + + return numSamples; +} + +#pragma mark - +#pragma mark RobotDecoder + +RobotDecoder::RobotDecoder(SegManager *segMan) : + _delayTime(this), + _segMan(segMan), + _status(kRobotStatusUninitialized), + _audioBuffer(nullptr), + _rawPalette((uint8 *)malloc(kRawPaletteSize)) {} + +RobotDecoder::~RobotDecoder() { + close(); + free(_rawPalette); + free(_audioBuffer); +} + +#pragma mark - +#pragma mark RobotDecoder - Initialization + +void RobotDecoder::initStream(const GuiResourceId robotId) { + const Common::String fileName = Common::String::format("%d.rbt", robotId); Common::SeekableReadStream *stream = SearchMan.createReadStreamForMember(fileName); + _fileOffset = 0; - if (!stream) { - warning("Unable to open robot file %s", fileName.c_str()); - return false; + if (stream == nullptr) { + error("Unable to open robot file %s", fileName.c_str()); + } + + const uint16 id = stream->readUint16LE(); + if (id != 0x16) { + error("Invalid robot file %s", fileName.c_str()); + } + + // TODO: Mac version not tested, so this could be totally wrong + _stream = new Common::SeekableSubReadStreamEndian(stream, 0, stream->size(), g_sci->getPlatform() == Common::kPlatformMacintosh, DisposeAfterUse::YES); + _stream->seek(2, SEEK_SET); + if (_stream->readUint32BE() != MKTAG('S', 'O', 'L', 0)) { + error("Resource %s is not Robot type!", fileName.c_str()); + } +} + +void RobotDecoder::initPlayback() { + _startFrameNo = 0; + _startTime = -1; + _startingFrameNo = -1; + _cueForceShowFrame = -1; + _previousFrameNo = -1; + _currentFrameNo = 0; + _status = kRobotStatusPaused; +} + +void RobotDecoder::initAudio() { + _syncFrame = true; + + _audioRecordInterval = RobotAudioStream::kRobotSampleRate / _frameRate; + + // TODO: Might actually be for all games newer than Lighthouse; check to + // see which games have this condition. + if (g_sci->getGameId() != GID_LIGHTHOUSE && !(_audioRecordInterval & 1)) { + ++_audioRecordInterval; + } + + _expectedAudioBlockSize = _audioBlockSize - kAudioBlockHeaderSize; + _audioBuffer = (byte *)realloc(_audioBuffer, kRobotZeroCompressSize + _expectedAudioBlockSize); + + if (_primerReservedSize != 0) { + const int32 primerHeaderPosition = _stream->pos(); + _totalPrimerSize = _stream->readSint32(); + const int16 compressionType = _stream->readSint16(); + _evenPrimerSize = _stream->readSint32(); + _oddPrimerSize = _stream->readSint32(); + _primerPosition = _stream->pos(); + + if (compressionType) { + error("Unknown audio header compression type %d", compressionType); + } + + if (_evenPrimerSize + _oddPrimerSize != _primerReservedSize) { + _stream->seek(primerHeaderPosition + _primerReservedSize, SEEK_SET); + } + } else if (_primerZeroCompressFlag) { + _evenPrimerSize = 19922; + _oddPrimerSize = 21024; + } + + _firstAudioRecordPosition = _evenPrimerSize * 2; + + const int usedEachFrame = (RobotAudioStream::kRobotSampleRate / 2) / _frameRate; + _maxSkippablePackets = MAX(0, _audioBlockSize / usedEachFrame - 1); +} + +void RobotDecoder::initVideo(const int16 x, const int16 y, const int16 scale, const reg_t plane, const bool hasPalette, const uint16 paletteSize) { + _position = Common::Point(x, y); + + if (scale != 128) { + _scaleInfo.x = scale; + _scaleInfo.y = scale; + _scaleInfo.signal = kScaleSignalDoScaling32; + } + + _plane = g_sci->_gfxFrameout->getPlanes().findByObject(plane); + if (_plane == nullptr) { + error("Invalid plane %04x:%04x passed to RobotDecoder::open", PRINT_REG(plane)); + } + + _minFrameRate = _frameRate - kMaxFrameRateDrift; + _maxFrameRate = _frameRate + kMaxFrameRateDrift; + + if (_xResolution == 0 || _yResolution == 0) { + // TODO: Default values were taken from RESOURCE.CFG hires property + // if it exists, so need to check games' configuration files for those + _xResolution = g_sci->_gfxFrameout->getCurrentBuffer().screenWidth; + _yResolution = g_sci->_gfxFrameout->getCurrentBuffer().screenHeight; } - return loadStream(stream); + if (hasPalette) { + _stream->read(_rawPalette, paletteSize); + } else { + _stream->seek(paletteSize, SEEK_CUR); + } + + _screenItemList.reserve(kScreenItemListSize); + _maxCelArea.reserve(kFixedCelListSize); + + // Fixed cel buffers are for version 5 and newer + _fixedCels.reserve(MIN(_maxCelsPerFrame, (int16)kFixedCelListSize)); + _celDecompressionBuffer.reserve(_maxCelArea[0] + SciBitmap::getBitmapHeaderSize() + kRawPaletteSize); + _celDecompressionArea = _maxCelArea[0]; +} + +void RobotDecoder::initRecordAndCuePositions() { + PositionList recordSizes; + _videoSizes.reserve(_numFramesTotal); + _recordPositions.reserve(_numFramesTotal); + recordSizes.reserve(_numFramesTotal); + + switch(_version) { + case 5: // 16-bit sizes and positions + for (int i = 0; i < _numFramesTotal; ++i) { + _videoSizes.push_back(_stream->readUint16()); + } + for (int i = 0; i < _numFramesTotal; ++i) { + recordSizes.push_back(_stream->readUint16()); + } + break; + case 6: // 32-bit sizes and positions + for (int i = 0; i < _numFramesTotal; ++i) { + _videoSizes.push_back(_stream->readSint32()); + } + for (int i = 0; i < _numFramesTotal; ++i) { + recordSizes.push_back(_stream->readSint32()); + } + break; + default: + error("Unknown Robot version %d", _version); + } + + for (int i = 0; i < kCueListSize; ++i) { + _cueTimes[i] = _stream->readSint32(); + } + + for (int i = 0; i < kCueListSize; ++i) { + _cueValues[i] = _stream->readUint16(); + } + + Common::copy(_cueTimes, _cueTimes + kCueListSize, _masterCueTimes); + + int bytesRemaining = (_stream->pos() - _fileOffset) % kRobotFrameSize; + if (bytesRemaining != 0) { + _stream->seek(kRobotFrameSize - bytesRemaining, SEEK_CUR); + } + + int position = _stream->pos(); + _recordPositions.push_back(position); + for (int i = 0; i < _numFramesTotal - 1; ++i) { + position += recordSizes[i]; + _recordPositions.push_back(position); + } +} + +#pragma mark - +#pragma mark RobotDecoder - Playback + +void RobotDecoder::open(const GuiResourceId robotId, const reg_t plane, const int16 priority, const int16 x, const int16 y, const int16 scale) { + if (_status != kRobotStatusUninitialized) { + warning("Last robot was not closed"); + close(); + } + + initStream(robotId); + + _version = _stream->readUint16(); + + // TODO: Version 4 for PQ:SWAT demo? + if (_version < 5 || _version > 6) { + error("Unsupported version %d of Robot resource", _version); + } + + debugC(kDebugLevelVideo, "Opening version %d robot %d", _version, robotId); + + initPlayback(); + + _audioBlockSize = _stream->readUint16(); + _primerZeroCompressFlag = _stream->readSint16(); + _stream->seek(2, SEEK_CUR); // unused + _numFramesTotal = _stream->readUint16(); + const uint16 paletteSize = _stream->readUint16(); + _primerReservedSize = _stream->readUint16(); + _xResolution = _stream->readSint16(); + _yResolution = _stream->readSint16(); + const bool hasPalette = (bool)_stream->readByte(); + _hasAudio = (bool)_stream->readByte(); + _stream->seek(2, SEEK_CUR); // unused + _frameRate = _normalFrameRate = _stream->readSint16(); + _isHiRes = (bool)_stream->readSint16(); + _maxSkippablePackets = _stream->readSint16(); + _maxCelsPerFrame = _stream->readSint16(); + + // used for memory preallocation of fixed cels + _maxCelArea.push_back(_stream->readSint32()); + _maxCelArea.push_back(_stream->readSint32()); + _maxCelArea.push_back(_stream->readSint32()); + _maxCelArea.push_back(_stream->readSint32()); + _stream->seek(8, SEEK_CUR); // reserved + + if (_hasAudio) { + initAudio(); + } else { + _stream->seek(_primerReservedSize, SEEK_CUR); + } + + _priority = priority; + initVideo(x, y, scale, plane, hasPalette, paletteSize); + initRecordAndCuePositions(); } void RobotDecoder::close() { - VideoDecoder::close(); + if (_status == kRobotStatusUninitialized) { + return; + } + + debugC(kDebugLevelVideo, "Closing robot"); - delete _fileStream; - _fileStream = 0; + _status = kRobotStatusUninitialized; + _videoSizes.clear(); + _recordPositions.clear(); + _celDecompressionBuffer.clear(); + _doVersion5Scratch.clear(); + delete _stream; + _stream = nullptr; + + for (CelHandleList::size_type i = 0; i < _celHandles.size(); ++i) { + if (_celHandles[i].status == CelHandleInfo::kFrameLifetime) { + _segMan->freeBitmap(_celHandles[i].bitmapId); + } + } + _celHandles.clear(); + + for (FixedCelsList::size_type i = 0; i < _fixedCels.size(); ++i) { + _segMan->freeBitmap(_fixedCels[i]); + } + _fixedCels.clear(); + + if (g_sci->_gfxFrameout->getPlanes().findByObject(_plane->_object) != nullptr) { + for (RobotScreenItemList::size_type i = 0; i < _screenItemList.size(); ++i) { + if (_screenItemList[i] != nullptr) { + g_sci->_gfxFrameout->deleteScreenItem(*_screenItemList[i]); + } + } + } + _screenItemList.clear(); - delete[] _frameTotalSize; - _frameTotalSize = 0; + if (_hasAudio) { + _audioList.reset(); + } } -void RobotDecoder::readNextPacket() { - // Get our track - RobotVideoTrack *videoTrack = (RobotVideoTrack *)getTrack(0); - videoTrack->increaseCurFrame(); - Graphics::Surface *surface = videoTrack->getSurface(); +void RobotDecoder::pause() { + if (_status != kRobotStatusPlaying) { + return; + } + + if (_hasAudio) { + _audioList.stopAudioNow(); + } + + _status = kRobotStatusPaused; + _frameRate = _normalFrameRate; +} - if (videoTrack->endOfTrack()) +void RobotDecoder::resume() { + if (_status != kRobotStatusPaused) { return; + } + + _startingFrameNo = _currentFrameNo; + _status = kRobotStatusPlaying; + if (_hasAudio) { + primeAudio(_currentFrameNo * 60 / _frameRate); + _syncFrame = true; + } + + setRobotTime(_currentFrameNo); + for (int i = 0; i < kCueListSize; ++i) { + if (_masterCueTimes[i] != -1 && _masterCueTimes[i] < _currentFrameNo) { + _cueTimes[i] = -1; + } else { + _cueTimes[i] = _masterCueTimes[i]; + } + } +} - // Read frame image header (24 bytes) - _fileStream->skip(3); - byte frameScale = _fileStream->readByte(); - uint16 frameWidth = _fileStream->readUint16(); - uint16 frameHeight = _fileStream->readUint16(); - _fileStream->skip(4); // unknown, almost always 0 - uint16 frameX = _fileStream->readUint16(); - uint16 frameY = _fileStream->readUint16(); - - // TODO: In v4 robot files, frameX and frameY have a different meaning. - // Set them both to 0 for v4 for now, so that robots in PQ:SWAT show up - // correctly. - if (_header.version == 4) - frameX = frameY = 0; - - uint16 compressedSize = _fileStream->readUint16(); - uint16 frameFragments = _fileStream->readUint16(); - _fileStream->skip(4); // unknown - uint32 decompressedSize = frameWidth * frameHeight * frameScale / 100; - - // FIXME: A frame's height + position can go off limits... why? With the - // following, we cut the contents to fit the frame - uint16 scaledHeight = CLIP<uint16>(decompressedSize / frameWidth, 0, surface->h - frameY); - - // FIXME: Same goes for the frame's width + position. In this case, we - // modify the position to fit the contents on screen. - if (frameWidth + frameX > surface->w) - frameX = surface->w - frameWidth; - - assert(frameWidth + frameX <= surface->w && scaledHeight + frameY <= surface->h); - - DecompressorLZS lzs; - byte *decompressedFrame = new byte[decompressedSize]; - byte *outPtr = decompressedFrame; - - if (_header.version == 4) { - // v4 has just the one fragment, it seems, and ignores the fragment count - Common::SeekableSubReadStream fragmentStream(_fileStream, _fileStream->pos(), _fileStream->pos() + compressedSize); - lzs.unpack(&fragmentStream, outPtr, compressedSize, decompressedSize); +void RobotDecoder::showFrame(const uint16 frameNo, const uint16 newX, const uint16 newY, const uint16 newPriority) { + debugC(kDebugLevelVideo, "Show frame %d (%d %d %d)", frameNo, newX, newY, newPriority); + + if (newX != kUnspecified) { + _position.x = newX; + } + + if (newY != kUnspecified) { + _position.y = newY; + } + + if (newPriority != kUnspecified) { + _priority = newPriority; + } + + _currentFrameNo = frameNo; + pause(); + + if (frameNo != _previousFrameNo) { + seekToFrame(frameNo); + doVersion5(false); } else { - for (uint16 i = 0; i < frameFragments; ++i) { - uint32 compressedFragmentSize = _fileStream->readUint32(); - uint32 decompressedFragmentSize = _fileStream->readUint32(); - uint16 compressionType = _fileStream->readUint16(); - - if (compressionType == 0) { - Common::SeekableSubReadStream fragmentStream(_fileStream, _fileStream->pos(), _fileStream->pos() + compressedFragmentSize); - lzs.unpack(&fragmentStream, outPtr, compressedFragmentSize, decompressedFragmentSize); - } else if (compressionType == 2) { // untested - _fileStream->read(outPtr, compressedFragmentSize); + for (RobotScreenItemList::size_type i = 0; i < _screenItemList.size(); ++i) { + if (_isHiRes) { + SciBitmap &bitmap = *_segMan->lookupBitmap(_celHandles[i].bitmapId); + + const int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth; + const int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight; + const int16 screenWidth = g_sci->_gfxFrameout->getCurrentBuffer().screenWidth; + const int16 screenHeight = g_sci->_gfxFrameout->getCurrentBuffer().screenHeight; + + if (scriptWidth == kLowResX && scriptHeight == kLowResY) { + const Ratio lowResToScreenX(screenWidth, kLowResX); + const Ratio lowResToScreenY(screenHeight, kLowResY); + const Ratio screenToLowResX(kLowResX, screenWidth); + const Ratio screenToLowResY(kLowResY, screenHeight); + + const int16 scaledX = _originalScreenItemX[i] + (_position.x * lowResToScreenX).toInt(); + const int16 scaledY1 = _originalScreenItemY[i] + (_position.y * lowResToScreenY).toInt(); + const int16 scaledY2 = scaledY1 + bitmap.getHeight() - 1; + + const int16 lowResX = (scaledX * screenToLowResX).toInt(); + const int16 lowResY = (scaledY2 * screenToLowResY).toInt(); + + bitmap.setDisplace(Common::Point( + (scaledX - (lowResX * lowResToScreenX).toInt()) * -1, + (lowResY * lowResToScreenY).toInt() - scaledY1 + )); + + _screenItemX[i] = lowResX; + _screenItemY[i] = lowResY; + } else { + const int16 scaledX = _originalScreenItemX[i] + _position.x; + const int16 scaledY = _originalScreenItemY[i] + _position.y + bitmap.getHeight() - 1; + bitmap.setDisplace(Common::Point(0, bitmap.getHeight() - 1)); + _screenItemX[i] = scaledX; + _screenItemY[i] = scaledY; + } + } else { + _screenItemX[i] = _originalScreenItemX[i] + _position.x; + _screenItemY[i] = _originalScreenItemY[i] + _position.y; + } + + if (_screenItemList[i] == nullptr) { + CelInfo32 celInfo; + celInfo.type = kCelTypeMem; + celInfo.bitmap = _celHandles[i].bitmapId; + ScreenItem *screenItem = new ScreenItem(_plane->_object, celInfo); + _screenItemList[i] = screenItem; + screenItem->_position = Common::Point(_screenItemX[i], _screenItemY[i]); + if (_priority == -1) { + screenItem->_fixedPriority = false; + } else { + screenItem->_priority = _priority; + screenItem->_fixedPriority = true; + } + g_sci->_gfxFrameout->addScreenItem(*screenItem); } else { - error("Unknown frame compression found: %d", compressionType); + ScreenItem *screenItem = _screenItemList[i]; + screenItem->_celInfo.bitmap = _celHandles[i].bitmapId; + screenItem->_position = Common::Point(_screenItemX[i], _screenItemY[i]); + if (_priority == -1) { + screenItem->_fixedPriority = false; + } else { + screenItem->_priority = _priority; + screenItem->_fixedPriority = true; + } + g_sci->_gfxFrameout->updateScreenItem(*screenItem); + } + } + } + + _previousFrameNo = frameNo; +} + +int16 RobotDecoder::getCue() const { + if (_status == kRobotStatusUninitialized || + _status == kRobotStatusPaused || + _syncFrame) { + return 0; + } + + if (_status == kRobotStatusEnd) { + return -1; + } + + const uint16 estimatedNextFrameNo = MIN(calculateNextFrameNo(_delayTime.predictedTicks()), _numFramesTotal); + + for (int i = 0; i < kCueListSize; ++i) { + if (_cueTimes[i] != -1 && _cueTimes[i] <= estimatedNextFrameNo) { + if (_cueTimes[i] >= _previousFrameNo) { + _cueForceShowFrame = _cueTimes[i] + 1; } - outPtr += decompressedFragmentSize; + _cueTimes[i] = -1; + return _cueValues[i]; } } - // Copy over the decompressed frame - byte *inFrame = decompressedFrame; - byte *outFrame = (byte *)surface->getPixels(); + return 0; +} - // Black out the surface - memset(outFrame, 0, surface->w * surface->h); +int16 RobotDecoder::getFrameNo() const { + if (_status == kRobotStatusUninitialized) { + return 0; + } - // Move to the correct y coordinate - outFrame += surface->w * frameY; + return _currentFrameNo; +} + +RobotDecoder::RobotStatus RobotDecoder::getStatus() const { + return _status; +} - for (uint16 y = 0; y < scaledHeight; y++) { - memcpy(outFrame + frameX, inFrame, frameWidth); - inFrame += frameWidth; - outFrame += surface->w; +bool RobotDecoder::seekToFrame(const int frameNo) { + return _stream->seek(_recordPositions[frameNo], SEEK_SET); +} + +void RobotDecoder::setRobotTime(const int frameNo) { + _startTime = getTickCount(); + _startFrameNo = frameNo; +} + +#pragma mark - +#pragma mark RobotDecoder - Timing + +RobotDecoder::DelayTime::DelayTime(RobotDecoder *decoder) : + _decoder(decoder) { + for (int i = 0; i < kDelayListSize; ++i) { + _timestamps[i] = i; + _delays[i] = 0; } - delete[] decompressedFrame; + _oldestTimestamp = 0; + _newestTimestamp = kDelayListSize - 1; + _startTime = 0; +} - uint32 audioChunkSize = _frameTotalSize[videoTrack->getCurFrame()] - (24 + compressedSize); +void RobotDecoder::DelayTime::startTiming() { + _startTime = _decoder->getTickCount(); +} -// TODO: The audio chunk size below is usually correct, but there are some -// exceptions (e.g. robot 4902 in Phantasmagoria, towards its end) -#if 0 - // Read frame audio header (14 bytes) - _fileStream->skip(2); // buffer position - _fileStream->skip(2); // unknown (usually 1) - _fileStream->skip(2); /*uint16 audioChunkSize = _fileStream->readUint16() + 8;*/ - _fileStream->skip(2); -#endif +void RobotDecoder::DelayTime::endTiming() { + const int timeDelta = _decoder->getTickCount() - _startTime; + for (uint i = 0; i < kDelayListSize; ++i) { + if (_timestamps[i] == _oldestTimestamp) { + _timestamps[i] = ++_newestTimestamp; + _delays[i] = timeDelta; + break; + } + } + ++_newestTimestamp; + _startTime = 0; + sortList(); +} - // Queue the next audio frame - // FIXME: For some reason, there are audio hiccups/gaps - if (_header.hasSound) { - RobotAudioTrack *audioTrack = (RobotAudioTrack *)getTrack(1); - _fileStream->skip(8); // header - audioChunkSize -= 8; - audioTrack->queueBuffer(g_sci->_audio->getDecodedRobotAudioFrame(_fileStream, audioChunkSize), audioChunkSize * 2); +bool RobotDecoder::DelayTime::timingInProgress() const { + return _startTime != 0; +} + +int RobotDecoder::DelayTime::predictedTicks() const { + return _delays[kDelayListSize / 2]; +} + +void RobotDecoder::DelayTime::sortList() { + for (uint i = 0; i < kDelayListSize - 1; ++i) { + int smallestDelay = _delays[i]; + uint smallestIndex = i; + + for (uint j = i + 1; j < kDelayListSize - 1; ++j) { + if (_delays[j] < smallestDelay) { + smallestDelay = _delays[j]; + smallestIndex = j; + } + } + + if (smallestIndex != i) { + SWAP(_delays[i], _delays[smallestIndex]); + SWAP(_timestamps[i], _timestamps[smallestIndex]); + } + } +} + +uint16 RobotDecoder::calculateNextFrameNo(const uint32 extraTicks) const { + return ticksToFrames(getTickCount() + extraTicks - _startTime) + _startFrameNo; +} + +uint32 RobotDecoder::ticksToFrames(const uint32 ticks) const { + return (ticks * _frameRate) / 60; +} + +uint32 RobotDecoder::getTickCount() const { + return g_sci->getTickCount(); +} + +#pragma mark - +#pragma mark RobotDecoder - Audio + +RobotDecoder::AudioList::AudioList() : + _blocks(), + _blocksSize(0), + _oldestBlockIndex(0), + _newestBlockIndex(0), + _startOffset(0), + _status(kRobotAudioReady) {} + +void RobotDecoder::AudioList::startAudioNow() { + submitDriverMax(); + g_sci->_audio32->resume(kRobotChannel); + _status = kRobotAudioPlaying; +} + +void RobotDecoder::AudioList::stopAudio() { + g_sci->_audio32->finishRobotAudio(); + freeAudioBlocks(); + _status = kRobotAudioStopping; +} + +void RobotDecoder::AudioList::stopAudioNow() { + if (_status == kRobotAudioPlaying || _status == kRobotAudioStopping || _status == kRobotAudioPaused) { + g_sci->_audio32->stopRobotAudio(); + _status = kRobotAudioStopped; + } + + freeAudioBlocks(); +} + +void RobotDecoder::AudioList::submitDriverMax() { + while (_blocksSize != 0) { + if (!_blocks[_oldestBlockIndex]->submit(_startOffset)) { + return; + } + + delete _blocks[_oldestBlockIndex]; + _blocks[_oldestBlockIndex] = nullptr; + ++_oldestBlockIndex; + if (_oldestBlockIndex == kAudioListSize) { + _oldestBlockIndex = 0; + } + + --_blocksSize; + } +} + +void RobotDecoder::AudioList::addBlock(const int position, const int size, const byte *data) { + assert(data != nullptr); + assert(size >= 0); + assert(position >= -1); + + if (_blocksSize == kAudioListSize) { + delete _blocks[_oldestBlockIndex]; + _blocks[_oldestBlockIndex] = nullptr; + ++_oldestBlockIndex; + if (_oldestBlockIndex == kAudioListSize) { + _oldestBlockIndex = 0; + } + --_blocksSize; + } + + if (_blocksSize == 0) { + _oldestBlockIndex = _newestBlockIndex = 0; } else { - _fileStream->skip(audioChunkSize); - } -} - -void RobotDecoder::readHeaderChunk() { - // Header (60 bytes) - _fileStream->skip(6); - _header.version = _fileStream->readUint16(); - _header.audioChunkSize = _fileStream->readUint16(); - _header.audioSilenceSize = _fileStream->readUint16(); - _fileStream->skip(2); - _header.frameCount = _fileStream->readUint16(); - _header.paletteDataSize = _fileStream->readUint16(); - _header.unkChunkDataSize = _fileStream->readUint16(); - _fileStream->skip(5); - _header.hasSound = _fileStream->readByte(); - _fileStream->skip(34); - - // Some videos (e.g. robot 1305 in Phantasmagoria and - // robot 184 in Lighthouse) have an unknown chunk before - // the palette chunk (probably used for sound preloading). - // Skip it here. - if (_header.unkChunkDataSize) - _fileStream->skip(_header.unkChunkDataSize); -} - -void RobotDecoder::readFrameSizesChunk() { - // The robot video file contains 2 tables, with one entry for each frame: - // - A table containing the size of the image in each video frame - // - A table containing the total size of each video frame. - // In v5 robots, the tables contain 16-bit integers, whereas in v6 robots, - // they contain 32-bit integers. - - _frameTotalSize = new uint32[_header.frameCount]; - - // TODO: The table reading code can probably be removed once the - // audio chunk size is figured out (check the TODO inside processNextFrame()) -#if 0 - // We don't need any of the two tables to play the video, so we ignore - // both of them. - uint16 wordSize = _header.version == 6 ? 4 : 2; - _fileStream->skip(_header.frameCount * wordSize * 2); -#else - switch (_header.version) { - case 4: - case 5: // sizes are 16-bit integers - // Skip table with frame image sizes, as we don't need it - _fileStream->skip(_header.frameCount * 2); - for (int i = 0; i < _header.frameCount; ++i) - _frameTotalSize[i] = _fileStream->readUint16(); - break; - case 6: // sizes are 32-bit integers - // Skip table with frame image sizes, as we don't need it - _fileStream->skip(_header.frameCount * 4); - for (int i = 0; i < _header.frameCount; ++i) - _frameTotalSize[i] = _fileStream->readUint32(); - break; - default: - error("Can't yet handle index table for robot version %d", _header.version); + ++_newestBlockIndex; + if (_newestBlockIndex == kAudioListSize) { + _newestBlockIndex = 0; + } } -#endif - // 2 more unknown tables - _fileStream->skip(1024 + 512); + _blocks[_newestBlockIndex] = new AudioBlock(position, size, data); + ++_blocksSize; +} - // Pad to nearest 2 kilobytes - uint32 curPos = _fileStream->pos(); - if (curPos & 0x7ff) - _fileStream->seek((curPos & ~0x7ff) + 2048); +void RobotDecoder::AudioList::reset() { + stopAudioNow(); + _startOffset = 0; + _status = kRobotAudioReady; } -RobotDecoder::RobotVideoTrack::RobotVideoTrack(int frameCount) : _frameCount(frameCount) { - _surface = new Graphics::Surface(); - _curFrame = -1; - _dirtyPalette = false; +void RobotDecoder::AudioList::prepareForPrimer() { + g_sci->_audio32->pause(kRobotChannel); + _status = kRobotAudioPaused; } -RobotDecoder::RobotVideoTrack::~RobotVideoTrack() { - _surface->free(); - delete _surface; +void RobotDecoder::AudioList::setAudioOffset(const int offset) { + _startOffset = offset; } -uint16 RobotDecoder::RobotVideoTrack::getWidth() const { - return _surface->w; +RobotDecoder::AudioList::AudioBlock::AudioBlock(const int position, const int size, const byte* const data) : + _position(position), + _size(size) { + _data = (byte *)malloc(size); + memcpy(_data, data, size); } -uint16 RobotDecoder::RobotVideoTrack::getHeight() const { - return _surface->h; +RobotDecoder::AudioList::AudioBlock::~AudioBlock() { + free(_data); } -Graphics::PixelFormat RobotDecoder::RobotVideoTrack::getPixelFormat() const { - return _surface->format; +bool RobotDecoder::AudioList::AudioBlock::submit(const int startOffset) { + assert(_data != nullptr); + RobotAudioStream::RobotAudioPacket packet(_data, _size, (_position - startOffset) * 2); + return g_sci->_audio32->playRobotAudio(packet); } -void RobotDecoder::RobotVideoTrack::readPaletteChunk(Common::SeekableSubReadStreamEndian *stream, uint16 chunkSize) { - byte *paletteData = new byte[chunkSize]; - stream->read(paletteData, chunkSize); +void RobotDecoder::AudioList::freeAudioBlocks() { + while (_blocksSize != 0) { + delete _blocks[_oldestBlockIndex]; + _blocks[_oldestBlockIndex] = nullptr; + ++_oldestBlockIndex; + if (_oldestBlockIndex == kAudioListSize) { + _oldestBlockIndex = 0; + } + + --_blocksSize; + } +} - // SCI1.1 palette - byte palFormat = paletteData[32]; - uint16 palColorStart = paletteData[25]; - uint16 palColorCount = READ_SCI11ENDIAN_UINT16(paletteData + 29); +bool RobotDecoder::primeAudio(const uint32 startTick) { + bool success = true; + _audioList.reset(); + + if (startTick == 0) { + _audioList.prepareForPrimer(); + byte *evenPrimerBuff = new byte[_evenPrimerSize]; + byte *oddPrimerBuff = new byte[_oddPrimerSize]; + + success = readPrimerData(evenPrimerBuff, oddPrimerBuff); + if (success) { + if (_evenPrimerSize != 0) { + _audioList.addBlock(0, _evenPrimerSize, evenPrimerBuff); + } + if (_oddPrimerSize != 0) { + _audioList.addBlock(1, _oddPrimerSize, oddPrimerBuff); + } + } + + delete[] evenPrimerBuff; + delete[] oddPrimerBuff; + } else { + assert(_evenPrimerSize * 2 >= _audioRecordInterval || _oddPrimerSize * 2 >= _audioRecordInterval); + + int audioStartFrame = 0; + int videoStartFrame = startTick * _frameRate / 60; + assert(videoStartFrame < _numFramesTotal); + + int audioStartPosition = (startTick * RobotAudioStream::kRobotSampleRate) / 60; + if (audioStartPosition & 1) { + audioStartPosition--; + } + _audioList.setAudioOffset(audioStartPosition); + _audioList.prepareForPrimer(); + + if (audioStartPosition < _evenPrimerSize * 2 || + audioStartPosition + 1 < _oddPrimerSize * 2) { + + byte *evenPrimerBuffer = new byte[_evenPrimerSize]; + byte *oddPrimerBuffer = new byte[_oddPrimerSize]; + success = readPrimerData(evenPrimerBuffer, oddPrimerBuffer); + if (success) { + int halfAudioStartPosition = audioStartPosition / 2; + if (audioStartPosition < _evenPrimerSize * 2) { + _audioList.addBlock(audioStartPosition, _evenPrimerSize - halfAudioStartPosition, &evenPrimerBuffer[halfAudioStartPosition]); + } + + if (audioStartPosition + 1 < _oddPrimerSize * 2) { + _audioList.addBlock(audioStartPosition + 1, _oddPrimerSize - halfAudioStartPosition, &oddPrimerBuffer[halfAudioStartPosition]); + } + } + + delete[] evenPrimerBuffer; + delete[] oddPrimerBuffer; + } - int palOffset = 37; - memset(_palette, 0, 256 * 3); + if (audioStartPosition >= _firstAudioRecordPosition) { + int audioRecordSize = _expectedAudioBlockSize; + assert(audioRecordSize > 0); + assert(_audioRecordInterval > 0); + assert(_firstAudioRecordPosition >= 0); - for (uint16 colorNo = palColorStart; colorNo < palColorStart + palColorCount; colorNo++) { - if (palFormat == kRobotPalVariable) - palOffset++; - _palette[colorNo * 3 + 0] = paletteData[palOffset++]; - _palette[colorNo * 3 + 1] = paletteData[palOffset++]; - _palette[colorNo * 3 + 2] = paletteData[palOffset++]; + audioStartFrame = (audioStartPosition - _firstAudioRecordPosition) / _audioRecordInterval; + assert(audioStartFrame < videoStartFrame); + + if (audioStartFrame > 0) { + int lastAudioFrame = audioStartFrame - 1; + int oddRemainder = lastAudioFrame & 1; + int audioRecordStart = (lastAudioFrame * _audioRecordInterval) + oddRemainder + _firstAudioRecordPosition; + int audioRecordEnd = (audioRecordStart + ((audioRecordSize - 1) * 2)) + oddRemainder + _firstAudioRecordPosition; + + if (audioStartPosition >= audioRecordStart && audioStartPosition <= audioRecordEnd) { + --audioStartFrame; + } + } + + assert(!(audioStartPosition & 1)); + if (audioStartFrame & 1) { + ++audioStartPosition; + } + + if (!readPartialAudioRecordAndSubmit(audioStartFrame, audioStartPosition)) { + return false; + } + + ++audioStartFrame; + assert(audioStartFrame < videoStartFrame); + + int oddRemainder = audioStartFrame & 1; + int audioRecordStart = (audioStartFrame * _audioRecordInterval) + oddRemainder + _firstAudioRecordPosition; + int audioRecordEnd = (audioRecordStart + ((audioRecordSize - 1) * 2)) + oddRemainder + _firstAudioRecordPosition; + + if (audioStartPosition >= audioRecordStart && audioStartPosition <= audioRecordEnd) { + if (!readPartialAudioRecordAndSubmit(audioStartFrame, audioStartPosition + 1)) { + return false; + } + + ++audioStartFrame; + } + } + + int audioPosition, audioSize; + for (int i = audioStartFrame; i < videoStartFrame; i++) { + if (!readAudioDataFromRecord(i, _audioBuffer, audioPosition, audioSize)) { + break; + } + + _audioList.addBlock(audioPosition, audioSize, _audioBuffer); + } } - _dirtyPalette = true; - delete[] paletteData; + return success; } -void RobotDecoder::RobotVideoTrack::calculateVideoDimensions(Common::SeekableSubReadStreamEndian *stream, uint32 *frameSizes) { - // This is an O(n) operation, as each frame has a different size. - // We need to know the actual frame size to have a constant video size. - uint32 pos = stream->pos(); +bool RobotDecoder::readPrimerData(byte *outEvenBuffer, byte *outOddBuffer) { + if (_primerReservedSize != 0) { + if (_totalPrimerSize != 0) { + _stream->seek(_primerPosition, SEEK_SET); + if (_evenPrimerSize > 0) { + _stream->read(outEvenBuffer, _evenPrimerSize); + } + + if (_oddPrimerSize > 0) { + _stream->read(outOddBuffer, _oddPrimerSize); + } + } + } else if (_primerZeroCompressFlag) { + memset(outEvenBuffer, 0, _evenPrimerSize); + memset(outOddBuffer, 0, _oddPrimerSize); + } else { + error("ReadPrimerData - Flags corrupt"); + } + + return !_stream->err(); +} + +bool RobotDecoder::readAudioDataFromRecord(const int frameNo, byte *outBuffer, int &outAudioPosition, int &outAudioSize) { + _stream->seek(_recordPositions[frameNo] + _videoSizes[frameNo], SEEK_SET); + _audioList.submitDriverMax(); + + // Compressed absolute position of the audio block in the audio stream + const int position = _stream->readSint32(); - uint16 width = 0, height = 0; + // Size of the block of audio, excluding the audio block header + int size = _stream->readSint32(); - for (int curFrame = 0; curFrame < _frameCount; curFrame++) { - stream->skip(4); - uint16 frameWidth = stream->readUint16(); - uint16 frameHeight = stream->readUint16(); - if (frameWidth > width) - width = frameWidth; - if (frameHeight > height) - height = frameHeight; - stream->skip(frameSizes[curFrame] - 8); + assert(size <= _expectedAudioBlockSize); + + if (position == 0) { + return false; } - stream->seek(pos); + if (size != _expectedAudioBlockSize) { + memset(outBuffer, 0, kRobotZeroCompressSize); + _stream->read(outBuffer + kRobotZeroCompressSize, size); + size += kRobotZeroCompressSize; + } else { + _stream->read(outBuffer, size); + } - _surface->create(width, height, Graphics::PixelFormat::createFormatCLUT8()); + outAudioPosition = position; + outAudioSize = size; + return !_stream->err(); } -RobotDecoder::RobotAudioTrack::RobotAudioTrack() { - _audioStream = Audio::makeQueuingAudioStream(11025, false); +bool RobotDecoder::readPartialAudioRecordAndSubmit(const int startFrame, const int startPosition) { + int audioPosition, audioSize; + bool success = readAudioDataFromRecord(startFrame, _audioBuffer, audioPosition, audioSize); + if (success) { + const int relativeStartOffset = (startPosition - audioPosition) / 2; + _audioList.addBlock(startPosition, audioSize - relativeStartOffset, _audioBuffer + relativeStartOffset); + } + + return success; } -RobotDecoder::RobotAudioTrack::~RobotAudioTrack() { - delete _audioStream; +#pragma mark - +#pragma mark RobotDecoder - Rendering + +uint16 RobotDecoder::getFrameSize(Common::Rect &outRect) const { + outRect.clip(0, 0); + for (RobotScreenItemList::size_type i = 0; i < _screenItemList.size(); ++i) { + ScreenItem &screenItem = *_screenItemList[i]; + outRect.extend(screenItem.getNowSeenRect(*_plane)); + } + + return _numFramesTotal; } -void RobotDecoder::RobotAudioTrack::queueBuffer(byte *buffer, int size) { - _audioStream->queueBuffer(buffer, size, DisposeAfterUse::YES, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN); +void RobotDecoder::doRobot() { + if (_status != kRobotStatusPlaying) { + return; + } + + if (!_syncFrame) { + if (_cueForceShowFrame != -1) { + _currentFrameNo = _cueForceShowFrame; + _cueForceShowFrame = -1; + } else { + const int nextFrameNo = calculateNextFrameNo(_delayTime.predictedTicks()); + if (nextFrameNo < _currentFrameNo) { + return; + } + _currentFrameNo = nextFrameNo; + } + } + + if (_currentFrameNo >= _numFramesTotal) { + const int finalFrameNo = _numFramesTotal - 1; + if (_previousFrameNo == finalFrameNo) { + _status = kRobotStatusEnd; + if (_hasAudio) { + _audioList.stopAudio(); + _frameRate = _normalFrameRate; + _hasAudio = false; + } + return; + } else { + _currentFrameNo = finalFrameNo; + } + } + + if (_currentFrameNo == _previousFrameNo) { + _audioList.submitDriverMax(); + return; + } + + if (_hasAudio) { + for (int candidateFrameNo = _previousFrameNo + _maxSkippablePackets + 1; candidateFrameNo < _currentFrameNo; candidateFrameNo += _maxSkippablePackets + 1) { + + _audioList.submitDriverMax(); + + int audioPosition, audioSize; + if (readAudioDataFromRecord(candidateFrameNo, _audioBuffer, audioPosition, audioSize)) { + _audioList.addBlock(audioPosition, audioSize, _audioBuffer); + } + } + _audioList.submitDriverMax(); + } + + _delayTime.startTiming(); + seekToFrame(_currentFrameNo); + doVersion5(); + if (_hasAudio) { + _audioList.submitDriverMax(); + } +} + +void RobotDecoder::frameAlmostVisible() { + if (_status == kRobotStatusPlaying && !_syncFrame) { + if (_previousFrameNo != _currentFrameNo) { + while (calculateNextFrameNo() < _currentFrameNo) { + _audioList.submitDriverMax(); + } + } + } +} + +void RobotDecoder::frameNowVisible() { + if (_status != kRobotStatusPlaying) { + return; + } + + if (_syncFrame) { + _syncFrame = false; + if (_hasAudio) { + _audioList.startAudioNow(); + _checkAudioSyncTime = _startTime + kAudioSyncCheckInterval; + } + + setRobotTime(_currentFrameNo); + } + + if (_delayTime.timingInProgress()) { + _delayTime.endTiming(); + } + + if (_hasAudio) { + _audioList.submitDriverMax(); + } + + if (_previousFrameNo != _currentFrameNo) { + _previousFrameNo = _currentFrameNo; + } + + if (!_syncFrame && _hasAudio && getTickCount() >= _checkAudioSyncTime) { + RobotAudioStream::StreamState status; + const bool success = g_sci->_audio32->queryRobotAudio(status); + if (!success) { + return; + } + + const int bytesPerFrame = status.rate / _normalFrameRate * (status.bits == 16 ? 2 : 1); + // check again in 1/3rd second + _checkAudioSyncTime = getTickCount() + 60 / 3; + + const int currentVideoFrameNo = calculateNextFrameNo() - _startingFrameNo; + const int currentAudioFrameNo = status.bytesPlaying / bytesPerFrame; + debugC(kDebugLevelVideo, "Video frame %d %s audio frame %d", currentVideoFrameNo, currentVideoFrameNo == currentAudioFrameNo ? "=" : currentVideoFrameNo < currentAudioFrameNo ? "<" : ">", currentAudioFrameNo); + if (currentVideoFrameNo < _numFramesTotal && + currentAudioFrameNo < _numFramesTotal) { + + bool shouldResetRobotTime = false; + + if (currentAudioFrameNo < currentVideoFrameNo - 1 && _frameRate != _minFrameRate) { + debugC(kDebugLevelVideo, "[v] Reducing frame rate"); + _frameRate = _minFrameRate; + shouldResetRobotTime = true; + } else if (currentAudioFrameNo > currentVideoFrameNo + 1 && _frameRate != _maxFrameRate) { + debugC(kDebugLevelVideo, "[^] Increasing frame rate"); + _frameRate = _maxFrameRate; + shouldResetRobotTime = true; + } else if (_frameRate != _normalFrameRate) { + debugC(kDebugLevelVideo, "[=] Setting to normal frame rate"); + _frameRate = _normalFrameRate; + shouldResetRobotTime = true; + } + + if (shouldResetRobotTime) { + if (currentAudioFrameNo < _currentFrameNo) { + setRobotTime(_currentFrameNo); + } else { + setRobotTime(currentAudioFrameNo); + } + } + } + } +} + +void RobotDecoder::expandCel(byte* target, const byte* source, const int16 celWidth, const int16 celHeight) const { + assert(source != nullptr && target != nullptr); + + const int sourceHeight = (celHeight * _verticalScaleFactor) / 100; + assert(sourceHeight > 0); + + const int16 numerator = celHeight; + const int16 denominator = sourceHeight; + int remainder = 0; + for (int16 y = sourceHeight - 1; y >= 0; --y) { + remainder += numerator; + int16 linesToDraw = remainder / denominator; + remainder %= denominator; + + while (linesToDraw--) { + memcpy(target, source, celWidth); + target += celWidth; + } + + source += celWidth; + } +} + +void RobotDecoder::setPriority(const int16 newPriority) { + _priority = newPriority; +} + +void RobotDecoder::doVersion5(const bool shouldSubmitAudio) { + const RobotScreenItemList::size_type oldScreenItemCount = _screenItemList.size(); + const int videoSize = _videoSizes[_currentFrameNo]; + _doVersion5Scratch.resize(videoSize); + + byte *videoFrameData = _doVersion5Scratch.begin(); + + if (!_stream->read(videoFrameData, videoSize)) { + error("RobotDecoder::doVersion5: Read error"); + } + + const RobotScreenItemList::size_type screenItemCount = READ_SCI11ENDIAN_UINT16(videoFrameData); + + if (screenItemCount > kScreenItemListSize) { + return; + } + + if (_hasAudio && + (getSciVersion() < SCI_VERSION_3 || shouldSubmitAudio)) { + int audioPosition, audioSize; + if (readAudioDataFromRecord(_currentFrameNo, _audioBuffer, audioPosition, audioSize)) { + _audioList.addBlock(audioPosition, audioSize, _audioBuffer); + } + } + + if (screenItemCount > oldScreenItemCount) { + _screenItemList.resize(screenItemCount); + _screenItemX.resize(screenItemCount); + _screenItemY.resize(screenItemCount); + _originalScreenItemX.resize(screenItemCount); + _originalScreenItemY.resize(screenItemCount); + } + + createCels5(videoFrameData + 2, screenItemCount, true); + for (RobotScreenItemList::size_type i = 0; i < screenItemCount; ++i) { + Common::Point position(_screenItemX[i], _screenItemY[i]); + +// TODO: Version 6 robot? +// int scaleXRemainder; + if (_scaleInfo.signal == kScaleSignalDoScaling32) { + position.x = (position.x * _scaleInfo.x) / 128; +// TODO: Version 6 robot? +// scaleXRemainder = (position.x * _scaleInfo.x) % 128; + position.y = (position.y * _scaleInfo.y) / 128; + } + + if (_screenItemList[i] == nullptr) { + CelInfo32 celInfo; + celInfo.bitmap = _celHandles[i].bitmapId; + ScreenItem *screenItem = new ScreenItem(_plane->_object, celInfo, position, _scaleInfo); + _screenItemList[i] = screenItem; + // TODO: Version 6 robot? + // screenItem->_field_30 = scaleXRemainder; + + if (_priority == -1) { + screenItem->_fixedPriority = false; + } else { + screenItem->_fixedPriority = true; + screenItem->_priority = _priority; + } + g_sci->_gfxFrameout->addScreenItem(*screenItem); + } else { + ScreenItem *screenItem = _screenItemList[i]; + screenItem->_celInfo.bitmap = _celHandles[i].bitmapId; + screenItem->_position = position; + // TODO: Version 6 robot? + // screenItem->_field_30 = scaleXRemainder; + + if (_priority == -1) { + screenItem->_fixedPriority = false; + } else { + screenItem->_fixedPriority = true; + screenItem->_priority = _priority; + } + g_sci->_gfxFrameout->updateScreenItem(*screenItem); + } + } + + for (RobotScreenItemList::size_type i = screenItemCount; i < oldScreenItemCount; ++i) { + if (_screenItemList[i] != nullptr) { + g_sci->_gfxFrameout->deleteScreenItem(*_screenItemList[i]); + _screenItemList[i] = nullptr; + } + } +} + +void RobotDecoder::createCels5(const byte *rawVideoData, const int16 numCels, const bool usePalette) { + preallocateCelMemory(rawVideoData, numCels); + for (int16 i = 0; i < numCels; ++i) { + rawVideoData += createCel5(rawVideoData, i, usePalette); + } +} + +uint32 RobotDecoder::createCel5(const byte *rawVideoData, const int16 screenItemIndex, const bool usePalette) { + _verticalScaleFactor = rawVideoData[1]; + const int16 celWidth = (int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 2); + const int16 celHeight = (int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 4); + const Common::Point celPosition((int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 10), + (int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 12)); + const uint16 dataSize = READ_SCI11ENDIAN_UINT16(rawVideoData + 14); + const int16 numDataChunks = (int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 16); + + rawVideoData += kCelHeaderSize; + + const int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth; + const int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight; + const int16 screenWidth = g_sci->_gfxFrameout->getCurrentBuffer().screenWidth; + const int16 screenHeight = g_sci->_gfxFrameout->getCurrentBuffer().screenHeight; + + Common::Point displace; + if (scriptWidth == kLowResX && scriptHeight == kLowResY) { + const Ratio lowResToScreenX(screenWidth, kLowResX); + const Ratio lowResToScreenY(screenHeight, kLowResY); + const Ratio screenToLowResX(kLowResX, screenWidth); + const Ratio screenToLowResY(kLowResY, screenHeight); + + const int16 scaledX = celPosition.x + (_position.x * lowResToScreenX).toInt(); + const int16 scaledY1 = celPosition.y + (_position.y * lowResToScreenY).toInt(); + const int16 scaledY2 = scaledY1 + celHeight - 1; + + const int16 lowResX = (scaledX * screenToLowResX).toInt(); + const int16 lowResY = (scaledY2 * screenToLowResY).toInt(); + + displace.x = (scaledX - (lowResX * lowResToScreenX).toInt()) * -1; + displace.y = (lowResY * lowResToScreenY).toInt() - scaledY1; + _screenItemX[screenItemIndex] = lowResX; + _screenItemY[screenItemIndex] = lowResY; + + debugC(kDebugLevelVideo, "Low resolution position c: %d %d l: %d/%d %d/%d d: %d %d s: %d/%d %d/%d x: %d y: %d", celPosition.x, celPosition.y, lowResX, scriptWidth, lowResY, scriptHeight, displace.x, displace.y, scaledX, screenWidth, scaledY2, screenHeight, scaledX - displace.x, scaledY2 - displace.y); + } else { + const int16 highResX = celPosition.x + _position.x; + const int16 highResY = celPosition.y + _position.y + celHeight - 1; + + displace.x = 0; + displace.y = celHeight - 1; + _screenItemX[screenItemIndex] = highResX; + _screenItemY[screenItemIndex] = highResY; + + debugC(kDebugLevelVideo, "High resolution position c: %d %d s: %d %d d: %d %d", celPosition.x, celPosition.y, highResX, highResY, displace.x, displace.y); + } + + _originalScreenItemX[screenItemIndex] = celPosition.x; + _originalScreenItemY[screenItemIndex] = celPosition.y; + + assert(_celHandles[screenItemIndex].area >= celWidth * celHeight); + + SciBitmap &bitmap = *_segMan->lookupBitmap(_celHandles[screenItemIndex].bitmapId); + assert(bitmap.getWidth() == celWidth && bitmap.getHeight() == celHeight); + assert(bitmap.getScaledWidth() == _xResolution && bitmap.getScaledHeight() == _yResolution); + assert(bitmap.getHunkPaletteOffset() == (uint32)bitmap.getWidth() * bitmap.getHeight() + SciBitmap::getBitmapHeaderSize()); + bitmap.setDisplace(displace); + + byte *targetBuffer = nullptr; + if (_verticalScaleFactor == 100) { + // direct copy to bitmap + targetBuffer = bitmap.getPixels(); + } else { + // go through squashed cel decompressor + _celDecompressionBuffer.resize(_celDecompressionArea >= celWidth * (celHeight * _verticalScaleFactor / 100)); + targetBuffer = _celDecompressionBuffer.begin(); + } + + for (int i = 0; i < numDataChunks; ++i) { + uint compressedSize = READ_SCI11ENDIAN_UINT32(rawVideoData); + uint decompressedSize = READ_SCI11ENDIAN_UINT32(rawVideoData + 4); + uint16 compressionType = READ_SCI11ENDIAN_UINT16(rawVideoData + 8); + rawVideoData += 10; + + switch (compressionType) { + case kCompressionLZS: { + Common::MemoryReadStream videoDataStream(rawVideoData, compressedSize, DisposeAfterUse::NO); + _decompressor.unpack(&videoDataStream, targetBuffer, compressedSize, decompressedSize); + break; + } + case kCompressionNone: + Common::copy(rawVideoData, rawVideoData + decompressedSize, targetBuffer); + break; + default: + error("Unknown compression type %d!", compressionType); + } + + rawVideoData += compressedSize; + targetBuffer += decompressedSize; + } + + if (_verticalScaleFactor != 100) { + expandCel(bitmap.getPixels(), _celDecompressionBuffer.begin(), celWidth, celHeight); + } + + if (usePalette) { + Common::copy(_rawPalette, _rawPalette + kRawPaletteSize, bitmap.getHunkPalette()); + } + + return kCelHeaderSize + dataSize; } -Audio::AudioStream *RobotDecoder::RobotAudioTrack::getAudioStream() const { - return _audioStream; +void RobotDecoder::preallocateCelMemory(const byte *rawVideoData, const int16 numCels) { + for (CelHandleList::size_type i = 0; i < _celHandles.size(); ++i) { + CelHandleInfo &celHandle = _celHandles[i]; + + if (celHandle.status == CelHandleInfo::kFrameLifetime) { + _segMan->freeBitmap(celHandle.bitmapId); + celHandle.bitmapId = NULL_REG; + celHandle.status = CelHandleInfo::kNoCel; + celHandle.area = 0; + } + } + _celHandles.resize(numCels); + + const int numFixedCels = MIN(numCels, (int16)kFixedCelListSize); + for (int i = 0; i < numFixedCels; ++i) { + CelHandleInfo &celHandle = _celHandles[i]; + + // NOTE: There was a check to see if the cel handle was not allocated + // here, for some reason, which would mean that nothing was ever + // allocated from fixed cels, because the _celHandles array just got + // deleted and recreated... + if (celHandle.bitmapId == NULL_REG) { + break; + } + + celHandle.bitmapId = _fixedCels[i]; + celHandle.status = CelHandleInfo::kRobotLifetime; + celHandle.area = _maxCelArea[i]; + } + + uint maxFrameArea = 0; + for (int i = 0; i < numCels; ++i) { + const int16 celWidth = (int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 2); + const int16 celHeight = (int16)READ_SCI11ENDIAN_UINT16(rawVideoData + 4); + const uint16 dataSize = READ_SCI11ENDIAN_UINT16(rawVideoData + 14); + const uint area = celWidth * celHeight; + + if (area > maxFrameArea) { + maxFrameArea = area; + } + + CelHandleInfo &celHandle = _celHandles[i]; + if (celHandle.status == CelHandleInfo::kRobotLifetime) { + if (_maxCelArea[i] < area) { + _segMan->freeBitmap(celHandle.bitmapId); + _segMan->allocateBitmap(&celHandle.bitmapId, celWidth, celHeight, 255, 0, 0, _xResolution, _yResolution, kRawPaletteSize, false, false); + celHandle.area = area; + celHandle.status = CelHandleInfo::kFrameLifetime; + } + } else if (celHandle.status == CelHandleInfo::kNoCel) { + _segMan->allocateBitmap(&celHandle.bitmapId, celWidth, celHeight, 255, 0, 0, _xResolution, _yResolution, kRawPaletteSize, false, false); + celHandle.area = area; + celHandle.status = CelHandleInfo::kFrameLifetime; + } else { + error("Cel Handle has bad status"); + } + + rawVideoData += kCelHeaderSize + dataSize; + } + + if (maxFrameArea > _celDecompressionBuffer.size()) { + _celDecompressionBuffer.resize(maxFrameArea); + } } } // End of namespace Sci |