Skip to content

Commit

Permalink
VIDEO: Support old-style stereo in VMDs
Browse files Browse the repository at this point in the history
This format is used by the stereo audio VMDs in Lighthouse.
  • Loading branch information
csnover committed Aug 20, 2017
1 parent 1d84497 commit 4ba8701
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 96 deletions.
240 changes: 144 additions & 96 deletions video/coktel_decoder.cpp
Expand Up @@ -1543,6 +1543,93 @@ Graphics::PixelFormat IMDDecoder::getPixelFormat() const {
return Graphics::PixelFormat::createFormatCLUT8();
}

class DPCMStream : public Audio::AudioStream {
public:
DPCMStream(Common::SeekableReadStream *stream, int rate, int channels, bool oldStereo) {
_stream = stream;
_rate = rate;
_channels = channels;
_oldStereo = oldStereo;
if (oldStereo) {
_buffer[0] = _buffer[1] = 0;
}
}

~DPCMStream() {
delete _stream;
}

int readBuffer(int16 *buffer, const int numSamples);
bool isStereo() const { return _channels == 2; }
int getRate() const { return _rate; }
bool endOfData() const { return _stream->pos() >= _stream->size() || _stream->eos() || _stream->err(); }

private:
Common::SeekableReadStream *_stream;
int _channels;
int _rate;
int _buffer[2];
bool _oldStereo;
};

int DPCMStream::readBuffer(int16 *buffer, const int numSamples) {
static const uint16 tableDPCM[128] = {
0x0000, 0x0008, 0x0010, 0x0020, 0x0030, 0x0040, 0x0050, 0x0060, 0x0070, 0x0080,
0x0090, 0x00A0, 0x00B0, 0x00C0, 0x00D0, 0x00E0, 0x00F0, 0x0100, 0x0110, 0x0120,
0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0180, 0x0190, 0x01A0, 0x01B0, 0x01C0,
0x01D0, 0x01E0, 0x01F0, 0x0200, 0x0208, 0x0210, 0x0218, 0x0220, 0x0228, 0x0230,
0x0238, 0x0240, 0x0248, 0x0250, 0x0258, 0x0260, 0x0268, 0x0270, 0x0278, 0x0280,
0x0288, 0x0290, 0x0298, 0x02A0, 0x02A8, 0x02B0, 0x02B8, 0x02C0, 0x02C8, 0x02D0,
0x02D8, 0x02E0, 0x02E8, 0x02F0, 0x02F8, 0x0300, 0x0308, 0x0310, 0x0318, 0x0320,
0x0328, 0x0330, 0x0338, 0x0340, 0x0348, 0x0350, 0x0358, 0x0360, 0x0368, 0x0370,
0x0378, 0x0380, 0x0388, 0x0390, 0x0398, 0x03A0, 0x03A8, 0x03B0, 0x03B8, 0x03C0,
0x03C8, 0x03D0, 0x03D8, 0x03E0, 0x03E8, 0x03F0, 0x03F8, 0x0400, 0x0440, 0x0480,
0x04C0, 0x0500, 0x0540, 0x0580, 0x05C0, 0x0600, 0x0640, 0x0680, 0x06C0, 0x0700,
0x0740, 0x0780, 0x07C0, 0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00,
0x0F00, 0x1000, 0x1400, 0x1800, 0x1C00, 0x2000, 0x3000, 0x4000
};

assert((numSamples % _channels) == 0);

int samples = 0;

// Our starting position
if (!_oldStereo && _stream->pos() == 0) {
for (int i = 0; i < _channels; i++)
*buffer++ = _buffer[i] = _stream->readSint16LE();

samples += _channels;
}

while (!endOfData() && samples < numSamples) {
if (_channels == 2 && _stream->size() == 1) {
warning("Buffer underrun in DPCMStream");
break;
}

for (int i = 0; i < _channels; i++) {
byte data = _stream->readByte();

if (data & 0x80)
_buffer[i] -= tableDPCM[data & 0x7f];
else
_buffer[i] += tableDPCM[data];

// Emulating x86 16-bit signed register overflow
if (_buffer[i] > 32767) {
_buffer[i] -= 65536;
} else if (_buffer[i] < -32768) {
_buffer[i] += 65536;
}

*buffer++ = _buffer[i];
}

samples += _channels;
}

return samples;
}

