/* 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/file.h" #include "common/memstream.h" #include "common/textconsole.h" #include "common/util.h" #include "common/substream.h" #include "agos/agos.h" #include "agos/sound.h" #include "audio/audiostream.h" #include "audio/decoders/flac.h" #include "audio/mixer.h" #include "audio/decoders/mp3.h" #include "audio/decoders/raw.h" #include "audio/decoders/voc.h" #include "audio/decoders/vorbis.h" #include "audio/decoders/wave.h" namespace AGOS { #define SOUND_BIG_ENDIAN true class BaseSound : Common::NonCopyable { protected: const Common::String _filename; uint32 *_offsets; Audio::Mixer *_mixer; bool _freeOffsets; Common::SeekableReadStream *getSoundStream(uint sound) const; public: BaseSound(Audio::Mixer *mixer, const Common::String &filename, uint32 base, bool bigEndian); BaseSound(Audio::Mixer *mixer, const Common::String &filename, uint32 *offsets); virtual ~BaseSound(); void playSound(uint sound, Audio::Mixer::SoundType type, Audio::SoundHandle *handle, bool loop, int vol = 0) { playSound(sound, sound, type, handle, loop, vol); } virtual void playSound(uint sound, uint loopSound, Audio::Mixer::SoundType type, Audio::SoundHandle *handle, bool loop, int vol = 0); virtual Audio::AudioStream *makeAudioStream(uint sound) = 0; }; BaseSound::BaseSound(Audio::Mixer *mixer, const Common::String &filename, uint32 base, bool bigEndian) : _mixer(mixer), _filename(filename), _offsets(NULL) { uint res = 0; uint32 size; Common::File file; if (!file.open(_filename)) error("BaseSound: Could not open file \"%s\"", filename.c_str()); file.seek(base + sizeof(uint32), SEEK_SET); if (bigEndian) size = file.readUint32BE(); else size = file.readUint32LE(); // The Feeble Files uses set amount of voice offsets if (size == 0) size = 40000; res = size / sizeof(uint32); _offsets = (uint32 *)malloc(size + sizeof(uint32)); _freeOffsets = true; file.seek(base, SEEK_SET); for (uint i = 0; i < res; i++) { if (bigEndian) _offsets[i] = base + file.readUint32BE(); else _offsets[i] = base + file.readUint32LE(); } _offsets[res] = file.size(); } BaseSound::BaseSound(Audio::Mixer *mixer, const Common::String &filename, uint32 *offsets) : _mixer(mixer), _filename(filename), _offsets(offsets), _freeOffsets(false) { } BaseSound::~BaseSound() { if (_freeOffsets) free(_offsets); } Common::SeekableReadStream *BaseSound::getSoundStream(uint sound) const { if (_offsets == NULL) return NULL; Common::File *file = new Common::File(); if (!file->open(_filename)) { warning("BaseSound::getSoundStream: Could not open file \"%s\"", _filename.c_str()); return NULL; } int i = 1; while (_offsets[sound + i] == _offsets[sound]) i++; uint end; if (_offsets[sound + i] > _offsets[sound]) { end = _offsets[sound + i]; } else { end = file->size(); } return new Common::SeekableSubReadStream(file, _offsets[sound], end, DisposeAfterUse::YES); } /////////////////////////////////////////////////////////////////////////////// #pragma mark - class LoopingAudioStream : public Audio::AudioStream { private: BaseSound *_parent; Audio::AudioStream *_stream; bool _loop; uint _sound; uint _loopSound; public: LoopingAudioStream(BaseSound *parent, uint sound, uint loopSound, bool loop); ~LoopingAudioStream(); int readBuffer(int16 *buffer, const int numSamples); bool isStereo() const { return _stream ? _stream->isStereo() : 0; } bool endOfData() const; int getRate() const { return _stream ? _stream->getRate() : 22050; } }; LoopingAudioStream::LoopingAudioStream(BaseSound *parent, uint sound, uint loopSound, bool loop) { _parent = parent; _sound = sound; _loop = loop; _loopSound = loopSound; _stream = _parent->makeAudioStream(sound); } LoopingAudioStream::~LoopingAudioStream() { delete _stream; } int LoopingAudioStream::readBuffer(int16 *buffer, const int numSamples) { if (!_loop) { return _stream->readBuffer(buffer, numSamples); } int16 *buf = buffer; int samplesLeft = numSamples; while (samplesLeft > 0) { int len = _stream->readBuffer(buf, samplesLeft); if (len < samplesLeft) { delete _stream; _stream = _parent->makeAudioStream(_loopSound); } samplesLeft -= len; buf += len; } return numSamples; } bool LoopingAudioStream::endOfData() const { if (!_stream) return true; if (_loop) return false; return _stream->endOfData(); } /////////////////////////////////////////////////////////////////////////////// #pragma mark - static void convertVolume(int &vol) { // DirectSound was originally used, which specifies volume // and panning differently than ScummVM does, using a logarithmic scale // rather than a linear one. // // Volume is a value between -10,000 and 0. // // In both cases, the -10,000 represents -100 dB. When panning, only // one speaker's volume is affected - just like in ScummVM - with // negative values affecting the left speaker, and positive values // affecting the right speaker. Thus -10,000 means the left speaker is // silent. int v = CLIP(vol, -10000, 0); if (v) { vol = (int)((double)Audio::Mixer::kMaxChannelVolume * pow(10.0, (double)v / 2000.0) + 0.5); } else { vol = Audio::Mixer::kMaxChannelVolume; } } static void convertPan(int &pan) { // DirectSound was originally used, which specifies volume // and panning differently than ScummVM does, using a logarithmic scale // rather than a linear one. // // Panning is a value between -10,000 and 10,000. // // In both cases, the -10,000 represents -100 dB. When panning, only // one speaker's volume is affected - just like in ScummVM - with // negative values affecting the left speaker, and positive values // affecting the right speaker. Thus -10,000 means the left speaker is // silent. int p = CLIP(pan, -10000, 10000); if (p < 0) { pan = (int)(255.0 * pow(10.0, (double)p / 2000.0) + 127.5); } else if (p > 0) { pan = (int)(255.0 * pow(10.0, (double)p / -2000.0) - 127.5); } else { pan = 0; } } /////////////////////////////////////////////////////////////////////////////// #pragma mark - // TODO: Move to a better place? void BaseSound::playSound(uint sound, uint loopSound, Audio::Mixer::SoundType type, Audio::SoundHandle *handle, bool loop, int vol) { convertVolume(vol); _mixer->playStream(type, handle, new LoopingAudioStream(this, sound, loopSound, loop), -1, vol); } class WavSound : public BaseSound { public: WavSound(Audio::Mixer *mixer, const Common::String &filename, uint32 base = 0) : BaseSound(mixer, filename, base, false) {} WavSound(Audio::Mixer *mixer, const Common::String &filename, uint32 *offsets) : BaseSound(mixer, filename, offsets) {} Audio::AudioStream *makeAudioStream(uint sound); }; Audio::AudioStream *WavSound::makeAudioStream(uint sound) { Common::SeekableReadStream *tmp = getSoundStream(sound); if (!tmp) return NULL; return Audio::makeWAVStream(tmp, DisposeAfterUse::YES); } /////////////////////////////////////////////////////////////////////////////// #pragma mark - class VocSound : public BaseSound { const byte _flags; public: VocSound(Audio::Mixer *mixer, const Common::String &filename, bool isUnsigned, uint32 base = 0, bool bigEndian = false) : BaseSound(mixer, filename, base, bigEndian), _flags(isUnsigned ? Audio::FLAG_UNSIGNED : 0) {} Audio::AudioStream *makeAudioStream(uint sound); }; Audio::AudioStream *VocSound::makeAudioStream(uint sound) { Common::SeekableReadStream *tmp = getSoundStream(sound); if (!tmp) return NULL; return Audio::makeVOCStream(tmp, _flags, DisposeAfterUse::YES); } /////////////////////////////////////////////////////////////////////////////// #pragma mark - // This class is only used by speech in Simon1 Amiga CD32 class RawSound : public BaseSound { const byte _flags; public: RawSound(Audio::Mixer *mixer, const Common::String &filename, bool isUnsigned) : BaseSound(mixer, filename, 0, SOUND_BIG_ENDIAN), _flags(isUnsigned ? Audio::FLAG_UNSIGNED : 0) {} Audio::AudioStream *makeAudioStream(uint sound); void playSound(uint sound, uint loopSound, Audio::Mixer::SoundType type, Audio::SoundHandle *handle, bool loop, int vol = 0); }; Audio::AudioStream *RawSound::makeAudioStream(uint sound) { if (_offsets == NULL) return NULL; Common::File *file = new Common::File(); if (!file->open(_filename)) { warning("RawSound::makeAudioStream: Could not open file \"%s\"", _filename.c_str()); return NULL; } file->seek(_offsets[sound], SEEK_SET); uint size = file->readUint32BE(); return Audio::makeRawStream(new Common::SeekableSubReadStream(file, _offsets[sound] + 4, _offsets[sound] + 4 + size, DisposeAfterUse::YES), 22050, _flags, DisposeAfterUse::YES); } void RawSound::playSound(uint sound, uint loopSound, Audio::Mixer::SoundType type, Audio::SoundHandle *handle, bool loop, int vol) { // Sound looping and volume are ignored. _mixer->playStream(type, handle, makeAudioStream(sound)); } /////////////////////////////////////////////////////////////////////////////// #pragma mark - /////////////////////////////////////////////////////////////////////////////// #pragma mark - #ifdef USE_MAD class MP3Sound : public BaseSound { public: MP3Sound(Audio::Mixer *mixer, const Common::String &filename, uint32 base = 0) : BaseSound(mixer, filename, base, false) {} Audio::AudioStream *makeAudioStream(uint sound) { Common::SeekableReadStream *tmp = getSoundStream(sound); if (!tmp) return NULL; return Audio::makeMP3Stream(tmp, DisposeAfterUse::YES); } }; #endif /////////////////////////////////////////////////////////////////////////////// #pragma mark - #ifdef USE_VORBIS class VorbisSound : public BaseSound { public: VorbisSound(Audio::Mixer *mixer, const Common::String &filename, uint32 base = 0) : BaseSound(mixer, filename, base, false) {} Audio::AudioStream *makeAudioStream(uint sound) { Common::SeekableReadStream *tmp = getSoundStream(sound); if (!tmp) return NULL; return Audio::makeVorbisStream(tmp, DisposeAfterUse::YES); } }; #endif /////////////////////////////////////////////////////////////////////////////// #pragma mark - #ifdef USE_FLAC class FLACSound : public BaseSound { public: FLACSound(Audio::Mixer *mixer, const Common::String &filename, uint32 base = 0) : BaseSound(mixer, filename, base, false) {} Audio::AudioStream *makeAudioStream(uint sound) { Common::SeekableReadStream *tmp = getSoundStream(sound); if (!tmp) return NULL; return Audio::makeFLACStream(tmp, DisposeAfterUse::YES); } }; #endif /////////////////////////////////////////////////////////////////////////////// #pragma mark - static BaseSound *makeSound(Audio::Mixer *mixer, const Common::String &basename) { #ifdef USE_FLAC if (Common::File::exists(basename + ".fla")) return new FLACSound(mixer, basename + ".fla"); #endif #ifdef USE_VORBIS if (Common::File::exists(basename + ".ogg")) return new VorbisSound(mixer, basename + ".ogg"); #endif #ifdef USE_MAD if (Common::File::exists(basename + ".mp3")) return new MP3Sound(mixer, basename + ".mp3"); #endif if (Common::File::exists(basename + ".wav")) return new WavSound(mixer, basename + ".wav"); if (Common::File::exists(basename + ".voc")) return new VocSound(mixer, basename + ".voc", true); return 0; } /////////////////////////////////////////////////////////////////////////////// #pragma mark - Sound::Sound(AGOSEngine *vm, const GameSpecificSettings *gss, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) { _voice = 0; _effects = 0; _effectsPaused = false; _ambientPaused = false; _sfx5Paused = false; _filenums = 0; _lastVoiceFile = 0; _offsets = 0; _hasEffectsFile = false; _hasVoiceFile = false; _ambientPlaying = 0; _soundQueuePtr = 0; _soundQueueNum = 0; _soundQueueSize = 0; _soundQueueFreq = 0; if (_vm->getFeatures() & GF_TALKIE) { loadVoiceFile(gss); if (_vm->getGameType() == GType_SIMON1) loadSfxFile(gss); } } Sound::~Sound() { delete _voice; delete _effects; free(_filenums); free(_offsets); } void Sound::loadVoiceFile(const GameSpecificSettings *gss) { // Game versions which use separate voice files if (_hasVoiceFile || _vm->getGameType() == GType_FF || _vm->getGameId() == GID_SIMON1CD32) return; _voice = makeSound(_mixer, gss->speech_filename); _hasVoiceFile = (_voice != 0); if (_hasVoiceFile) return; if (_vm->getGameType() == GType_SIMON2) { // for simon2 mac/amiga, only read index file Common::File file; if (file.open("voices.idx")) { int end = file.size(); _filenums = (uint16 *)malloc((end / 6 + 1) * 2); _offsets = (uint32 *)malloc((end / 6 + 1 + 1) * 4); for (int i = 1; i <= end / 6; i++) { _filenums[i] = file.readUint16BE(); _offsets[i] = file.readUint32BE(); } // We need to add a terminator entry otherwise we get an out of // bounds read when the offset table is accessed in // BaseSound::getSoundStream. _offsets[end / 6 + 1] = 0; _hasVoiceFile = true; return; } } const bool dataIsUnsigned = true; if (Common::File::exists(gss->speech_filename)) { _hasVoiceFile = true; if (_vm->getGameType() == GType_PP) _voice = new WavSound(_mixer, gss->speech_filename); else _voice = new VocSound(_mixer, gss->speech_filename, dataIsUnsigned); } } void Sound::loadSfxFile(const GameSpecificSettings *gss) { if (_hasEffectsFile) return; _effects = makeSound(_mixer, gss->effects_filename); _hasEffectsFile = (_effects != 0); if (_hasEffectsFile) return; const bool dataIsUnsigned = true; if (Common::File::exists(gss->effects_filename)) { _hasEffectsFile = true; _effects = new VocSound(_mixer, gss->effects_filename, dataIsUnsigned); } } // This method is only used by Simon1 Amiga CD32 & Windows void Sound::readSfxFile(const Common::String &filename) { if (_hasEffectsFile) return; _mixer->stopHandle(_effectsHandle); if (!Common::File::exists(filename)) { error("readSfxFile: Can't load sfx file %s", filename.c_str()); } const bool dataIsUnsigned = (_vm->getGameId() != GID_SIMON1CD32); delete _effects; if (_vm->getGameId() == GID_SIMON1CD32) { _effects = new VocSound(_mixer, filename, dataIsUnsigned, 0, SOUND_BIG_ENDIAN); } else _effects = new WavSound(_mixer, filename); } // This method is only used by Simon2 void Sound::loadSfxTable(const char *gameFilename, uint32 base) { stopAllSfx(); delete _effects; const bool dataIsUnsigned = true; if (_vm->getPlatform() == Common::kPlatformWindows || (_vm->getFeatures() & GF_WAVSFX)) _effects = new WavSound(_mixer, gameFilename, base); else _effects = new VocSound(_mixer, gameFilename, dataIsUnsigned, base, false); } // This method is only used by Simon1 Amiga CD32 void Sound::readVoiceFile(const Common::String &filename) { _mixer->stopHandle(_voiceHandle); if (!Common::File::exists(filename)) error("readVoiceFile: Can't load voice file %s", filename.c_str()); const bool dataIsUnsigned = false; delete _voice; _voice = new RawSound(_mixer, filename, dataIsUnsigned); } void Sound::playVoice(uint sound) { if (_filenums) { if (_lastVoiceFile != _filenums[sound]) { _mixer->stopHandle(_voiceHandle); char filename[16]; _lastVoiceFile = _filenums[sound]; sprintf(filename, "voices%d.dat", _filenums[sound]); if (!Common::File::exists(filename)) error("playVoice: Can't load voice file %s", filename); delete _voice; _voice = new WavSound(_mixer, filename, _offsets); } } if (!_voice) return; _mixer->stopHandle(_voiceHandle); if (_vm->getGameType() == GType_PP) { if (sound < 11) _voice->playSound(sound, sound + 1, Audio::Mixer::kMusicSoundType, &_voiceHandle, true, -1500); else _voice->playSound(sound, sound, Audio::Mixer::kMusicSoundType, &_voiceHandle, true); } else { _voice->playSound(sound, Audio::Mixer::kSpeechSoundType, &_voiceHandle, false); } } void Sound::playEffects(uint sound) { if (!_effects) return; if (_effectsPaused) return; if (_vm->getGameType() == GType_SIMON1) _mixer->stopHandle(_effectsHandle); _effects->playSound(sound, Audio::Mixer::kSFXSoundType, &_effectsHandle, false); } void Sound::playAmbient(uint sound) { if (!_effects) return; if (sound == _ambientPlaying) return; _ambientPlaying = sound; if (_ambientPaused) return; _mixer->stopHandle(_ambientHandle); _effects->playSound(sound, Audio::Mixer::kSFXSoundType, &_ambientHandle, true); } bool Sound::hasVoice() const { return _hasVoiceFile; } bool Sound::isSfxActive() const { return _mixer->isSoundHandleActive(_effectsHandle); } bool Sound::isVoiceActive() const { return _mixer->isSoundHandleActive(_voiceHandle); } void Sound::stopAllSfx() { _mixer->stopHandle(_ambientHandle); _mixer->stopHandle(_effectsHandle); _mixer->stopHandle(_sfx5Handle); _ambientPlaying = 0; } void Sound::stopSfx() { _mixer->stopHandle(_effectsHandle); } void Sound::stopVoice() { _mixer->stopHandle(_voiceHandle); } void Sound::stopAll() { _mixer->stopAll(); _ambientPlaying = 0; } void Sound::effectsPause(bool b) { _effectsPaused = b; _sfx5Paused = b; } void Sound::ambientPause(bool b) { _ambientPaused = b; if (_ambientPaused && _ambientPlaying) { _mixer->stopHandle(_ambientHandle); } else if (_ambientPlaying) { uint tmp = _ambientPlaying; _ambientPlaying = 0; playAmbient(tmp); } } // Personal Nightmare specific void Sound::handleSoundQueue() { if (isSfxActive()) return; _vm->_sampleEnd = 1; if (_soundQueuePtr) { playRawData(_soundQueuePtr, _soundQueueNum, _soundQueueSize, _soundQueueFreq); _vm->_sampleWait = 1; _vm->_sampleEnd = 0; _soundQueuePtr = 0; _soundQueueNum = 0; _soundQueueSize = 0; _soundQueueFreq = 0; } } void Sound::queueSound(byte *ptr, uint16 sound, uint32 size, uint16 freq) { if (_effectsPaused) return; // Only a single sound can be queued _soundQueuePtr = ptr; _soundQueueNum = sound; _soundQueueSize = size; _soundQueueFreq = freq; } // Elvira 1/2 and Waxworks specific void Sound::playRawData(byte *soundData, uint sound, uint size, uint freq) { if (_effectsPaused) return; byte *buffer = (byte *)malloc(size); memcpy(buffer, soundData, size); byte flags = 0; if (_vm->getPlatform() == Common::kPlatformDOS && _vm->getGameId() != GID_ELVIRA2) flags = Audio::FLAG_UNSIGNED; Audio::AudioStream *stream = Audio::makeRawStream(buffer, size, freq, flags); _mixer->playStream(Audio::Mixer::kSFXSoundType, &_effectsHandle, stream); } // Feeble Files specific void Sound::playAmbientData(byte *soundData, uint sound, uint pan, uint vol) { if (sound == _ambientPlaying) return; _ambientPlaying = sound; if (_ambientPaused) return; _mixer->stopHandle(_ambientHandle); playSoundData(&_ambientHandle, soundData, sound, pan, vol, true); } void Sound::playSfxData(byte *soundData, uint sound, uint pan, uint vol) { if (_effectsPaused) return; playSoundData(&_effectsHandle, soundData, sound, pan, vol, false); } void Sound::playSfx5Data(byte *soundData, uint sound, uint pan, uint vol) { if (_sfx5Paused) return; _mixer->stopHandle(_sfx5Handle); playSoundData(&_sfx5Handle, soundData, sound, pan, vol, true); } void Sound::playVoiceData(byte *soundData, uint sound) { _mixer->stopHandle(_voiceHandle); playSoundData(&_voiceHandle, soundData, sound); } void Sound::playSoundData(Audio::SoundHandle *handle, byte *soundData, uint sound, int pan, int vol, bool loop) { int size = READ_LE_UINT32(soundData + 4) + 8; Common::SeekableReadStream *stream = new Common::MemoryReadStream(soundData, size); Audio::RewindableAudioStream *sndStream = Audio::makeWAVStream(stream, DisposeAfterUse::YES); convertVolume(vol); convertPan(pan); _mixer->playStream(Audio::Mixer::kSFXSoundType, handle, Audio::makeLoopingAudioStream(sndStream, loop ? 0 : 1), -1, vol, pan); } void Sound::stopSfx5() { _mixer->stopHandle(_sfx5Handle); } void Sound::switchVoiceFile(const GameSpecificSettings *gss, uint disc) { if (_lastVoiceFile == disc) return; _mixer->stopHandle(_voiceHandle); delete _voice; _hasVoiceFile = false; _lastVoiceFile = disc; char filename[16]; sprintf(filename, "%s%u", gss->speech_filename, disc); _voice = makeSound(_mixer, filename); _hasVoiceFile = (_voice != 0); if (!_hasVoiceFile) error("switchVoiceFile: Can't load voice file %s", filename); } } // End of namespace AGOS