/* 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 "common/system.h" #include "common/timer.h" #include "scumm/actor.h" #include "scumm/scumm_v7.h" #include "scumm/sound.h" #include "scumm/imuse_digi/dimuse_engine.h" #include "scumm/imuse_digi/dimuse_defs.h" #include "scumm/imuse_digi/dimuse_bndmgr.h" #include "scumm/imuse_digi/dimuse_sndmgr.h" #include "scumm/imuse_digi/dimuse_tables.h" #include "audio/audiostream.h" #include "audio/mixer.h" namespace Scumm { void IMuseDigital::timer_handler(void *refCon) { IMuseDigital *diMUSE = (IMuseDigital *)refCon; diMUSE->callback(); } IMuseDigital::IMuseDigital(ScummEngine_v7 *scumm, int sampleRate, Audio::Mixer *mixer, Common::Mutex *mutex, bool lowLatencyMode) : _vm(scumm), _mixer(mixer), _mutex(mutex) { assert(_vm); assert(mixer); // 50 Hz rate for the callback _callbackFps = DIMUSE_TIMER_BASE_RATE_HZ; _usecPerInt = DIMUSE_TIMER_BASE_RATE_USEC; _lowLatencyMode = lowLatencyMode; _internalSampleRate = sampleRate; _internalFeedSize = (int)(DIMUSE_BASE_FEEDSIZE * ((float)_internalSampleRate / DIMUSE_BASE_SAMPLERATE)); if (_lowLatencyMode) { _internalFeedSize *= 2; } _splayer = nullptr; _isEarlyDiMUSE = (_vm->_game.id == GID_FT || (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO)); if (_isEarlyDiMUSE) { memset(_ftCrossfadeBuffer, 0, sizeof(_ftCrossfadeBuffer)); } _curMixerMusicVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType); _curMixerSpeechVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType); _curMixerSFXVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType); _currentSpeechVolume = 0; _currentSpeechFrequency = 0; _currentSpeechPan = 0; _waveOutXorTrigger = 0; _waveOutWriteIndex = 0; _waveOutDisableWrite = 0; _waveOutPreferredFeedSize = 0; _dispatchFadeSize = 0; _stopSequenceFlag = 0; _scriptInitializedFlag = 0; _callbackInterruptFlag = 0; _spooledMusicEnabled = true; _radioChatterSFX = false; _isEngineDisabled = false; _checkForUnderrun = false; _underrunCooldown = 0; _audioNames = nullptr; _numAudioNames = 0; _emptyMarker[0] = '\0'; _internalMixer = new IMuseDigiInternalMixer(mixer, _internalSampleRate, _isEarlyDiMUSE, _lowLatencyMode); _groupsHandler = new IMuseDigiGroupsHandler(this); _fadesHandler = new IMuseDigiFadesHandler(this); _triggersHandler = new IMuseDigiTriggersHandler(this); _filesHandler = new IMuseDigiFilesHandler(this, scumm); diMUSEInitialize(); diMUSEInitializeScript(); if (_vm->_game.id == GID_CMI) { _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_SPEECH, 176000, 44000, 88000); _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_MUSIC, 528000, 44000, 352000); } else if (_vm->_game.id == GID_DIG && !isFTSoundEngine()) { _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_SPEECH, 132000, 22000, 44000); _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_MUSIC, 660000, 11000, 132000); } else { _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_SPEECH, 110000, 22000, 44000); _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_MUSIC, 220000, 22000, 44000); } _filesHandler->allocSoundBuffer(DIMUSE_BUFFER_SFX, 198000, 0, 0); if (_mixer->getOutputBufSize() != 0) { // Let's find the optimal value for the maximum number of streams which can stay in the queue at once; // (A number which is too low can lead to buffer underrun, while the higher the number is, the higher is the audio latency) _maxQueuedStreams = (int)ceil((_mixer->getOutputBufSize() / _waveOutPreferredFeedSize) / ((float)_mixer->getOutputRate() / _internalSampleRate)); // This mixer's optimal output sample rate for this audio engine is one which is a multiple of 22050Hz; // if we're dealing with one which is a multiple of 48000Hz, compensate the number of queued streams... if (_mixer->getOutputRate() % _internalSampleRate) { _maxQueuedStreams++; } // The lower optimal bound is always 4, except if we're operating in low latency mode _maxQueuedStreams = MAX(_mixer->getOutputBufSize() <= 1024 ? 3 : 4, _maxQueuedStreams); } else { debug(5, "IMuseDigital::IMuseDigital(): WARNING: output audio buffer size not specified for this platform, defaulting _maxQueuedStreams to 4"); _maxQueuedStreams = 4; } // This value has been calculated in a way that is a good compromise // between the absence of underruns and the lowest latency achievable. // Of course this is never perfect, given the vast number of platforms // we support, so the value might eventually be corrected by our custom // adaptive buffer underrun correction routine. _nominalBufferCount = _maxQueuedStreams; _underrunCooldown = _maxQueuedStreams; _vm->getTimerManager()->installTimerProc(timer_handler, 1000000 / _callbackFps, this, "IMuseDigital"); } IMuseDigital::~IMuseDigital() { _vm->getTimerManager()->removeTimerProc(timer_handler); _filesHandler->deallocSoundBuffer(DIMUSE_BUFFER_SPEECH); _filesHandler->deallocSoundBuffer(DIMUSE_BUFFER_MUSIC); _filesHandler->deallocSoundBuffer(DIMUSE_BUFFER_SFX); cmdsDeinit(); diMUSETerminate(); delete _internalMixer; delete _groupsHandler; delete _fadesHandler; delete _triggersHandler; delete _filesHandler; // Deinit the Dispatch module free(_dispatchBuffer); _dispatchBuffer = nullptr; // Deinit the WaveOut module free(_waveOutOutputBuffer); _waveOutOutputBuffer = nullptr; free(_waveOutLowLatencyOutputBuffer); _waveOutLowLatencyOutputBuffer = nullptr; free(_audioNames); } int IMuseDigital::roundRobinSetBufferCount() { int minStreams = MAX(_nominalBufferCount - 5, 1); int maxStreams = _nominalBufferCount + 5; _maxQueuedStreams++; if (_maxQueuedStreams > maxStreams) { _maxQueuedStreams = minStreams; } return _maxQueuedStreams; } void IMuseDigital::adaptBufferCount() { _maxQueuedStreams++; _nominalBufferCount = _maxQueuedStreams; } void IMuseDigital::stopSound(int sound) { diMUSEStopSound(sound); } void IMuseDigital::stopAllSounds() { diMUSEStopAllSounds(); } int IMuseDigital::isSoundRunning(int soundId) { return diMUSEGetParam(soundId, DIMUSE_P_SND_TRACK_NUM) > 0; } int IMuseDigital::startVoice(int soundId, const char *soundName, byte speakingActorId) { _filesHandler->closeSoundImmediatelyById(soundId); int fileDoesNotExist = 0; if (_vm->_game.id == GID_DIG) { if (!strcmp(soundName, "PIG.018")) fileDoesNotExist = _filesHandler->setCurrentSpeechFilename("PIG.019"); else fileDoesNotExist = _filesHandler->setCurrentSpeechFilename(soundName); if (fileDoesNotExist) return 1; fillStreamsWhileMusicCritical(5); // WORKAROUND for this particular sound file not playing (this is a bug in the original): // this is happening because the sound buffer responsible for speech // is still busy with the previous speech file playing during the SAN // movie. We just stop the SMUSH speech sound before playing NEXUS.029. if (!strcmp(soundName, "NEXUS.029")) { diMUSEStopSound(DIMUSE_SMUSH_SOUNDID + DIMUSE_BUFFER_SPEECH); } // Set up a trigger for extracting mouth sync times; // see Sound::extractSyncsFromDiMUSEMarker() for details. // Setting up a trigger with an empty marker is a shortcut for // activating the trigger for any marker. diMUSESetTrigger(kTalkSoundID, 0, DIMUSE_C_GET_MARKER_SYNCS); diMUSEStartStream(kTalkSoundID, 127, DIMUSE_BUFFER_SPEECH); diMUSESetParam(kTalkSoundID, DIMUSE_P_GROUP, DIMUSE_GROUP_SPEECH); if (speakingActorId == _vm->VAR(_vm->VAR_EGO)) { diMUSESetParam(kTalkSoundID, DIMUSE_P_MAILBOX, 0); diMUSESetParam(kTalkSoundID, DIMUSE_P_VOLUME, 127); } else { diMUSESetParam(kTalkSoundID, DIMUSE_P_MAILBOX, _radioChatterSFX); diMUSESetParam(kTalkSoundID, DIMUSE_P_VOLUME, 88); } _filesHandler->closeSound(kTalkSoundID); } else if (_vm->_game.id == GID_CMI) { fileDoesNotExist = _filesHandler->setCurrentSpeechFilename(soundName); if (fileDoesNotExist) return 1; diMUSEStartStream(kTalkSoundID, 127, DIMUSE_BUFFER_SPEECH); diMUSESetParam(kTalkSoundID, DIMUSE_P_GROUP, DIMUSE_GROUP_SPEECH); // Let's not give the occasion to raise errors here if (_vm->isValidActor(_vm->VAR(_vm->VAR_TALK_ACTOR))) { Actor *a = _vm->derefActor(_vm->VAR(_vm->VAR_TALK_ACTOR), "IMuseDigital::startVoice"); if (_vm->VAR(_vm->VAR_VOICE_MODE) == 2) diMUSESetParam(kTalkSoundID, DIMUSE_P_VOLUME, 0); else diMUSESetParam(kTalkSoundID, DIMUSE_P_VOLUME, a->_talkVolume); diMUSESetParam(kTalkSoundID, DIMUSE_P_TRANSPOSE, a->_talkFrequency); diMUSESetParam(kTalkSoundID, DIMUSE_P_PAN, a->_talkPan); _currentSpeechVolume = a->_talkVolume; _currentSpeechFrequency = a->_talkFrequency; _currentSpeechPan = a->_talkPan; } // The interpreter really calls for processStreams two times in a row, // and who am I to contradict it? diMUSEProcessStreams(); diMUSEProcessStreams(); } return 0; } // Used by FT and DIG demo int IMuseDigital::startVoice(const char *fileName, ScummFile *file, uint32 offset, uint32 size) { _filesHandler->setCurrentFtSpeechFile(fileName, file, offset, size); diMUSEStopSound(kTalkSoundID); diMUSEStartStream(kTalkSoundID, 127, DIMUSE_BUFFER_SPEECH); diMUSESetParam(kTalkSoundID, DIMUSE_P_GROUP, DIMUSE_GROUP_SPEECH); return 0; } static void skipLegacyTrackEntry(Common::Serializer &s) { s.skip(1, VER(31)); // t.pan s.skip(4, VER(31)); // t.vol s.skip(4, VER(31)); // t.volFadeDest s.skip(4, VER(31)); // t.volFadeStep s.skip(4, VER(31)); // t.volFadeDelay s.skip(1, VER(31)); // t.volFadeUsed s.skip(4, VER(31)); // t.soundId s.skip(15, VER(31)); // t.soundName s.skip(1, VER(31)); // t.used s.skip(1, VER(31)); // t.toBeRemoved s.skip(1, VER(31)); // t.souStreamUsed s.skip(1, VER(31), VER(76)); // mixerStreamRunning s.skip(4, VER(31)); // t.soundPriority s.skip(4, VER(31)); // t.regionOffset s.skip(4, VER(31), VER(31)); // trackOffset s.skip(4, VER(31)); // t.dataOffset s.skip(4, VER(31)); // t.curRegion s.skip(4, VER(31)); // t.curHookId s.skip(4, VER(31)); // t.volGroupId s.skip(4, VER(31)); // t.soundType s.skip(4, VER(31)); // t.feedSize s.skip(4, VER(31)); // t.dataMod12Bit s.skip(4, VER(31)); // t.mixerFlags s.skip(4, VER(31), VER(42)); // mixerVol s.skip(4, VER(31), VER(42)); // mixerPan s.skip(1, VER(45)); // t.sndDataExtComp } void IMuseDigital::saveLoadEarly(Common::Serializer &s) { Common::StackLock lock(*_mutex, "IMuseDigital::saveLoadEarly()"); if (s.isLoading()) { diMUSEStopAllSounds(); _filesHandler->closeSoundImmediatelyById(kTalkSoundID); } if (s.getVersion() < 103) { // Just load the current state and sequence, and play them debug(5, "IMuseDigital::saveLoadEarly(): old savegame detected (version %d), game may load with an undesired audio status", s.getVersion()); s.skip(4, VER(31), VER(42)); // _volVoice s.skip(4, VER(31), VER(42)); // _volSfx s.skip(4, VER(31), VER(42)); // _volMusic s.syncAsSint32LE(_curMusicState, VER(31)); s.syncAsSint32LE(_curMusicSeq, VER(31)); s.syncAsSint32LE(_curMusicCue, VER(31)); s.syncAsSint32LE(_nextSeqToPlay, VER(31)); s.syncAsByte(_radioChatterSFX, VER(76)); s.syncArray(_attributes, 188, Common::Serializer::Sint32LE, VER(31)); for (int j = 0; j < 16; ++j) skipLegacyTrackEntry(s); int stateSoundId = 0; int seqSoundId = 0; if (_vm->_game.id == GID_DIG) { stateSoundId = _digStateMusicTable[_curMusicState].soundId; seqSoundId = _digSeqMusicTable[_curMusicSeq].soundId; } else { if (_vm->_game.features & GF_DEMO) { stateSoundId = _comiDemoStateMusicTable[_curMusicState].soundId; } else { stateSoundId = _comiStateMusicTable[_curMusicState].soundId; seqSoundId = _comiSeqMusicTable[_curMusicSeq].soundId; } } _curMusicState = 0; _curMusicSeq = 0; scriptSetSequence(seqSoundId); scriptSetState(stateSoundId); scriptSetCuePoint(_curMusicCue); _curMusicCue = 0; } else { diMUSESaveLoad(s); if (s.isLoading() && _vm->isUsingOriginalGUI()) { diMUSESetMusicGroupVol(diMUSEGetMusicGroupVol()); diMUSESetVoiceGroupVol(diMUSEGetVoiceGroupVol()); diMUSESetSFXGroupVol(diMUSEGetSFXGroupVol()); } } if (_vm->_game.id == GID_CMI && s.isLoading()) { fillStreamsWhileMusicCritical(10); } } void IMuseDigital::refreshScripts() { if (isFTSoundEngine()) { diMUSEProcessStreams(); } else if (!_vm->isSmushActive()) { diMUSEProcessStreams(); diMUSERefreshScript(); } } void IMuseDigital::setRadioChatterSFX(bool state) { _radioChatterSFX = state; } int IMuseDigital::startSfx(int soundId, int priority) { diMUSEStartSound(soundId, priority); diMUSESetParam(soundId, DIMUSE_P_GROUP, DIMUSE_GROUP_SFX); return 0; } void IMuseDigital::callback() { if (_cmdsPauseCount) return; if (!_callbackInterruptFlag) { _callbackInterruptFlag = 1; diMUSEHeartbeat(); _callbackInterruptFlag = 0; } } void IMuseDigital::diMUSEHeartbeat() { // This is what happens: // - Usual audio stuff like fetching and playing sound (and everything // within waveOutCallback()) happens at a base 50Hz rate; // - Triggers and fades handling happens at a (somewhat hacky) 60Hz rate; // - Music gain reduction happens at a 10Hz rate. int soundId, foundGroupId, musicTargetVolume, musicEffVol, musicVol, tempVol, tempEffVol, factor, step; waveOutCallback(); if (!_vm->isUsingOriginalGUI()) { // Update volumes if (_curMixerMusicVolume != _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType)) { _curMixerMusicVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType); diMUSESetMusicGroupVol(CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2, 0, 127)); } if (_curMixerSpeechVolume != _mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType)) { _curMixerSpeechVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType); diMUSESetVoiceGroupVol(CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType) / 2, 0, 127)); } if (_curMixerSFXVolume != _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType)) { _curMixerSFXVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType); diMUSESetSFXGroupVol(CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType) / 2, 0, 127)); } } // Handle fades and triggers _cmdsRunning60HzCount += _usecPerInt; while (_cmdsRunning60HzCount >= DIMUSE_TIMER_FADES_RATE_USEC) { _cmdsRunning60HzCount -= DIMUSE_TIMER_FADES_RATE_USEC; _fadesHandler->loop(); _triggersHandler->loop(); } _cmdsRunning10HzCount += _usecPerInt; if (_cmdsRunning10HzCount < DIMUSE_TIMER_GAIN_RED_RATE_USEC) return; do { // SPEECH GAIN REDUCTION 10Hz _cmdsRunning10HzCount -= DIMUSE_TIMER_GAIN_RED_RATE_USEC; soundId = 0; musicTargetVolume = _groupsHandler->setGroupVol(DIMUSE_GROUP_MUSIC, -1); while (1) { // Check all tracks to see if there's a speech file playing soundId = waveGetNextSound(soundId); if (!soundId) break; foundGroupId = -1; if (_filesHandler->getNextSound(soundId) == 2) { foundGroupId = waveGetParam(soundId, DIMUSE_P_GROUP); // Check the groupId of this sound } if (foundGroupId == DIMUSE_GROUP_SPEECH) { // Remember: when a speech file stops playing this block stops // being executed, so musicTargetVolume returns back to its original value factor = _isEarlyDiMUSE ? 82 : 80; musicTargetVolume = (musicTargetVolume * factor) / 128; break; } } musicEffVol = _groupsHandler->setGroupVol(DIMUSE_GROUP_MUSICEFF, -1); // MUSIC EFFECTIVE VOLUME GROUP (used for gain reduction) musicVol = _groupsHandler->setGroupVol(DIMUSE_GROUP_MUSIC, -1); // MUSIC VOLUME SUBGROUP (keeps track of original music volume) if (musicEffVol < musicTargetVolume) { // If there is gain reduction already going on... tempEffVol = musicEffVol + 3; if (tempEffVol < musicTargetVolume) { if (musicVol <= tempEffVol) { musicVol = tempEffVol; } } else if (musicVol <= musicTargetVolume) { // Bring up the music volume immediately when speech stops playing musicVol = musicTargetVolume; } _groupsHandler->setGroupVol(DIMUSE_GROUP_MUSICEFF, musicVol); } else if (musicEffVol > musicTargetVolume) { // Bring music volume down to target volume with a -18 (or -6 for FT & DIG demo) step // if there's speech playing or else, just cap it to the target if it's out of range step = _isEarlyDiMUSE ? 6 : 18; tempVol = musicEffVol - step; if (tempVol <= musicTargetVolume) { if (musicVol >= musicTargetVolume) { musicVol = musicTargetVolume; } } else { if (musicVol >= tempVol) { musicVol = tempVol; } } _groupsHandler->setGroupVol(DIMUSE_GROUP_MUSICEFF, musicVol); } } while (_cmdsRunning10HzCount >= DIMUSE_TIMER_GAIN_RED_RATE_USEC); } void IMuseDigital::setPriority(int soundId, int priority) { diMUSESetParam(soundId, DIMUSE_P_PRIORITY, priority); } void IMuseDigital::setVolume(int soundId, int volume) { diMUSESetParam(soundId, DIMUSE_P_VOLUME, volume); if (soundId == kTalkSoundID) _currentSpeechVolume = volume; } void IMuseDigital::setPan(int soundId, int pan) { diMUSESetParam(soundId, DIMUSE_P_PAN, pan); if (soundId == kTalkSoundID) _currentSpeechPan = pan; } void IMuseDigital::setFrequency(int soundId, int frequency) { diMUSESetParam(soundId, DIMUSE_P_TRANSPOSE, frequency); if (soundId == kTalkSoundID) _currentSpeechFrequency = frequency; } int IMuseDigital::getCurSpeechVolume() const { return _currentSpeechVolume; } int IMuseDigital::getCurSpeechPan() const { return _currentSpeechPan; } int IMuseDigital::getCurSpeechFrequency() const { return _currentSpeechFrequency; } void IMuseDigital::flushTracks() { _filesHandler->flushSounds(); } // This is used in order to avoid crash everything // if a compressed audio resource file is found void IMuseDigital::disableEngine() { _isEngineDisabled = true; } bool IMuseDigital::isEngineDisabled() { return _isEngineDisabled; } void IMuseDigital::stopSMUSHAudio() { if (!isFTSoundEngine()) { if (_vm->_game.id == GID_DIG) { int foundSoundId, paused; int32 bufSize, criticalSize, freeSpace; foundSoundId = diMUSEGetNextSound(0); while (foundSoundId) { if (diMUSEGetParam(foundSoundId, DIMUSE_P_SND_HAS_STREAM)) { diMUSEQueryStream(foundSoundId, bufSize, criticalSize, freeSpace, paused); // Here, the original engine for DIG explicitly asks for "bufSize == 193900"; // since this works half of the time in ScummVM (because of how we handle sound buffers), // we do the check but we alternatively check for the SMUSH channel soundId, so to cover the // remaining cases. This fixes instances in which exiting from a cutscene leaves both // DiMUSE streams locked, with speech consequently unable to play and a "WARNING: three // streams in use" message from streamerProcessStreams() if (bufSize == 193900 || foundSoundId == DIMUSE_SMUSH_SOUNDID + DIMUSE_BUFFER_SFX) diMUSEStopSound(foundSoundId); } foundSoundId = diMUSEGetNextSound(foundSoundId); } } diMUSESetSequence(0); } } bool IMuseDigital::isFTSoundEngine() { return _isEarlyDiMUSE; } int32 IMuseDigital::getCurMusicPosInMs() { int soundId, curSoundId; curSoundId = 0; soundId = 0; while (1) { curSoundId = diMUSEGetNextSound(curSoundId); if (!curSoundId) break; if (diMUSEGetParam(curSoundId, DIMUSE_P_SND_HAS_STREAM) && diMUSEGetParam(curSoundId, DIMUSE_P_STREAM_BUFID) == DIMUSE_BUFFER_MUSIC) { soundId = curSoundId; return diMUSEGetParam(soundId, DIMUSE_P_SND_POS_IN_MS); } } return diMUSEGetParam(soundId, DIMUSE_P_SND_POS_IN_MS); } int32 IMuseDigital::getCurVoiceLipSyncWidth() { int32 width, height; getSpeechLipSyncInfo(width, height); return width; } int32 IMuseDigital::getCurVoiceLipSyncHeight() { int32 width, height; getSpeechLipSyncInfo(width, height); return height; } int32 IMuseDigital::getCurMusicLipSyncWidth(int syncId) { int32 width, height; getMusicLipSyncInfo(syncId, width, height); return width; } int32 IMuseDigital::getCurMusicLipSyncHeight(int syncId) { int32 width, height; getMusicLipSyncInfo(syncId, width, height); return height; } void IMuseDigital::getSpeechLipSyncInfo(int32 &width, int32 &height) { int curSpeechPosInMs; width = 0; height = 0; if (diMUSEGetParam(kTalkSoundID, DIMUSE_P_SND_TRACK_NUM) > 0) { curSpeechPosInMs = diMUSEGetParam(kTalkSoundID, DIMUSE_P_SND_POS_IN_MS); diMUSELipSync(kTalkSoundID, 0, _vm->VAR(_vm->VAR_SYNC) + curSpeechPosInMs + 50, width, height); } } void IMuseDigital::getMusicLipSyncInfo(int syncId, int32 &width, int32 &height) { int soundId; int speechSoundId; int curSpeechPosInMs; soundId = 0; speechSoundId = 0; width = 0; height = 0; while (1) { soundId = diMUSEGetNextSound(soundId); if (!soundId) break; if (diMUSEGetParam(soundId, DIMUSE_P_SND_HAS_STREAM)) { if (diMUSEGetParam(soundId, DIMUSE_P_STREAM_BUFID) == DIMUSE_BUFFER_MUSIC) { speechSoundId = soundId; break; } } } if (speechSoundId) { curSpeechPosInMs = diMUSEGetParam(speechSoundId, DIMUSE_P_SND_POS_IN_MS); diMUSELipSync(speechSoundId, syncId, _vm->VAR(_vm->VAR_SYNC) + curSpeechPosInMs + 50, width, height); } } int32 IMuseDigital::getSoundElapsedTimeInMs(int soundId) { if (diMUSEGetParam(soundId, DIMUSE_P_SND_HAS_STREAM)) { return diMUSEGetParam(soundId, DIMUSE_P_SND_POS_IN_MS); } return 0; } void IMuseDigital::pause(bool p) { if (p) { debug(5, "IMuseDigital::pause(): pausing..."); diMUSEPause(); } else { debug(5, "IMuseDigital::pause(): resuming..."); diMUSEResume(); } } void IMuseDigital::setAudioNames(int32 num, char *names) { free(_audioNames); _numAudioNames = num; _audioNames = names; } int IMuseDigital::getSoundIdByName(const char *soundName) { if (soundName && soundName[0] != 0) { for (int r = 0; r < _numAudioNames; r++) { if (strcmp(soundName, &_audioNames[r * 9]) == 0) { return r; } } } return 0; } void IMuseDigital::setSmushPlayer(SmushPlayer *splayer) { _splayer = splayer; // Perform a first-time volume update for both SMUSH and iMUSE diMUSESetMusicGroupVol(CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2, 0, 127)); diMUSESetVoiceGroupVol(CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType) / 2, 0, 127)); diMUSESetSFXGroupVol(CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType) / 2, 0, 127)); } void IMuseDigital::receiveAudioFromSMUSH(uint8 *srcBuf, int32 inFrameCount, int32 feedSize, int32 mixBufStartIndex, int volume, int pan, bool is11025Hz) { _internalMixer->mix(srcBuf, inFrameCount, 8, 1, feedSize, mixBufStartIndex, volume, pan, is11025Hz); } void IMuseDigital::floodMusicBuffer() { while (!isMusicStreamIdle()) { diMUSEProcessStreams(); } } void IMuseDigital::fillStreamsWhileMusicCritical(int fillTimesAfter) { if (!isFTSoundEngine()) { while (isMusicCritical()) { diMUSEProcessStreams(); } } for (int i = 0; i < fillTimesAfter; i++) { diMUSEProcessStreams(); } } bool IMuseDigital::isMusicStreamIdle() { int32 bufSize, criticalSize, freeSpace; int paused; IMuseDigiSndBuffer *bufInfo = _filesHandler->getBufInfo(DIMUSE_BUFFER_MUSIC); if (!queryNextSoundFile(bufSize, criticalSize, freeSpace, paused)) return true; return (paused > 0) || (bufSize - bufInfo->loadSize < freeSpace); } bool IMuseDigital::isMusicCritical() { int32 bufSize, criticalSize, freeSpace; int paused; if (!queryNextSoundFile(bufSize, criticalSize, freeSpace, paused)) return false; return (paused == 0) && freeSpace <= criticalSize; } bool IMuseDigital::queryNextSoundFile(int32 &bufSize, int32 &criticalSize, int32 &freeSpace, int &paused) { int soundId; if (isFTSoundEngine()) { soundId = diMUSEQueryStream(0, bufSize, criticalSize, freeSpace, paused); if (soundId) { while (freeSpace >= criticalSize) { soundId = diMUSEQueryStream(soundId, bufSize, criticalSize, freeSpace, paused); if (!soundId) return false; } return true; } } else { soundId = diMUSEGetNextSound(0); while (soundId) { if (diMUSEGetParam(soundId, DIMUSE_P_SND_HAS_STREAM) && (diMUSEGetParam(soundId, DIMUSE_P_GROUP) == DIMUSE_GROUP_MUSIC || diMUSEGetParam(soundId, DIMUSE_P_GROUP) == DIMUSE_GROUP_MUSICEFF)) { diMUSEQueryStream(soundId, bufSize, criticalSize, freeSpace, paused); return true; } soundId = diMUSEGetNextSound(soundId); } } return false; } int IMuseDigital::getSampleRate() { return _internalSampleRate; } int IMuseDigital::getFeedSize() { return _internalFeedSize; } void IMuseDigital::parseScriptCmds(int cmd, int soundId, int sub_cmd, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n, int o, int p) { int b = soundId; int c = sub_cmd; int id; int volume = b; switch (cmd) { case DIMUSE_C_KLUDGE_SET_STATE: diMUSESetState(soundId); break; case DIMUSE_C_KLUDGE_SET_SEQUENCE: diMUSESetSequence(soundId); break; case DIMUSE_C_KLUDGE_SET_CUE_POINT: diMUSESetCuePoint(soundId); break; case DIMUSE_C_KLUDGE_SET_ATTRIBUTE: diMUSESetAttribute(b, c); break; case DIMUSE_C_KLUDGE_SET_SFX_VOLUME: if (!_vm->isUsingOriginalGUI()) { volume = CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType) / 2, 0, 127); } diMUSESetSFXGroupVol(volume); break; case DIMUSE_C_KLUDGE_SET_VOICE_VOLUME: if (!_vm->isUsingOriginalGUI()) { volume = CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType) / 2, 0, 127); } diMUSESetVoiceGroupVol(volume); break; case DIMUSE_C_KLUDGE_SET_MUSIC_VOLUME: if (!_vm->isUsingOriginalGUI()) { volume = CLIP(_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2, 0, 127); } diMUSESetMusicGroupVol(volume); break; case DIMUSE_C_KLUDGE_STOP_ALL_SNDS: case DIMUSE_C_KLUDGE_SET_PARAM: case DIMUSE_C_KLUDGE_FADE_PARAM: cmdsHandleCmd(cmd, nullptr, soundId, sub_cmd, d, e, f, g, h, i, j, k, l, m, n, o); break; case DIMUSE_C_KLUDGE_START_STREAM: if (_vm->_game.id == GID_FT) { id = getSoundIdByName("kstand"); _filesHandler->openSound(id); } else if (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO) { // Special opcode used in place of the first setState instruction _filesHandler->openSound(soundId); diMUSEStartStream(soundId, 126, DIMUSE_BUFFER_MUSIC); } break; case DIMUSE_C_KLUDGE_SWITCH_STREAM: if (_vm->_game.id == GID_DIG && _vm->_game.features & GF_DEMO) { _filesHandler->openSound(c); diMUSESwitchStream(soundId, c, _ftCrossfadeBuffer, sizeof(_ftCrossfadeBuffer), 0); _filesHandler->closeSound(soundId); } break; default: debug("IMuseDigital::parseScriptCmds(): WARNING: unhandled command %d", cmd); } } int IMuseDigital::diMUSETerminate() { if (_scriptInitializedFlag) { diMUSEStopAllSounds(); _filesHandler->closeAllSounds(); } return 0; } int IMuseDigital::diMUSEInitialize() { return cmdsHandleCmd(DIMUSE_C_INIT); } int IMuseDigital::diMUSEPause() { return cmdsHandleCmd(DIMUSE_C_PAUSE); } int IMuseDigital::diMUSEResume() { return cmdsHandleCmd(DIMUSE_C_RESUME); } void IMuseDigital::diMUSESaveLoad(Common::Serializer &ser) { cmdsSaveLoad(ser); } int IMuseDigital::diMUSESetGroupVol(int groupId, int volume) { return cmdsHandleCmd(DIMUSE_C_SET_GRP_VOL, nullptr, groupId, volume); } int IMuseDigital::diMUSEStartSound(int soundId, int priority) { return cmdsHandleCmd(DIMUSE_C_START_SND, nullptr, soundId, priority); } int IMuseDigital::diMUSEStopSound(int soundId) { debug(5, "IMuseDigital::diMUSEStopSound(): %d", soundId); return cmdsHandleCmd(DIMUSE_C_STOP_SND, nullptr, soundId); } int IMuseDigital::diMUSEStopAllSounds() { debug(5, "IMuseDigital::diMUSEStopAllSounds()"); return cmdsHandleCmd(DIMUSE_C_STOP_ALL_SNDS); } int IMuseDigital::diMUSEGetNextSound(int soundId) { return cmdsHandleCmd(DIMUSE_C_GET_NEXT_SND, nullptr, soundId); } int IMuseDigital::diMUSESetParam(int soundId, int paramId, int value) { return cmdsHandleCmd(DIMUSE_C_SET_PARAM, nullptr, soundId, paramId, value); } int IMuseDigital::diMUSEGetParam(int soundId, int paramId) { return cmdsHandleCmd(DIMUSE_C_GET_PARAM, nullptr, soundId, paramId); } int IMuseDigital::diMUSEFadeParam(int soundId, int opcode, int destValue, int fadeLength) { return cmdsHandleCmd(DIMUSE_C_FADE_PARAM, nullptr, soundId, opcode, destValue, fadeLength); } int IMuseDigital::diMUSESetHook(int soundId, int hookId) { return cmdsHandleCmd(DIMUSE_C_SET_HOOK, nullptr, soundId, hookId); } int IMuseDigital::diMUSESetTrigger(int soundId, int marker, int opcode, int d, int e, int f, int g, int h, int i, int j, int k, int l, int m, int n) { return cmdsHandleCmd(DIMUSE_C_SET_TRIGGER, nullptr, soundId, marker, opcode, d, e, f, g, h, i, j, k, l, m, n); } int IMuseDigital::diMUSEStartStream(int soundId, int priority, int bufferId) { return cmdsHandleCmd(DIMUSE_C_START_STREAM, nullptr, soundId, priority, bufferId); } int IMuseDigital::diMUSESwitchStream(int oldSoundId, int newSoundId, int fadeDelay, int fadeSyncFlag2, int fadeSyncFlag1) { return cmdsHandleCmd(DIMUSE_C_SWITCH_STREAM, nullptr, oldSoundId, newSoundId, fadeDelay, fadeSyncFlag2, fadeSyncFlag1); } // Variation for FT and DIG demo int IMuseDigital::diMUSESwitchStream(int oldSoundId, int newSoundId, uint8 *crossfadeBuffer, int crossfadeBufferSize, int vocLoopFlag) { return cmdsHandleCmd(DIMUSE_C_SWITCH_STREAM, crossfadeBuffer, oldSoundId, newSoundId, -1, crossfadeBufferSize, vocLoopFlag); } int IMuseDigital::diMUSEProcessStreams() { return cmdsHandleCmd(DIMUSE_C_PROCESS_STREAMS); } int IMuseDigital::diMUSEQueryStream(int soundId, int32 &bufSize, int32 &criticalSize, int32 &freeSpace, int &paused) { return waveQueryStream(soundId, bufSize, criticalSize, freeSpace, paused); } int IMuseDigital::diMUSEFeedStream(int soundId, uint8 *srcBuf, int32 sizeToFeed, int paused) { return cmdsHandleCmd(DIMUSE_C_FEED_STREAM, srcBuf, soundId, -1, sizeToFeed, paused); } int IMuseDigital::diMUSELipSync(int soundId, int syncId, int msPos, int32 &width, int32 &height) { return waveLipSync(soundId, syncId, msPos, width, height); } int IMuseDigital::diMUSEGetMusicGroupVol() { if (_vm->isUsingOriginalGUI()) { return diMUSESetGroupVol(DIMUSE_GROUP_MUSIC, -1); } return _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / 2; } int IMuseDigital::diMUSEGetSFXGroupVol() { if (_vm->isUsingOriginalGUI()) { return diMUSESetGroupVol(DIMUSE_GROUP_SFX, -1); } return _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType) / 2; } int IMuseDigital::diMUSEGetVoiceGroupVol() { if (_vm->isUsingOriginalGUI()) { return diMUSESetGroupVol(DIMUSE_GROUP_SPEECH, -1); } return _mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType) / 2; } int IMuseDigital::diMUSESetMusicGroupVol(int volume) { debug(5, "IMuseDigital::diMUSESetMusicGroupVol(): %d", volume); if (_isEarlyDiMUSE) _splayer->setGroupVolume(GRP_BKGMUS, volume); return diMUSESetGroupVol(DIMUSE_GROUP_MUSIC, volume); } int IMuseDigital::diMUSESetSFXGroupVol(int volume) { debug(5, "IMuseDigital::diMUSESetSFXGroupVol(): %d", volume); if (_isEarlyDiMUSE) _splayer->setGroupVolume(GRP_SFX, volume); return diMUSESetGroupVol(DIMUSE_GROUP_SFX, volume); } int IMuseDigital::diMUSESetVoiceGroupVol(int volume) { debug(5, "IMuseDigital::diMUSESetVoiceGroupVol(): %d", volume); if (_isEarlyDiMUSE) _splayer->setGroupVolume(GRP_SPEECH, volume); return diMUSESetGroupVol(DIMUSE_GROUP_SPEECH, volume); } void IMuseDigital::diMUSEUpdateGroupVolumes() { waveUpdateGroupVolumes(); } int IMuseDigital::diMUSEInitializeScript() { return scriptParse(DIMUSE_C_SCRIPT_INIT, -1, -1); } void IMuseDigital::diMUSERefreshScript() { scriptParse(DIMUSE_C_SCRIPT_REFRESH, -1, -1); } int IMuseDigital::diMUSESetState(int soundId) { return scriptParse(DIMUSE_C_SCRIPT_SET_STATE, soundId, -1); } int IMuseDigital::diMUSESetSequence(int soundId) { return scriptParse(DIMUSE_C_SCRIPT_SET_SEQUENCE, soundId, -1); } int IMuseDigital::diMUSESetCuePoint(int cueId) { return scriptParse(DIMUSE_C_SCRIPT_CUE_POINT, cueId, -1); } int IMuseDigital::diMUSESetAttribute(int attrIndex, int attrVal) { return scriptParse(DIMUSE_C_SCRIPT_SET_ATTRIBUTE, attrIndex, attrVal); } void IMuseDigital::diMUSEEnableSpooledMusic() { _spooledMusicEnabled = true; } void IMuseDigital::diMUSEDisableSpooledMusic() { _spooledMusicEnabled = true; diMUSESetState(0); diMUSESetSequence(0); _spooledMusicEnabled = false; } // Debugger utility functions void IMuseDigital::listStates() { _vm->getDebugger()->debugPrintf("+---------------------------------+\n"); _vm->getDebugger()->debugPrintf("| stateId | name |\n"); _vm->getDebugger()->debugPrintf("+---------+-----------------------+\n"); if (_vm->_game.id == GID_CMI) { if (_vm->_game.features & GF_DEMO) { for (int i = 0; _comiDemoStateMusicTable[i].soundId != -1; i++) { _vm->getDebugger()->debugPrintf("| %4d | %20s |\n", _comiDemoStateMusicTable[i].soundId, _comiDemoStateMusicTable[i].name); } } else { for (int i = 0; _comiStateMusicTable[i].soundId != -1; i++) { _vm->getDebugger()->debugPrintf("| %4d | %20s |\n", _comiStateMusicTable[i].soundId, _comiStateMusicTable[i].name); } } } else if (_vm->_game.id == GID_DIG) { for (int i = 0; _digStateMusicTable[i].soundId != -1; i++) { _vm->getDebugger()->debugPrintf("| %4d | %20s |\n", _digStateMusicTable[i].soundId, _digStateMusicTable[i].name); } } else if (_vm->_game.id == GID_FT) { for (int i = 0; _ftStateMusicTable[i].name[0]; i++) { _vm->getDebugger()->debugPrintf("| %4d | %21s |\n", i, _ftStateMusicTable[i].name); } } _vm->getDebugger()->debugPrintf("+---------+-----------------------+\n\n"); } void IMuseDigital::listSeqs() { _vm->getDebugger()->debugPrintf("+--------------------------------+\n"); _vm->getDebugger()->debugPrintf("| seqId | name |\n"); _vm->getDebugger()->debugPrintf("+---------+----------------------+\n"); if (_vm->_game.id == GID_CMI) { for (int i = 0; _comiSeqMusicTable[i].soundId != -1; i++) { _vm->getDebugger()->debugPrintf("| %4d | %20s |\n", _comiSeqMusicTable[i].soundId, _comiSeqMusicTable[i].name); } } else if (_vm->_game.id == GID_DIG) { for (int i = 0; _digSeqMusicTable[i].soundId != -1; i++) { _vm->getDebugger()->debugPrintf("| %4d | %20s |\n", _digSeqMusicTable[i].soundId, _digSeqMusicTable[i].name); } } else if (_vm->_game.id == GID_FT) { for (int i = 0; _ftSeqNames[i].name[0]; i++) { _vm->getDebugger()->debugPrintf("| %4d | %20s |\n", i, _ftSeqNames[i].name); } } _vm->getDebugger()->debugPrintf("+---------+----------------------+\n\n"); } void IMuseDigital::listCues() { int curId = -1; if (_curMusicSeq) { _vm->getDebugger()->debugPrintf("Available cues for current sequence:\n"); _vm->getDebugger()->debugPrintf("+---------------------------------------+\n"); _vm->getDebugger()->debugPrintf("| cueName | transitionType | volume |\n"); _vm->getDebugger()->debugPrintf("+-------------+----------------+--------+\n"); for (int i = 0; i < 4; i++) { curId = ((_curMusicSeq - 1) * 4) + i; _vm->getDebugger()->debugPrintf("| %9s | %d | %3d |\n", _ftSeqMusicTable[curId].audioName, (int)_ftSeqMusicTable[curId].transitionType, (int)_ftSeqMusicTable[curId].volume); } _vm->getDebugger()->debugPrintf("+-------------+----------------+--------+\n\n"); } else { _vm->getDebugger()->debugPrintf("Current sequence is NULL, no cues available.\n\n"); } } void IMuseDigital::listTracks() { _vm->getDebugger()->debugPrintf("Virtual audio tracks currently playing:\n"); _vm->getDebugger()->debugPrintf("+-------------------------------------------------------------------------+\n"); _vm->getDebugger()->debugPrintf("| # | soundId | group | hasStream | vol/effVol/pan | priority | jumpHook |\n"); _vm->getDebugger()->debugPrintf("+---+---------+-------+-----------+-----------------+----------+----------+\n"); for (int i = 0; i < _trackCount; i++) { IMuseDigiTrack curTrack = _tracks[i]; if (curTrack.soundId != 0) { _vm->getDebugger()->debugPrintf("| %1d | %5d | %d | %d | %3d/%3d/%3d | %3d | %3d |\n", i, curTrack.soundId, curTrack.group, diMUSEGetParam(curTrack.soundId, DIMUSE_P_SND_HAS_STREAM), curTrack.vol, curTrack.effVol, curTrack.pan, curTrack.priority, curTrack.jumpHook); } else { _vm->getDebugger()->debugPrintf("| %1d | --- | --- | --- | ---/---/--- | --- | --- |\n", i); } } _vm->getDebugger()->debugPrintf("+---+---------+-------+-----------+-----------------+----------+----------+\n\n"); } void IMuseDigital::listGroups() { _vm->getDebugger()->debugPrintf("Volume groups:\n"); _vm->getDebugger()->debugPrintf("\tSFX: %3d\n", _groupsHandler->getGroupVol(DIMUSE_GROUP_SFX)); _vm->getDebugger()->debugPrintf("\tSPEECH: %3d\n", _groupsHandler->getGroupVol(DIMUSE_GROUP_SPEECH)); _vm->getDebugger()->debugPrintf("\tMUSIC: %3d\n", _groupsHandler->getGroupVol(DIMUSE_GROUP_MUSIC)); _vm->getDebugger()->debugPrintf("\tMUSICEFF: %3d\n\n", _groupsHandler->getGroupVol(DIMUSE_GROUP_MUSICEFF)); } } // End of namespace Scumm