2019-03-05 18:28:12 +00:00
|
|
|
/* 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.
|
|
|
|
*
|
2021-12-26 17:47:58 +00:00
|
|
|
* 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.
|
2019-03-05 18:28:12 +00:00
|
|
|
*
|
|
|
|
* 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
|
2021-12-26 17:47:58 +00:00
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2019-03-05 18:28:12 +00:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "common/debug.h"
|
|
|
|
#include "common/endian.h"
|
|
|
|
#include "common/system.h"
|
2022-01-09 10:50:27 +00:00
|
|
|
#include "common/memstream.h"
|
2019-03-05 18:28:12 +00:00
|
|
|
#include "common/file.h"
|
|
|
|
#include "common/textconsole.h"
|
|
|
|
|
|
|
|
#include "audio/decoders/raw.h"
|
2022-08-29 16:34:25 +00:00
|
|
|
#include "audio/decoders/apc.h"
|
2019-03-05 18:28:12 +00:00
|
|
|
|
2021-04-06 17:09:03 +00:00
|
|
|
#include "image/codecs/hlz.h"
|
2022-08-29 16:34:25 +00:00
|
|
|
#include "image/codecs/hnm.h"
|
|
|
|
|
|
|
|
#include "video/hnm_decoder.h"
|
2019-03-05 18:28:12 +00:00
|
|
|
|
|
|
|
namespace Video {
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
HNMDecoder::HNMDecoder(const Graphics::PixelFormat &format, bool loop,
|
|
|
|
byte *initialPalette) : _regularFrameDelayMs(uint32(-1)),
|
|
|
|
_videoTrack(nullptr), _audioTrack(nullptr), _stream(nullptr), _format(format),
|
|
|
|
_loop(loop), _initialPalette(initialPalette), _alignedChunks(false),
|
|
|
|
_dataBuffer(nullptr), _dataBufferAlloc(0) {
|
|
|
|
if (initialPalette && format.bytesPerPixel >= 2) {
|
|
|
|
error("Invalid pixel format while initial palette is set");
|
|
|
|
}
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
HNMDecoder::~HNMDecoder() {
|
|
|
|
close();
|
|
|
|
|
2019-05-20 18:49:01 +00:00
|
|
|
delete[] _initialPalette;
|
|
|
|
|
2019-03-05 18:28:12 +00:00
|
|
|
// We don't deallocate _videoTrack and _audioTrack as they are owned by base class
|
|
|
|
}
|
|
|
|
|
|
|
|
bool HNMDecoder::loadStream(Common::SeekableReadStream *stream) {
|
|
|
|
close();
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
// Take the ownership of stream immediately
|
|
|
|
_stream = stream;
|
|
|
|
|
2019-03-05 18:28:12 +00:00
|
|
|
uint32 tag = stream->readUint32BE();
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
/* All HNM versions are supported */
|
2022-01-08 10:58:03 +00:00
|
|
|
if (tag != MKTAG('H', 'N', 'M', '4') &&
|
2022-08-29 16:34:25 +00:00
|
|
|
tag != MKTAG('U', 'B', 'B', '2') &&
|
|
|
|
tag != MKTAG('H', 'N', 'M', '6')) {
|
2019-03-05 18:28:12 +00:00
|
|
|
close();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
byte audioflags = 0;
|
|
|
|
uint16 soundBits = 0, soundFormat = 0;
|
|
|
|
if (tag == MKTAG('H', 'N', 'M', '6')) {
|
|
|
|
//uint16 ukn6_1 = stream->readUint16LE();
|
|
|
|
stream->skip(2);
|
|
|
|
audioflags = stream->readByte();
|
|
|
|
//byte bpp = stream->readByte();
|
|
|
|
stream->skip(1);
|
|
|
|
} else {
|
|
|
|
//uint32 ukn = stream->readUint32BE();
|
|
|
|
stream->skip(4);
|
|
|
|
}
|
2019-03-05 18:28:12 +00:00
|
|
|
uint16 width = stream->readUint16LE();
|
|
|
|
uint16 height = stream->readUint16LE();
|
|
|
|
//uint32 filesize = stream->readUint32LE();
|
|
|
|
stream->skip(4);
|
|
|
|
uint32 frameCount = stream->readUint32LE();
|
|
|
|
//uint32 tabOffset = stream->readUint32LE();
|
|
|
|
stream->skip(4);
|
2022-08-29 16:34:25 +00:00
|
|
|
if (tag == MKTAG('H', 'N', 'M', '4') ||
|
|
|
|
tag == MKTAG('U', 'B', 'B', '2')) {
|
|
|
|
soundBits = stream->readUint16LE();
|
|
|
|
soundFormat = stream->readUint16LE();
|
|
|
|
} else {
|
|
|
|
//uint16 ukn6_2 = stream->readUint16LE();
|
|
|
|
//uint16 ukn6_3 = stream->readUint16LE();
|
|
|
|
stream->skip(4);
|
|
|
|
}
|
2019-03-05 18:28:12 +00:00
|
|
|
uint32 frameSize = stream->readUint32LE();
|
|
|
|
|
2019-05-30 10:36:09 +00:00
|
|
|
byte unknownStr[16];
|
|
|
|
byte copyright[16];
|
2019-03-05 18:28:12 +00:00
|
|
|
stream->read(unknownStr, sizeof(unknownStr));
|
|
|
|
stream->read(copyright, sizeof(copyright));
|
|
|
|
|
|
|
|
if (_loop) {
|
|
|
|
// This will force loop mode
|
|
|
|
frameCount = 0;
|
|
|
|
}
|
|
|
|
|
2022-01-12 07:50:40 +00:00
|
|
|
// When no audio use a factor of 1 for audio timestamp
|
|
|
|
uint32 audioSampleRate = 1;
|
2022-08-29 16:34:25 +00:00
|
|
|
// When no sound display a frame every X ms if not overriden
|
|
|
|
if (_regularFrameDelayMs == uint32(-1)) {
|
|
|
|
if (tag == MKTAG('H', 'N', 'M', '4') ||
|
|
|
|
tag == MKTAG('U', 'B', 'B', '2')) {
|
|
|
|
// For HNM4&5 it's every 80 ms
|
|
|
|
_regularFrameDelayMs = 80;
|
|
|
|
} else if (tag == MKTAG('H', 'N', 'M', '6')) {
|
|
|
|
// For HNM6 it's every 66 ms
|
|
|
|
_regularFrameDelayMs = 66;
|
|
|
|
}
|
|
|
|
}
|
2022-01-12 07:50:40 +00:00
|
|
|
|
2022-01-08 10:58:03 +00:00
|
|
|
_videoTrack = nullptr;
|
|
|
|
_audioTrack = nullptr;
|
|
|
|
if (tag == MKTAG('H', 'N', 'M', '4')) {
|
2022-08-29 16:34:25 +00:00
|
|
|
if (_format.bytesPerPixel >= 2) {
|
|
|
|
// Bad constructor used
|
|
|
|
close();
|
|
|
|
return false;
|
|
|
|
}
|
2022-01-08 10:58:03 +00:00
|
|
|
if (soundFormat == 2 && soundBits != 0) {
|
|
|
|
// HNM4 is Mono 22050Hz
|
|
|
|
_audioTrack = new DPCMAudioTrack(soundFormat, soundBits, 22050, false, getSoundType());
|
2022-01-12 07:50:40 +00:00
|
|
|
audioSampleRate = 22050;
|
2022-01-08 10:58:03 +00:00
|
|
|
}
|
2022-01-12 07:50:40 +00:00
|
|
|
_videoTrack = new HNM4VideoTrack(width, height, frameSize, frameCount,
|
|
|
|
_regularFrameDelayMs, audioSampleRate,
|
2022-01-08 10:58:03 +00:00
|
|
|
_initialPalette);
|
2022-01-12 07:50:40 +00:00
|
|
|
} else if (tag == MKTAG('U', 'B', 'B', '2')) {
|
2022-08-29 16:34:25 +00:00
|
|
|
if (_format.bytesPerPixel >= 2) {
|
|
|
|
// Bad constructor used
|
|
|
|
close();
|
|
|
|
return false;
|
|
|
|
}
|
2022-01-08 10:58:03 +00:00
|
|
|
if (soundFormat == 2 && soundBits == 0) {
|
|
|
|
// UBB2 is Stereo 22050Hz
|
|
|
|
_audioTrack = new DPCMAudioTrack(soundFormat, 16, 22050, true, getSoundType());
|
2022-01-12 07:50:40 +00:00
|
|
|
audioSampleRate = 22050;
|
2022-01-08 10:58:03 +00:00
|
|
|
}
|
2022-01-12 07:50:40 +00:00
|
|
|
_videoTrack = new HNM5VideoTrack(width, height, frameSize, frameCount,
|
|
|
|
_regularFrameDelayMs, audioSampleRate,
|
|
|
|
_initialPalette);
|
2022-08-29 16:34:25 +00:00
|
|
|
} else if (tag == MKTAG('H', 'N', 'M', '6')) {
|
|
|
|
if (_format.bytesPerPixel < 2) {
|
|
|
|
// Bad constructor used or bad format given
|
|
|
|
close();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
_alignedChunks = true;
|
|
|
|
if (audioflags & 1) {
|
|
|
|
byte stereo = (audioflags >> 7) & 1;
|
|
|
|
audioSampleRate = ((audioflags >> 4) & 6) * 11025;
|
|
|
|
_audioTrack = new APCAudioTrack(audioSampleRate, stereo, getSoundType());
|
|
|
|
}
|
|
|
|
_videoTrack = new HNM6VideoTrack(width, height, frameSize, frameCount,
|
|
|
|
_regularFrameDelayMs, audioSampleRate,
|
|
|
|
_format);
|
2019-03-05 18:28:12 +00:00
|
|
|
} else {
|
2022-01-08 10:58:03 +00:00
|
|
|
// We should never be here
|
|
|
|
close();
|
|
|
|
return false;
|
|
|
|
}
|
2022-01-12 07:50:40 +00:00
|
|
|
addTrack(_videoTrack);
|
2022-01-08 10:58:03 +00:00
|
|
|
if (_audioTrack) {
|
|
|
|
addTrack(_audioTrack);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void HNMDecoder::close() {
|
|
|
|
VideoDecoder::close();
|
|
|
|
// Tracks are cleant by VideoDecoder::close
|
|
|
|
_videoTrack = nullptr;
|
|
|
|
_audioTrack = nullptr;
|
|
|
|
|
|
|
|
delete _stream;
|
|
|
|
_stream = nullptr;
|
2022-01-09 10:50:27 +00:00
|
|
|
|
|
|
|
delete[] _dataBuffer;
|
|
|
|
_dataBuffer = nullptr;
|
|
|
|
_dataBufferAlloc = 0;
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void HNMDecoder::readNextPacket() {
|
|
|
|
// We are called to feed a frame
|
|
|
|
// Each chunk is packetized and a packet seems to contain only one frame
|
|
|
|
uint32 superchunkRemaining = _stream->readUint32LE();
|
|
|
|
if (!superchunkRemaining) {
|
|
|
|
if (!_loop) {
|
|
|
|
error("End of file but still requesting data");
|
|
|
|
} else {
|
|
|
|
// Looping: read back from start of file, skip header and read a new super chunk header
|
|
|
|
_videoTrack->restart();
|
|
|
|
_stream->seek(64, SEEK_SET);
|
|
|
|
superchunkRemaining = _stream->readUint32LE();
|
|
|
|
}
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
superchunkRemaining = superchunkRemaining & 0x00ffffff;
|
|
|
|
if (superchunkRemaining < 4) {
|
|
|
|
error("Invalid superchunk header");
|
|
|
|
}
|
|
|
|
superchunkRemaining -= 4;
|
|
|
|
|
|
|
|
if (_dataBufferAlloc < superchunkRemaining) {
|
|
|
|
delete[] _dataBuffer;
|
|
|
|
_dataBuffer = new byte[superchunkRemaining];
|
|
|
|
_dataBufferAlloc = superchunkRemaining;
|
|
|
|
}
|
|
|
|
if (_stream->read(_dataBuffer, superchunkRemaining) != superchunkRemaining) {
|
|
|
|
error("Not enough data in file");
|
|
|
|
}
|
2019-03-05 18:28:12 +00:00
|
|
|
|
2022-01-12 07:50:40 +00:00
|
|
|
// We use -1 here to discrimate a possibly empty sound frame
|
|
|
|
uint32 audioNumSamples = -1;
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
byte *data_p = _dataBuffer;
|
|
|
|
while (superchunkRemaining > 0) {
|
|
|
|
if (superchunkRemaining < 8) {
|
|
|
|
error("Not enough data in superchunk");
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32 chunkSize = READ_LE_UINT32(data_p);
|
|
|
|
data_p += sizeof(uint32);
|
|
|
|
uint16 chunkType = READ_BE_UINT16(data_p);
|
|
|
|
data_p += sizeof(uint16);
|
|
|
|
uint16 flags = READ_LE_UINT16(data_p);
|
|
|
|
data_p += sizeof(uint16);
|
|
|
|
|
|
|
|
if (superchunkRemaining < chunkSize) {
|
|
|
|
error("Chunk has a bogus size");
|
|
|
|
}
|
2019-03-05 18:28:12 +00:00
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
if (chunkType == MKTAG16('S', 'D') ||
|
|
|
|
chunkType == MKTAG16('A', 'A') ||
|
|
|
|
chunkType == MKTAG16('B', 'B')) {
|
2019-03-05 18:28:12 +00:00
|
|
|
if (_audioTrack) {
|
2022-08-29 16:34:25 +00:00
|
|
|
audioNumSamples = _audioTrack->decodeSound(chunkType, data_p, chunkSize - 8);
|
2019-03-05 18:28:12 +00:00
|
|
|
} else {
|
2021-04-11 17:28:31 +00:00
|
|
|
warning("Got audio data without an audio track");
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-01-09 10:50:27 +00:00
|
|
|
_videoTrack->decodeChunk(data_p, chunkSize - 8, chunkType, flags);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
if (_alignedChunks) {
|
|
|
|
chunkSize = ((chunkSize + 3) / 4) * 4;
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
data_p += (chunkSize - 8);
|
2019-03-05 18:28:12 +00:00
|
|
|
superchunkRemaining -= chunkSize;
|
|
|
|
}
|
2022-01-12 07:50:40 +00:00
|
|
|
_videoTrack->newFrame(audioNumSamples);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
2022-01-12 07:50:40 +00:00
|
|
|
HNMDecoder::HNMVideoTrack::HNMVideoTrack(uint32 frameCount,
|
|
|
|
uint32 regularFrameDelayMs, uint32 audioSampleRate) :
|
|
|
|
_frameCount(frameCount), _curFrame(-1), _regularFrameDelayMs(regularFrameDelayMs),
|
|
|
|
_nextFrameStartTime(0, audioSampleRate) {
|
2019-03-05 18:28:12 +00:00
|
|
|
restart();
|
2022-01-08 10:58:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
HNMDecoder::HNM45VideoTrack::HNM45VideoTrack(uint32 width, uint32 height, uint32 frameSize,
|
2022-01-12 07:50:40 +00:00
|
|
|
uint32 frameCount, uint32 regularFrameDelayMs, uint32 audioSampleRate,
|
|
|
|
const byte *initialPalette) :
|
|
|
|
HNMVideoTrack(frameCount, regularFrameDelayMs, audioSampleRate) {
|
2019-03-05 18:28:12 +00:00
|
|
|
|
2019-05-20 18:49:01 +00:00
|
|
|
// Get the currently loaded palette for undefined colors
|
|
|
|
if (initialPalette) {
|
|
|
|
memcpy(_palette, initialPalette, 256 * 3);
|
|
|
|
} else {
|
|
|
|
memset(_palette, 0, 256 * 3);
|
|
|
|
}
|
2020-02-16 19:18:55 +00:00
|
|
|
_dirtyPalette = true;
|
2019-03-05 18:28:12 +00:00
|
|
|
|
2021-04-07 00:08:20 +00:00
|
|
|
if (width * height > frameSize) {
|
|
|
|
error("Invalid frameSize: expected %d, got %d", width * height, frameSize);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
2021-03-07 20:07:30 +00:00
|
|
|
_frameBufferC = new byte[frameSize]();
|
|
|
|
_frameBufferP = new byte[frameSize]();
|
2021-04-07 00:08:20 +00:00
|
|
|
|
2022-01-08 10:58:03 +00:00
|
|
|
// We will use _frameBufferC/P as the surface pixels, just init there with nullptr to avoid unintended usage of surface
|
2021-04-07 00:08:20 +00:00
|
|
|
const Graphics::PixelFormat &f = Graphics::PixelFormat::createFormatCLUT8();
|
2021-04-11 17:20:06 +00:00
|
|
|
_surface.init(width, height, width * f.bytesPerPixel, nullptr, f);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
2022-01-08 10:58:03 +00:00
|
|
|
HNMDecoder::HNM45VideoTrack::~HNM45VideoTrack() {
|
2019-03-05 18:28:12 +00:00
|
|
|
// Don't free _surface as we didn't used create() but init()
|
|
|
|
delete[] _frameBufferC;
|
|
|
|
_frameBufferC = nullptr;
|
|
|
|
delete[] _frameBufferP;
|
|
|
|
_frameBufferP = nullptr;
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
void HNMDecoder::HNM45VideoTrack::newFrame(uint32 frameDelay) {
|
|
|
|
// Frame done
|
|
|
|
_curFrame++;
|
|
|
|
|
|
|
|
// Add regular frame delay if we had no sound
|
|
|
|
// We can't rely on a detection in the header as some soundless HNM indicate they have
|
|
|
|
if (frameDelay == uint32(-1)) {
|
|
|
|
_nextFrameStartTime = _nextFrameStartTime.addMsecs(_regularFrameDelayMs);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// HNM decoders use sound double buffering to pace the frames
|
|
|
|
// First frame is loaded in first buffer, second frame in second buffer
|
|
|
|
// It's only for third frame that we wait for the first buffer to be free
|
|
|
|
// It's presentation time is then delayed by the number of sound samples in the first frame
|
|
|
|
if (_lastFrameDelaySamps) {
|
|
|
|
_nextFrameStartTime = _nextFrameStartTime.addFrames(_lastFrameDelaySamps);
|
|
|
|
}
|
|
|
|
_lastFrameDelaySamps = frameDelay;
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM45VideoTrack::decodePalette(byte *data, uint32 size) {
|
2019-03-05 18:28:12 +00:00
|
|
|
while (true) {
|
|
|
|
if (size < 2) {
|
|
|
|
break;
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
uint start = *(data++);
|
|
|
|
uint count = *(data++);
|
2019-03-05 18:28:12 +00:00
|
|
|
size -= 2;
|
|
|
|
|
|
|
|
if (start == 255 && count == 255) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (count == 0) {
|
|
|
|
count = 256;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (size < count * 3) {
|
|
|
|
error("Invalid palette chunk data");
|
|
|
|
}
|
|
|
|
if (start + count > 256) {
|
|
|
|
error("Invalid palette start/count values");
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
if (size < count * 3) {
|
|
|
|
error("Not enough data for palette");
|
|
|
|
}
|
|
|
|
|
2019-03-05 18:28:12 +00:00
|
|
|
byte *palette_ptr = &_palette[start * 3];
|
|
|
|
for (; count > 0; count--) {
|
2022-01-09 10:50:27 +00:00
|
|
|
byte r = *(data++);
|
|
|
|
byte g = *(data++);
|
|
|
|
byte b = *(data++);
|
2019-03-05 18:28:12 +00:00
|
|
|
*(palette_ptr++) = r * 4;
|
|
|
|
*(palette_ptr++) = g * 4;
|
|
|
|
*(palette_ptr++) = b * 4;
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
size -= count * 3;
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
_dirtyPalette = true;
|
|
|
|
}
|
|
|
|
|
2022-01-08 10:58:03 +00:00
|
|
|
HNMDecoder::HNM4VideoTrack::HNM4VideoTrack(uint32 width, uint32 height, uint32 frameSize,
|
2022-01-12 07:50:40 +00:00
|
|
|
uint32 frameCount, uint32 regularFrameDelayMs, uint32 audioSampleRate,
|
|
|
|
const byte *initialPalette) :
|
|
|
|
HNM45VideoTrack(width, height, frameSize, frameCount,
|
|
|
|
regularFrameDelayMs, audioSampleRate, initialPalette) {
|
2022-01-08 10:58:03 +00:00
|
|
|
|
|
|
|
_frameBufferF = new byte[frameSize]();
|
|
|
|
}
|
|
|
|
|
|
|
|
HNMDecoder::HNM4VideoTrack::~HNM4VideoTrack() {
|
|
|
|
// Don't free _surface as we didn't used create() but init()
|
|
|
|
delete[] _frameBufferF;
|
|
|
|
_frameBufferF = nullptr;
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM4VideoTrack::decodeChunk(byte *data, uint32 size,
|
2022-01-08 10:58:03 +00:00
|
|
|
uint16 chunkType, uint16 flags) {
|
|
|
|
if (chunkType == MKTAG16('P', 'L')) {
|
2022-01-09 10:50:27 +00:00
|
|
|
decodePalette(data, size);
|
2022-01-08 10:58:03 +00:00
|
|
|
} else if (chunkType == MKTAG16('I', 'Z')) {
|
2022-01-09 10:50:27 +00:00
|
|
|
if (size < 4) {
|
|
|
|
error("Not enough data for IZ");
|
|
|
|
}
|
|
|
|
decodeIntraframe(data + 4, size - 4);
|
2022-01-08 10:58:03 +00:00
|
|
|
presentFrame(flags);
|
|
|
|
} else if (chunkType == MKTAG16('I', 'U')) {
|
|
|
|
if ((flags & 1) == 1) {
|
2022-01-09 10:50:27 +00:00
|
|
|
decodeInterframeA(data, size);
|
2022-01-08 10:58:03 +00:00
|
|
|
} else {
|
2022-01-09 10:50:27 +00:00
|
|
|
decodeInterframe(data, size);
|
2022-01-08 10:58:03 +00:00
|
|
|
}
|
|
|
|
presentFrame(flags);
|
|
|
|
} else {
|
|
|
|
error("HNM4: Got %d chunk: size %d", chunkType, size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM4VideoTrack::decodeInterframe(byte *data, uint32 size) {
|
2021-04-07 00:08:20 +00:00
|
|
|
SWAP(_frameBufferC, _frameBufferP);
|
|
|
|
|
|
|
|
uint16 width = _surface.w;
|
|
|
|
bool eop = false;
|
|
|
|
|
2022-01-08 11:37:02 +00:00
|
|
|
uint32 currentPos = 0;
|
2021-04-07 00:08:20 +00:00
|
|
|
|
|
|
|
while (!eop) {
|
|
|
|
if (size < 1) {
|
|
|
|
warning("Not enough data in chunk for interframe block");
|
|
|
|
break;
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
byte countFlgs = *(data++);
|
2021-04-07 00:08:20 +00:00
|
|
|
size -= 1;
|
|
|
|
byte count = countFlgs & 0x1f;
|
|
|
|
byte flgs = (countFlgs >> 5) & 0x7;
|
|
|
|
|
|
|
|
if (count == 0) {
|
|
|
|
int c, fill;
|
|
|
|
switch (flgs) {
|
|
|
|
case 0:
|
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for case 0");
|
|
|
|
}
|
|
|
|
// Copy next two bytes of input to the output
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++);
|
2021-04-07 00:08:20 +00:00
|
|
|
_frameBufferC[currentPos++] = c;
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++);
|
2021-04-07 00:08:20 +00:00
|
|
|
_frameBufferC[currentPos++] = c;
|
|
|
|
size -= 2;
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
if (size < 1) {
|
|
|
|
error("Not enough data for case 1");
|
|
|
|
}
|
|
|
|
// Skip (next byte of input) * 2 bytes of output
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++) * 2;
|
2021-04-07 00:08:20 +00:00
|
|
|
currentPos += c;
|
|
|
|
size -= 1;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for case 2");
|
|
|
|
}
|
|
|
|
// Skip (next word of input) * 2 bytes of output
|
2022-01-09 10:50:27 +00:00
|
|
|
c = READ_LE_UINT16(data) * 2;
|
|
|
|
data += 2;
|
2021-04-07 00:08:20 +00:00
|
|
|
currentPos += c;
|
|
|
|
size -= 2;
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for case 3");
|
|
|
|
}
|
|
|
|
// Fill (next byte of input) * 2 of output with (next byte of input)
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++) * 2;
|
|
|
|
fill = *(data++);
|
2021-04-07 11:31:47 +00:00
|
|
|
memset(&_frameBufferC[currentPos], fill, c);
|
|
|
|
currentPos += c;
|
2021-04-07 00:08:20 +00:00
|
|
|
size -= 2;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// End of picture
|
|
|
|
eop = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for count > 0");
|
|
|
|
}
|
|
|
|
|
|
|
|
bool backward = (flgs & 0x4) != 0;
|
|
|
|
bool backline = (flgs & 0x2) != 0;
|
|
|
|
bool previous = (flgs & 0x1) != 0;
|
2022-01-09 10:50:27 +00:00
|
|
|
int offset = READ_LE_UINT16(data);
|
|
|
|
data += 2;
|
2021-04-07 00:08:20 +00:00
|
|
|
size -= 2;
|
2022-01-09 10:50:27 +00:00
|
|
|
bool swap = (offset & 0x1) != 0;
|
2021-04-07 00:08:20 +00:00
|
|
|
|
|
|
|
offset = currentPos + (offset & 0xFFFE) - 0x8000;
|
|
|
|
if (offset < 0) {
|
|
|
|
error("Invalid offset");
|
|
|
|
}
|
|
|
|
|
|
|
|
byte *ptr;
|
|
|
|
if (previous) {
|
|
|
|
ptr = _frameBufferP;
|
|
|
|
} else {
|
|
|
|
ptr = _frameBufferC;
|
|
|
|
}
|
|
|
|
|
|
|
|
int shft1, shft2;
|
|
|
|
if (backline) {
|
|
|
|
const int twolinesabove = -(width * 2);
|
|
|
|
shft1 = twolinesabove + 1;
|
|
|
|
shft2 = 0;
|
|
|
|
} else {
|
|
|
|
shft1 = 0;
|
|
|
|
shft2 = 1;
|
|
|
|
}
|
2022-01-08 10:58:03 +00:00
|
|
|
if (swap) {
|
2021-04-07 00:08:20 +00:00
|
|
|
SWAP(shft1, shft2);
|
2022-01-08 10:58:03 +00:00
|
|
|
}
|
2021-04-07 00:08:20 +00:00
|
|
|
|
2021-04-11 17:17:36 +00:00
|
|
|
int src_inc = backward ? -2 : 2;
|
|
|
|
|
2021-04-07 00:08:20 +00:00
|
|
|
while (count--) {
|
2021-04-11 17:17:36 +00:00
|
|
|
byte b0 = ptr[offset + shft1];
|
|
|
|
byte b1 = ptr[offset + shft2];
|
|
|
|
_frameBufferC[currentPos++] = b0;
|
|
|
|
_frameBufferC[currentPos++] = b1;
|
|
|
|
|
|
|
|
offset += src_inc;
|
2021-04-07 00:08:20 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM4VideoTrack::decodeInterframeA(byte *data, uint32 size) {
|
2019-03-05 18:28:12 +00:00
|
|
|
SWAP(_frameBufferC, _frameBufferP);
|
|
|
|
|
|
|
|
uint16 width = _surface.w;
|
|
|
|
bool eop = false;
|
|
|
|
|
2022-01-08 11:37:02 +00:00
|
|
|
uint32 currentPos = 0;
|
2019-03-05 18:28:12 +00:00
|
|
|
|
|
|
|
while (!eop) {
|
|
|
|
if (size < 1) {
|
|
|
|
warning("Not enough data in chunk for interframe block");
|
|
|
|
break;
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
byte countFlgs = *(data++);
|
2019-03-05 18:28:12 +00:00
|
|
|
size -= 1;
|
|
|
|
byte count = countFlgs & 0x3f;
|
|
|
|
byte flgs = (countFlgs >> 6) & 0x3;
|
|
|
|
|
|
|
|
if (count == 0) {
|
|
|
|
byte c;
|
|
|
|
switch (flgs) {
|
|
|
|
case 0:
|
|
|
|
if (size < 1) {
|
|
|
|
error("Not enough data for case 0");
|
|
|
|
}
|
|
|
|
// Move in image
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++);
|
2019-03-05 18:28:12 +00:00
|
|
|
currentPos += c;
|
|
|
|
size -= 1;
|
|
|
|
break;
|
|
|
|
case 1:
|
2022-01-09 10:50:27 +00:00
|
|
|
if (size < 2) {
|
2019-03-05 18:28:12 +00:00
|
|
|
error("Not enough data for case 1");
|
|
|
|
}
|
|
|
|
// New pixels
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++);
|
2019-03-05 18:28:12 +00:00
|
|
|
_frameBufferC[currentPos] = c;
|
2022-01-09 10:50:27 +00:00
|
|
|
c = *(data++);
|
2019-03-05 18:28:12 +00:00
|
|
|
_frameBufferC[currentPos + width] = c;
|
|
|
|
currentPos++;
|
|
|
|
size -= 2;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
// New line
|
|
|
|
currentPos += width;
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
// End of picture
|
|
|
|
eop = true;
|
|
|
|
break;
|
2019-10-19 18:29:56 +00:00
|
|
|
default:
|
|
|
|
error("BUG: Shouldn't be here");
|
|
|
|
break;
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for count > 0");
|
|
|
|
}
|
|
|
|
|
|
|
|
bool negative = (flgs & 0x2) != 0;
|
|
|
|
bool previous = (flgs & 0x1) != 0;
|
2022-01-09 10:50:27 +00:00
|
|
|
int offset = READ_LE_UINT16(data);
|
|
|
|
data += 2;
|
2019-03-05 18:28:12 +00:00
|
|
|
size -= 2;
|
|
|
|
|
|
|
|
if (negative) {
|
|
|
|
offset -= 0x10000;
|
|
|
|
}
|
|
|
|
offset += currentPos;
|
|
|
|
if (offset < 0) {
|
|
|
|
error("Invalid offset");
|
|
|
|
}
|
|
|
|
|
|
|
|
byte *ptr;
|
|
|
|
if (previous) {
|
|
|
|
ptr = _frameBufferP;
|
|
|
|
} else {
|
|
|
|
ptr = _frameBufferC;
|
|
|
|
}
|
2022-01-09 12:24:50 +00:00
|
|
|
memcpy(&_frameBufferC[currentPos], &ptr[offset], count);
|
|
|
|
memcpy(&_frameBufferC[currentPos + width], &ptr[offset + width], count);
|
|
|
|
currentPos += count;
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM4VideoTrack::decodeIntraframe(byte *data, uint32 size) {
|
|
|
|
Common::MemoryReadStream stream(data, size);
|
|
|
|
Image::HLZDecoder::decodeFrameInPlace(stream, size, _frameBufferC);
|
2022-01-08 11:37:02 +00:00
|
|
|
memcpy(_frameBufferP, _frameBufferC, (uint32)_surface.w * (uint32)_surface.h);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
2021-04-11 17:20:06 +00:00
|
|
|
void HNMDecoder::HNM4VideoTrack::presentFrame(uint16 flags) {
|
2021-04-07 00:08:20 +00:00
|
|
|
int width = _surface.w;
|
|
|
|
int height = _surface.h;
|
|
|
|
|
|
|
|
if ((flags & 1) == 1) {
|
2021-04-11 17:20:06 +00:00
|
|
|
// High resolution HNM4A: no deinterlacing, use current image directly
|
|
|
|
_surface.setPixels(_frameBufferC);
|
2021-04-07 00:08:20 +00:00
|
|
|
} else if ((width % 4) == 0) {
|
2021-04-11 17:20:06 +00:00
|
|
|
// HNM4: deinterlacing must not alter the framebuffer as it will get reused as previous source for next frame
|
2021-04-07 11:31:47 +00:00
|
|
|
uint32 *input = (uint32 *)_frameBufferC;
|
|
|
|
uint32 *line0 = (uint32 *)_frameBufferF;
|
|
|
|
uint32 *line1 = (uint32 *)(_frameBufferF + width);
|
2021-04-07 00:08:20 +00:00
|
|
|
int count = (height) / 2;
|
|
|
|
while (count--) {
|
2021-04-07 11:31:47 +00:00
|
|
|
int i;
|
2021-04-07 00:08:20 +00:00
|
|
|
for (i = 0; i < width / 4; i++) {
|
2021-04-07 11:31:47 +00:00
|
|
|
uint32 p0 = *input++;
|
|
|
|
uint32 p4 = *input++;
|
|
|
|
|
|
|
|
#ifndef SCUMM_LITTLE_ENDIAN
|
2022-01-08 10:58:03 +00:00
|
|
|
*line0++ = ((p4 & 0xFF00) >> 8) | ((p4 & 0xFF000000) >> 16) |
|
|
|
|
((p0 & 0xFF00) << 8) | (p0 & 0xFF000000);
|
|
|
|
*line1++ = ((p0 & 0xFF0000) << 8) | ((p0 & 0xFF) << 16) |
|
|
|
|
((p4 & 0xFF0000) >> 8) | (p4 & 0xFF);
|
2021-04-07 11:31:47 +00:00
|
|
|
#else
|
2022-01-08 10:58:03 +00:00
|
|
|
*line0++ = (p0 & 0xFF) | ((p0 & 0xFF0000) >> 8) |
|
|
|
|
((p4 & 0xFF) << 16) | ((p4 & 0xFF0000) << 8);
|
|
|
|
*line1++ = ((p0 & 0xFF00) >> 8) | ((p0 & 0xFF000000) >> 16) |
|
|
|
|
((p4 & 0xFF00) << 8) | (p4 & 0xFF000000);
|
2021-04-07 11:31:47 +00:00
|
|
|
#endif
|
2021-04-07 00:08:20 +00:00
|
|
|
}
|
2021-04-07 11:31:47 +00:00
|
|
|
line0 += width / 4;
|
|
|
|
line1 += width / 4;
|
2021-04-07 00:08:20 +00:00
|
|
|
}
|
2021-04-11 17:20:06 +00:00
|
|
|
_surface.setPixels(_frameBufferF);
|
2021-04-07 00:08:20 +00:00
|
|
|
} else {
|
|
|
|
error("HNMDecoder::HNM4VideoTrack::postprocess(%x): Unexpected width: %d", flags, width);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM5VideoTrack::decodeChunk(byte *data, uint32 size,
|
2022-01-08 10:58:03 +00:00
|
|
|
uint16 chunkType, uint16 flags) {
|
|
|
|
if (chunkType == MKTAG16('P', 'L')) {
|
2022-01-09 10:50:27 +00:00
|
|
|
decodePalette(data, size);
|
2022-01-08 10:58:03 +00:00
|
|
|
} else if (chunkType == MKTAG16('I', 'V')) {
|
2022-01-09 10:50:27 +00:00
|
|
|
decodeFrame(data, size);
|
2022-01-08 10:58:03 +00:00
|
|
|
} else {
|
|
|
|
error("HNM5: Got %d chunk: size %d", chunkType, size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
static inline byte *HNM5_getSourcePtr(byte *&data, uint32 &size,
|
2022-01-08 20:38:07 +00:00
|
|
|
byte *previous, byte *current, int16 pitch, byte currentMode) {
|
|
|
|
int32 offset;
|
|
|
|
byte offb;
|
|
|
|
|
|
|
|
#define HNM5_DECODE_OFFSET_CST(src, constant_off) \
|
2022-01-09 10:50:27 +00:00
|
|
|
offset = constant_off + READ_LE_UINT16(data); \
|
|
|
|
data += 2; \
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 2; \
|
|
|
|
return (src + offset)
|
|
|
|
#define HNM5_DECODE_OFFSET(src, xbits, xbase, ybase) \
|
2022-01-09 10:50:27 +00:00
|
|
|
offb = *(data++); \
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 1; \
|
|
|
|
offset = ((offb >> xbits) + ybase) * pitch + \
|
|
|
|
(offb & ((1 << xbits) - 1)) + xbase; \
|
|
|
|
return (src + offset)
|
|
|
|
|
|
|
|
switch (currentMode) {
|
|
|
|
case 2:
|
|
|
|
HNM5_DECODE_OFFSET_CST(previous, -32768);
|
|
|
|
case 3:
|
|
|
|
HNM5_DECODE_OFFSET_CST(previous, -32768);
|
|
|
|
case 4:
|
|
|
|
HNM5_DECODE_OFFSET_CST(previous, -32768);
|
|
|
|
case 5:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -8);
|
|
|
|
case 6:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -8);
|
|
|
|
case 7:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -8);
|
|
|
|
case 8:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -8);
|
|
|
|
case 9:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -8);
|
|
|
|
case 10:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -2);
|
|
|
|
case 11:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -14);
|
|
|
|
case 12:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -2);
|
|
|
|
case 13:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -2);
|
|
|
|
case 14:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -14);
|
|
|
|
case 15:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -14);
|
|
|
|
case 16:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -8);
|
|
|
|
case 17:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -8);
|
|
|
|
case 18:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -2);
|
|
|
|
case 19:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -14);
|
|
|
|
case 20:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -2);
|
|
|
|
case 21:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -2);
|
|
|
|
case 22:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -14);
|
|
|
|
case 23:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -14);
|
|
|
|
case 24:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -8);
|
|
|
|
case 25:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -8);
|
|
|
|
case 26:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -2);
|
|
|
|
case 27:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -8, -14);
|
|
|
|
case 28:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -2);
|
|
|
|
case 29:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -2);
|
|
|
|
case 30:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -2, -14);
|
|
|
|
case 31:
|
|
|
|
HNM5_DECODE_OFFSET(previous, 4, -14, -14);
|
|
|
|
case 32:
|
|
|
|
HNM5_DECODE_OFFSET_CST(current, -65536);
|
|
|
|
case 33:
|
|
|
|
HNM5_DECODE_OFFSET(current, 5, -16, -8);
|
|
|
|
case 34:
|
|
|
|
HNM5_DECODE_OFFSET(current, 4, -8, -16);
|
|
|
|
case 35:
|
|
|
|
HNM5_DECODE_OFFSET(current, 4, -24, -16);
|
|
|
|
case 36:
|
|
|
|
HNM5_DECODE_OFFSET(current, 4, 8, -16);
|
|
|
|
case 37:
|
|
|
|
HNM5_DECODE_OFFSET(current, 3, -4, -32);
|
|
|
|
case 38:
|
|
|
|
HNM5_DECODE_OFFSET(current, 3, -12, -32);
|
|
|
|
case 39:
|
|
|
|
HNM5_DECODE_OFFSET(current, 3, 4, -32);
|
|
|
|
case 40:
|
|
|
|
HNM5_DECODE_OFFSET_CST(current, -65536);
|
|
|
|
case 41:
|
|
|
|
HNM5_DECODE_OFFSET(current, 5, -16, -8);
|
|
|
|
case 42:
|
|
|
|
HNM5_DECODE_OFFSET(current, 4, -8, -16);
|
|
|
|
case 43:
|
|
|
|
HNM5_DECODE_OFFSET(current, 4, -24, -16);
|
|
|
|
case 44:
|
|
|
|
HNM5_DECODE_OFFSET(current, 4, 8, -16);
|
|
|
|
case 45:
|
|
|
|
HNM5_DECODE_OFFSET(current, 3, -4, -32);
|
|
|
|
case 46:
|
|
|
|
HNM5_DECODE_OFFSET(current, 3, -12, -32);
|
|
|
|
case 47:
|
|
|
|
HNM5_DECODE_OFFSET(current, 3, 4, -32);
|
|
|
|
default:
|
|
|
|
error("BUG: Invalid offset mode");
|
|
|
|
}
|
|
|
|
|
|
|
|
#undef HNM5_DECODE_OFFSET_CST
|
|
|
|
#undef HNM5_DECODE_OFFSET
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline void HNM5_copy(byte *dst, byte *src, int16 pitch,
|
|
|
|
byte copyMode, byte width, byte height) {
|
|
|
|
switch (copyMode) {
|
|
|
|
case 0:
|
|
|
|
// Copy
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
memcpy(dst, src, width);
|
|
|
|
dst += pitch;
|
|
|
|
src += pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
// Horizontal reverse
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
byte *dp = dst;
|
|
|
|
byte *sp = src;
|
|
|
|
for (byte col = width; col > 0; col--) {
|
|
|
|
*(dp++) = *(sp--);
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 12:57:00 +00:00
|
|
|
dst += pitch;
|
|
|
|
src += pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
// Vertical reverse
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
memcpy(dst, src, width);
|
|
|
|
dst += pitch;
|
|
|
|
src -= pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
// Horiz-Vert reverse
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
byte *dp = dst;
|
|
|
|
byte *sp = src;
|
|
|
|
for (byte col = width; col > 0; col--) {
|
|
|
|
*(dp++) = *(sp--);
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 12:57:00 +00:00
|
|
|
dst += pitch;
|
|
|
|
src -= pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
// Swap
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
byte *dp = dst;
|
|
|
|
byte *sp = src;
|
|
|
|
for (byte col = width; col > 0; col--) {
|
2022-01-08 20:38:07 +00:00
|
|
|
*dp = *sp;
|
2022-01-09 12:57:00 +00:00
|
|
|
dp++;
|
|
|
|
sp += pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 12:57:00 +00:00
|
|
|
dst += pitch;
|
|
|
|
src += 1;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 5:
|
|
|
|
// Swap Horiz-Reverse
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
byte *dp = dst;
|
|
|
|
byte *sp = src;
|
|
|
|
for (byte col = width; col > 0; col--) {
|
2022-01-08 20:38:07 +00:00
|
|
|
*dp = *sp;
|
2022-01-09 12:57:00 +00:00
|
|
|
dp++;
|
|
|
|
sp -= pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 12:57:00 +00:00
|
|
|
dst += pitch;
|
|
|
|
src += 1;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 6:
|
|
|
|
// Swap Vert-Reverse
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
byte *dp = dst;
|
|
|
|
byte *sp = src;
|
|
|
|
for (byte col = width; col > 0; col--) {
|
2022-01-08 20:38:07 +00:00
|
|
|
*dp = *sp;
|
2022-01-09 12:57:00 +00:00
|
|
|
dp++;
|
|
|
|
sp += pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 12:57:00 +00:00
|
|
|
dst += pitch;
|
|
|
|
src -= 1;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 7:
|
|
|
|
// Swap Vert-Reverse
|
2022-01-09 12:57:00 +00:00
|
|
|
for (; height > 0; height--) {
|
|
|
|
byte *dp = dst;
|
|
|
|
byte *sp = src;
|
|
|
|
for (byte col = width; col > 0; col--) {
|
2022-01-08 20:38:07 +00:00
|
|
|
*dp = *sp;
|
2022-01-09 12:57:00 +00:00
|
|
|
dp++;
|
|
|
|
sp -= pitch;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 12:57:00 +00:00
|
|
|
dst += pitch;
|
|
|
|
src -= 1;
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
error("BUG: Invalid copy mode");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static const byte HNM5_WIDTHS[3][32] = {
|
|
|
|
{
|
|
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
|
|
18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 48, 52, 56
|
|
|
|
}, /* 2 */
|
|
|
|
{
|
|
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
|
|
17, 18, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44
|
|
|
|
}, /* 3 */
|
|
|
|
{
|
|
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
|
|
|
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 28, 30, 32, 34, 36, 38
|
|
|
|
}, /* 4 */
|
|
|
|
};
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
void HNMDecoder::HNM5VideoTrack::decodeFrame(byte *data, uint32 size) {
|
2022-01-08 10:58:03 +00:00
|
|
|
SWAP(_frameBufferC, _frameBufferP);
|
|
|
|
|
2022-01-08 20:38:07 +00:00
|
|
|
uint16 pitch = _surface.pitch;
|
|
|
|
bool eop = false;
|
|
|
|
|
2022-01-12 07:52:09 +00:00
|
|
|
byte height = -1;
|
|
|
|
byte currentMode = -1;
|
2022-01-08 20:38:07 +00:00
|
|
|
uint32 currentPos = 0;
|
|
|
|
|
|
|
|
while (!eop) {
|
|
|
|
if (size < 1) {
|
|
|
|
warning("Not enough data in chunk for frame block");
|
|
|
|
break;
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
byte opcode = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 1;
|
|
|
|
|
|
|
|
if (opcode == 0x20) {
|
2022-01-12 07:52:09 +00:00
|
|
|
assert(height != byte(-1));
|
2022-01-08 20:38:07 +00:00
|
|
|
if (size < 1) {
|
|
|
|
error("Not enough data for opcode 0x20");
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
uint width = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 1;
|
|
|
|
width++;
|
|
|
|
for (byte row = 0; row < height; row++) {
|
|
|
|
memcpy(&_frameBufferC[currentPos + row * pitch], &_frameBufferP[currentPos + row * pitch], width);
|
|
|
|
}
|
|
|
|
currentPos += width;
|
|
|
|
} else if (opcode == 0x60) {
|
|
|
|
// Maximal pixels height is 4
|
2022-01-12 07:52:09 +00:00
|
|
|
assert(height != byte(-1) && height <= 4);
|
2022-01-08 20:38:07 +00:00
|
|
|
if (size < height) {
|
|
|
|
error("Not enough data for opcode 0x60");
|
|
|
|
}
|
|
|
|
for (byte row = 0; row < height; row++) {
|
2022-01-09 10:50:27 +00:00
|
|
|
_frameBufferC[currentPos + row * pitch] = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
size -= height;
|
2022-01-08 20:38:07 +00:00
|
|
|
currentPos += 1;
|
|
|
|
} else if (opcode == 0xA0) {
|
2022-01-12 07:52:09 +00:00
|
|
|
assert(height != byte(-1));
|
2022-01-08 20:38:07 +00:00
|
|
|
if (size < 1) {
|
|
|
|
error("Not enough data for opcode 0x20");
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
uint width = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 1;
|
|
|
|
width += 2;
|
|
|
|
|
|
|
|
if (size < height * width) {
|
|
|
|
error("Not enough data for opcode 0xA0");
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
for (uint col = 0; col < width; col++) {
|
|
|
|
for (byte row = 0; row < height; row++) {
|
|
|
|
_frameBufferC[currentPos + row * pitch + col] = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
size -= height * width;
|
2022-01-08 20:38:07 +00:00
|
|
|
currentPos += width;
|
|
|
|
} else if (opcode == 0xE0) {
|
|
|
|
if (size < 1) {
|
|
|
|
error("Not enough data for opcode 0xE0");
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
byte subop = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 1;
|
|
|
|
|
|
|
|
if (subop == 0x00) {
|
2022-01-12 07:52:09 +00:00
|
|
|
assert(height != byte(-1));
|
2022-01-08 20:38:07 +00:00
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for opcode 0xE0 0x00");
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
uint width = *(data++);
|
|
|
|
byte px = *(data++);
|
2022-01-08 20:38:07 +00:00
|
|
|
size -= 2;
|
|
|
|
|
|
|
|
width += 1;
|
|
|
|
|
|
|
|
for (byte row = 0; row < height; row++) {
|
|
|
|
memset(&_frameBufferC[currentPos + row * pitch], px, width);
|
|
|
|
}
|
|
|
|
currentPos += width;
|
|
|
|
} else if (subop == 0x01) {
|
2022-01-12 07:52:09 +00:00
|
|
|
if (height != byte(-1)) {
|
2022-01-08 20:38:07 +00:00
|
|
|
currentPos += (height - 1) * pitch;
|
|
|
|
}
|
|
|
|
|
|
|
|
eop = true;
|
|
|
|
} else {
|
|
|
|
// Reconfigure decoder at line start
|
|
|
|
assert((currentPos % pitch) == 0);
|
|
|
|
assert(subop < 48);
|
|
|
|
|
2022-01-12 07:52:09 +00:00
|
|
|
if (height != byte(-1)) {
|
2022-01-08 20:38:07 +00:00
|
|
|
currentPos += (height - 1) * pitch;
|
|
|
|
}
|
|
|
|
|
|
|
|
currentMode = subop;
|
|
|
|
|
|
|
|
if (( 8 <= subop && subop <= 15) ||
|
|
|
|
(32 <= subop && subop <= 39) ||
|
|
|
|
(subop == 2) || (subop == 5)) {
|
|
|
|
height = 2;
|
|
|
|
} else if ((16 <= subop && subop <= 23) ||
|
|
|
|
(40 <= subop && subop <= 47) ||
|
|
|
|
(subop == 3) || (subop == 6)) {
|
|
|
|
height = 3;
|
|
|
|
} else if ((24 <= subop && subop <= 31) ||
|
|
|
|
(subop == 4) || (subop == 7)) {
|
|
|
|
height = 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
} else {
|
2022-01-12 07:52:09 +00:00
|
|
|
assert(height != byte(-1));
|
2022-01-08 20:38:07 +00:00
|
|
|
assert(2 <= height && height <= 4);
|
|
|
|
byte index = opcode & 0x1f;
|
|
|
|
byte copyMode = (opcode >> 5) & 0x7;
|
|
|
|
byte width = HNM5_WIDTHS[height - 2][index];
|
|
|
|
|
2022-01-09 10:50:27 +00:00
|
|
|
// HNM5_getSourcePtr can consume 1 byte but the data can not end like this so check for maximum
|
2022-01-08 20:38:07 +00:00
|
|
|
if (size < 2) {
|
|
|
|
error("Not enough data for opcode 0x%02X", opcode);
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
byte *src = HNM5_getSourcePtr(data, size, _frameBufferP, _frameBufferC, pitch, currentMode);
|
2022-01-08 20:38:07 +00:00
|
|
|
|
|
|
|
HNM5_copy(_frameBufferC + currentPos, src + currentPos, pitch, copyMode, width, height);
|
|
|
|
currentPos += width;
|
|
|
|
}
|
|
|
|
}
|
2022-01-08 10:58:03 +00:00
|
|
|
|
2022-01-08 20:38:07 +00:00
|
|
|
_surface.setPixels(_frameBufferC);
|
2022-01-08 10:58:03 +00:00
|
|
|
}
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
HNMDecoder::HNM6VideoTrack::HNM6VideoTrack(uint32 width, uint32 height, uint32 frameSize,
|
|
|
|
uint32 frameCount, uint32 regularFrameDelayMs, uint32 audioSampleRate,
|
|
|
|
const Graphics::PixelFormat &format) :
|
|
|
|
HNMVideoTrack(frameCount, regularFrameDelayMs, audioSampleRate),
|
|
|
|
_decoder(Image::createHNM6Decoder(width, height, format, frameSize, true)),
|
|
|
|
_surface(nullptr) {
|
|
|
|
}
|
|
|
|
|
|
|
|
HNMDecoder::HNM6VideoTrack::~HNM6VideoTrack() {
|
|
|
|
delete _decoder;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16 HNMDecoder::HNM6VideoTrack::getWidth() const {
|
|
|
|
return _decoder->getWidth();
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16 HNMDecoder::HNM6VideoTrack::getHeight() const {
|
|
|
|
return _decoder->getHeight();
|
|
|
|
}
|
|
|
|
|
|
|
|
Graphics::PixelFormat HNMDecoder::HNM6VideoTrack::getPixelFormat() const {
|
|
|
|
return _decoder->getPixelFormat();
|
|
|
|
}
|
|
|
|
|
|
|
|
void HNMDecoder::HNM6VideoTrack::decodeChunk(byte *data, uint32 size,
|
|
|
|
uint16 chunkType, uint16 flags) {
|
|
|
|
if (chunkType == MKTAG16('I', 'X') ||
|
|
|
|
chunkType == MKTAG16('I', 'W')) {
|
|
|
|
Common::MemoryReadStream stream(data, size);
|
|
|
|
_surface = _decoder->decodeFrame(stream);
|
|
|
|
} else {
|
|
|
|
error("HNM6: Got %d chunk: size %d", chunkType, size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void HNMDecoder::HNM6VideoTrack::newFrame(uint32 frameDelay) {
|
|
|
|
// Frame done
|
|
|
|
_curFrame++;
|
|
|
|
|
|
|
|
// In HNM6 first frame contains the sound for the 32 following frames (pre-buffering)
|
|
|
|
// Then other frames contain constant size sound chunks except for the 32 last ones
|
|
|
|
if (!_lastFrameDelaySamps) {
|
|
|
|
// Add regular frame delay if we had no sound
|
|
|
|
if (frameDelay == uint32(-1)) {
|
|
|
|
_nextFrameStartTime = _nextFrameStartTime.addMsecs(_regularFrameDelayMs);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
assert((frameDelay & 31) == 0);
|
|
|
|
_lastFrameDelaySamps = frameDelay / 32;
|
|
|
|
_nextFrameStartTime = _nextFrameStartTime.addFrames(_lastFrameDelaySamps);
|
|
|
|
} else {
|
|
|
|
if (frameDelay != uint32(-1)) {
|
|
|
|
assert(frameDelay == _lastFrameDelaySamps);
|
|
|
|
}
|
|
|
|
_nextFrameStartTime = _nextFrameStartTime.addFrames(_lastFrameDelaySamps);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 11:47:28 +00:00
|
|
|
HNMDecoder::DPCMAudioTrack::DPCMAudioTrack(uint16 format, uint16 bits, uint sampleRate, bool stereo,
|
2022-08-29 16:34:25 +00:00
|
|
|
Audio::Mixer::SoundType soundType) : HNMAudioTrack(soundType), _audioStream(nullptr),
|
2022-01-08 20:49:19 +00:00
|
|
|
_gotLUT(false), _lastSampleL(0), _lastSampleR(0), _sampleRate(sampleRate), _stereo(stereo) {
|
2019-03-05 18:28:12 +00:00
|
|
|
if (bits != 16) {
|
|
|
|
error("Unsupported audio bits");
|
|
|
|
}
|
2021-04-11 17:28:31 +00:00
|
|
|
if (format != 2) {
|
|
|
|
warning("Unsupported %d audio format", format);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
2021-10-04 11:47:28 +00:00
|
|
|
// Format 2 is 16-bits DPCM
|
|
|
|
_audioStream = Audio::makeQueuingAudioStream(_sampleRate, _stereo);
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
HNMDecoder::DPCMAudioTrack::~DPCMAudioTrack() {
|
|
|
|
delete _audioStream;
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
uint32 HNMDecoder::DPCMAudioTrack::decodeSound(uint16 chunkType, byte *data, uint32 size) {
|
2019-03-05 18:28:12 +00:00
|
|
|
if (!_gotLUT) {
|
|
|
|
if (size < 256 * sizeof(*_lut)) {
|
|
|
|
error("Invalid first sound chunk");
|
|
|
|
}
|
2022-01-09 10:50:27 +00:00
|
|
|
memcpy(_lut, data, 256 * sizeof(*_lut));
|
|
|
|
data += 256 * sizeof(*_lut);
|
2019-03-05 18:28:12 +00:00
|
|
|
size -= 256 * sizeof(*_lut);
|
|
|
|
#ifndef SCUMM_LITTLE_ENDIAN
|
2019-05-26 16:29:57 +00:00
|
|
|
for (uint i = 0; i < 256; i++) {
|
2019-03-05 18:28:12 +00:00
|
|
|
_lut[i] = FROM_LE_16(_lut[i]);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
_gotLUT = true;
|
|
|
|
}
|
|
|
|
|
2022-01-08 20:49:19 +00:00
|
|
|
if (size == 0) {
|
2022-01-12 07:50:40 +00:00
|
|
|
return 0;
|
2022-01-08 20:49:19 +00:00
|
|
|
}
|
2021-10-04 11:47:28 +00:00
|
|
|
|
2022-01-08 20:49:19 +00:00
|
|
|
uint16 *out = (uint16 *)malloc(size * sizeof(*out));
|
|
|
|
uint16 *p = out;
|
2021-10-04 11:47:28 +00:00
|
|
|
|
2022-01-12 07:50:40 +00:00
|
|
|
uint32 numSamples = size;
|
|
|
|
|
2022-01-08 20:49:19 +00:00
|
|
|
byte flags = Audio::FLAG_16BITS;
|
2019-03-05 18:28:12 +00:00
|
|
|
#ifdef SCUMM_LITTLE_ENDIAN
|
2022-01-08 20:49:19 +00:00
|
|
|
flags |= Audio::FLAG_LITTLE_ENDIAN;
|
2019-03-05 18:28:12 +00:00
|
|
|
#endif
|
2022-01-08 20:49:19 +00:00
|
|
|
if (_audioStream->isStereo()) {
|
2022-01-12 07:50:40 +00:00
|
|
|
numSamples /= 2;
|
|
|
|
|
2022-01-08 20:49:19 +00:00
|
|
|
uint16 sampleL = _lastSampleL;
|
|
|
|
uint16 sampleR = _lastSampleR;
|
|
|
|
byte deltaId;
|
2022-01-12 07:50:40 +00:00
|
|
|
for (uint32 i = 0; i < numSamples; i++, p += 2) {
|
2022-01-09 10:50:27 +00:00
|
|
|
deltaId = *(data++);
|
2022-01-08 20:49:19 +00:00
|
|
|
sampleL += _lut[deltaId];
|
2022-01-09 10:50:27 +00:00
|
|
|
deltaId = *(data++);
|
2022-01-08 20:49:19 +00:00
|
|
|
sampleR += _lut[deltaId];
|
|
|
|
p[0] = sampleL;
|
|
|
|
p[1] = sampleR;
|
|
|
|
}
|
|
|
|
_lastSampleL = sampleL;
|
|
|
|
_lastSampleR = sampleR;
|
2021-10-04 11:47:28 +00:00
|
|
|
|
2022-01-08 20:49:19 +00:00
|
|
|
flags |= Audio::FLAG_STEREO;
|
|
|
|
} else {
|
|
|
|
uint16 sample = _lastSampleL;
|
|
|
|
byte deltaId;
|
2022-01-12 07:50:40 +00:00
|
|
|
for (uint32 i = 0; i < numSamples; i++, p++) {
|
2022-01-09 10:50:27 +00:00
|
|
|
deltaId = *(data++);
|
2022-01-08 20:49:19 +00:00
|
|
|
sample += _lut[deltaId];
|
|
|
|
*p = sample;
|
|
|
|
}
|
|
|
|
_lastSampleL = sample;
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
2022-01-08 20:49:19 +00:00
|
|
|
|
|
|
|
_audioStream->queueBuffer((byte *)out, size * sizeof(*out), DisposeAfterUse::YES, flags);
|
2022-01-12 07:50:40 +00:00
|
|
|
return numSamples;
|
2019-03-05 18:28:12 +00:00
|
|
|
}
|
|
|
|
|
2022-08-29 16:34:25 +00:00
|
|
|
HNMDecoder::APCAudioTrack::APCAudioTrack(uint sampleRate, byte stereo,
|
|
|
|
Audio::Mixer::SoundType soundType) : HNMAudioTrack(soundType),
|
|
|
|
_audioStream(Audio::makeAPCStream(sampleRate, stereo)) {
|
|
|
|
}
|
|
|
|
|
|
|
|
HNMDecoder::APCAudioTrack::~APCAudioTrack() {
|
|
|
|
delete _audioStream;
|
|
|
|
}
|
|
|
|
|
|
|
|
Audio::AudioStream *HNMDecoder::APCAudioTrack::getAudioStream() const {
|
|
|
|
return _audioStream;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32 HNMDecoder::APCAudioTrack::decodeSound(uint16 chunkType, byte *data, uint32 size) {
|
|
|
|
if (chunkType == MKTAG16('A', 'A')) {
|
|
|
|
Common::MemoryReadStream *stream = new Common::MemoryReadStream(data, size);
|
|
|
|
if (!_audioStream->init(*stream)) {
|
|
|
|
error("Invalid APC stream");
|
|
|
|
}
|
|
|
|
// Remove header for sample counting
|
|
|
|
size -= stream->pos();
|
|
|
|
_audioStream->queuePacket(stream);
|
|
|
|
// stream is freed now
|
|
|
|
stream = nullptr;
|
|
|
|
} else if (chunkType == MKTAG16('B', 'B')) {
|
|
|
|
_audioStream->queuePacket(new Common::MemoryReadStream(data, size));
|
|
|
|
}
|
|
|
|
// 2 4-bit samples in one byte
|
|
|
|
uint32 numSamples = size;
|
|
|
|
if (!_audioStream->isStereo()) {
|
|
|
|
numSamples *= 2;
|
|
|
|
}
|
|
|
|
return numSamples;
|
|
|
|
}
|
|
|
|
|
2019-03-05 18:28:12 +00:00
|
|
|
} // End of namespace Video
|