From 46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5 Mon Sep 17 00:00:00 2001 From: Colin Snover Date: Fri, 18 Mar 2016 22:55:56 -0500 Subject: SCI32: Rewrite digital audio engine This provides a complete implementation of kDoAudio through SCI2.1mid, plus partial implementation of SCI3 features. Digital audio calls shunted through kDoSound have also been updated to go through the SCI32 audio mixer, though these shunts are a bit hacky because the ScummVM implementation of kDoSound does not currently match how SSCI kDoSound is designed. It is probably possible in the future to just replace the SCI1.1 audio code (audio.cpp) with the new SCI32 code, since the major differences seem to be that (1) SCI1.1 only supported one digital audio playback channel (this is configurable already), (2) it had extra commands for CD audio playback and queued sample playback. --- engines/sci/sound/decoders/sol.cpp | 272 +++++++++++++++++++++++++++++++++++++ engines/sci/sound/decoders/sol.h | 89 ++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 engines/sci/sound/decoders/sol.cpp create mode 100644 engines/sci/sound/decoders/sol.h (limited to 'engines/sci/sound/decoders') diff --git a/engines/sci/sound/decoders/sol.cpp b/engines/sci/sound/decoders/sol.cpp new file mode 100644 index 0000000000..280b24fd3a --- /dev/null +++ b/engines/sci/sound/decoders/sol.cpp @@ -0,0 +1,272 @@ +/* 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. + * + */ + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" +#include "common/substream.h" +#include "common/util.h" +#include "engines/sci/sci.h" +#include "engines/sci/sound/decoders/sol.h" + +namespace Sci { + +// Note that the 16-bit version is also used in coktelvideo.cpp +static const uint16 tableDPCM16[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 +}; + +static const byte tableDPCM8[8] = { 0, 1, 2, 3, 6, 10, 15, 21 }; + +/** + * Decompresses 16-bit DPCM compressed audio. Each byte read + * outputs one sample into the decompression buffer. + */ +static void deDPCM16(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, int16 &sample) { + for (uint32 i = 0; i < numBytes; ++i) { + const uint8 delta = audioStream.readByte(); + if (delta & 0x80) { + sample -= tableDPCM16[delta & 0x7f]; + } else { + sample += tableDPCM16[delta]; + } + sample = CLIP(sample, -32768, 32767); + *out++ = TO_LE_16(sample); + } +} + +/** + * Decompresses one half of an 8-bit DPCM compressed audio + * byte. + */ +static void deDPCM8Nibble(int16 *out, uint8 &sample, uint8 delta) { + if (delta & 8) { + sample -= tableDPCM8[delta & 7]; + } else { + sample += tableDPCM8[delta & 7]; + } + sample = CLIP(sample, 0, 255); + *out = (sample << 8) ^ 0x8000; +} + +/** + * Decompresses 8-bit DPCM compressed audio. Each byte read + * outputs two samples into the decompression buffer. + */ +static void deDPCM8(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, uint8 &sample) { + for (uint32 i = 0; i < numBytes; ++i) { + const uint8 delta = audioStream.readByte(); + deDPCM8Nibble(out++, sample, delta >> 4); + deDPCM8Nibble(out++, sample, delta & 0xf); + } +} + +# pragma mark - + +template +SOLStream::SOLStream(Common::SeekableReadStream *stream, const DisposeAfterUse::Flag disposeAfterUse, const int32 dataOffset, const uint16 sampleRate, const int32 rawDataSize) : + _stream(stream, disposeAfterUse), + _dataOffset(dataOffset), + _sampleRate(sampleRate), + // SSCI aligns the size of SOL data to 32 bits + _rawDataSize(rawDataSize & ~3) { + // TODO: This is not valid for stereo SOL files, which + // have interleaved L/R compression so need to store the + // carried values for each channel separately. See + // 60900.aud from Lighthouse for an example stereo file + if (S16BIT) { + _dpcmCarry16 = 0; + } else { + _dpcmCarry8 = 0x80; + } + + const uint8 compressionRatio = 2; + const uint8 numChannels = STEREO ? 2 : 1; + const uint8 bytesPerSample = S16BIT ? 2 : 1; + _length = Audio::Timestamp((_rawDataSize * compressionRatio * 1000) / (_sampleRate * numChannels * bytesPerSample), 60); + } + +template +bool SOLStream::seek(const Audio::Timestamp &where) override { + if (where != 0) { + // In order to seek in compressed SOL files, all + // previous bytes must be known since it uses + // differential compression. Therefore, only seeking + // to the beginning is supported now (SSCI does not + // offer seeking anyway) + return false; + } + + if (S16BIT) { + _dpcmCarry16 = 0; + } else { + _dpcmCarry8 = 0x80; + } + + return _stream->seek(_dataOffset, SEEK_SET); +} + +template +Audio::Timestamp SOLStream::getLength() const override { + return _length; +} + +template +int SOLStream::readBuffer(int16 *buffer, const int numSamples) override { + // Reading an odd number of 8-bit samples will result in a loss of samples + // since one byte represents two samples and we do not store the second + // nibble in this case; it should never happen in reality + assert(S16BIT || (numSamples % 2) == 0); + + const int samplesPerByte = S16BIT ? 1 : 2; + + int32 bytesToRead = numSamples / samplesPerByte; + if (_stream->pos() + bytesToRead > _rawDataSize) { + bytesToRead = _rawDataSize - _stream->pos(); + } + + if (S16BIT) { + deDPCM16(buffer, *_stream, bytesToRead, _dpcmCarry16); + } else { + deDPCM8(buffer, *_stream, bytesToRead, _dpcmCarry8); + } + + const int samplesRead = bytesToRead * samplesPerByte; + return samplesRead; +} + +template +bool SOLStream::isStereo() const override { + return STEREO; +} + +template +int SOLStream::getRate() const override { + return _sampleRate; +} + +template +bool SOLStream::endOfData() const override { + return _stream->eos() || _stream->pos() >= _dataOffset + _rawDataSize; +} + +template +bool SOLStream::rewind() override { + return seek(0); +} + +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) { + + // TODO: Might not be necessary? Makes seeking work, but + // not sure if audio is ever actually seeked in SSCI. + const int32 initialPosition = stream->pos(); + + byte header[6]; + if (stream->read(header, sizeof(header)) != sizeof(header)) { + return nullptr; + } + + if (header[0] != 0x8d || READ_BE_UINT32(header + 2) != MKTAG('S', 'O', 'L', 0)) { + return nullptr; + } + + const uint8 headerSize = header[1]; + const uint16 sampleRate = stream->readUint16LE(); + const byte flags = stream->readByte(); + const uint32 dataSize = stream->readUint32LE(); + + if (flags & kCompressed) { + if (flags & kStereo && flags & k16Bit) { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else if (flags & kStereo) { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else if (flags & k16Bit) { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } + } + + byte rawFlags = Audio::FLAG_LITTLE_ENDIAN; + if (flags & k16Bit) { + rawFlags |= Audio::FLAG_16BITS; + } else { + rawFlags |= Audio::FLAG_UNSIGNED; + } + + if (flags & kStereo) { + rawFlags |= Audio::FLAG_STEREO; + } + + return Audio::makeRawStream(new Common::SeekableSubReadStream(stream, initialPosition + headerSize, initialPosition + headerSize + dataSize, disposeAfterUse), sampleRate, rawFlags, disposeAfterUse); +} + +// TODO: This needs to be removed when resource manager is fixed +// to not split audio into two parts +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *headerStream, Common::SeekableReadStream *dataStream, DisposeAfterUse::Flag disposeAfterUse) { + + if (headerStream->readUint32BE() != MKTAG('S', 'O', 'L', 0)) { + return nullptr; + } + + const uint16 sampleRate = headerStream->readUint16LE(); + const byte flags = headerStream->readByte(); + const int32 dataSize = headerStream->readSint32LE(); + + if (flags & kCompressed) { + if (flags & kStereo && flags & k16Bit) { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else if (flags & kStereo) { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else if (flags & k16Bit) { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } + } + + byte rawFlags = Audio::FLAG_LITTLE_ENDIAN; + if (flags & k16Bit) { + rawFlags |= Audio::FLAG_16BITS; + } else { + rawFlags |= Audio::FLAG_UNSIGNED; + } + + if (flags & kStereo) { + rawFlags |= Audio::FLAG_STEREO; + } + + return Audio::makeRawStream(dataStream, sampleRate, rawFlags, disposeAfterUse); +} + +} diff --git a/engines/sci/sound/decoders/sol.h b/engines/sci/sound/decoders/sol.h new file mode 100644 index 0000000000..1046d0b213 --- /dev/null +++ b/engines/sci/sound/decoders/sol.h @@ -0,0 +1,89 @@ +/* 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. + * + */ + +#ifndef SCI_SOUND_DECODERS_SOL_H +#define SCI_SOUND_DECODERS_SOL_H +#include "audio/audiostream.h" +#include "common/stream.h" + +namespace Sci { + +enum SOLFlags { + kCompressed = 1, + k16Bit = 4, + kStereo = 16 +}; + +template +class SOLStream : public Audio::SeekableAudioStream { +private: + /** + * Read stream containing possibly-compressed SOL audio. + */ + Common::DisposablePtr _stream; + + /** + * Start offset of the audio data in the read stream. + */ + int32 _dataOffset; + + /** + * Sample rate of audio data. + */ + uint16 _sampleRate; + + /** + * The raw (possibly-compressed) size of audio data in + * the stream. + */ + int32 _rawDataSize; + + /** + * The last sample from the previous DPCM decode. + */ + union { + int16 _dpcmCarry16; + uint8 _dpcmCarry8; + }; + + /** + * The calculated length of the stream. + */ + Audio::Timestamp _length; + + virtual bool seek(const Audio::Timestamp &where) override; + virtual Audio::Timestamp getLength() const override; + virtual int readBuffer(int16 *buffer, const int numSamples) override; + virtual bool isStereo() const override; + virtual int getRate() const override; + virtual bool endOfData() const override; + virtual bool rewind() override; + +public: + SOLStream(Common::SeekableReadStream *stream, const DisposeAfterUse::Flag disposeAfterUse, const int32 dataOffset, const uint16 sampleRate, const int32 rawDataSize); +}; + +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse); + +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *headerStream, Common::SeekableReadStream *dataStream, DisposeAfterUse::Flag disposeAfterUse); +} +#endif -- cgit v1.2.3