/* 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. * */ // ROQ video player based on this specification by Dr. Tim Ferguson: // http://www.csse.monash.edu.au/~timf/videocodec/idroq.txt #include "groovie/roq.h" #include "groovie/graphics.h" #include "groovie/groovie.h" #include "common/debug.h" #include "common/textconsole.h" #include "graphics/jpeg.h" #include "graphics/palette.h" #ifdef USE_RGB_COLOR // Required for the YUV to RGB conversion #include "graphics/conversion.h" #endif #include "audio/mixer.h" #include "audio/decoders/raw.h" namespace Groovie { ROQPlayer::ROQPlayer(GroovieEngine *vm) : #ifdef DITHER _dither(NULL), #endif VideoPlayer(vm), _codingTypeCount(0), _fg(&_vm->_graphicsMan->_foreground), _bg(&_vm->_graphicsMan->_background) { // Create the work surfaces _currBuf = new Graphics::Surface(); _prevBuf = new Graphics::Surface(); if (_vm->_mode8bit) { byte pal[256 * 3]; #ifdef DITHER // Initialize to a black palette memset(pal, 0, 256 * 3); // Build a basic color palette for (int r = 0; r < 4; r++) { for (int g = 0; g < 4; g++) { for (int b = 0; b < 4; b++) { byte col = (r << 4) | (g << 2) | (b << 0); pal[3 * col + 0] = r << 6; pal[3 * col + 1] = g << 6; pal[3 * col + 2] = b << 6; } } } // Initialize the dithering algorithm _paletteLookup = new Graphics::PaletteLUT(8, Graphics::PaletteLUT::kPaletteYUV); _paletteLookup->setPalette(pal, Graphics::PaletteLUT::kPaletteRGB, 8); for (int i = 0; (i < 64) && !_vm->shouldQuit(); i++) { debug("Groovie::ROQ: Building palette table: %02d/63", i); _paletteLookup->buildNext(); } #else // !DITHER // Set a grayscale palette for (int i = 0; i < 256; i++) { pal[(i * 3) + 0] = i; pal[(i * 3) + 1] = i; pal[(i * 3) + 2] = i; } #endif // DITHER _syst->getPaletteManager()->setPalette(pal, 0, 256); } } ROQPlayer::~ROQPlayer() { // Free the buffers _currBuf->free(); delete _currBuf; _prevBuf->free(); delete _prevBuf; #ifdef DITHER // Free the dithering algorithm delete _dither; delete _paletteLookup; #endif } uint16 ROQPlayer::loadInternal() { // Begin reading the file debugC(1, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Loading video"); // Read the file header ROQBlockHeader blockHeader; if (!readBlockHeader(blockHeader)) { return 0; } // Verify the file signature if (blockHeader.type != 0x1084) { return 0; } // Clear the dirty flag _dirty = true; // Reset the codebooks _num2blocks = 0; _num4blocks = 0; if ((blockHeader.size == 0) && (blockHeader.param == 0)) { // Set the offset scaling to 2 _offScale = 2; // Hardcoded FPS return 30; } else if (blockHeader.size == (uint32)-1) { // Set the offset scaling to 1 _offScale = 1; // In this case the block parameter is the framerate return blockHeader.param; } else { warning("Groovie::ROQ: Invalid header with size=%d and param=%d", blockHeader.size, blockHeader.param); return 0; } } void ROQPlayer::buildShowBuf() { #ifdef DITHER // Start a new frame dithering _dither->newFrame(); #endif for (int line = 0; line < _bg->h; line++) { byte *out = (byte *)_bg->getBasePtr(0, line); byte *in = (byte *)_currBuf->getBasePtr(0, line / _scaleY); for (int x = 0; x < _bg->w; x++) { if (_vm->_mode8bit) { #ifdef DITHER *out = _dither->dither(*in, *(in + 1), *(in + 2), x); #else // Just use the luminancy component *out = *in; #endif // DITHER #ifdef USE_RGB_COLOR } else { // Do the format conversion (YUV -> RGB -> Screen format) byte r, g, b; Graphics::YUV2RGB(*in, *(in + 1), *(in + 2), r, g, b); // FIXME: this is fixed to 16bit *(uint16 *)out = (uint16)_vm->_pixelFormat.RGBToColor(r, g, b); #endif // USE_RGB_COLOR } // Skip to the next pixel out += _vm->_pixelFormat.bytesPerPixel; if (!(x % _scaleX)) in += _currBuf->format.bytesPerPixel; } #ifdef DITHER _dither->nextLine(); #endif } // Swap buffers Graphics::Surface *tmp = _prevBuf; _prevBuf = _currBuf; _currBuf = tmp; } bool ROQPlayer::playFrameInternal() { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Playing frame"); // Process the needed blocks until the next video frame bool endframe = false; while (!endframe && !_file->eos()) { endframe = processBlock(); } if (_dirty) { // Build the show buffer from the current buffer buildShowBuf(); } // Wait until the current frame can be shown waitFrame(); if (_dirty) { // Update the screen _syst->copyRectToScreen((byte *)_bg->getBasePtr(0, 0), _bg->pitch, 0, (_syst->getHeight() - _bg->h) / 2, _bg->w, _bg->h); _syst->updateScreen(); // Clear the dirty flag _dirty = false; } // Return whether the video has ended return _file->eos(); } bool ROQPlayer::readBlockHeader(ROQBlockHeader &blockHeader) { if (_file->eos()) { return false; } else { blockHeader.type = _file->readUint16LE(); blockHeader.size = _file->readUint32LE(); blockHeader.param = _file->readUint16LE(); debugC(10, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Block type = 0x%02X", blockHeader.type); debugC(10, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Block size = 0x%08X", blockHeader.size); debugC(10, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Block param = 0x%04X", blockHeader.param); return true; } } bool ROQPlayer::processBlock() { // Read the header of the block ROQBlockHeader blockHeader; if (!readBlockHeader(blockHeader)) { return true; } // Calculate where the block should end int32 endpos = _file->pos() + blockHeader.size; // Detect the end of the video if (_file->eos()) { return false; } bool ok = true; bool endframe = false; switch (blockHeader.type) { case 0x1001: // Video info ok = processBlockInfo(blockHeader); break; case 0x1002: // Quad codebook definition ok = processBlockQuadCodebook(blockHeader); break; case 0x1011: // Quad vector quantised video frame ok = processBlockQuadVector(blockHeader); _dirty = true; endframe = true; break; case 0x1012: // Still image (JPEG) ok = processBlockStill(blockHeader); _dirty = true; endframe = true; break; case 0x1013: // Hang assert(blockHeader.size == 0 && blockHeader.param == 0); endframe = true; break; case 0x1020: // Mono sound samples ok = processBlockSoundMono(blockHeader); break; case 0x1021: // Stereo sound samples ok = processBlockSoundStereo(blockHeader); break; case 0x1030: // Audio container endpos = _file->pos(); ok = processBlockAudioContainer(blockHeader); break; default: warning("Groovie::ROQ: Unknown block type: 0x%04X", blockHeader.type); ok = false; _file->skip(blockHeader.size); } if (endpos != _file->pos()) warning("Groovie::ROQ: BLOCK %04x Should have ended at %d, and has ended at %d", blockHeader.type, endpos, _file->pos()); // End the frame when the graphics have been modified or when there's an error return endframe || !ok; } bool ROQPlayer::processBlockInfo(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing info block"); // Verify the block header if (blockHeader.type != 0x1001 || blockHeader.size != 8 || (blockHeader.param != 0 && blockHeader.param != 1)) { warning("Groovie::ROQ: BlockInfo size=%d param=%d", blockHeader.size, blockHeader.param); return false; } // Save the alpha channel size _alpha = blockHeader.param; // Read the information uint16 width = _file->readUint16LE(); uint16 height = _file->readUint16LE(); uint16 unk1 = _file->readUint16LE(); uint16 unk2 = _file->readUint16LE(); if (unk1 != 8 || unk2 != 4) { warning("Groovie::ROQ: unk1 = %d, unk2 = %d", unk1, unk2); return false; } // If the size of the image has changed, resize the buffers if ((width != _currBuf->w) || (height != _currBuf->h)) { // Calculate the maximum scale that fits the screen _scaleX = MIN(_syst->getWidth() / width, 2); _scaleY = MIN(_syst->getHeight() / height, 2); // Free the previous surfaces _currBuf->free(); _prevBuf->free(); // Allocate new buffers // These buffers use YUV data, since we can not describe it with a // PixelFormat struct we just add some dummy PixelFormat with the // correct bytes per pixel value. Since the surfaces are only used // internally and no code assuming RGB data is present is used on // them it should be just fine. _currBuf->create(width, height, Graphics::PixelFormat(3, 0, 0, 0, 0, 0, 0, 0, 0)); _prevBuf->create(width, height, Graphics::PixelFormat(3, 0, 0, 0, 0, 0, 0, 0, 0)); // Clear the buffers with black YUV values byte *ptr1 = (byte *)_currBuf->getBasePtr(0, 0); byte *ptr2 = (byte *)_prevBuf->getBasePtr(0, 0); for (int i = 0; i < width * height; i++) { *ptr1++ = 0; *ptr1++ = 128; *ptr1++ = 128; *ptr2++ = 0; *ptr2++ = 128; *ptr2++ = 128; } #ifdef DITHER // Reset the dithering algorithm with the new width delete _dither; _dither = new Graphics::SierraLight(width * _scaleX, _paletteLookup); #endif } return true; } bool ROQPlayer::processBlockQuadCodebook(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing quad codebook block"); // Get the number of 2x2 pixel blocks to read int newNum2blocks = blockHeader.param >> 8; if (newNum2blocks == 0) { newNum2blocks = 256; } if (newNum2blocks > _num2blocks) _num2blocks = newNum2blocks; // Get the number of 4x4 pixel blocks _num4blocks = blockHeader.param & 0xFF; if ((_num4blocks == 0) && (blockHeader.size > (uint32)_num2blocks * (6 + _alpha * 4))) { _num4blocks = 256; } // Read the 2x2 codebook for (int i = 0; i < newNum2blocks; i++) { // Read the 4 Y components and their alpha channel for (int j = 0; j < 4; j++) { _codebook2[i * 10 + j * 2] = _file->readByte(); _codebook2[i * 10 + j * 2 + 1] = _alpha ? _file->readByte() : 255; } // Read the subsampled Cb and Cr _file->read(&_codebook2[i * 10 + 8], 2); } // Read the 4x4 codebook _file->read(_codebook4, _num4blocks * 4); return true; } bool ROQPlayer::processBlockQuadVector(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing quad vector block"); // Get the mean motion vectors int8 Mx = blockHeader.param >> 8; int8 My = blockHeader.param & 0xFF; // Calculate where the block should end int32 endpos =_file->pos() + blockHeader.size; // Reset the coding types _codingTypeCount = 0; // Traverse the image in 16x16 macroblocks for (int macroY = 0; macroY < _currBuf->h; macroY += 16) { for (int macroX = 0; macroX < _currBuf->w; macroX += 16) { // Traverse the macroblock in 8x8 blocks for (int blockY = 0; blockY < 16; blockY += 8) { for (int blockX = 0; blockX < 16; blockX += 8) { processBlockQuadVectorBlock(macroX + blockX, macroY + blockY, Mx, My); } } } } // HACK: Skip the remaining bytes int32 skipBytes = endpos -_file->pos(); if (skipBytes > 0) { _file->skip(skipBytes); if (skipBytes != 2) { warning("Groovie::ROQ: Skipped %d bytes", skipBytes); } } return true; } void ROQPlayer::processBlockQuadVectorBlock(int baseX, int baseY, int8 Mx, int8 My) { uint16 codingType = getCodingType(); switch (codingType) { case 0: // MOT: Skip block break; case 1: { // FCC: Copy an existing block byte argument = _file->readByte(); int16 DDx = 8 - (argument >> 4); int16 DDy = 8 - (argument & 0x0F); copy(8, baseX, baseY, DDx - Mx, DDy - My); break; } case 2: // SLD: Quad vector quantisation // Upsample the 4x4 pixel block paint8(_file->readByte(), baseX, baseY); break; case 3: // CCC: // Traverse the block in 4x4 sub-blocks for (int subBlockY = 0; subBlockY < 8; subBlockY += 4) { for (int subBlockX = 0; subBlockX < 8; subBlockX += 4) { processBlockQuadVectorBlockSub(baseX + subBlockX, baseY + subBlockY, Mx, My); } } break; } } void ROQPlayer::processBlockQuadVectorBlockSub(int baseX, int baseY, int8 Mx, int8 My) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing quad vector sub block"); uint16 codingType = getCodingType(); switch (codingType) { case 0: // MOT: Skip block break; case 1: { // FCC: Copy an existing block byte argument = _file->readByte(); int16 DDx = 8 - (argument >> 4); int16 DDy = 8 - (argument & 0x0F); copy(4, baseX, baseY, DDx - Mx, DDy - My); break; } case 2: // SLD: Quad vector quantisation paint4(_file->readByte(), baseX, baseY); break; case 3: paint2(_file->readByte(), baseX , baseY); paint2(_file->readByte(), baseX + 2, baseY); paint2(_file->readByte(), baseX , baseY + 2); paint2(_file->readByte(), baseX + 2, baseY + 2); break; } } bool ROQPlayer::processBlockStill(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing still (JPEG) block"); warning("Groovie::ROQ: JPEG frame (unfinshed)"); Graphics::JPEG *jpg = new Graphics::JPEG(); jpg->read(_file); byte *y = (byte *)jpg->getComponent(1)->getBasePtr(0, 0); byte *u = (byte *)jpg->getComponent(2)->getBasePtr(0, 0); byte *v = (byte *)jpg->getComponent(3)->getBasePtr(0, 0); byte *ptr = (byte *)_currBuf->getBasePtr(0, 0); for (int i = 0; i < _currBuf->w * _currBuf->h; i++) { *ptr++ = *y++; *ptr++ = *u++; *ptr++ = *v++; } delete jpg; return true; } bool ROQPlayer::processBlockSoundMono(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing mono sound block"); // Verify the block header if (blockHeader.type != 0x1020) { return false; } // Initialize the audio stream if needed if (!_audioStream) { _audioStream = Audio::makeQueuingAudioStream(22050, false); Audio::SoundHandle sound_handle; g_system->getMixer()->playStream(Audio::Mixer::kPlainSoundType, &sound_handle, _audioStream); } // Create the audio buffer int16 *buffer = (int16 *)malloc(blockHeader.size * 2); // Initialize the prediction with the block parameter int16 prediction = blockHeader.param ^ 0x8000; // Process the data for (uint16 i = 0; i < blockHeader.size; i++) { int16 data = _file->readByte(); if (data < 0x80) { prediction += data * data; } else { data -= 0x80; prediction -= data * data; } buffer[i] = prediction; } // Queue the read buffer byte flags = Audio::FLAG_16BITS; #ifdef SCUMM_LITTLE_ENDIAN flags |= Audio::FLAG_LITTLE_ENDIAN; #endif _audioStream->queueBuffer((byte *)buffer, blockHeader.size * 2, DisposeAfterUse::YES, flags); return true; } bool ROQPlayer::processBlockSoundStereo(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing stereo sound block"); // Verify the block header if (blockHeader.type != 0x1021) { return false; } // Initialize the audio stream if needed if (!_audioStream) { _audioStream = Audio::makeQueuingAudioStream(22050, true); Audio::SoundHandle sound_handle; g_system->getMixer()->playStream(Audio::Mixer::kPlainSoundType, &sound_handle, _audioStream); } // Create the audio buffer int16 *buffer = (int16 *)malloc(blockHeader.size * 2); // Initialize the prediction with the block parameter int16 predictionLeft = (blockHeader.param & 0xFF00) ^ 0x8000; int16 predictionRight = (blockHeader.param << 8) ^ 0x8000; bool left = true; // Process the data for (uint16 i = 0; i < blockHeader.size; i++) { int16 data = _file->readByte(); if (left) { if (data < 0x80) { predictionLeft += data * data; } else { data -= 0x80; predictionLeft -= data * data; } buffer[i] = predictionLeft; } else { if (data < 0x80) { predictionRight += data * data; } else { data -= 0x80; predictionRight -= data * data; } buffer[i] = predictionRight; } left = !left; } // Queue the read buffer byte flags = Audio::FLAG_16BITS | Audio::FLAG_STEREO; #ifdef SCUMM_LITTLE_ENDIAN flags |= Audio::FLAG_LITTLE_ENDIAN; #endif _audioStream->queueBuffer((byte *)buffer, blockHeader.size * 2, DisposeAfterUse::YES, flags); return true; } bool ROQPlayer::processBlockAudioContainer(ROQBlockHeader &blockHeader) { debugC(5, kGroovieDebugVideo | kGroovieDebugAll, "Groovie::ROQ: Processing audio container block: 0x%04X", blockHeader.param); return true; } byte ROQPlayer::getCodingType() { _codingType <<= 2; if (!_codingTypeCount) { _codingType = _file->readUint16LE(); _codingTypeCount = 8; } _codingTypeCount--; return (_codingType >> 14); } void ROQPlayer::paint2(byte i, int destx, int desty) { if (i > _num2blocks) { error("Groovie::ROQ: Invalid 2x2 block %d (%d available)", i, _num2blocks); } byte *block = &_codebook2[i * 10]; byte u = block[8]; byte v = block[9]; byte *ptr = (byte *)_currBuf->getBasePtr(destx, desty); for (int y = 0; y < 2; y++) { for (int x = 0; x < 2; x++) { // Basic alpha test // TODO: Blending if (*(block + 1) > 128) { *ptr = *block; *(ptr + 1) = u; *(ptr + 2) = v; } ptr += 3; block += 2; } ptr += _currBuf->pitch - 6; } } void ROQPlayer::paint4(byte i, int destx, int desty) { if (i > _num4blocks) { error("Groovie::ROQ: Invalid 4x4 block %d (%d available)", i, _num4blocks); } byte *block4 = &_codebook4[i * 4]; for (int origy = 0; origy < 4; origy += 2) { for (int origx = 0; origx < 4; origx += 2) { paint2(*block4, destx + origx, desty + origy); block4++; } } } void ROQPlayer::paint8(byte i, int destx, int desty) { if (i > _num4blocks) { error("Groovie::ROQ: Invalid 4x4 block %d (%d available)", i, _num4blocks); } byte *block4 = &_codebook4[i * 4]; for (int y4 = 0; y4 < 2; y4++) { for (int x4 = 0; x4 < 2; x4++) { byte *block2 = &_codebook2[(*block4) * 10]; byte u = block2[8]; byte v = block2[9]; block4++; for (int y2 = 0; y2 < 2; y2++) { for (int x2 = 0; x2 < 2; x2++) { for (int repy = 0; repy < 2; repy++) { for (int repx = 0; repx < 2; repx++) { // Basic alpha test // TODO: Blending if (*(block2 + 1) > 128) { byte *ptr = (byte *)_currBuf->getBasePtr(destx + x4*4 + x2*2 + repx, desty + y4*4 + y2*2 + repy); *ptr = *block2; *(ptr + 1) = u; *(ptr + 2) = v; } } } block2 += 2; } } } } } void ROQPlayer::copy(byte size, int destx, int desty, int offx, int offy) { offx *= _offScale / _scaleX; offy *= _offScale / _scaleY; // Get the beginning of the first line byte *dst = (byte *)_currBuf->getBasePtr(destx, desty); byte *src = (byte *)_prevBuf->getBasePtr(destx + offx, desty + offy); for (int i = 0; i < size; i++) { // Copy the current line memcpy(dst, src, size * _currBuf->format.bytesPerPixel); // Move to the beginning of the next line dst += _currBuf->pitch; src += _currBuf->pitch; } } } // End of Groovie namespace