/* 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 "audio/audiostream.h" #include "common/config-manager.h" #include "common/timer.h" #include "engines/engine.h" #include "scumm/cdda.h" #include "scumm/music.h" #include "scumm/scumm.h" #include "scumm/soundcd.h" #include "scumm/soundse.h" namespace Scumm { SoundCD::SoundCD(ScummEngine *parent, Audio::Mixer *mixer, SoundSE *soundSE, bool useReplacementAudioTracks): _vm(parent), _mixer(mixer), _soundSE(soundSE), _useReplacementAudioTracks(useReplacementAudioTracks) { _fileBasedCDStatus.playing = false; _fileBasedCDStatus.track = 0; _fileBasedCDStatus.start = 0; _fileBasedCDStatus.duration = 0; _fileBasedCDStatus.numLoops = 0; _fileBasedCDStatus.volume = Audio::Mixer::kMaxChannelVolume; _fileBasedCDStatus.balance = 0; _hasFileBasedCDAudio = (_vm->_game.id == GID_LOOM && Common::File::exists("CDDA.SOU")) || ((_vm->_game.features & GF_DOUBLEFINE_PAK) && _vm->_game.id == GID_MONKEY); _loomOvertureTransition = DEFAULT_LOOM_OVERTURE_TRANSITION + ConfMan.getInt("loom_overture_ticks"); _fileBasedCDAudioHandle = new Audio::SoundHandle(); } SoundCD::~SoundCD() { stopCDTimer(); stopCD(); delete _fileBasedCDAudioHandle; } void SoundCD::stopCDSound(int sound) { if (sound != 0 && sound == _currentCDSound) { _currentCDSound = 0; _musicTimer = 0; _replacementTrackStartTime = 0; stopCD(); stopCDTimer(); } } void SoundCD::stopAllCDSounds() { if (_currentCDSound != 0) { _currentCDSound = 0; _musicTimer = 0; _replacementTrackStartTime = 0; stopCD(); stopCDTimer(); } } static void cdTimerHandler(void *refCon) { SoundCD *snd = (SoundCD *)refCon; // FIXME: Turn off the timer when it's no longer needed. In theory, it // should be possible to check with pollCD(), but since CD sound isn't // properly restarted when reloading a saved game, I don't dare to. if ((snd->_cdMusicTimerMod++ & 3) == 0) { snd->_cdMusicTimer++; } } void SoundCD::startCDTimer() { if (_useReplacementAudioTracks) return; // This CD timer implementation strictly follows the original interpreters for // Monkey Island 1 CD and Loom CD: it works by incrementing _cdMusicTimerMod and _cdMusicTimer // at each quarter frame (see ScummEngine::setTimerAndShakeFrequency() for what the exact // frequency rate is for the particular game and engine version being ran). // // Again as per the interpreters, VAR_MUSIC_TIMER is then updated inside the SCUMM main loop. int32 interval = 1000000 / _vm->getTimerFrequency(); // LOOM Steam uses a fixed 240Hz rate. This was probably done to get rid of some // audio glitches which are confirmed to be in the original. So let's activate this // fix for the DOS version of LOOM as well, if enhancements are enabled. if (_vm->_game.id == GID_LOOM && (_hasFileBasedCDAudio || _vm->enhancementEnabled(kEnhMinorBugFixes))) interval = 1000000 / LOOM_STEAM_CDDA_RATE; _vm->getTimerManager()->removeTimerProc(&cdTimerHandler); _vm->getTimerManager()->installTimerProc(&cdTimerHandler, interval, this, "scummCDtimer"); } void SoundCD::stopCDTimer() { if (_useReplacementAudioTracks) return; _vm->getTimerManager()->removeTimerProc(&cdTimerHandler); } int SoundCD::playCDTrackFromSoundID(int soundId) { int loops = 1; int start = 0; int end = 0; int trackNr = getCDTrackIdFromSoundId(soundId, loops, start, end); _currentCDSound = soundId; playCDTrack(trackNr, loops == 0xff ? -1 : loops, start, end <= start ? 0 : end - start); return trackNr; } void SoundCD::playCDTrack(int track, int numLoops, int startFrame, int duration) { // Reset the music timer variable at the start of a new track _vm->VAR(_vm->VAR_MUSIC_TIMER) = 0; _cdMusicTimerMod = 0; _cdMusicTimer = 0; // Play it if (!_soundsPaused) playCDTrackInternal(track, numLoops, startFrame, duration); // Start the timer after starting the track. Starting an MP3 track is // almost instantaneous, but a CD player may take some time. Hopefully // playCD() will block during that delay. startCDTimer(); } void SoundCD::playCDTrackInternal(int track, int numLoops, int startFrame, int duration) { _fileBasedCDStatus.track = track; _fileBasedCDStatus.numLoops = numLoops; _fileBasedCDStatus.start = startFrame; _fileBasedCDStatus.duration = duration; if (!_hasFileBasedCDAudio) { g_system->getAudioCDManager()->play(track, numLoops, startFrame, duration); } else { // Stop any currently playing track _mixer->stopHandle(*_fileBasedCDAudioHandle); Audio::SeekableAudioStream *stream = nullptr; if (_vm->_game.id == GID_LOOM) { stream = makeCDDAStream("CDDA.SOU", DisposeAfterUse::YES); } else if (_soundSE) { stream = _soundSE->getAudioStreamFromIndex(track, kSoundSETypeCDAudio); } if (!stream) return; Audio::Timestamp start = Audio::Timestamp(0, startFrame, 75); Audio::Timestamp end = Audio::Timestamp(0, startFrame + duration, 75); _mixer->playStream(Audio::Mixer::kMusicSoundType, _fileBasedCDAudioHandle, Audio::makeLoopingAudioStream(stream, start, end, (numLoops < 1) ? numLoops + 1 : numLoops)); } } void SoundCD::stopCD() { if (!_hasFileBasedCDAudio) g_system->getAudioCDManager()->stop(); else _mixer->stopHandle(*_fileBasedCDAudioHandle); } int SoundCD::pollCD() const { if (!_hasFileBasedCDAudio) return g_system->getAudioCDManager()->isPlaying(); else return _mixer->isSoundHandleActive(*_fileBasedCDAudioHandle); } void SoundCD::updateCD() { if (!_hasFileBasedCDAudio) g_system->getAudioCDManager()->update(); } AudioCDManager::Status SoundCD::getCDStatus() { if (!_hasFileBasedCDAudio) return g_system->getAudioCDManager()->getStatus(); else { AudioCDManager::Status info = _fileBasedCDStatus; info.playing = _mixer->isSoundHandleActive(*_fileBasedCDAudioHandle); return info; } } int SoundCD::getCDTrackIdFromSoundId(int soundId, int &loops, int &start, int &end) { if (_vm->_game.id == GID_LOOM && _vm->_game.version == 4) { loops = 0; start = -1; return 1; } if (soundId != -1 && _vm->getResourceAddress(rtSound, soundId)) { uint8 *ptr = _vm->getResourceAddress(rtSound, soundId) + 0x18; int track = ptr[0]; loops = ptr[1]; start = (ptr[2] * 60 + ptr[3]) * 75 + ptr[4]; end = (ptr[5] * 60 + ptr[6]) * 75 + ptr[7]; // Add the user-specified adjustments. if (_vm->_game.id == GID_MONKEY && track == 17) { int adjustment = ConfMan.getInt(start == 0 ? "mi1_intro_adjustment" : "mi1_outlook_adjustment"); start += ((75 * adjustment) / 100); } return track; } loops = 1; return -1; } bool SoundCD::isRolandLoom() const { return (_vm->_game.id == GID_LOOM) && (_vm->_game.version == 3) && (_vm->_game.platform == Common::kPlatformDOS) && (_vm->VAR(_vm->VAR_SOUNDCARD) == 4); } #define JIFFIES_TO_TICKS(x) (40 * ((double)(x)) / _vm->getTimerFrequency()) #define TICKS_TO_JIFFIES(x) ((double)(x) * (_vm->getTimerFrequency() / 40)) #define TICKS_TO_TIMER(x) ((((x)*204) / _loomOvertureTransition) + 1) #define TIMER_TO_TICKS(x) ((((x)-1) * _loomOvertureTransition) / 204) void SoundCD::updateMusicTimer() { bool isLoomOverture = (isRolandLoom() && _currentCDSound == 56 && !(_vm->_game.features & GF_DEMO)); // If the replacement track has ended, reset the timer to 0 like when // playing the original music. We make an exception for the Overture, // since it may need to keep running after the track has ended. // // This is also why we can't query the CD audio manager for the current // position. That, and the fact that the CD manager does not provide // this information at the time of writing. if (!pollCD() && !isLoomOverture) { _currentCDSound = 0; _musicTimer = 0; _replacementTrackStartTime = 0; return; } // Time is measured in "ticks", with ten ticks per second. This should // be exact enough, while providing an easily understandable unit of // measurement for the adjustment slider. // The rate at which the timer is advanced is hard-coded for the Loom // Overture. When playing the original music the rate is apparently // based on the MIDI tempo of it. But at least for Loom, the Overture // seems to be the only piece of music where timing matters. // These are the values the timer will have to reach or exceed for the // Overture to work correctly: // 4 - Fade in the "OVERTURE" text // 198 - Fade down the "OVERTURE" text // 204 - Show the LucasFilm logo // 278 - End the Overture // VAR_TOTAL_TIMER measures time in "jiffies", or frames. This will // eventually overflow, but I don't expect that to ever be a problem. int32 now = _vm->VAR(_vm->VAR_TIMER_TOTAL); int32 ticks = JIFFIES_TO_TICKS(now - _replacementTrackStartTime); // If the track ends before the timer reaches 198, skip ahead. (If the // timer didn't even reach 4 you weren't really trying, and must be // punished for that!) if (isLoomOverture && !pollCD()) { int32 fadeDownTick = TIMER_TO_TICKS(198); if (ticks < fadeDownTick) { _replacementTrackStartTime = now - TICKS_TO_JIFFIES(fadeDownTick); ticks = fadeDownTick; } } _musicTimer = TICKS_TO_TIMER(ticks); // But don't let the timer exceed 278 until the Overture has ended, or // the music will be cut off. if (isLoomOverture && pollCD() && _musicTimer >= 278) _musicTimer = 277; } int SoundCD::restoreAfterLoad() { int trackNr = -1; if (_currentCDSound) { if (_useReplacementAudioTracks) { trackNr = getReplacementAudioTrack(_currentCDSound); } else if (_vm->_game.platform != Common::kPlatformFMTowns) { int loops, start, end; trackNr = getCDTrackIdFromSoundId(_currentCDSound, loops, start, end); } } return trackNr; } void SoundCD::restoreCDAudioAfterLoad(AudioCDManager::Status &info) { // FM-Towns versions handle this in Player_Towns_v1::restoreAfterLoad(). if (_vm->_game.platform == Common::kPlatformFMTowns) return; int track, numLoops, start, duration; // FIXME: We still have a problem if the user saves more than once during // the same sound, because then info.start no longer refers to the original // start of the sound. We can fix that for some games, but probably not // where it really counts, i.e. VGA Loom. if (_currentCDSound) { int end; track = getCDTrackIdFromSoundId(_currentCDSound, numLoops, start, end); duration = (end <= start) ? 0 : end - start; } else { track = info.track; numLoops = info.numLoops; start = info.start; duration = info.duration; } // We only try to restore the position of the track if it's a non-looping // sound. (This is particularly important for the talkie version of Loom.) // For looping sounds, the timer may point beyond the end of the track. // And even if it's not, the next loop would start from that point, rather // than the beginning of the track. if (numLoops == 1) { int musicTimer = _vm->VAR(_vm->VAR_MUSIC_TIMER); if (_useReplacementAudioTracks) { // Special handling for Loom with replacement audio tracks. There // the music timer is mentioned in "ticks" (tenths of a second), // and the "start" field may have been modified if we're saving // more than once during the same track. start = (75 * TIMER_TO_TICKS(musicTimer)) / 10; } else { // The one game where timing really matters is probably the VGA // version of Loom. We move the offset forward by a little bit to // restore the correct sync. (At least that's what the old version // of this code said, and who am I to argue?) start += (int)(musicTimer * 1.25); if (duration) duration -= musicTimer; } } playCDTrackInternal(track, numLoops, start, duration); } bool SoundCD::triggerCDSound(int soundID) { if (_useReplacementAudioTracks) { // Note that music does not loop. Probably because it's likely // to be interrupted by sound effects before it's over anyway. // // In the FM Towns version, music does play continuously (each // track has two versions), probably because CD audio and sound // effects are played independent of each other. Personally I // find the game harder when the music is allowed to drown out // the sound effects. int trackNr = getReplacementAudioTrack(soundID); if (trackNr != -1) { _currentCDSound = soundID; _replacementTrackStartTime = _vm->VAR(_vm->VAR_TIMER_TOTAL); _musicTimer = 0; g_system->getAudioCDManager()->play(trackNr, 1, 0, 0, true); return true; } } if (_vm->_game.id == GID_LOOM && _vm->_game.platform == Common::kPlatformPCEngine) { if (soundID >= 13 && soundID <= 32) { static const char tracks[20] = {3, 4, 5, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 19, 20, 21}; _currentCDSound = soundID; // The original game had hard-coded lengths for all // tracks, but this one track is the only one (as far // as we know) where this actually matters. See bug // #4914 - LOOM-PCE: Music stops prematurely. int track = tracks[soundID - 13]; if (track == 6) { playCDTrack(track, 1, 0, 260); } else { playCDTrack(track, 1, 0, 0); } } else { if (_vm->_musicEngine) { _vm->_musicEngine->startSound(soundID); } } return true; } return false; } int SoundCD::getReplacementAudioTrack(int soundID) { int trackNr = -1; if (_vm->_game.id == GID_LOOM) { if (_vm->_game.features & GF_DEMO) { // If I understand correctly, the shorter demo only // has the Loom intro music. The longer demo has a // couple of tracks that it will cycle through if // you leave the demo running. if (isRolandLoom()) soundID -= 10; switch (soundID) { case 19: trackNr = 2; break; case 20: trackNr = 4; break; case 21: trackNr = 7; break; case 23: trackNr = 8; break; case 26: trackNr = 3; break; } } else { if (isRolandLoom()) soundID -= 32; // The first track, the Overture, only exists as a // Roland track. if (soundID >= 24 && soundID <= 32) { trackNr = soundID - 23; } else if (soundID == 19) { trackNr = 10; } else if (soundID == 21) { trackNr = 11; } } } if (trackNr != -1 && !_vm->existExtractedCDAudioFiles(trackNr)) trackNr = -1; return trackNr; } void SoundCD::pauseCDSounds(bool pause) { if ((_vm->_game.features & GF_AUDIOTRACKS) && _vm->VAR_MUSIC_TIMER != 0xFF && _vm->VAR(_vm->VAR_MUSIC_TIMER) > 0) { if (pause) stopCDTimer(); else startCDTimer(); } } } // End of namespace Scumm