From b72d66a393865468c529fbcbf9afe248289b8f1b Mon Sep 17 00:00:00 2001 From: Eugene Sandulenko Date: Mon, 28 Jun 2021 01:04:46 +0200 Subject: [PATCH] SAGA2: Initial code for voice playback --- engines/saga2/audio.cpp | 2 +- engines/saga2/hresmgr.cpp | 3 +- engines/saga2/module.mk | 1 + engines/saga2/noise.cpp | 39 ++- engines/saga2/shorten.cpp | 564 ++++++++++++++++++++++++++++++++++++++ engines/saga2/shorten.h | 56 ++++ 6 files changed, 650 insertions(+), 15 deletions(-) create mode 100644 engines/saga2/shorten.cpp create mode 100644 engines/saga2/shorten.h diff --git a/engines/saga2/audio.cpp b/engines/saga2/audio.cpp index 673578d4cd0..4224e95b68f 100644 --- a/engines/saga2/audio.cpp +++ b/engines/saga2/audio.cpp @@ -135,7 +135,7 @@ bool audioInterface::saying(soundSegment s) { bool audioInterface::active(void) { warning("STUB: audioInterface::active()"); - return false; + return true; } void audioInterface::enable(volumeTarget i, bool onOff) { warning("STUB: audioInterface::enable()"); diff --git a/engines/saga2/hresmgr.cpp b/engines/saga2/hresmgr.cpp index 4f7d5ccf5b4..f8ec74fb632 100644 --- a/engines/saga2/hresmgr.cpp +++ b/engines/saga2/hresmgr.cpp @@ -85,7 +85,7 @@ hResContext::hResContext(hResContext *sire, hResID id, const char desc[]) { _parent = sire; - debugC(3, kDebugResources, "Creating context %x (%s)", id, tag2str(id)); + debugC(3, kDebugResources, "Creating context %x (%s), %s", id, tag2str(id), desc); if ((entry = _parent->findEntry(id)) == nullptr) { debugC(3, kDebugResources, "Could not create context"); return; @@ -197,6 +197,7 @@ bool hResContext::seek(hResID id) { // resource _data is actually a path name _handle = openExternal(_res->_handle); + return (_handle != nullptr); } diff --git a/engines/saga2/module.mk b/engines/saga2/module.mk index c15a9685b41..cd89c17b978 100644 --- a/engines/saga2/module.mk +++ b/engines/saga2/module.mk @@ -62,6 +62,7 @@ MODULE_OBJS := \ sagafunc.o \ savefile.o \ sensor.o \ + shorten.o \ speech.o \ spelcast.o \ speldata.o \ diff --git a/engines/saga2/noise.cpp b/engines/saga2/noise.cpp index c355826e94c..62779a74603 100644 --- a/engines/saga2/noise.cpp +++ b/engines/saga2/noise.cpp @@ -25,6 +25,7 @@ */ #include "common/config-manager.h" +#include "audio/mixer.h" #include "saga2/saga2.h" #include "saga2/fta.h" @@ -39,6 +40,7 @@ #include "saga2/audqueue.h" #include "saga2/audiosys.h" #include "saga2/hresmgr.h" +#include "saga2/shorten.h" namespace Saga2 { @@ -220,8 +222,6 @@ void startAudio(void) { (uint32) 400000 // sound buffer size ); - return; - bool disVoice = true, disMusic = true, disSound = true, disLoops = true; if (audio->active()) { @@ -463,6 +463,7 @@ void playSound(uint32 s) { // on disk sfx (x2 buffered) void playLongSound(uint32 s) { + warning("playLongSound(%d)", s); if (hResCheckResID(longRes, s)) audio->queueVoice(s, longSoundDec); else @@ -473,6 +474,8 @@ void playLongSound(uint32 s) { // on disk voice (x2 buffered) void playVoice(uint32 s) { + warning("playVoice(%d)", s); + if (hResCheckResID(voiceRes, s)) { if (s) audio->queueVoice(s, voiceDec, Here); @@ -485,6 +488,8 @@ void playVoice(uint32 s) { // supplemental interface for speech bool sayVoice(uint32 s[]) { + warning("sayVoice(%d)", s[0]); + bool worked = false; if (hResCheckResID(voiceRes, s)) { @@ -500,15 +505,21 @@ bool sayVoice(uint32 s[]) { // main loop playback void _playLoop(uint32 s) { - warning("STUB: playLoop(%d)", s); + warning("STUB: _playLoop(%d)", s); currentLoop = s; - if (currentLoop == audio->currentLoop()) + if (currentLoop == audio->currentLoop() && 0) return; audio->stopLoop(); - if (hResCheckResID(loopRes, s)) - audio->queueLoop(s, loopDec, 0, Here); + + byte *data = loopRes->loadResource(s, "loop sound"); + uint32 size = loopRes->getSize(s, "loop sound"); + + warning("Size: %d", size); + + Common::hexdump(data, MIN(size, 256U)); + audio->queueLoop(s, loopDec, 0, Here); } //----------------------------------------------------------------------- @@ -541,16 +552,18 @@ void playSoundAt(uint32 s, Location playAt) { //----------------------------------------------------------------------- // voice playback w/ attenuation +Audio::SoundHandle _speechSoundHandle; + bool sayVoiceAt(uint32 s[], Point32 p) { - bool worked = false; + warning("sayVoiceAt(%s, %d,%d)", tag2str(s[0]), p.x, p.y); - if (hResCheckResID(voiceRes, s)) { - audio->queueVoice(s, voiceDec, p); - if (audio->talking()) - worked = true; - } + Common::SeekableReadStream *stream = loadResourceToStream(voiceRes, s[0], "voice data"); - return worked; + Audio::AudioStream *aud = makeShortenStream(*stream); + + g_system->getMixer()->playStream(Audio::Mixer::kSpeechSoundType, &_speechSoundHandle, aud); + + return true; } bool sayVoiceAt(uint32 s[], Location playAt) { diff --git a/engines/saga2/shorten.cpp b/engines/saga2/shorten.cpp new file mode 100644 index 00000000000..a6be9f7f750 --- /dev/null +++ b/engines/saga2/shorten.cpp @@ -0,0 +1,564 @@ +/* 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 "common/scummsys.h" +#include "common/stream.h" +#include "common/textconsole.h" +#include "audio/audiostream.h" + +#include "saga/shorten.h" + +// Based on etree's Shorten tool, version 3.6.1 +// http://etree.org/shnutils/shorten/ +// and +// https://github.com/soiaf/Java-Shorten-decoder + +// FIXME: This doesn't work yet correctly + +#include "common/util.h" + +#include "audio/decoders/raw.h" + +namespace Saga2 { + +#define MASKTABSIZE 33 +#define MAX_SUPPORTED_VERSION 3 +#define DEFAULT_BLOCK_SIZE 256 + +enum kShortenTypes { + kTypeAU1 = 0, // lossless ulaw + kTypeS8 = 1, // signed 8 bit + kTypeU8 = 2, // unsigned 8 bit + kTypeS16HL = 3, // signed 16 bit shorts: high-low + kTypeU16HL = 4, // unsigned 16 bit shorts: high-low + kTypeS16LH = 5, // signed 16 bit shorts: low-high + kTypeU16LH = 6, // unsigned 16 bit shorts: low-high + kTypeULaw = 7, // lossy ulaw + kTypeAU2 = 8, // new ulaw with zero mapping + kTypeAU3 = 9, // lossless alaw + kTypeALaw = 10, // lossy alaw + kTypeWAV = 11, // WAV + kTypeAIFF = 12, // AIFF + kTypeEOF = 13, + kTypeGenericULaw = 128, + kTypeGenericALaw = 129 +}; + +enum kShortenCommands { + kCmdDiff0 = 0, + kCmdDiff1 = 1, + kCmdDiff2 = 2, + kCmdDiff3 = 3, + kCmdQuit = 4, + kCmdBlockSize = 5, + kCmdBitShift = 6, + kCmdQLPC = 7, + kCmdZero = 8, + kCmdVerbatim = 9 +}; + +#ifndef M_LN2 +#define M_LN2 0.69314718055994530942 +#endif + +// --------------------------------------------------------------------------- + +class ShortenGolombReader { +public: + ShortenGolombReader(Common::ReadStream *stream, int version); + ~ShortenGolombReader() {} + uint32 getUint32(uint32 numBits); // UINT_GET + int32 getURice(uint32 numBits); // uvar_get + int32 getSRice(uint32 numBits); // var_get +private: + int _version; + uint32 _nbitget; + uint32 _buf; + uint32 _masktab[MASKTABSIZE]; + Common::ReadStream *_stream; +}; + +ShortenGolombReader::ShortenGolombReader(Common::ReadStream *stream, int version) { + _stream = stream; + _version = version; + uint32 val = 0; + _masktab[0] = 0; + _nbitget = 0; + _buf = 0; + + for (int i = 1; i < MASKTABSIZE; i++) { + val <<= 1; + val |= 1; + _masktab[i] = val; + } +} + +int32 ShortenGolombReader::getURice(uint32 numBits) { + int32 result = 0; + + if (!_nbitget) { + _buf = _stream->readUint32BE(); + _nbitget = 32; + } + + for (result = 0; !(_buf & (1L << --_nbitget)); result++) { + if (!_nbitget) { + _buf = _stream->readUint32BE(); + _nbitget = 32; + } + } + + while (numBits != 0) { + if (_nbitget >= numBits) { + result = (result << numBits) | ((_buf >> (_nbitget - numBits)) & _masktab[numBits]); + _nbitget -= numBits; + numBits = 0; + } else { + result = (result << _nbitget) | (_buf & _masktab[_nbitget]); + _buf = _stream->readUint32BE(); + numBits -= _nbitget; + _nbitget = 32; + } + } + + return result; +} + +int32 ShortenGolombReader::getSRice(uint32 numBits) { + uint32 uvar = (uint32) getURice(numBits + 1); + return (uvar & 1) ? (int32) ~(uvar >> 1) : (int32) (uvar >> 1); +} + +uint32 ShortenGolombReader::getUint32(uint32 numBits) { + return (_version == 0) ? (uint32)getURice(numBits) : (uint32)getURice(getURice(2)); +} + +// --------------------------------------------------------------------------- + +byte *loadShortenFromStream(Common::ReadStream &stream, int &size, int &rate, byte &flags) { + int32 *buffer[2], *offset[2]; // up to 2 channels + int32 *oldValues[2]; + byte *unpackedBuffer = 0; + byte *pBuf = unpackedBuffer; + int prevSize = 0; + int32 *lpc = 0; + + ShortenGolombReader *gReader; + uint32 i, j, version, mean, type, channels, blockSize; + uint32 maxLPC = 0, lpcqOffset = 0; + int32 bitShift = 0, wrap = 0; + flags = 0; + size = 0; + + // Read header + byte magic[4]; + stream.read(magic, 4); + if (memcmp(magic, "ajkg", 4) != 0) { + warning("loadShortenFromStream: No 'ajkg' header"); + return NULL; + } + + version = stream.readByte(); + + if (version > MAX_SUPPORTED_VERSION) { + warning("loadShortenFromStream: Can't decode version %d, maximum supported version is %d", version, MAX_SUPPORTED_VERSION); + return NULL; + } + + mean = (version < 2) ? 0 : 4; + + gReader = new ShortenGolombReader(&stream, version); + + // Get file type + type = gReader->getUint32(4); + + switch (type) { + case kTypeS8: + mean = 0; + break; + case kTypeU8: + flags |= Audio::FLAG_UNSIGNED; + mean = 0x80; + break; + case kTypeS16LH: + flags |= Audio::FLAG_LITTLE_ENDIAN; + // fallthrough + case kTypeS16HL: + flags |= Audio::FLAG_16BITS; + mean = 0; + break; + case kTypeU16LH: + flags |= Audio::FLAG_LITTLE_ENDIAN; + // fallthrough + case kTypeU16HL: + flags |= Audio::FLAG_16BITS; + flags |= Audio::FLAG_UNSIGNED; + mean = 0x8000; + break; + case kTypeWAV: + // TODO: Perhaps implement this if we find WAV Shorten encoded files + warning("loadShortenFromStream: Type WAV is not supported"); + delete gReader; + return NULL; + case kTypeAIFF: + // TODO: Perhaps implement this if we find AIFF Shorten encoded files + warning("loadShortenFromStream: Type AIFF is not supported"); + delete gReader; + return NULL; + case kTypeAU1: + case kTypeAU2: + case kTypeAU3: + case kTypeULaw: + case kTypeALaw: + case kTypeEOF: + case kTypeGenericULaw: + case kTypeGenericALaw: + default: + warning("loadShortenFromStream: Type %d is not supported", type); + delete gReader; + return NULL; + } + + // Get channels + channels = gReader->getUint32(0); + if (channels != 1 && channels != 2) { + warning("loadShortenFromStream: Only 1 or 2 channels are supported, stream contains %d channels", channels); + delete gReader; + return NULL; + } + + // Get block size + if (version > 0) { + blockSize = gReader->getUint32((int)(log((double) DEFAULT_BLOCK_SIZE) / M_LN2)); + maxLPC = gReader->getUint32(2); + mean = gReader->getUint32(0); + uint32 skipBytes = gReader->getUint32(1); + if (skipBytes > 0) { + prevSize = size; + size += skipBytes; + unpackedBuffer = (byte *) realloc(unpackedBuffer, size); + pBuf = unpackedBuffer + prevSize; + for (i = 0; i < skipBytes; i++) { + *pBuf++ = gReader->getUint32(7) & 0xFF; + } + } + } else { + blockSize = DEFAULT_BLOCK_SIZE; + } + + wrap = MAX(3, maxLPC); + + // Initialize buffers + for (i = 0; i < channels; i++) { + buffer[i] = (int32 *)malloc((blockSize + wrap) * 4); + offset[i] = (int32 *)malloc((MAX(1, mean)) * 4); + oldValues[i] = (int32 *)malloc(64 * 4); + memset(buffer[i], 0, (blockSize + wrap) * 4); + memset(offset[i], 0, (MAX(1, mean)) * 4); + memset(oldValues[i], 0, 64 * 4); + } + + if (maxLPC > 0) + lpc = (int32 *) malloc(maxLPC * 4); + + if (version > 1) + lpcqOffset = 1 << 5; + + // Init offset + int32 offsetMean = 0; + uint32 blocks = MAX(1, mean); + + if (type == kTypeU8) + offsetMean = 0x80; + else if (type == kTypeU16HL || type == kTypeU16LH) + offsetMean = 0x8000; + + for (uint32 channel = 0; channel < channels; channel++) + for (uint32 block = 0; block < blocks; block++) + offset[channel][block] = offsetMean; + + + uint32 curChannel = 0, cmd = 0; + + // Parse Shorten commands + while (true) { + cmd = gReader->getURice(2); + + if (cmd == kCmdQuit) + break; + + switch (cmd) { + case kCmdZero: + case kCmdDiff0: + case kCmdDiff1: + case kCmdDiff2: + case kCmdDiff3: + case kCmdQLPC: + { + int32 channelOffset = 0, energy = 0; + uint32 lpcNum = 0; + + if (cmd != kCmdZero) { + energy = gReader->getURice(3); + // hack for version 0 + if (version == 0) + energy--; + } + + // Find mean offset (code duplicated below) + if (mean == 0) { + channelOffset = offset[curChannel][0]; + } else { + int32 sum = (version < 2) ? 0 : mean / 2; + for (i = 0; i < mean; i++) + sum += offset[curChannel][i]; + + channelOffset = sum / mean; + + if (version >= 2 && bitShift > 0) + channelOffset = (channelOffset >> (bitShift - 1)) >> 1; + } + + switch (cmd) { + case kCmdZero: + for (i = 0; i < blockSize; i++) + buffer[curChannel][i] = 0; + break; + case kCmdDiff0: + for (i = 0; i < blockSize; i++) + buffer[curChannel][i] = gReader->getSRice(energy) + channelOffset; + break; + case kCmdDiff1: + for (i = 0; i < blockSize; i++) { + if (i == 0) + buffer[curChannel][i] = gReader->getSRice(energy) + oldValues[curChannel][0]; + else + buffer[curChannel][i] = gReader->getSRice(energy) + buffer[curChannel][i - 1]; + } + break; + case kCmdDiff2: + for (i = 0; i < blockSize; i++) { + if (i == 0) + buffer[curChannel][i] = gReader->getSRice(energy) + 2 * oldValues[curChannel][0] - oldValues[curChannel][1]; + else if (i == 1) + buffer[curChannel][i] = gReader->getSRice(energy) + 2 * buffer[curChannel][0] - oldValues[curChannel][0]; + else + buffer[curChannel][i] = gReader->getSRice(energy) + 2 * buffer[curChannel][i - 1] - buffer[curChannel][i - 2]; + } + break; + case kCmdDiff3: + for (i = 0; i < blockSize; i++) { + if (i == 0) + buffer[curChannel][i] = gReader->getSRice(energy) + 3 * (oldValues[curChannel][0] - oldValues[curChannel][1]) + oldValues[curChannel][2]; + else if (i == 1) + buffer[curChannel][i] = gReader->getSRice(energy) + 3 * (buffer[curChannel][0] - oldValues[curChannel][0]) + oldValues[curChannel][1]; + else if (i == 2) + buffer[curChannel][i] = gReader->getSRice(energy) + 3 * (buffer[curChannel][1] - buffer[curChannel][0]) + oldValues[curChannel][0]; + else + buffer[curChannel][i] = gReader->getSRice(energy) + 3 * (buffer[curChannel][i - 1] - buffer[curChannel][i - 2]) + buffer[curChannel][i - 3]; + } + break; + case kCmdQLPC: + lpcNum = gReader->getURice(2); + + // Safeguard: if maxLPC < lpcNum, realloc the lpc buffer + if (maxLPC < lpcNum) { + warning("Safeguard: maxLPC < lpcNum (should never happen)"); + maxLPC = lpcNum; + int32 *tmp = (int32 *) realloc(lpc, maxLPC * 4); + if ((tmp != NULL) || (maxLPC == 0)) { + lpc = tmp; + } else { + error("loadShortenFromStream(): Error while reallocating memory"); + } + } + + for (i = 0; i < lpcNum; i++) + lpc[i] = gReader->getSRice(5); + + for (i = 0; i < lpcNum; i++) + buffer[curChannel][i - lpcNum] -= channelOffset; + + for (i = 0; i < blockSize; i++) { + int32 sum = lpcqOffset; + for (j = 0; j < lpcNum; j++) { + // FIXME: The original code did an invalid memory access here + // (if i and j are 0, the array index requested is -1) + // I've removed those invalid writes, since they happen all the time (even when curChannel is 0) + if (i <= j) // ignore invalid table/memory access + continue; + sum += lpc[j] * buffer[curChannel][i - j - 1]; + } + buffer[curChannel][i] = gReader->getSRice(energy) + (sum >> 5); + } + + if (channelOffset > 0) + for (i = 0; i < blockSize; i++) + buffer[curChannel][i] += channelOffset; + + break; + default: + break; + } + + // Store mean value, if appropriate (duplicated code from above) + if (mean > 0) { + int32 sum = (version < 2) ? 0 : blockSize / 2; + for (i = 0; i < blockSize; i++) + sum += buffer[curChannel][i]; + + for (i = 1; i < mean; i++) + offset[curChannel][i - 1] = offset[curChannel][i]; + + offset[curChannel][mean - 1] = sum / blockSize; + + if (version >= 2 && bitShift > 0) + offset[curChannel][mean - 1] = offset[curChannel][mean - 1] << bitShift; + } + + + // Do the wrap + for (i = 0; i < 64; ++i) + oldValues[curChannel][i] = 0; + + uint arrayTerminator = MIN(64, blockSize); + for (i = 0; i < arrayTerminator; ++i) + oldValues[curChannel][i] = buffer[curChannel][blockSize - (i + 1)]; + + // Fix bitshift + if (bitShift > 0) { + for (i = 0; i < blockSize; i++) + buffer[curChannel][i] <<= bitShift; + } + + if (curChannel == channels - 1) { + int dataSize = (flags & Audio::FLAG_16BITS) ? 2 : 1; + int limit = (flags & Audio::FLAG_16BITS) ? 32767 : 127; + limit = (flags & Audio::FLAG_UNSIGNED) ? limit * 2 + 1 : limit; + + prevSize = size; + size += (blockSize * dataSize); + byte *tmp = (byte *) realloc(unpackedBuffer, size); + if ((tmp != NULL) || (size == 0)) { + unpackedBuffer = tmp; + } else { + error("loadShortenFromStream(): Error while reallocating memory"); + } + pBuf = unpackedBuffer + prevSize; + + if (flags & Audio::FLAG_16BITS) { + for (i = 0; i < blockSize; i++) { + for (j = 0; j < channels; j++) { + int16 val = (int16)(MIN(buffer[j][i], limit) & 0xFFFF); + // values are written in LE + *pBuf++ = (byte) (val & 0xFF); + *pBuf++ = (byte) ((val >> 8) & 0xFF); + } + } + } else { + for (i = 0; i < blockSize; i++) + for (j = 0; j < channels; j++) + *pBuf++ = (byte)(MIN(buffer[j][i], limit) & 0xFF); + } + } + curChannel = (curChannel + 1) % channels; + + } + break; + case kCmdBlockSize: + blockSize = gReader->getUint32((uint32)log((double) blockSize / M_LN2)); + break; + case kCmdBitShift: + bitShift = gReader->getURice(2); + break; + case kCmdVerbatim: + { + + uint32 vLen = (uint32)gReader->getURice(5); + prevSize = size; + size += vLen; + byte *tmp = (byte *) realloc(unpackedBuffer, size); + if ((tmp != NULL) || (size == 0)) { + unpackedBuffer = tmp; + } else { + error("loadShortenFromStream(): Error while reallocating memory"); + } + pBuf = unpackedBuffer + prevSize; + + while (vLen--) { + *pBuf++ = (byte)(gReader->getURice(8) & 0xFF); + } + + } + break; + default: + warning("loadShortenFromStream: Unknown command: %d", cmd); + + // Cleanup + for (i = 0; i < channels; i++) { + free(buffer[i]); + free(offset[i]); + free(oldValues[i]); + } + + if (maxLPC > 0) + free(lpc); + + if (size > 0) + free(unpackedBuffer); + + delete gReader; + return NULL; + break; + } + } + + // Rate is always 44100Hz + rate = 44100; + + // Cleanup + for (i = 0; i < channels; i++) { + free(buffer[i]); + free(offset[i]); + free(oldValues[i]); + } + + if (maxLPC > 0) + free(lpc); + + delete gReader; + return unpackedBuffer; +} + +Audio::AudioStream *makeShortenStream(Common::SeekableReadStream &stream) { + int size, rate; + byte *data, flags; + data = loadShortenFromStream(stream, size, rate, flags); + + if (!data) + return 0; + + // Since we allocated our own buffer for the data, we must specify DisposeAfterUse::YES. + return Audio::makeRawStream(data, size, rate, flags); +} + +} // End of namespace Saga2 diff --git a/engines/saga2/shorten.h b/engines/saga2/shorten.h new file mode 100644 index 00000000000..f305964bf34 --- /dev/null +++ b/engines/saga2/shorten.h @@ -0,0 +1,56 @@ +/* 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 SOUND_SHORTEN_H +#define SOUND_SHORTEN_H + +#include "common/scummsys.h" + +namespace Audio { +class AudioStream; +} + +namespace Common { +class ReadStream; +} + +namespace Saga2 { + +/** + * Try to load a Shorten file from the given stream. Returns true if + * successful. In that case, the stream's seek position will be set to the + * start of the audio data, and size, rate and flags contain information + * necessary for playback. + */ +byte *loadShortenFromStream(Common::ReadStream &stream, int &size, int &rate, byte &flags); + +/** + * Try to load a Shorten file from the given stream and create an AudioStream + * from that data. + * + * This function uses loadShortenFromStream() internally. + */ +Audio::AudioStream *makeShortenStream(Common::SeekableReadStream &stream); + +} // End of namespace Saga2 + +#endif