Files
scummvm-cursorfix/engines/grim/movie/codecs/smush_decoder.cpp
2026-02-02 04:50:13 +01:00

739 lines
19 KiB
C++

/* 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 3 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, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/endian.h"
#include "common/events.h"
#include "common/file.h"
#include "common/rational.h"
#include "common/system.h"
#include "common/timer.h"
#include "common/memstream.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
#include "audio/decoders/raw.h"
#include "engines/grim/debug.h"
#include "engines/grim/movie/codecs/codec48.h"
#include "engines/grim/movie/codecs/blocky8.h"
#include "engines/grim/movie/codecs/blocky16.h"
#include "engines/grim/movie/codecs/smush_decoder.h"
#include "engines/grim/movie/codecs/vima.h"
namespace Grim {
#define ANNO_HEADER "MakeAnim animation type 'Bl16' parameters: "
#define BUFFER_SIZE 16385
#define SMUSH_SPEED 66667
bool SmushDecoder::_demo = false;
static uint16 smushDestTable[5786];
SmushDecoder::SmushDecoder() {
_file = nullptr;
_videoLooping = false;
_startPos = 0;
_frames = nullptr;
_videoTrack = nullptr;
_audioTrack = nullptr;
_videoPause = false;
}
SmushDecoder::~SmushDecoder() {
delete _videoTrack;
delete _audioTrack;
delete[] _frames;
}
void SmushDecoder::init() {
_videoTrack->init();
_audioTrack->init();
}
void SmushDecoder::initFrames() {
delete[] _frames;
_frames = new Frame[_videoTrack->getFrameCount()];
int seekPos = _file->pos();
int curFrame = -1;
_file->seek(_startPos, SEEK_SET);
while (curFrame < _videoTrack->getFrameCount() - 1) {
Frame &frame = _frames[++curFrame];
frame.frame = curFrame;
frame.pos = _file->pos();
frame.keyframe = false;
uint32 tag = _file->readUint32BE();
uint32 size;
if (tag == MKTAG('A', 'N', 'N', 'O')) {
size = _file->readUint32BE();
_file->seek(size, SEEK_CUR);
tag = _file->readUint32BE();
}
assert(tag == MKTAG('F', 'R', 'M', 'E'));
size = _file->readUint32BE();
while (size > 0) {
uint32 subType = _file->readUint32BE();
uint32 subSize = _file->readUint32BE();
int32 subPos = _file->pos();
if (subType == MKTAG('B', 'l', '1', '6')) {
_file->seek(18, SEEK_CUR);
if (_file->readByte() == 0) {
frame.keyframe = true;
}
}
size -= subSize + 8 + (subSize & 1);
_file->seek(subPos + subSize + (subSize & 1), SEEK_SET);
}
_file->seek(size, SEEK_CUR);
}
_file->seek(seekPos, SEEK_SET);
}
void SmushDecoder::close() {
VideoDecoder::close();
_audioTrack = nullptr;
_videoTrack = nullptr;
_videoLooping = false;
_startPos = 0;
delete[] _frames;
_frames = nullptr;
if (_file) {
delete _file;
_file = nullptr;
}
}
bool SmushDecoder::readHeader() {
if (!_file) {
return false;
}
uint32 mainTag = _file->readUint32BE();
uint32 pos = _file->pos();
uint32 expectedTag = 0;
uint32 size = _file->readUint32BE(); // file-size
// Verify that we have the correct combination of headers.
if (mainTag == MKTAG('A', 'N', 'I', 'M')) { // Demo
expectedTag = MKTAG('A', 'H', 'D', 'R');
} else if (mainTag == MKTAG('S', 'A', 'N', 'M')) { // Retail
expectedTag = MKTAG('S', 'H', 'D', 'R');
} else {
error("Invalid SMUSH-header");
}
uint32 tag = _file->readUint32BE();
size = _file->readUint32BE();
pos = _file->pos();
assert(tag == expectedTag);
(void)expectedTag;
if (tag == MKTAG('A', 'H', 'D', 'R')) { // Demo
uint32 version = _file->readUint16LE();
uint16 nbFrames = _file->readUint16LE();
_file->readUint16BE(); // unknown
int width = -1;
int height = -1;
_videoLooping = false;
_startPos = 0;
_videoTrack = new SmushVideoTrack(width, height, SMUSH_SPEED, nbFrames, false);
_videoTrack->_x = -1;
_videoTrack->_y = -1;
addTrack(_videoTrack);
_file->read(_videoTrack->getPal(), 0x300);
int audioRate = 11025;
if (version == 2) {
_file->readUint32LE(); // framerate
_file->readUint32LE();
audioRate = _file->readUint32LE();
}
_file->readUint32BE();
_file->readUint32BE();
_audioTrack = new SmushAudioTrack(getSoundType(), false, audioRate, 2);
addTrack(_audioTrack);
return true;
} else if (tag == MKTAG('S', 'H', 'D', 'R')) { // Retail
_file->readUint16LE();
uint16 nbFrames = _file->readUint32LE();
_file->readUint16LE();
int width = _file->readUint16LE();
int height = _file->readUint16LE();
_file->readUint16LE();
int frameRate = _file->readUint32LE();
int16 flags = _file->readUint16LE();
// Output information for checking out the flags
if (Debug::isChannelEnabled(Debug::Movie | Debug::Info)) {
warning("SMUSH Flags:");
for (int i = 0; i < 16; i++) {
warning(" %d", (flags & (1 << i)) != 0);
}
}
_file->seek(pos + size + (size & 1), SEEK_SET);
_videoLooping = true;
// If the video is NOT looping, setLooping will set the speed to the proper value
_videoTrack = new SmushVideoTrack(width, height, frameRate, nbFrames, true);
addTrack(_videoTrack);
return handleFramesHeader();
}
return false;
}
bool SmushDecoder::handleFramesHeader() {
uint32 tag;
int32 size;
int pos = 0;
int freq = 0;
int channels = 0;
tag = _file->readUint32BE();
if (tag != MKTAG('F', 'L', 'H', 'D')) {
return false;
}
size = _file->readUint32BE();
byte *f_header = new byte[size];
_file->read(f_header, size);
do {
if (READ_BE_UINT32(f_header + pos) == MKTAG('B', 'l', '1', '6')) {
pos += READ_BE_UINT32(f_header + pos + 4) + 8;
} else if (READ_BE_UINT32(f_header + pos) == MKTAG('W', 'a', 'v', 'e')) {
freq = READ_LE_UINT32(f_header + pos + 8);
channels = READ_LE_UINT32(f_header + pos + 12);
pos += 20;
} else {
error("SmushDecoder::handleFramesHeader() unknown tag");
}
} while (pos < size);
delete[] f_header;
_audioTrack = new SmushAudioTrack(getSoundType(), true, freq, channels);
addTrack(_audioTrack);
return true;
}
bool SmushDecoder::loadStream(Common::SeekableReadStream *stream) {
close();
_file = stream;
// Load the video
if (!readHeader()) {
warning("Failure loading SMUSH-file");
return false;
}
_startPos = _file->pos();
init();
return true;
}
const Graphics::Surface *SmushDecoder::decodeNextFrame() {
handleFrame();
// We might be interested in getting the last frame even after the video ends:
if (endOfVideo()) {
return _videoTrack->decodeNextFrame();
}
return VideoDecoder::decodeNextFrame();
}
void SmushDecoder::setLooping(bool l) {
_videoLooping = l;
if (!_videoLooping) {
_videoTrack->setMsPerFrame(SMUSH_SPEED);
}
}
void SmushDecoder::handleFrame() {
uint32 tag;
int32 size;
if (isPaused()) {
return;
}
if (_videoTrack->endOfTrack()) { // Looping is handled outside, by rewinding the video.
_audioTrack->stop(); // HACK: Avoids the movie playing past the last frame
// pauseVideo(true);
return;
}
tag = _file->readUint32BE();
size = _file->readUint32BE();
if (tag == MKTAG('A', 'N', 'N', 'O')) {
char *anno;
byte *data;
data = new byte[size];
_file->read(data, size);
anno = (char *)data;
if (strncmp(anno, ANNO_HEADER, sizeof(ANNO_HEADER) - 1) == 0) {
//char *annoData = anno + sizeof(ANNO_HEADER);
// Examples:
// Water streaming around boat from Manny's balcony
// MakeAnim animation type 'Bl16' parameters: 10000;12000;100;1;0;0;0;0;25;0;
// Water in front of the Blue Casket
// MakeAnim animation type 'Bl16' parameters: 20000;25000;100;1;0;0;0;0;25;0;
// Scrimshaw exterior:
// MakeAnim animation type 'Bl16' parameters: 6000;8000;100;0;0;0;0;0;2;0;
// Lola engine room (loops a limited number of times?):
// MakeAnim animation type 'Bl16' parameters: 6000;8000;90;1;0;0;0;0;2;0;
Debug::debug(Debug::Movie, "Announcement data: %s\n", anno);
// It looks like the announcement data is actually for setting some of the
// header parameters, not for any looping purpose
} else {
Debug::debug(Debug::Movie, "Announcement header not understood: %s\n", anno);
}
delete[] anno;
tag = _file->readUint32BE();
size = _file->readUint32BE();
}
assert(tag == MKTAG('F', 'R', 'M', 'E'));
handleFRME(_file, size);
_videoTrack->finishFrame();
}
void SmushDecoder::handleFRME(Common::SeekableReadStream *stream, uint32 size) {
int blockSize = size;
byte *block = new byte[size];
stream->read(block, size);
Common::MemoryReadStream *memStream = new Common::MemoryReadStream(block, size, DisposeAfterUse::NO);
while (size > 0) {
uint32 subType = memStream->readUint32BE();
uint32 subSize = memStream->readUint32BE();
uint32 subPos = memStream->pos();
switch (subType) {
// Retail only:
case MKTAG('B', 'l', '1', '6'):
_videoTrack->handleBlocky16(memStream, subSize);
break;
case MKTAG('W', 'a', 'v', 'e'):
_audioTrack->handleVIMA(memStream, blockSize);
break;
// Demo only:
case MKTAG('F', 'O', 'B', 'J'):
_videoTrack->handleFrameObject(memStream, subSize);
break;
case MKTAG('I', 'A', 'C', 'T'):
_audioTrack->handleIACT(memStream, subSize);
break;
case MKTAG('X', 'P', 'A', 'L'):
_videoTrack->handleDeltaPalette(memStream, subSize);
break;
default:
Debug::error(Debug::Movie, "SmushDecoder::handleFrame() unknown tag");
}
size -= subSize + 8 + (subSize & 1);
memStream->seek(subPos + subSize + (subSize & 1), SEEK_SET);
}
delete memStream;
delete[] block;
}
bool SmushDecoder::rewind() {
return seekToFrame(0);
}
bool SmushDecoder::seekIntern(const Audio::Timestamp &time) {
int32 wantedFrame = (uint32)((time.msecs() / 1000.0f) * _videoTrack->getFrameRate().toDouble());
if (wantedFrame != 0) {
Debug::debug(Debug::Movie, "Seek to time: %d, frame: %d", time.msecs(), wantedFrame);
Debug::debug(Debug::Movie, "Current frame: %d", _videoTrack->getCurFrame());
}
if (wantedFrame > _videoTrack->getFrameCount()) {
return false;
}
if (!_frames) {
initFrames();
}
// Track down the keyframe
int keyframe = 0;
for (int i = wantedFrame; i >= 0; --i) {
if (_frames[i].keyframe) {
keyframe = i;
break;
}
}
_videoTrack->setFrameStart(keyframe);
// VIMA frames are 50 frames ahead of time, so we have to make sure we have 50 frames
// of audio before the wantedFrame. Here we use 51 to have a bit of safe margin
if (wantedFrame - keyframe < 51) {
keyframe = wantedFrame - 51;
}
if (keyframe < 0) {
keyframe = 0;
}
_file->seek(_frames[keyframe].pos, SEEK_SET);
_videoTrack->setCurFrame(keyframe - 1);
while (_videoTrack->getCurFrame() < wantedFrame - 1) {
decodeNextFrame();
}
// As said, VIMA is 50 frames ahead of time. Every frame it pushes 1470 samples, and 50 * 1470 = 73500.
// The first frame, instead of 1470, it pushes 73500 samples to have this 50-frames-time.
// So if we have used frame 0 as keyframe we can remove safely time * rate samples, and we will
// still have the 50 frames margin. If we have used a later frame as keyframe we don't have the 73500
// samples pushed the first frame, so we have to be careful not to remove too much data,
// otherwise the audio will start at a later point. (72030 == 73500 - 1470)
int offset = (keyframe == 0 ? 0 : 72030);
// Skip decoded audio between the keyframe and the target frame
Audio::Timestamp delay = 0;
if (_videoTrack->getCurFrame() > 0) {
delay = _videoTrack->getFrameTime(_videoTrack->getCurFrame());
}
if (keyframe > 0) {
delay = delay - _videoTrack->getFrameTime(keyframe);
}
int32 sampleCount = (delay.msecs() / 1000.f) * _audioTrack->getRate() - offset;
_audioTrack->skipSamples(sampleCount);
VideoDecoder::seekIntern(time);
return true;
}
SmushDecoder::SmushVideoTrack::SmushVideoTrack(int width, int height, int fps, int numFrames, bool is16Bit) {
if (!is16Bit) { // Demo
_format = Graphics::PixelFormat::createFormatCLUT8();
_codec48 = new Codec48Decoder();
_blocky8 = new Blocky8();
_blocky16 = nullptr;
} else {
_format = Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0);
_codec48 = nullptr;
_blocky8 = nullptr;
_blocky16 = new Blocky16();
_blocky16->init(width, height);
}
_width = width;
_height = height;
_nbframes = numFrames;
_is16Bit = is16Bit;
_x = 0;
_y = 0;
setMsPerFrame(fps);
_curFrame = 0;
for (int i = 0; i < 0x300; i++) {
_palette[i] = 0;
_deltaPal[i] = 0;
_dirtyPalette = false;
}
_frameStart = 0;
}
SmushDecoder::SmushVideoTrack::~SmushVideoTrack() {
delete _codec48;
delete _blocky8;
delete _blocky16;
_surface.free();
}
void SmushDecoder::SmushVideoTrack::init() {
_curFrame = -1;
_frameStart = -1;
if (_is16Bit) { // Retail only
_surface.create(_width, _height, _format);
}
}
void SmushDecoder::SmushVideoTrack::finishFrame() {
_curFrame++;
}
void SmushDecoder::SmushVideoTrack::setFrameStart(int frame) {
_frameStart = frame - 1;
}
void SmushDecoder::SmushVideoTrack::handleBlocky16(Common::SeekableReadStream *stream, uint32 size) {
if (_curFrame < _frameStart) {
return;
}
assert(_is16Bit);
byte *ptr = new byte[size];
stream->read(ptr, size);
_blocky16->decode((byte *)_surface.getPixels(), ptr);
delete[] ptr;
}
void SmushDecoder::SmushVideoTrack::handleFrameObject(Common::SeekableReadStream *stream, uint32 size) {
if (_curFrame < _frameStart) {
return;
}
assert(!_is16Bit);
assert(size >= 14);
byte codec = stream->readByte();
assert(codec == 47 || codec == 48);
/* byte codecParam = */ stream->readByte();
_x = stream->readSint16LE();
_y = stream->readSint16LE();
uint16 width = stream->readUint16LE();
uint16 height = stream->readUint16LE();
if (width != _width || height != _height) {
_width = width;
_height = height;
_surface.create(_width, _height, _format);
_codec48->init(_width, _height);
_blocky8->init(_width, _height);
}
stream->readUint16LE();
stream->readUint16LE();
size -= 14;
byte *ptr = new byte[size];
stream->read(ptr, size);
if (codec == 47) {
_blocky8->decode((byte *)_surface.getPixels(), ptr);
} else if (codec == 48) {
_codec48->decode((byte *)_surface.getPixels(), ptr);
}
delete[] ptr;
}
static byte delta_color(byte org_color, int16 delta_color) {
int t = (org_color * 129 + delta_color) / 128;
return CLIP(t, 0, 255);
}
void SmushDecoder::SmushVideoTrack::handleDeltaPalette(Common::SeekableReadStream *stream, int32 size) {
if (size == 0x300 * 3 + 4) {
stream->seek(4, SEEK_CUR);
for (int i = 0; i < 0x300; i++) {
_deltaPal[i] = stream->readUint16LE();
}
stream->read(_palette, 0x300);
_dirtyPalette = true;
} else if (size == 6) {
for (int i = 0; i < 0x300; i++) {
_palette[i] = delta_color(_palette[i], _deltaPal[i]);
}
_dirtyPalette = true;
} else {
error("SmushDecoder::handleDeltaPalette() Wrong size for DeltaPalette");
}
}
Graphics::Surface *SmushDecoder::SmushVideoTrack::decodeNextFrame() {
return &_surface;
}
void SmushDecoder::SmushVideoTrack::setMsPerFrame(int ms) {
_frameRate = Common::Rational(1000000, ms);
}
SmushDecoder::SmushAudioTrack::SmushAudioTrack(Audio::Mixer::SoundType soundType, bool isVima, int freq, int channels) :
AudioTrack(soundType) {
_isVima = isVima;
_channels = channels;
_freq = freq;
_queueStream = Audio::makeQueuingAudioStream(_freq, (_channels == 2));
_IACTpos = 0;
}
SmushDecoder::SmushAudioTrack::~SmushAudioTrack() {
delete _queueStream;
}
void SmushDecoder::SmushAudioTrack::init() {
_IACTpos = 0;
if (_isVima) {
vimaInit(smushDestTable);
}
}
void SmushDecoder::SmushAudioTrack::handleVIMA(Common::SeekableReadStream *stream, uint32 size) {
if (size < 8)
return;
int decompressedSize = stream->readUint32BE();
if (decompressedSize == MKTAG('P', 'S', 'A', 'D')) {
decompressedSize = stream->readUint32BE();
if (decompressedSize > (int)size - 8)
decompressedSize = size - 8;
if (decompressedSize < 10)
return;
stream->skip(10);
decompressedSize -= 10;
byte *src = (byte *)malloc(decompressedSize);
stream->read(src, decompressedSize);
int flags = Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN;
if (_channels == 2) {
flags |= Audio::FLAG_STEREO;
}
if (!_queueStream) {
_queueStream = Audio::makeQueuingAudioStream(_freq, (_channels == 2));
}
_queueStream->queueBuffer(src, decompressedSize, DisposeAfterUse::YES, flags);
return;
}
if (decompressedSize < 0) {
stream->readUint32BE();
decompressedSize = stream->readUint32BE();
}
byte *src = new byte[size];
stream->read(src, size);
// this will be deleted using free() by the stream, so allocate it using malloc().
int16 *dst = (int16 *)malloc(decompressedSize * _channels * 2);
decompressVima(src, dst, decompressedSize * _channels * 2, smushDestTable, true);
int flags = Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN;
if (_channels == 2) {
flags |= Audio::FLAG_STEREO;
}
if (!_queueStream) {
_queueStream = Audio::makeQueuingAudioStream(_freq, (_channels == 2));
}
_queueStream->queueBuffer((byte *)dst, decompressedSize * _channels * 2, DisposeAfterUse::YES, flags);
delete[] src;
}
void SmushDecoder::SmushAudioTrack::handleIACT(Common::SeekableReadStream *stream, int32 size) {
byte *src = new byte[size];
stream->read(src, size);
int32 bsize = size - 18;
const byte *d_src = src + 18;
while (bsize > 0) {
if (_IACTpos >= 2) {
int32 len = READ_BE_UINT16(_IACToutput) + 2;
len -= _IACTpos;
if (len > bsize) {
memcpy(_IACToutput + _IACTpos, d_src, bsize);
_IACTpos += bsize;
bsize = 0;
} else {
// this will be deleted using free() by the stream, so allocate it using malloc().
byte *output_data = (byte *)malloc(4096);
memcpy(_IACToutput + _IACTpos, d_src, len);
byte *dst = output_data;
byte *d_src2 = _IACToutput;
d_src2 += 2;
int32 count = 1024;
byte variable1 = *d_src2++;
byte variable2 = variable1 / 16;
variable1 &= 0x0f;
do {
byte value;
value = *(d_src2++);
if (value == 0x80) {
*dst++ = *d_src2++;
*dst++ = *d_src2++;
} else {
int16 val = (int8)value << variable2;
*dst++ = val >> 8;
*dst++ = (byte)(val);
}
value = *(d_src2++);
if (value == 0x80) {
*dst++ = *d_src2++;
*dst++ = *d_src2++;
} else {
int16 val = (int8)value << variable1;
*dst++ = val >> 8;
*dst++ = (byte)(val);
}
} while (--count);
if (!_queueStream) {
_queueStream = Audio::makeQueuingAudioStream(22050, true);
}
_queueStream->queueBuffer(output_data, 0x1000, DisposeAfterUse::YES, Audio::FLAG_STEREO | Audio::FLAG_16BITS);
bsize -= len;
d_src += len;
_IACTpos = 0;
}
} else {
if (bsize > 1 && _IACTpos == 0) {
*(_IACToutput + 0) = *d_src++;
_IACTpos = 1;
bsize--;
}
*(_IACToutput + _IACTpos) = *d_src++;
_IACTpos++;
bsize--;
}
}
delete[] src;
}
bool SmushDecoder::SmushAudioTrack::seek(const Audio::Timestamp &time) {
return true;
}
void SmushDecoder::SmushAudioTrack::skipSamples(int sampleCount) {
if (sampleCount <= 0)
return;
if (_queueStream->isStereo())
sampleCount *= 2;
int16 *tempBuffer = new int16[sampleCount];
_queueStream->readBuffer(tempBuffer, sampleCount);
delete[] tempBuffer;
}
} // end of namespace Grim