/* 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 3 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, see . * */ #include "scumm/actor.h" #include "scumm/file.h" #include "scumm/imuse/imuse.h" #include "scumm/resource.h" #include "scumm/scumm.h" #include "scumm/he/sound_he.h" #include "scumm/he/mixer_he.h" #include "scumm/he/intern_he.h" #include "scumm/util.h" #include "common/config-manager.h" #include "common/memstream.h" #include "common/timer.h" #include "common/util.h" #include "common/translation.h" #include "audio/audiostream.h" #include "audio/decoders/adpcm.h" #include "audio/mixer.h" #include "audio/decoders/raw.h" #include "audio/decoders/wave.h" #include "audio/decoders/mp3.h" #include "audio/decoders/vorbis.h" #include "audio/decoders/flac.h" namespace Scumm { SoundHE::SoundHE(ScummEngine *parent, Audio::Mixer *mixer, Common::Mutex *mutex) : Sound(parent, mixer, false), _vm((ScummEngine_v60he *)parent), _mutex(mutex), _overrideFreq(0), _heSpoolingMusicTable(nullptr), _heSpoolingMusicCount(0) { _createSndId = 0; _createSndLastAppend = 0; _createSndLastPos = 0; _baseSndSize = 0; memset(_heChannel, 0, sizeof(_heChannel)); memset(_soundCallbackScripts, 0, sizeof(_soundCallbackScripts)); bool useMilesSoundSystem = parent->_game.id == GID_MOONBASE || parent->_game.id == GID_BASEBALL2003 || parent->_game.id == GID_BASKETBALL || parent->_game.id == GID_FOOTBALL2002 || parent->_game.id == GID_SOCCER2004 || parent->_game.id == GID_PJGAMES; _heMixer = new HEMixer(_mixer, _vm, useMilesSoundSystem); } SoundHE::~SoundHE() { free(_heSpoolingMusicTable); if (_heSpoolingMusicFile.isOpen()) _heSpoolingMusicFile.close(); delete _heMixer; } void SoundHE::startSound(int sound, int heOffset, int heChannel, int heFlags, int heFreq, int hePan, int heVol) { if (_vm->VAR_LAST_SOUND != 0xFF) _vm->VAR(_vm->VAR_LAST_SOUND) = sound; if (heFlags & ScummEngine_v70he::HESndFlags::HE_SND_QUICK_START) { triggerDigitalSound(sound, heOffset, heChannel, heFlags); } else { Sound::startSound(sound, heOffset, heChannel, heFlags, heFreq, hePan, heVol); } } void SoundHE::addSoundToQueue(int sound, int heOffset, int heChannel, int heFlags, int heFreq, int hePan, int heVol) { int i = _soundQueuePos; if (_vm->_game.heversion < 95 && heChannel == -1) heChannel = 1; while (i--) { if (_soundQueue[i].sound == sound && !(heFlags & ScummEngine_v70he::HESndFlags::HE_SND_APPEND)) // Sound is already queued return; } Sound::addSoundToQueue(sound, heOffset, heChannel, heFlags, heFreq, hePan, heVol); } void SoundHE::modifySound(int sound, int offset, int frequencyShift, int pan, int volume, int flags) { int channel = hsFindSoundChannel(sound); if (channel >= 0 && _heChannel[channel].sound) { // The implementation for this is only available for the Miles mixer if (_heMixer->isMilesActive()) { _heMixer->milesModifySound(channel, offset, HESoundModifiers(frequencyShift, pan, volume), flags); } } } void SoundHE::processSoundQueues() { int snd, heOffset, heChannel, heFlags, heFreq, hePan, heVol; for (int i = 0; i <_soundQueuePos; i++) { snd = _soundQueue[i].sound; heOffset = _soundQueue[i].offset; heChannel = _soundQueue[i].channel; heFlags = _soundQueue[i].flags; heFreq = _soundQueue[i].freq; hePan = _soundQueue[i].pan; heVol = _soundQueue[i].vol; HESoundModifiers modifiers = _vm->_game.heversion < 99 ? HESoundModifiers() : HESoundModifiers(heFreq, hePan, heVol); if (snd) { if (((_vm->_game.heversion >= 80) && (_vm->_game.heversion < 95)) && (_soundQueue[i].flags & ScummEngine_v70he::HESndFlags::HE_SND_VOL)) { setSoundVolume(snd, _soundQueue[i].vol); } else { triggerSound(snd, heOffset, heChannel, heFlags, modifiers); } } } _soundQueuePos = 0; Sound::processSoundQueues(); } int SoundHE::isSoundRunning(int sound) const { // If our sound is a channel number, search for it // between the currently playing sounds first, then // search the sound queue... if (_vm->_game.heversion >= 70 || (_vm->_game.heversion < 70 && sound > 0)) { if (sound >= HSND_CHANNEL_0) { int channel = sound - HSND_CHANNEL_0; sound = _heChannel[channel].sound; if (sound) return sound; for (int i = 0; i < _soundQueuePos; i++) { if (_soundQueue[i].channel == channel) { return _soundQueue[i].sound; } } return 0; } // ...otherwise the sound parameter is a proper // sound number, so search the queue int i = _soundQueuePos; while (i) { if (sound == _soundQueue[--i].sound) return sound; } } // If it's not in the queue, go check if it is actually playing if (_vm->_game.heversion >= 70 || sound == HSND_TALKIE_SLOT) { if (sound >= HSND_CHANNEL_0) { sound = _heChannel[sound - HSND_CHANNEL_0].sound; } if (hsFindSoundChannel(sound) != -1) { return sound; } return 0; } else { if (sound == -2) { sound = _heChannel[0].sound; if (hsFindSoundChannel(sound) != -1) { return sound; } } else if (sound == -1) { sound = _currentMusic; if (_vm->_musicEngine && _vm->_musicEngine->getSoundStatus(sound)) { return sound; } if (is3DOSound(sound) && hsFindSoundChannel(sound) != -1) { return sound; } } else if (sound > 0) { if (hsFindSoundChannel(sound) != -1) { return sound; } if (_vm->_musicEngine && _vm->_musicEngine->getSoundStatus(sound)) { return sound; } } return 0; } } bool SoundHE::isSoundInUse(int sound) const { return isSoundRunning(sound) != 0; } void SoundHE::stopSound(int sound) { int channel = -1; if (_vm->_game.heversion < 70) { if (sound == -2) { sound = _heChannel[0].sound; } else if (sound == -1) { sound = _currentMusic; Sound::stopSound(sound); for (int i = 0; i < ARRAYSIZE(_heChannel); i++) { if (_heChannel[i].sound == sound) { _heChannel[i].clearChannel(); } } return; } } if (sound >= HSND_CHANNEL_0 && sound <= HSND_CHANNEL_7) { channel = sound - HSND_CHANNEL_0; if (_heChannel[channel].sound) { sound = _heChannel[channel].sound; stopDigitalSound(sound); } for (int i = 0; i < ARRAYSIZE(_soundQueue); i++) { if (_soundQueue[i].channel == channel) _soundQueue[i].sound = 0; } } else { if (_vm->_game.heversion >= 95 && sound == HSND_DYN_SOUND_CHAN) { for (int i = 0; i < ARRAYSIZE(_soundQueue); i++) { if (_soundQueue[i].channel == HSND_DYN_SOUND_CHAN) { _soundQueue[i].sound = 0; } } } else { if (hsFindSoundChannel(sound) != -1) { stopDigitalSound(sound); } for (int i = 0; i < ARRAYSIZE(_soundQueue); i++) { if (_soundQueue[i].sound == sound) _soundQueue[i].sound = 0; } } } if ((sound == HSND_TALKIE_SLOT) || (_vm->VAR_TALK_CHANNEL != 0xFF && channel == _vm->VAR(_vm->VAR_TALK_CHANNEL))) { _vm->_talkDelay = 0; } } void SoundHE::stopAllSounds() { if (_vm->_game.heversion >= 95) stopSound(HSND_DYN_SOUND_CHAN); for (int i = HSND_CHANNEL_0; i <= HSND_CHANNEL_7; i++) { stopSound(i); } // Empty the sound queue _soundQueuePos = 0; memset(_soundQueue, 0, sizeof(_soundQueue)); } int SoundHE::hsFindSoundChannel(int sound) const { if (sound >= HSND_CHANNEL_0) { int channel = sound - HSND_CHANNEL_0; if (channel < 0 || channel > HSND_MAX_CHANNELS - 1) { error("SoundHE::hsFindSoundChannel(): ERROR: Channel %d out of range (%d-%d)", channel, 0, HSND_MAX_CHANNELS - 1); } return channel; } for (int i = 0; i < ARRAYSIZE(_heChannel); ++i) { if (sound == _heChannel[i].sound) { return i; } } return -1; } void SoundHE::setupSound() { Sound::setupSound(); if (_vm->_game.heversion >= 70) { setupHEMusicFile(); } } void SoundHE::pauseSounds(bool pause) { // For MIDI audio if (_vm->_imuse) _vm->_imuse->pause(pause); _heMixer->pauseMixerSubSystem(pause); } void SoundHE::stopDigitalSound(int sound) { if (sound == HSND_TALKIE_SLOT) { _vm->_haveMsg = 3; _vm->_talkDelay = 0; } hsStopDigitalSound(sound); } int SoundHE::getNextDynamicChannel() { int firstChan, minAge, chosenChan; firstChan = _vm->VAR(_vm->VAR_START_DYN_SOUND_CHANNELS); if (firstChan < 0) { _vm->VAR(_vm->VAR_START_DYN_SOUND_CHANNELS) = HSND_MAX_CHANNELS; firstChan = HSND_MAX_CHANNELS; } if (firstChan < HSND_MAX_CHANNELS) { for (int i = firstChan; i < HSND_MAX_CHANNELS; i++) { if (!_heChannel[i].sound) return i; } minAge = INT_MAX; chosenChan = -1; for (int i = firstChan; i < HSND_MAX_CHANNELS; i++) { if (_heChannel[i].age <= minAge) { minAge = _heChannel[i].age; chosenChan = i; } } // Found it! if (chosenChan != -1) return chosenChan; // ...just get the first one, then :-) return firstChan; } return 1; } bool SoundHE::isSoundCodeUsed(int sound) { int chan; if ((chan = hsFindSoundChannel(sound)) != -1) { if (_heChannel[chan].hasSoundTokens) { return true; } } return false; } int SoundHE::getChannelPosition(int channel) { int soundPos; int frequency = _vm->_game.heversion >= 95 ? _heChannel[channel].frequency : HSND_DEFAULT_FREQUENCY; soundPos = (int)(((uint64)_vm->getHETimer(HSND_TIMER_SLOT + channel) * (uint64)frequency) / 1000); return soundPos; } int SoundHE::getSoundPosition(int sound) { int channel = hsFindSoundChannel(sound); if (channel != -1) { return getChannelPosition(channel); } else { return (_vm->_game.heversion > 72) ? 0 : channel; } } int SoundHE::getSoundVar(int sound, int var) { if (_vm->_game.heversion >= 90 && var == HSND_SNDVAR_TOKENS) { return isSoundCodeUsed(sound); } assertRange(0, var, HSND_MAX_SOUND_VARS - 1, "sound variable"); int chan = hsFindSoundChannel(sound); if (chan != -1) { debug(5, "SoundHE::getSoundVar(): sound %d var %d result %d", sound, var, _heChannel[chan].soundVars[var]); return _heChannel[chan].soundVars[var]; } else { return 0; } } void SoundHE::setSoundVar(int sound, int var, int val) { assertRange(0, var, HSND_MAX_SOUND_VARS - 1, "sound variable"); int chan = -1; for (int i = 0; i < ARRAYSIZE(_heChannel); i ++) { if (_heChannel[i].sound == sound) chan = i; } if (chan != -1) { debug(5, "SoundHE::setSoundVar(): sound %d var %d val %d", sound, var, val); _heChannel[chan].soundVars[var] = val; } } void SoundHE::setSoundVolume(int sound, int volume) { int channel; if (sound < HSND_CHANNEL_0) { if ((channel = hsFindSoundChannel(sound)) == -1) { return; } } else { channel = sound - HSND_CHANNEL_0; } _heMixer->changeChannelVolume(channel, volume, true); } void SoundHE::setOverrideFreq(int freq) { _overrideFreq = freq; } void SoundHE::setupHEMusicFile() { uint32 id, len; Common::Path musicFilename(_vm->generateFilename(-4)); // For engine restarts if (_heSpoolingMusicFile.isOpen()) _heSpoolingMusicFile.close(); if (_heSpoolingMusicFile.open(musicFilename)) { id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32BE(); if (id == MKTAG('S', 'O', 'N', 'G')) { // Older versions had a much simpler file structure if (_vm->_game.heversion < 80) { // Skip header wrapping file _heSpoolingMusicFile.seek(16, SEEK_SET); _heSpoolingMusicCount = _heSpoolingMusicFile.readUint32LE(); } else { // HE 80 and above id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32BE(); if (id == MKTAG('S', 'G', 'H', 'D')) { _heSpoolingMusicCount = _heSpoolingMusicFile.readUint32LE(); _heSpoolingMusicFile.seek(len - 8 - 4, SEEK_CUR); } else { _heSpoolingMusicFile.close(); debug(5, "setupHEMusicFile(): Invalid spooling file '%s', couldn't find SGHD tag, found %s", musicFilename.toString().c_str(), tag2str(id)); return; } } debug(5, "setupHEMusicFile(): music files count = %d", _heSpoolingMusicCount); _heSpoolingMusicTable = (HESpoolingMusicItem *)malloc(_heSpoolingMusicCount * sizeof(HESpoolingMusicItem)); if (_heSpoolingMusicTable != nullptr) { for (int i = 0; i < _heSpoolingMusicCount; i++) { // For later versions we check that we are actually reading a SGEN section... if (_vm->_game.heversion >= 80) { id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32BE(); if (id != MKTAG('S', 'G', 'E', 'N')) { _heSpoolingMusicFile.close(); debug(5, "setupHEMusicFile(): Invalid spooling file '%s', couldn't find SGEN tag, found %s", musicFilename.toString().c_str(), tag2str(id)); return; } } _heSpoolingMusicTable[i].song = _heSpoolingMusicFile.readSint32LE(); _heSpoolingMusicTable[i].offset = _heSpoolingMusicFile.readSint32LE(); _heSpoolingMusicTable[i].size = _heSpoolingMusicFile.readSint32LE(); int amountToRead = _vm->_game.heversion >= 80 ? 9 : 13; int readAmount = 0; for (readAmount = 0; readAmount < amountToRead; readAmount++) { _heSpoolingMusicTable[i].filename[readAmount] = _heSpoolingMusicFile.readByte(); // Early string termination if (_heSpoolingMusicTable[i].filename[readAmount] == '\0') break; } // Not all filenames have a string termination char, let's include it; // we don't need to do that if we had an early string termination if (readAmount == amountToRead) _heSpoolingMusicTable[i].filename[amountToRead] = '\0'; debug(5, "setupHEMusicFile(): read music file '%s' song %d, offset %d, size %d", _heSpoolingMusicTable[i].filename, _heSpoolingMusicTable[i].song, _heSpoolingMusicTable[i].offset, _heSpoolingMusicTable[i].size); } _heMixer->setSpoolingSongsTable(_heSpoolingMusicTable, _heSpoolingMusicCount); } else { debug(5, "setupHEMusicFile(): Can't allocate table for spooling music file '%s'", musicFilename.toString().c_str()); } } else { debug(5, "setupHEMusicFile(): Invalid file '%s', couldn't find SONG tag, found %s", musicFilename.toString().c_str(), tag2str(id)); } } else { debug(5, "setupHEMusicFile(): Can't open spooling music file '%s'", musicFilename.toString().c_str()); } } bool SoundHE::getHEMusicDetails(int id, int &musicOffs, int &musicSize) { for (int i = 0; i < _heSpoolingMusicCount; i++) { if (_heSpoolingMusicTable[i].song == id) { musicOffs = _heSpoolingMusicTable[i].offset; musicSize = _heSpoolingMusicTable[i].size; return true; } } return false; } void SoundHE::handleSoundFrame() { if (_vm->_game.heversion < 80) return; _soundsDebugFrameCounter++; if (_stopActorTalkingFlag) { _vm->stopTalk(); _vm->_haveMsg = 3; _stopActorTalkingFlag = false; } if (_vm->_game.heversion >= 95) unqueueSoundCallbackScripts(); runSoundCode(); checkSoundTimeouts(); } void SoundHE::feedMixer() { _heMixer->feedMixer(); } void SoundHE::unqueueSoundCallbackScripts() { if (_inUnqueueCallbackScripts) return; _inUnqueueCallbackScripts++; for (int i = 0; i < _soundCallbacksQueueSize; i++) { if (_soundCallbackScripts[i].sound) { int args[NUM_SCRIPT_LOCAL]; memset(args, 0, sizeof(args)); args[0] = _soundCallbackScripts[i].sound; args[1] = _soundCallbackScripts[i].channel; args[2] = 0; args[3] = 0; debug(5, "SoundHE::unqueueSoundCallbackScripts(): dequeued callback for sound %d in channel %d", _soundCallbackScripts[i].sound, _soundCallbackScripts[i].channel); _vm->runScript(_vm->VAR(_vm->VAR_SOUND_CALLBACK_SCRIPT), 0, 0, args); } _soundCallbackScripts[i].sound = 0; _soundCallbackScripts[i].channel = 0; _soundCallbackScripts[i].whatFrame = 0; } _soundCallbacksQueueSize = 0; _inUnqueueCallbackScripts--; } void SoundHE::checkSoundTimeouts() { for (int chan = 0; chan < ARRAYSIZE(_heChannel); chan++) { if (_heChannel[chan].sound == 0 || _heChannel[chan].timeout == 0) continue; if (_vm->getHETimer(chan + HSND_TIMER_SLOT) > _heChannel[chan].timeout) { digitalSoundCallback(HSND_SOUND_TIMEOUT, chan); } } } void SoundHE::digitalSoundCallback(int message, int channel, bool earlyCallback) { // The action done for each sound is always the same; // it's useful to keep track of the message for debugging // purposes though... switch (message) { case HSND_SOUND_TIMEOUT: debug(5, "SoundHE::digitalSoundCallback(): TIMEOUT, channel %d", channel); break; case HSND_SOUND_ENDED: if (earlyCallback) debug(5, "SoundHE::digitalSoundCallback(): ENDED with EARLY CALLBACK, channel %d", channel); else debug(5, "SoundHE::digitalSoundCallback(): ENDED, channel %d", channel); break; case HSND_SOUND_STOPPED: debug(5, "SoundHE::digitalSoundCallback(): STOPPED, channel %d", channel); break; default: warning("SoundHE::digitalSoundCallback(): WARNING: invalid message (%d), channel = %d", message, channel); break; } _inSoundCallbackFlag = true; int sound = _heChannel[channel].sound; switch (message) { case HSND_SOUND_TIMEOUT: case HSND_SOUND_ENDED: case HSND_SOUND_STOPPED: if (sound == HSND_TALKIE_SLOT) { // In the original this was used to prevent an edge case bug // which caused recursive resource accesses/allocations. // I think there's no harm in doing that ourselves too... if (_vm->_insideCreateResource == 0) { _vm->stopTalk(); } else { _stopActorTalkingFlag = true; } } _heChannel[channel].clearChannel(); if (_vm->_game.heversion >= 80) queueSoundCallbackScript(sound, channel, message); break; } _inSoundCallbackFlag = false; } void SoundHE::queueSoundCallbackScript(int sound, int channel, int message) { // Avoid queueing up a sound callback if the mixer is not available. if (!_mixer->isReady()) return; debug(5, "SoundHE::queueSoundCallbackScript(): callback for channel %d, sound %d, attempting queueing...", channel, sound); // Check if we are about to duplicate this event... for (int i = 0; i < _soundCallbacksQueueSize; i++) { if ((sound == _soundCallbackScripts[i].sound) && (channel == _soundCallbackScripts[i].channel)) { _soundAlreadyInQueueCount++; debug(5, "SoundHE::queueSoundCallbackScript(): callback for channel %d, sound %d, already in list.", channel, sound); return; } } // Finally queue up sound... _soundCallbackScripts[_soundCallbacksQueueSize].sound = sound; _soundCallbackScripts[_soundCallbacksQueueSize].channel = channel; _soundCallbackScripts[_soundCallbacksQueueSize].whatFrame = _soundsDebugFrameCounter; _soundCallbacksQueueSize++; if (_soundCallbacksQueueSize >= HSND_MAX_CALLBACK_SCRIPTS) { error("SoundHE::queueSoundCallbackScript(): ERROR: Got too many sound callbacks (got %d, max %d), message %d", _soundCallbacksQueueSize, HSND_MAX_CALLBACK_SCRIPTS, message); } } void SoundHE::runSoundCode() { byte *soundPtr; int chan, soundPos, len, freq; for (chan = 0; chan < ARRAYSIZE(_heChannel); chan++) { if (_heChannel[chan].sound == 0) { continue; } if (_heChannel[chan].codeOffset == -1) { continue; } soundPos = getChannelPosition(chan) + _vm->VAR(_vm->VAR_SOUND_TOKEN_OFFSET); soundPos = MAX(0, soundPos); if (_heChannel[chan].codeBuffer == nullptr) { soundPtr = _vm->getResourceAddress(rtSound, _heChannel[chan].sound); } else { soundPtr = _heChannel[chan].codeBuffer; } assert(soundPtr); soundPtr += _heChannel[chan].codeOffset; len = READ_LE_UINT16(soundPtr); freq = READ_LE_UINT32(soundPtr + sizeof(uint16)); while (soundPos > freq) { debug(5, "SoundHE::runSoundCode(): Channel %d Timer %d Time %d", chan, soundPos, freq); processSoundOpcodes(_heChannel[chan].sound, soundPtr + sizeof(uint16) + sizeof(uint32), _heChannel[chan].soundVars); _heChannel[chan].codeOffset += len; // The original runs the following section again on purpose if (_heChannel[chan].codeBuffer == nullptr) { soundPtr = _vm->getResourceAddress(rtSound, _heChannel[chan].sound); } else { soundPtr = _heChannel[chan].codeBuffer; } assert(soundPtr); soundPtr += _heChannel[chan].codeOffset; len = READ_LE_UINT16(soundPtr); freq = READ_LE_UINT32(soundPtr + sizeof(uint16)); if (len == 0) { _heChannel[chan].codeOffset = -1; break; } } } } void SoundHE::processSoundOpcodes(int sound, byte *codePtr, int *soundVars) { int arg, opcode, var, val; while (READ_LE_UINT16(codePtr) != 0) { codePtr += 2; opcode = READ_LE_UINT16(codePtr); codePtr += 2; opcode = (opcode & ~HSND_SBNG_MAGIC_MASK) >> 4; arg = opcode & HSND_SBNG_VARORVAL; opcode &= ~HSND_SBNG_VARORVAL; debug(5, "SoundHE::processSoundOpcodes(): sound %d opcode %d", sound, opcode); switch (opcode) { case HSND_SBNG_END: // Continue - 0 break; case HSND_SBNG_FACE: // Set talk state - 16 val = READ_LE_UINT16(codePtr); codePtr += 2; setSoundVar(sound, 19, val); break; case HSND_SBNG_SET_SET: // Set var - 32 var = READ_LE_UINT16(codePtr); codePtr += 2; val = READ_LE_UINT16(codePtr); codePtr += 2; if (arg == 2) { val = getSoundVar(sound, val); } setSoundVar(sound, var, val); break; case HSND_SBNG_SET_ADD: // Add - 48 var = READ_LE_UINT16(codePtr); codePtr += 2; val = READ_LE_UINT16(codePtr); codePtr += 2; if (arg == 2) { val = getSoundVar(sound, val); } val = getSoundVar(sound, var) + val; setSoundVar(sound, var, val); break; case HSND_SBNG_SET_SUB: // Subtract - 56 var = READ_LE_UINT16(codePtr); codePtr += 2; val = READ_LE_UINT16(codePtr); codePtr += 2; if (arg == 2) { val = getSoundVar(sound, val); } val = getSoundVar(sound, var) - val; setSoundVar(sound, var, val); break; case HSND_SBNG_SET_MUL: // Multiple - 64 var = READ_LE_UINT16(codePtr); codePtr += 2; val = READ_LE_UINT16(codePtr); codePtr += 2; if (arg == 2) { val = getSoundVar(sound, val); } val = getSoundVar(sound, var) * val; setSoundVar(sound, var, val); break; case HSND_SBNG_SET_DIV: // Divide - 80 var = READ_LE_UINT16(codePtr); codePtr += 2; val = READ_LE_UINT16(codePtr); codePtr += 2; if (arg == 2) { val = getSoundVar(sound, val); } if (!val) { val = 1; // Safeguard for division by zero warning("SoundHE::processSoundOpcodes(): Incorrect value 0 for processSoundOpcodes() kludge DIV"); } val = getSoundVar(sound, var) / val; setSoundVar(sound, var, val); break; case HSND_SBNG_SET_INC: // Increment - 96 var = READ_LE_UINT16(codePtr); codePtr += 2; val = getSoundVar(sound, var) + 1; setSoundVar(sound, var, val); break; case HSND_SBNG_SET_DEC: // Decrement - 104 var = READ_LE_UINT16(codePtr); codePtr += 2; val = getSoundVar(sound, var) - 1; setSoundVar(sound, var, val); break; default: error("SoundHE::processSoundOpcodes(): Illegal sound %d opcode %d", sound, opcode); } } } void SoundHE::triggerSound(int soundId, int heOffset, int heChannel, int heFlags, HESoundModifiers modifiers) { _dynamicSoundAgeCounter++; if (_vm->_game.heversion >= 95) { if (heChannel == HSND_DYN_SOUND_CHAN) { heChannel = getNextDynamicChannel(); } } if (soundId >= _vm->_numSounds) { triggerSpoolingSound(soundId, heOffset, heChannel, heFlags, modifiers); return; } byte *soundAddr = (byte *)_vm->getResourceAddress(rtSound, soundId); if ((READ_BE_UINT32(soundAddr) == MKTAG('D', 'I', 'G', 'I')) || (READ_BE_UINT32(soundAddr) == MKTAG('T', 'A', 'L', 'K')) || (READ_BE_UINT32(soundAddr) == MKTAG('M', 'R', 'A', 'W'))) { triggerDigitalSound(soundId, heOffset, heChannel, heFlags); } else if (READ_BE_UINT32(soundAddr) == MKTAG('M', 'I', 'D', 'I')) { triggerMidiSound(soundId, heOffset); } else if (READ_BE_UINT32(soundAddr) == MKTAG('W', 'S', 'O', 'U')) { triggerRIFFSound(soundId, heOffset, heChannel, heFlags, modifiers); } else if (READ_BE_UINT32(soundAddr) == MKTAG('X', 'S', 'O', 'U')) { #ifdef ENABLE_HE triggerXSOUSound(soundId, heOffset, heChannel, heFlags); #endif } else { error("SoundHE::triggerSound(): Illegal sound %d type %s", soundId, tag2str(READ_BE_UINT32(soundAddr))); } } void SoundHE::triggerSpoolingSound(int song, int offset, int channel, int flags, HESoundModifiers modifiers) { if (_heSpoolingMusicCount != 0) { for (int i = 0; i < _heSpoolingMusicCount; i++) { if (_heSpoolingMusicTable[i].song == song) { debug(5, "SoundHE::triggerSpoolingSound(): Starting spooling sound %d with offset %d, on channel %d with flags %d", song, offset, channel, flags); Common::Path filename(_vm->generateFilename(-4)); int fileOffset = 0; int songsize = 0; uint32 id, len; hsStopDigitalSound(_heChannel[channel].sound); if (_heSpoolingMusicTable[i].offset != 0) { fileOffset = _heSpoolingMusicTable[i].offset; if (_vm->_game.heversion < 80) fileOffset += HSND_RES_OFFSET_SOUND_DATA; _heSpoolingMusicFile.seek(fileOffset, SEEK_SET); songsize = _heSpoolingMusicTable[i].size; if (_vm->_game.heversion < 80) songsize -= HSND_RES_OFFSET_SOUND_DATA; } else { _heSpoolingMusicFile.close(); filename = _heSpoolingMusicTable[i].filename; if (!_heSpoolingMusicFile.open(filename)) { debug("SoundHE::triggerSpoolingSound(): Can't open music file '%s'", filename.toString().c_str()); if (_vm->_game.heversion < 95) { _vm->VAR(_vm->VAR_ERROR_FLAG) = -1; } else { _vm->VAR(_vm->VAR_OPERATION_FAILURE) = -1; } return; } else if (_vm->_game.heversion < 80) { _heSpoolingMusicFile.seek(HSND_RES_OFFSET_SOUND_DATA, SEEK_SET); songsize = _heSpoolingMusicFile.size() - HSND_RES_OFFSET_SOUND_DATA; } } // Old spooled music format (raw audio) if (_vm->_game.heversion < 80) { _heMixer->startSpoolingChannel( channel, song, _heSpoolingMusicFile, songsize, HSND_DEFAULT_FREQUENCY, HSND_MAX_VOLUME, channel, CHANNEL_ACTIVE); _heChannel[channel].sound = song; _heChannel[channel].priority = 255; _vm->setHETimer(channel + HSND_TIMER_SLOT); _vm->VAR(_vm->VAR_ERROR_FLAG) = 0; return; } _heChannel[channel].codeOffset = -1; _heChannel[channel].codeBuffer = nullptr; _heChannel[channel].hasSoundTokens = false; id = _heSpoolingMusicFile.readUint32BE(); if (id == MKTAG('R', 'I', 'F', 'F')) { len = _heSpoolingMusicFile.readUint32LE(); id = _heSpoolingMusicFile.readUint32BE(); if (id != MKTAG('W', 'A', 'V', 'E')) error("SoundHE::triggerSpoolingSound(): Illegal music .wav file %d", song); while (true) { id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32LE(); if (id == MKTAG('d', 'a', 't', 'a')) { id = MKTAG('S', 'D', 'A', 'T'); // Convert it to a SDAT tag len += 8; // Compensate the length of the header break; } else if (id == MKTAG('S', 'B', 'N', 'G')) { if (len > ARRAYSIZE(_heSpoolingCodeBuffer)) { error("SoundHE::triggerSpoolingSound(): Spooling sound %d code too large", song); } _heSpoolingMusicFile.read(_heSpoolingCodeBuffer, len); _heChannel[channel].codeOffset = 0; _heChannel[channel].codeBuffer = _heSpoolingCodeBuffer; _heChannel[channel].hasSoundTokens = true; } else if (id == MKTAG('f', 'm', 't', ' ')) { PCMWaveFormat pcm; if (len < sizeof(pcm)) error("SoundHE::triggerSpoolingSound(): Illegal .wav format length in song %d", song); pcm.wFormatTag = _heSpoolingMusicFile.readUint16LE(); pcm.wChannels = _heSpoolingMusicFile.readUint16LE(); pcm.dwSamplesPerSec = _heSpoolingMusicFile.readUint32LE(); pcm.dwAvgBytesPerSec = _heSpoolingMusicFile.readUint32LE(); pcm.wBlockAlign = _heSpoolingMusicFile.readUint16LE(); pcm.wBitsPerSample = _heSpoolingMusicFile.readUint16LE(); if (_heMixer->isMilesActive()) { if (pcm.wFormatTag != WAVE_FORMAT_PCM && pcm.wFormatTag != WAVE_FORMAT_IMA_ADPCM) { error("SoundHE::triggerSpoolingSound(): Illegal .wav format for Miles mixer, song %d - %d", song, pcm.wFormatTag); } } else { if (pcm.wFormatTag != WAVE_FORMAT_PCM || pcm.wChannels != 1 || pcm.dwSamplesPerSec != HSND_DEFAULT_FREQUENCY || pcm.wBitsPerSample != 8) { error("SoundHE::triggerSpoolingSound(): Illegal .wav format for software mixer, song %d - %d, %d, %d, %d", song, pcm.wFormatTag, pcm.wChannels, pcm.dwSamplesPerSec, pcm.wBitsPerSample); } } // Skip over the rest of this block. _heSpoolingMusicFile.seek(len - sizeof(pcm), SEEK_CUR); } else { _heSpoolingMusicFile.seek(len, SEEK_CUR); } } } else if (id == MKTAG('D', 'I', 'G', 'I')) { len = _heSpoolingMusicFile.readUint32BE(); id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32BE(); if (id == MKTAG('H', 'S', 'H', 'D')) { _heSpoolingMusicFile.seek(len - 8, SEEK_CUR); } else { error("SoundHE::triggerSpoolingSound(): Illegal spooling sound %d, id %s", song, tag2str(id)); } id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32BE(); if (id == MKTAG('S', 'B', 'N', 'G')) { if (len > sizeof(_heSpoolingCodeBuffer)) { error("SoundHE::triggerSpoolingSound(): Spooling sound %d code too large", song); } _heSpoolingMusicFile.read(_heSpoolingCodeBuffer, len - 8); _heChannel[channel].codeOffset = 0; _heChannel[channel].codeBuffer = _heSpoolingCodeBuffer; _heChannel[channel].hasSoundTokens = true; id = _heSpoolingMusicFile.readUint32BE(); len = _heSpoolingMusicFile.readUint32BE(); } } else { error("SoundHE::triggerSpoolingSound(): Illegal spooling sound %d, id %s", song, tag2str(id)); } if (id == MKTAG('S', 'D', 'A', 'T')) { songsize = len - 8; } else { error("SoundHE::triggerSpoolingSound(): Illegal spooling sound %d, id %s", song, tag2str(id)); } if (_heMixer->isMilesActive()) { if (offset) debug("SoundHE::triggerSpoolingSound(): Starting offsets into music files not supported with Miles currently"); _heMixer->milesStartSpoolingChannel(channel, filename.toString('/').c_str(), fileOffset, flags, modifiers); } else { // Start the music track at a specified offset if (offset) { _heSpoolingMusicFile.seek(offset, SEEK_CUR); if (offset >= songsize || offset < 0) { error("SoundHE::triggerSpoolingSound(): Invalid offset %d for sound %d (song size %d)", offset, song, songsize); } songsize -= offset; } _heMixer->startSpoolingChannel(channel, song, _heSpoolingMusicFile, songsize, HSND_DEFAULT_FREQUENCY, HSND_MAX_VOLUME, channel, CHANNEL_ACTIVE); } _vm->setHETimer(channel + HSND_TIMER_SLOT); _heChannel[channel].age = _dynamicSoundAgeCounter; _heChannel[channel].sound = song; _heChannel[channel].priority = 255; _heChannel[channel].frequency = HSND_DEFAULT_FREQUENCY; if (_vm->_game.heversion < 95) { _vm->VAR(_vm->VAR_ERROR_FLAG) = 0; } else { _vm->VAR(_vm->VAR_OPERATION_FAILURE) = 0; } return; } } } // Error situation if (_vm->_game.heversion < 95) { _vm->VAR(_vm->VAR_ERROR_FLAG) = -1; } else { _vm->VAR(_vm->VAR_OPERATION_FAILURE) = -1; } } void SoundHE::triggerRIFFSound(int soundId, int heOffset, int heChannel, int heFlags, HESoundModifiers modifiers) { PCMWaveFormat pFmt; uint8 *soundDataPtr = nullptr; int sampleCount = 0; int soundPriority = 128; int soundCodeOffset = -1; bool parsedFmt = false; // For uninit var warning... pFmt.wFormatTag = 0; pFmt.wChannels = 0; pFmt.dwSamplesPerSec = 0; pFmt.dwAvgBytesPerSec = 0; pFmt.wBlockAlign = 0; pFmt.wBitsPerSample = 0; // Let's begin by fetching the sound address... uint8 *wsouPtr = (byte *)_vm->getResourceAddress(rtSound, soundId); // We only accept the WSOU format compliant files, // which are WAV files with a WSOU header wrapped around. // Still, let's not use an assertion like the original does, // and let's bail out gracefully instead.... if (READ_BE_UINT32(wsouPtr) != MKTAG('W', 'S', 'O', 'U')) { debug("SoundHE::triggerRIFFSound(): Couldn't find WSOU tag for sound %d, bailing out...", soundId); return; } // Skip over the WSOU header and hope to find a RIFF header... uint8 *soundPtr = wsouPtr + 8; if (READ_BE_UINT32(soundPtr) != MKTAG('R', 'I', 'F', 'F')) { debug("SoundHE::triggerRIFFSound(): Couldn't find RIFF tag for sound %d, bailing out...", soundId); return; } // Since all sub-blocks must be padded to be even, we want the RIFF block length to be even... int riffLength = READ_LE_UINT32(soundPtr + 4); if ((riffLength & 1) != 0) { debug("SoundHE::triggerRIFFSound(): RIFF block length not even (%d) for sound %d, bailing out...", riffLength, soundId); return; } uint8 *wavePtr = soundPtr + 8; if (READ_BE_UINT32(wavePtr) != MKTAG('W', 'A', 'V', 'E')) { debug("SoundHE::triggerRIFFSound(): Couldn't find WAVE tag for sound %d, bailing out...", soundId); return; } // Now that we've made all the safety checks needed, fetch the sound data... wavePtr += 4; riffLength -= 4; // Parse the various blocks... while (riffLength > 0) { int chunkId = READ_BE_UINT32(wavePtr); int chunkLength = READ_LE_UINT32(wavePtr + 4); wavePtr += 8; riffLength -= 8; if (chunkLength < 0) { debug("SoundHE::triggerRIFFSound(): Illegal chunk length - %d bytes, bailing out...", chunkLength); return; } if (chunkLength > riffLength) { debug("SoundHE::triggerRIFFSound(): Chunk extends beyond file end - %d versus %d, bailing out...", chunkLength, riffLength); return; } switch (chunkId) { case MKTAG('f', 'm', 't', ' '): { uint8 *tempPtr = wavePtr; pFmt.wFormatTag = READ_LE_UINT16(tempPtr); tempPtr += 2; pFmt.wChannels = READ_LE_UINT16(tempPtr); tempPtr += 2; pFmt.dwSamplesPerSec = READ_LE_UINT32(tempPtr); tempPtr += 4; pFmt.dwAvgBytesPerSec = READ_LE_UINT32(tempPtr); tempPtr += 4; pFmt.wBlockAlign = READ_LE_UINT16(tempPtr); tempPtr += 2; pFmt.wBitsPerSample = READ_LE_UINT16(tempPtr); tempPtr += 2; if (pFmt.wFormatTag != WAVE_FORMAT_PCM && pFmt.wFormatTag != WAVE_FORMAT_IMA_ADPCM) { debug("SoundHE::triggerRIFFSound(): Unsupported .wav sound type %d - only PCM and IMA ADPCM are supported.", pFmt.wFormatTag); return; } parsedFmt = true; break; } case MKTAG('d', 'a', 't', 'a'): assert(parsedFmt); soundDataPtr = wavePtr; sampleCount = (chunkLength * 8) / (pFmt.wChannels * pFmt.wBitsPerSample); break; case MKTAG('X', 'S', 'H', '2'): { // Check for the optional sound flag block int optionalBlockFlags = READ_LE_UINT32(wavePtr); if (optionalBlockFlags & XSH2_FLAG_HAS_PRIORITY) soundPriority = READ_LE_UINT32(wavePtr + 4); break; } case MKTAG('S', 'B', 'N', 'G'): soundCodeOffset = wavePtr - wsouPtr; break; default: break; } // Round up to the next multiple of two chunkLength = (chunkLength + 1) & ~1; wavePtr += chunkLength; riffLength -= chunkLength; } if (riffLength != 0 || !parsedFmt || soundDataPtr == nullptr) return; int sampleFrequency = pFmt.dwSamplesPerSec; int bitsPerSample = pFmt.wBitsPerSample; int sampleChannelCount = pFmt.wChannels; int soundDataOffset = soundDataPtr - soundPtr; // Make sure that the sound has a high enough priority to play if (_heChannel[heChannel].sound && soundId != HSND_TALKIE_SLOT && _heChannel[heChannel].sound != HSND_TALKIE_SLOT) { if (soundPriority < _heChannel[heChannel].priority) return; } // Finally start the sound hsStartDigitalSound(soundId, heOffset, soundPtr, soundDataOffset, rtSound, soundId, sampleCount, sampleFrequency, heChannel, soundPriority, soundCodeOffset, heFlags, bitsPerSample, sampleChannelCount, modifiers); } #ifdef ENABLE_HE void SoundHE::triggerXSOUSound(int heSound, int heOffset, int heChannel, int heFlags) { int sampleFrequency, bitsPerSample, sampleChannelCount, soundPriority; int soundCodeOffset, soundDataOffset, sampleCount; byte *soundCodeBlockPtr, *soundHeaderBlock, *soundDataPtr, *soundPtr; byte *optionalHeaderBlock; int32 optionalBlockFlags; soundPtr = (byte *)_vm->getResourceAddress(rtSound, heSound); // Fetch the sound data format... soundHeaderBlock = (byte *)((ScummEngine_v71he *)_vm)->findWrappedBlock(MKTAG('X', 'S', 'H', 'D'), soundPtr, 0, true); soundHeaderBlock += 8; sampleCount = READ_LE_UINT32(soundHeaderBlock + 0); sampleFrequency = READ_LE_UINT32(soundHeaderBlock + 4); bitsPerSample = READ_LE_UINT32(soundHeaderBlock + 8); sampleChannelCount = READ_LE_UINT32(soundHeaderBlock + 12); soundDataPtr = (byte *)((ScummEngine_v71he *)_vm)->findWrappedBlock(MKTAG('X', 'D', 'A', 'T'), soundPtr, 0, true); soundDataOffset = ((soundDataPtr - soundPtr) + 8); // Check for the optional sound flag block containing the priority... soundPriority = 128; optionalHeaderBlock = (byte *)((ScummEngine_v71he *)_vm)->findWrappedBlock(MKTAG('X', 'S', 'H', '2'), soundPtr, 0, false); if (optionalHeaderBlock) { optionalHeaderBlock += 8; optionalBlockFlags = READ_LE_UINT32(optionalHeaderBlock); optionalHeaderBlock += 4; if (optionalBlockFlags & XSH2_FLAG_HAS_PRIORITY) { soundPriority = READ_LE_UINT32(optionalHeaderBlock); optionalHeaderBlock += 4; } } // Check for the optional SBNG block... soundCodeBlockPtr = (byte *)((ScummEngine_v71he *)_vm)->findWrappedBlock(MKTAG('S', 'B', 'N', 'G'), soundPtr, 0, false); if (soundCodeBlockPtr == nullptr) { soundCodeOffset = -1; } else { soundCodeOffset = (soundCodeBlockPtr - soundPtr) + 8; } // Make sure that the sound has a high enough priority to play if (_heChannel[heChannel].sound && heSound != HSND_TALKIE_SLOT && _heChannel[heChannel].sound != HSND_TALKIE_SLOT) { if (soundPriority < _heChannel[heChannel].priority) return; } // Finally start the sound hsStartDigitalSound( heSound, heOffset, soundPtr, soundDataOffset, rtSound, heSound, sampleCount, sampleFrequency, heChannel, soundPriority, soundCodeOffset, heFlags, bitsPerSample, sampleChannelCount, HESoundModifiers()); } #endif void SoundHE::hsStartDigitalSound(int sound, int offset, byte *addr, int soundData, int globType, int globNum, int sampleCount, int frequency, int channel, int priority, int soundCode, int flags, int bitsPerSample, int soundChannelCount, HESoundModifiers modifiers) { int earlyCallbackByteCount = 0; int index; uint32 hflags; debug(5, "SoundHE::hsStartDigitalSound(): Starting sound %d with offset %d, on channel %d with flags %d", sound, offset, channel, flags); hflags = (flags & ScummEngine_v70he::HESndFlags::HE_SND_LOOP) ? CHANNEL_LOOPING : 0; hflags |= (flags & ScummEngine_v70he::HESndFlags::HE_SND_SOFT_SOUND) ? CHANNEL_SOFT_REMIX : 0; // If there's an early channel callback value, fetch it... if (_vm->_game.heversion >= 95) { index = _vm->VAR_EARLY_CHAN_1_CALLBACK + channel; if ((index >= _vm->VAR_EARLY_CHAN_1_CALLBACK) && (index <= _vm->VAR_EARLY_CHAN_3_CALLBACK)) { earlyCallbackByteCount = _vm->VAR(index); } else { earlyCallbackByteCount = 0; } } if (_vm->_game.heversion >= 95) { _heMixer->startChannelNew( channel, globType, globNum, soundData, offset, (sampleCount - offset), frequency, bitsPerSample, soundChannelCount, modifiers, channel, CHANNEL_ACTIVE | CHANNEL_CALLBACK_EARLY | hflags, earlyCallbackByteCount); } else if (_vm->_game.heversion >= 80) { // HE 80 doesn't make checks on the range for the early callback channel _heMixer->startChannel( channel, globType, globNum, soundData + offset, (sampleCount - offset), HSND_DEFAULT_FREQUENCY, HSND_MAX_VOLUME, channel, CHANNEL_ACTIVE | CHANNEL_CALLBACK_EARLY | hflags, _vm->VAR(_vm->VAR_EARLY_CHAN_1_CALLBACK + channel)); } else { // Sub HE 80 codepath, simpler than the newer versions // If we're trying to start a speech sound, we have to signal an early callback if (_vm->_game.heversion >= 70 && channel == _vm->VAR(_vm->VAR_TALK_CHANNEL)) { earlyCallbackByteCount = ((_vm->VAR(_vm->VAR_TIMER_NEXT) * frequency) / 60); earlyCallbackByteCount *= _vm->VAR(_vm->VAR_EARLY_TALKIE_CALLBACK); _heMixer->startChannel( channel, globType, globNum, HSND_RES_OFFSET_SOUND_DATA + offset, sampleCount - offset, frequency, HSND_MAX_VOLUME, channel, CHANNEL_ACTIVE | CHANNEL_CALLBACK_EARLY | hflags, earlyCallbackByteCount); } else { _heMixer->startChannel( channel, globType, globNum, HSND_RES_OFFSET_SOUND_DATA + offset, sampleCount - offset, frequency, HSND_MAX_VOLUME, channel, CHANNEL_ACTIVE | hflags); } _heChannel[channel].sound = sound; _heChannel[channel].priority = priority; _vm->setHETimer(channel + HSND_TIMER_SLOT); return; } _vm->setHETimer(channel + HSND_TIMER_SLOT); _heChannel[channel].age = _dynamicSoundAgeCounter; _heChannel[channel].sound = sound; _heChannel[channel].priority = priority; _heChannel[channel].codeOffset = soundCode; _heChannel[channel].hasSoundTokens = (soundCode != -1); _heChannel[channel].frequency = frequency; if (flags & ScummEngine_v70he::HESndFlags::HE_SND_LOOP) { _heChannel[channel].timeout = 0; } else { if (_vm->_game.heversion >= 95) { uint64 timeOut; timeOut = (uint64)(sampleCount - offset) * (uint64)1000; _heChannel[channel].timeout = (int)(timeOut / frequency) + 2000; } else { uint32 timeOut; timeOut = (uint32)(sampleCount - offset) * (uint32)1000; _heChannel[channel].timeout = (int)(timeOut / frequency) + 2000; } } int overrideDuration; if (_heMixer->audioOverrideExists(globNum, true, &overrideDuration)) { _heChannel[channel].timeout = overrideDuration; _heChannel[channel].codeOffset = -1; _heChannel[channel].hasSoundTokens = false; } for (int i = 0; i < HSND_MAX_SOUND_VARS; i++) { _heChannel[channel].soundVars[i] = 0; } } void SoundHE::hsStopDigitalSound(int sound) { int channel; if ((channel = hsFindSoundChannel(sound)) != -1) { _heMixer->stopChannel(channel); _heChannel[channel].sound = 0; memset(&_heChannel[channel], 0, sizeof(_heChannel[channel])); } } void SoundHE::triggerDigitalSound(int sound, int offset, int channel, int flags) { byte *soundAddr; int soundCode, soundData, bitsPerSample, sampleChannels; const byte *soundResPtr; uint32 soundLength, soundFrequency; int soundPriority; debug(5, "SoundHE::triggerDigitalSound(sound=%d, offset=%d, channel=%d, flags=%08x)", sound, offset, channel, flags); soundAddr = (byte *)_vm->getResourceAddress(rtSound, sound); // Is this a MRAW music file from the 3DO games? Then update _currentMusic // and throw the sound on the last channel, since otherwise speech will interrupt it... if (READ_BE_UINT32(soundAddr) == MKTAG('M', 'R', 'A', 'W')) { _currentMusic = sound; channel = HSND_MAX_CHANNELS - 1; } // Don't let digital sounds interrupt speech... if (_heChannel[channel].sound == HSND_TALKIE_SLOT && sound != HSND_TALKIE_SLOT) { return; } soundPriority = soundAddr[HSND_RES_OFFSET_KILL_PRIO]; if (_vm->_game.heversion < 95 && _overrideFreq) { soundFrequency = _overrideFreq; _overrideFreq = 0; } else { soundFrequency = READ_LE_UINT16(&soundAddr[HSND_RES_OFFSET_FREQUENCY]); } bitsPerSample = 8; sampleChannels = 1; // Check sound priority if (_heChannel[channel].sound && (_heChannel[channel].sound != HSND_TALKIE_SLOT) && (sound != HSND_TALKIE_SLOT)) { if (soundPriority < _heChannel[channel].priority) { return; // Don't start new sound, prio too low } } soundResPtr = _vm->findResource(MKTAG('S', 'B', 'N', 'G'), soundAddr); if (soundResPtr == nullptr) { soundCode = -1; } else { soundCode = soundResPtr - soundAddr + 8; } soundResPtr = _vm->findResource(MKTAG('S', 'D', 'A', 'T'), soundAddr); if (soundResPtr == nullptr) error("SoundHE::triggerDigitalSound(): Can't find SDAT section in sound %d", sound); soundData = soundResPtr - soundAddr + 8; soundLength = READ_BE_UINT32(soundResPtr + 4) - 8; // Check for a valid offset... if ((uint32)offset >= soundLength) { debug(5, "SoundHE::triggerDigitalSound(): WARNING: Sound %d started past end offset %d size %d", sound, offset, soundLength); offset = 0; } hsStartDigitalSound( sound, offset, soundAddr, soundData, rtSound, sound, soundLength, soundFrequency, channel, soundPriority, soundCode, flags, bitsPerSample, sampleChannels, HESoundModifiers()); } void SoundHE::triggerMidiSound(int soundId, int heOffset) { if (_vm->_imuse) { // This is used in the DOS version of Fatty Bear's // Birthday Surprise to change the note on the piano // when not using a digitized instrument. _vm->_imuse->stopSound(_currentMusic); _currentMusic = soundId; _vm->_imuse->startSoundWithNoteOffset(soundId, heOffset); } else if (_vm->_musicEngine) { _vm->_musicEngine->stopSound(_currentMusic); _currentMusic = soundId; _vm->_musicEngine->startSoundWithTrackID(soundId, heOffset); } } void SoundHE::playVoice(uint32 offset, uint32 length) { byte *ptr; int talkieChannel = (_vm->VAR_TALK_CHANNEL != 0xFF) ? _vm->VAR(_vm->VAR_TALK_CHANNEL) : 0; if (offset == 0 || length == 0) { debug(5, "SoundHE::playVoice(): Attempted playing 0 offset/length talkie from scripts, bailing out..."); return; } if (ConfMan.getBool("speech_mute")) return; if (_sfxFilename.empty()) { // This happens in the Pajama Sam's Lost & Found demo, on the // main menu screen, so don't make it a fatal error. warning("SoundHE::playVoice(): Speech file is not found"); return; } ScummFile file(_vm); if (!_vm->openFile(file, Common::Path(_sfxFilename))) { warning("SoundHE::playVoice(): Could not open speech file %s", _sfxFilename.c_str()); return; } file.setEnc(_sfxFileEncByte); // Speech audio doesn't have a unique ID, // so we use the file offset instead. // _heTalkOffset is used at tryLoadSoundOverride. _heTalkOffset = offset; _digiSndMode |= DIGI_SND_MODE_TALKIE; _heMixer->stopChannel(talkieChannel); _vm->_res->nukeResource(rtSound, HSND_TALKIE_SLOT); file.seek(offset, SEEK_SET); if (_vm->hasLocalizer()) { file.seek(offset + 4, SEEK_SET); length = file.readUint32BE(); file.seek(offset, SEEK_SET); } _vm->_res->createResource(rtSound, 1, length); ptr = _vm->getResourceAddress(rtSound, 1); file.read(ptr, length); addSoundToQueue(HSND_TALKIE_SLOT, 0, talkieChannel, 0, HSND_BASE_FREQ_FACTOR, HSND_SOUND_PAN_CENTER, HSND_MAX_VOLUME); } void SoundHE::playVoiceFile(char *filename) { // This is unimplemented on purpose! I haven't found a game which uses this; // after all, this is a development path and it doesn't seem to affect any game // version I had the chance to test. Emphasis on "had the chance to test": // I don't know if there's actually a game which uses this! // // Originally this served the purpose of fetching voice lines from separate files // instead of using a single .HE2 bundle. // // Again, we should never end up in here, but IF WE DO we issue a message box. GUIErrorMessageWithURL(_( "Unimplemented development codepath encountered within the sound engine,\n" "please file a ticket at https://bugs.scummvm.org."), "https://bugs.scummvm.org"); warning("SoundHE::playVoiceFile(): Unimplemented development codepath"); } #ifdef ENABLE_HE void SoundHE::createSound(int baseSound, int sound) { byte *baseSndPtr, *sndPtr, *baseSndSbngPtr, *sndSbngPtr, *dep, *sep; int baseSndLeft, sndSize, channel; if (sound == -1) { debug(5, "SoundHE::createSound(): Resetting append position..."); _createSndLastAppend = 0; _createSndLastPos = 0; _baseSndSize = 0; return; } if (baseSound != _createSndId) { _createSndId = baseSound; _createSndLastAppend = 0; _createSndLastPos = 0; _baseSndSize = 0; } debug(5, "SoundHE::createSound(): Appending sound %d to base sound %d", sound, baseSound); _vm->ensureResourceLoaded(rtSound, baseSound); _vm->ensureResourceLoaded(rtSound, sound); _vm->_res->lock(rtSound, baseSound); _vm->_res->lock(rtSound, sound); baseSndPtr = _vm->getResourceAddress(rtSound, baseSound); sndPtr = _vm->getResourceAddress(rtSound, sound); channel = hsFindSoundChannel(baseSound); bool sndIsWav = findWavBlock(MKTAG('d', 'a', 't', 'a'), baseSndPtr) != nullptr; if (!sndIsWav) { // For non-WAV files we have to deal with sound variables (i.e. skip them :-) ) baseSndSbngPtr = ((ScummEngine_v71he *)_vm)->heFindResource(MKTAG('S', 'B', 'N', 'G'), baseSndPtr); if (baseSndSbngPtr != nullptr) { sndSbngPtr = ((ScummEngine_v71he *)_vm)->heFindResource(MKTAG('S', 'B', 'N', 'G'), sndPtr); if (sndSbngPtr != nullptr) { if (channel != -1 && (_heChannel[channel].codeOffset > 0)) { memcpy(baseSndSbngPtr + 8, baseSndPtr + _heChannel[channel].codeOffset, (READ_BE_UINT32(baseSndSbngPtr + 4) - 8) - (_heChannel[channel].codeOffset - (baseSndSbngPtr - baseSndPtr + 8))); _heChannel[channel].codeOffset = baseSndSbngPtr + 8 - baseSndPtr; dep = baseSndSbngPtr + 8; while (READ_LE_UINT16(dep) != 0) dep += READ_LE_UINT16(dep); } else { _heChannel[channel].codeOffset = baseSndSbngPtr + 8 - baseSndPtr; dep = baseSndSbngPtr + 8; } sep = sndSbngPtr + 8; while (READ_LE_UINT16(sep) != 0) sep += READ_LE_UINT16(sep); memcpy(dep, sndSbngPtr + 8, sep - sndSbngPtr - 8 + 2); while (READ_LE_UINT16(dep)) { WRITE_LE_UINT32(dep + 2, READ_LE_UINT32(dep + 2) + _createSndLastPos); dep += READ_LE_UINT16(dep); } } } } // Find where the actual sound data is located... if (sndIsWav) { baseSndPtr = const_cast(findWavBlock(MKTAG('d', 'a', 't', 'a'), baseSndPtr)); if (baseSndPtr == nullptr) error("SoundHE::createSound(): Bad format for sound %d, couldn't find data tag", baseSound); sndPtr = const_cast(findWavBlock(MKTAG('d', 'a', 't', 'a'), sndPtr)); if (sndPtr == nullptr) error("SoundHE::createSound(): Bad format for sound %d, couldn't find data tag", sound); if (_baseSndSize == 0) { _baseSndSize = READ_LE_UINT32(baseSndPtr + sizeof(uint32)) - 8; } sndSize = READ_LE_UINT32(sndPtr + sizeof(uint32)) - 8; } else { baseSndPtr = ((ScummEngine_v71he *)_vm)->heFindResource(MKTAG('S', 'D', 'A', 'T'), baseSndPtr); if (baseSndPtr == nullptr) error("SoundHE::createSound(): Bad format for sound %d, couldn't find SDAT tag", baseSound); sndPtr = ((ScummEngine_v71he *)_vm)->heFindResource(MKTAG('S', 'D', 'A', 'T'), sndPtr); if (sndPtr == nullptr) error("SoundHE::createSound(): Bad format for sound %d, couldn't find SDAT tag", sound); _baseSndSize = READ_BE_UINT32(baseSndPtr + sizeof(uint32)) - 8; sndSize = READ_BE_UINT32(sndPtr + sizeof(uint32)) - 8; } // Finally perform the merging of the sound data baseSndLeft = _baseSndSize - _createSndLastAppend; baseSndPtr += 8; sndPtr += 8; if (sndSize < baseSndLeft) { memcpy(baseSndPtr + _createSndLastAppend, sndPtr, sndSize); _createSndLastAppend += sndSize; } else { memcpy(baseSndPtr + _createSndLastAppend, sndPtr, baseSndLeft); if (sndSize - baseSndLeft) { memcpy(baseSndPtr, sndPtr + baseSndLeft, sndSize - baseSndLeft); } _createSndLastAppend = sndSize - baseSndLeft; } _createSndLastPos += sndSize; _vm->_res->unlock(rtSound, baseSound); _vm->_res->unlock(rtSound, sound); } const byte *SoundHE::findWavBlock(uint32 tag, const byte *block) { const byte *wsouPtr = block; // For compatibility reason with old sound formats this // doesn't error out, and instead gracefully returns a nullptr. if (READ_BE_UINT32(wsouPtr) != MKTAG('W', 'S', 'O', 'U')) return nullptr; // Skip over the WSOU header... const byte *soundPtr = wsouPtr + 8; if (READ_BE_UINT32(soundPtr) != MKTAG('R', 'I', 'F', 'F')) error("SoundHE::findWavBlock(): Expected RIFF block"); int riffLength = READ_LE_UINT32(soundPtr + 4); assert((riffLength & 1) == 0); // It must be even, since all sub-blocks must be padded to even. // Skip over RIFF and length and go to the actual sound data... const byte *wavePtr = soundPtr + 8; assert(READ_BE_UINT32(wavePtr) == MKTAG('W', 'A', 'V', 'E')); wavePtr += 4; // Skip over the WAVE tag riffLength -= 4; // Walk the nested blocks of the .wav file... while (riffLength > 0) { uint32 chunkID = READ_BE_UINT32(wavePtr); uint32 chunkLength = READ_LE_UINT32(wavePtr + 4); if ((int)chunkLength < 0) error("SoundHE::findWavBlock(): Illegal chunk length - %d bytes", chunkLength); if ((int)chunkLength > (int)riffLength) error("SoundHE::findWavBlock(): Chunk extends beyond file end - %d versus %d", chunkLength, riffLength); riffLength -= 8; if (chunkID == tag) return wavePtr; wavePtr += 8; // Round up to the next multiple of two. chunkLength = (chunkLength + 1) & ~1; wavePtr += chunkLength; riffLength -= chunkLength; } return nullptr; } #endif int SoundHE::getCurrentSpeechOffset() { return _heTalkOffset; } bool SoundHE::is3DOSound(int sound) const { byte *soundAddr = _vm->getResourceAddress(rtSound, sound); if (soundAddr == nullptr) return false; return READ_BE_UINT32(soundAddr) == MKTAG('M', 'R', 'A', 'W'); } } // End of namespace Scumm