VMDDecoder::File::File() {
offset = 0;
Expand Down Expand Up @@ -1581,7 +1668,7 @@ VMDDecoder::VMDDecoder(Audio::Mixer *mixer, Audio::Mixer::SoundType soundType) :
_soundLastFilledFrame(0), _audioFormat(kAudioFormat8bitRaw),
_hasVideo(false), _videoCodec(0), _blitMode(0), _bytesPerPixel(0),
_firstFramePos(0), _videoBufferSize(0), _externalCodec(false), _codec(0),
_subtitle(-1), _isPaletted(true), _autoStartSound(true) {
_subtitle(-1), _isPaletted(true), _autoStartSound(true), _oldStereoBuffer(nullptr) {

_videoBuffer [0] = 0;
_videoBuffer [1] = 0;
Expand Down Expand Up @@ -1625,7 +1712,7 @@ bool VMDDecoder::seek(int32 frame, int whence, bool restart) {
delete _audioStream;

_soundStage = kSoundLoaded;
_audioStream = Audio::makeQueuingAudioStream(_soundFreq, _soundStereo != 0);
createAudioStream();
}

_subtitle = -1;
Expand Down Expand Up @@ -1915,13 +2002,19 @@ bool VMDDecoder::assessAudioProperties() {

}
} else {
if (_soundStereo == 2) {
supportedFormat = false;
}

_soundBytesPerSample = 1;
_audioFormat = kAudioFormat8bitRaw;
_soundHeaderSize = 0;
_soundDataSize = _soundSliceSize;

if (_soundStereo > 0)
supportedFormat = false;
if (_soundStereo == 1) {
_audioFormat = kAudioFormat16bitDPCM;
} else {
_audioFormat = kAudioFormat8bitRaw;
}
}

if (!supportedFormat) {
Expand All @@ -1930,14 +2023,12 @@ bool VMDDecoder::assessAudioProperties() {
return false;
}

_frameRate = Common::Rational(_soundFreq, _soundSliceSize);
_frameRate = Common::Rational(_soundFreq, _soundSliceSize / (_soundStereo == 1 ? 2 : 1));

_hasSound = true;
_soundEnabled = true;
_soundStage = kSoundLoaded;

_audioStream = Audio::makeQueuingAudioStream(_soundFreq, _soundStereo != 0);

createAudioStream();
return true;
}

Expand Down Expand Up @@ -2074,6 +2165,7 @@ void VMDDecoder::close() {
_soundDataSize = 0;
_soundLastFilledFrame = 0;
_audioFormat = kAudioFormat8bitRaw;
_oldStereoBuffer = nullptr;

_hasVideo = false;
_videoCodec = 0;
Expand Down Expand Up @@ -2123,9 +2215,9 @@ void VMDDecoder::processFrame() {

bool startSound = false;

for (uint16 i = 0; i < _partsPerFrame; i++) {
uint32 pos = _stream->pos();
_stream->seek(_frames[_curFrame].offset, SEEK_SET);

for (uint16 i = 0; i < _partsPerFrame; i++) {
Part &part = _frames[_curFrame].parts[i];

if (part.type == kPartTypeAudio) {
Expand All @@ -2147,7 +2239,7 @@ void VMDDecoder::processFrame() {

if (_soundEnabled) {
uint32 mask = _stream->readUint32LE();
filledSoundSlices(part.size - 4, mask);
filledSoundSlices(part.size - /* mask size */ 4, mask);

if (_soundStage == kSoundLoaded)
startSound = true;
Expand Down Expand Up @@ -2176,8 +2268,6 @@ void VMDDecoder::processFrame() {
_stream->skip(part.size);
}

_stream->seek(pos + part.size);

} else if ((part.type == kPartTypeVideo) && !_hasVideo) {

warning("VMDDecoder::processFrame(): Header claims there's no video, but video found (%d)", part.size);
Expand Down Expand Up @@ -2470,6 +2560,14 @@ void VMDDecoder::blit24(const Graphics::Surface &srcSurf, Common::Rect &rect) {
}

void VMDDecoder::emptySoundSlice(uint32 size) {
if (_soundStereo == 1) {
// Technically an empty slice could be used at the very beginning of the
// stream, but anywhere else it would need to dynamically calculate the
// delta between the current sample and zero sample level and the steps
// to get a zero level
error("Old-style stereo cannot be filled with an empty slice");
}

byte *soundBuf = (byte *)malloc(size);

if (soundBuf) {
Expand All @@ -2488,6 +2586,17 @@ void VMDDecoder::filledSoundSlice(uint32 size) {
return;
}

if (_soundStereo == 1) {
void *buf = malloc(size);
assert(buf);
const uint32 numBytesRead = _stream->read(buf, size);
assert(numBytesRead == size);
const uint32 numBytesWritten = _oldStereoBuffer->write(buf, size);
assert(numBytesWritten == size);
free(buf);
return;
}

Common::SeekableReadStream *data = _stream->readStream(size);
Audio::AudioStream *sliceStream = 0;

Expand All @@ -2508,16 +2617,21 @@ void VMDDecoder::filledSoundSlices(uint32 size, uint32 mask) {
uint8 max;
uint8 n = evaluateMask(mask, fillInfo, max);

int32 extraSize;

extraSize = size - n * _soundDataSize;
// extraSize is needed by videos in some games (GK2) or audio data will be
// incomplete
int32 extraSize = size - n * _soundDataSize;

if (_soundSlicesCount > 32)
extraSize -= (_soundSlicesCount - 32) * _soundDataSize;

if (n > 0)
extraSize /= n;

// extraSize cannot be negative or audio data will be incomplete in some
// games (old-style stereo videos in Lighthouse)
if (extraSize < 0)
extraSize = 0;

for (uint8 i = 0; i < max; i++)
if (fillInfo[i])
filledSoundSlice(_soundDataSize + extraSize);
Expand All @@ -2528,6 +2642,14 @@ void VMDDecoder::filledSoundSlices(uint32 size, uint32 mask) {
filledSoundSlice((_soundSlicesCount - 32) * _soundDataSize + _soundHeaderSize);
}

void VMDDecoder::createAudioStream() {
_audioStream = Audio::makeQueuingAudioStream(_soundFreq, _soundStereo != 0);
if (_soundStereo == 1) {
_oldStereoBuffer = new Common::MemoryReadWriteStream(DisposeAfterUse::YES);
_audioStream->queueAudioStream(new DPCMStream(_oldStereoBuffer, _soundFreq, 2, true));
}
}

uint8 VMDDecoder::evaluateMask(uint32 mask, bool *fillInfo, uint8 &max) {
max = MIN<int>(_soundSlicesCount - 1, 31);

Expand Down Expand Up @@ -2555,86 +2677,12 @@ Audio::AudioStream *VMDDecoder::create8bitRaw(Common::SeekableReadStream *stream
return Audio::makeRawStream(stream, _soundFreq, flags, DisposeAfterUse::YES);
}

class DPCMStream : public Audio::AudioStream {
public:
DPCMStream(Common::SeekableReadStream *stream, int rate, int channels) {
_stream = stream;
_rate = rate;
_channels = channels;
}

~DPCMStream() {
delete _stream;
}

int readBuffer(int16 *buffer, const int numSamples);
bool isStereo() const { return _channels == 2; }
int getRate() const { return _rate; }
bool endOfData() const { return _stream->pos() >= _stream->size() || _stream->eos() || _stream->err(); }

private:
Common::SeekableReadStream *_stream;
int _channels;
int _rate;
int _buffer[2];
};

int DPCMStream::readBuffer(int16 *buffer, const int numSamples) {
static const uint16 tableDPCM[128] = {
0x0000, 0x0008, 0x0010, 0x0020, 0x0030, 0x0040, 0x0050, 0x0060, 0x0070, 0x0080,
0x0090, 0x00A0, 0x00B0, 0x00C0, 0x00D0, 0x00E0, 0x00F0, 0x0100, 0x0110, 0x0120,
0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0180, 0x0190, 0x01A0, 0x01B0, 0x01C0,
0x01D0, 0x01E0, 0x01F0, 0x0200, 0x0208, 0x0210, 0x0218, 0x0220, 0x0228, 0x0230,
0x0238, 0x0240, 0x0248, 0x0250, 0x0258, 0x0260, 0x0268, 0x0270, 0x0278, 0x0280,
0x0288, 0x0290, 0x0298, 0x02A0, 0x02A8, 0x02B0, 0x02B8, 0x02C0, 0x02C8, 0x02D0,
0x02D8, 0x02E0, 0x02E8, 0x02F0, 0x02F8, 0x0300, 0x0308, 0x0310, 0x0318, 0x0320,
0x0328, 0x0330, 0x0338, 0x0340, 0x0348, 0x0350, 0x0358, 0x0360, 0x0368, 0x0370,
0x0378, 0x0380, 0x0388, 0x0390, 0x0398, 0x03A0, 0x03A8, 0x03B0, 0x03B8, 0x03C0,
0x03C8, 0x03D0, 0x03D8, 0x03E0, 0x03E8, 0x03F0, 0x03F8, 0x0400, 0x0440, 0x0480,
0x04C0, 0x0500, 0x0540, 0x0580, 0x05C0, 0x0600, 0x0640, 0x0680, 0x06C0, 0x0700,
0x0740, 0x0780, 0x07C0, 0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00,
0x0F00, 0x1000, 0x1400, 0x1800, 0x1C00, 0x2000, 0x3000, 0x4000
};

assert((numSamples % _channels) == 0);

int samples = 0;

// Our starting position
if (_stream->pos() == 0) {
for (int i = 0; i < _channels; i++)
*buffer++ = _buffer[i] = _stream->readSint16LE();

samples += _channels;
}

while (!endOfData() && samples < numSamples) {
for (int i = 0; i < _channels; i++) {
byte data = _stream->readByte();

if (data & 0x80)
_buffer[i] -= tableDPCM[data & 0x7f];
else
_buffer[i] += tableDPCM[data];

// Emulating x86 16-bit signed register overflow
if (_buffer[i] > 32767) {
_buffer[i] -= 65536;
} else if (_buffer[i] < -32768) {
_buffer[i] += 65536;
}

*buffer++ = _buffer[i];
}

samples += _channels;
}

return samples;
}

Audio::AudioStream *VMDDecoder::create16bitDPCM(Common::SeekableReadStream *stream) {
return new DPCMStream(stream, _soundFreq, (_soundStereo == 0) ? 1 : 2);
// Old-style stereo audio blocks are not self-contained so cannot be played
// using this mechanism
assert(_soundStereo != 1);

return new DPCMStream(stream, _soundFreq, (_soundStereo == 0) ? 1 : 2, false);
}

class VMD_ADPCMStream : public Audio::DVI_ADPCMStream {
Expand Down
13 changes: 13 additions & 0 deletions video/coktel_decoder.h
Expand Up @@ -43,6 +43,7 @@

namespace Common {
struct Rect;
class MemoryReadWriteStream;
class SeekableReadStream;
}
namespace Audio {
Expand Down Expand Up @@ -503,6 +504,17 @@ friend class AdvancedVMDDecoder;
AudioFormat _audioFormat;
bool _autoStartSound;

/**
* Old stereo format packs a DPCM stream into audio packets without ensuring
* that each packet contains an even amount of samples. In order for the
* stream to play back correctly, all audio data needs to be pushed into a
* single data buffer and read from there.
*
* This buffer is owned by _audioStream and will be disposed when
* _audioStream is disposed.
*/
Common::MemoryReadWriteStream *_oldStereoBuffer;

// Video properties
bool _hasVideo;
uint32 _videoCodec;
Expand Down Expand Up @@ -545,6 +557,7 @@ friend class AdvancedVMDDecoder;
void emptySoundSlice (uint32 size);
void filledSoundSlice (uint32 size);
void filledSoundSlices(uint32 size, uint32 mask);
void createAudioStream();

uint8 evaluateMask(uint32 mask, bool *fillInfo, uint8 &max);

Expand Down

0 comments on commit 4ba8701

Please sign in to comment.