/* 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/file.h" #include "common/memstream.h" #include "audio/mixer.h" #include "audio/audiostream.h" #include "audio/decoders/wave.h" #include "bagel/bagel.h" #include "bagel/boflib/sound.h" #include "bagel/boflib/string_functions.h" #include "bagel/boflib/event_loop.h" #include "bagel/boflib/file_functions.h" #include "bagel/boflib/log.h" #include "bagel/music.h" //#include "bagel/spacebar/boflib/app.h" namespace Bagel { #define MEMORY_THRESHOLD 20000L #define MEMORY_MARGIN 100000L char CBofSound::_szDrivePath[MAX_DIRPATH]; CBofSound *CBofSound::_pSoundChain = nullptr; // Pointer to chain of linked Sounds int CBofSound::_nCount = 0; // Count of currently active Sounds int CBofSound::_nWavCount = 1; // Available wave sound devices int CBofSound::_nMidiCount = 1; // Available midi sound devices void *CBofSound::_pMainWnd = nullptr; // Window for message processing bool CBofSound::_bInit = false; CQueue *CBofSound::_cQueue[NUM_QUEUES]; int CBofSound::_nSlotVol[NUM_QUEUES]; CBofSound::CBofSound() { _wLoops = 1; addToSoundChain(); } CBofSound::CBofSound(void *pWnd, const char *pszPathName, uint16 wFlags, const int nLoops) { _wLoops = (uint16)nLoops; initialize(pWnd, pszPathName, wFlags); addToSoundChain(); } void CBofSound::addToSoundChain() { // Insert this sound into the sound list if (_pSoundChain != nullptr) { _pSoundChain->Insert(this); // _pSoundchain must always be the head of the list assert(_pSoundChain == _pSoundChain->getHead()); } else { _pSoundChain = this; } } void CBofSound::initialize(void *pWnd, const char *pszPathName, uint16 wFlags) { // Validate input assert(pszPathName != nullptr); assert(strlen(pszPathName) < MAX_FNAME); // // Initialize data fields // _pWnd = _pMainWnd; if (pWnd != nullptr) { _pWnd = pWnd; if (_pMainWnd == nullptr) _pMainWnd = pWnd; } _bPlaying = false; // Not yet playing _bStarted = false; _wFlags = wFlags; // Flags for playing _bPaused = false; // Not suspended _bExtensionsUsed = false; // No extended flags used. _szFileName[0] = '\0'; _handle = {}; _pFileBuf = nullptr; _nVol = VOLUME_INDEX_DEFAULT; _bInQueue = false; _iQSlot = 0; for (int i = 0; i < NUM_QUEUES; i++) { _nSlotVol[i] = VOLUME_INDEX_DEFAULT; } // Mixed assumes Asynchronous if ((_wFlags & SOUND_MIX) == SOUND_MIX) { _wFlags |= SOUND_ASYNCH; } if (_wFlags & SOUND_MIDI) { _chType = g_engine->isSpaceBar() ? SOUND_TYPE_XM : SOUND_TYPE_SMF; } else { _chType = SOUND_TYPE_WAV; } if (pszPathName != nullptr && (_wFlags & SND_MEMORY)) { // In-memory wave sound _pFileBuf = (byte *)const_cast(pszPathName); _iFileSize = 999999; } else if (pszPathName != nullptr) { if ((_szDrivePath[0] != '\0') && (*pszPathName == '.')) pszPathName++; else if (!strncmp(pszPathName, ".\\", 2)) pszPathName += 2; char szTempPath[MAX_DIRPATH]; snprintf(szTempPath, MAX_DIRPATH, "%s%s", _szDrivePath, pszPathName); strreplaceStr(szTempPath, "\\\\", "\\"); strreplaceStr(szTempPath, "\\", "/"); // Continue as long as this file exists if (fileExists(szTempPath)) { fileGetFullPath(_szFileName, szTempPath); if (!(_wFlags & SOUND_QUEUE)) { if (_wFlags & SOUND_WAVE || _wFlags & SOUND_MIX) { loadSound(); } } } else if (_wFlags & SOUND_MIDI) { // Try both MIDI formats strreplaceStr(szTempPath, ".MID", ".MOV"); if (fileExists(szTempPath)) { fileGetFullPath(_szFileName, szTempPath); _chType = SOUND_TYPE_QT; } else { reportError(ERR_FFIND, szTempPath); } } else { reportError(ERR_FFIND, szTempPath); } } } CBofSound::~CBofSound() { assert(isValidObject(this)); stop(); releaseSound(); if (this == _pSoundChain) { // special case head of chain _pSoundChain = (CBofSound *)_pSoundChain->getNext(); } } void CBofSound::initialize() { for (int i = 0; i < NUM_QUEUES; ++i) _cQueue[i] = new CQueue(); resetQVolumes(); } void CBofSound::resetQVolumes() { // Set Q Volumes to default for (int i = 0; i < NUM_QUEUES; i++) { _nSlotVol[i] = VOLUME_INDEX_DEFAULT; } } void CBofSound::shutdown() { // Auto-delete any remaining sounds clearSounds(); for (int i = 0; i < NUM_QUEUES; ++i) delete _cQueue[i]; } void CBofSound::setVolume(int nVolume) { assert(nVolume >= VOLUME_INDEX_MIN && nVolume <= VOLUME_INDEX_MAX); _nVol = CLIP(nVolume, VOLUME_INDEX_MIN, VOLUME_INDEX_MAX); Audio::Mixer *mixer = g_system->getMixer(); byte vol = VOLUME_SVM(_nVol); mixer->setChannelVolume(_handle, vol); mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, vol); mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, vol); g_engine->_midi->setVolume(vol); } void CBofSound::setVolume(int nMidiVolume, int nWaveVolume) { assert(nMidiVolume >= VOLUME_INDEX_MIN && nMidiVolume <= VOLUME_INDEX_MAX); assert(nWaveVolume >= VOLUME_INDEX_MIN && nWaveVolume <= VOLUME_INDEX_MAX); // Set master wave volume int clippedVol = CLIP(nWaveVolume, VOLUME_INDEX_MIN, VOLUME_INDEX_MAX); g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, VOLUME_SVM(clippedVol)); // Set master Midi volume clippedVol = CLIP(nMidiVolume, VOLUME_INDEX_MIN, VOLUME_INDEX_MAX); g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, VOLUME_SVM(clippedVol)); g_engine->_midi->setVolume(VOLUME_SVM(clippedVol)); } bool CBofSound::play(uint32 dwBeginHere, uint32 TimeFormatFlag) { assert(isValidObject(this)); // Assume success bool bSuccess = true; if (_errCode == ERR_NONE) { // We must be attached to a valid window assert((_pWnd != nullptr) || (_pMainWnd != nullptr)); // If already playing, then stop and start again if (playing()) { // Can't replay an autodelete sound assert(!(_wFlags & SOUND_AUTODELETE)); stop(); } // WAVE and MIX are mutually exclusive assert(!((_wFlags & SOUND_WAVE) && (_wFlags & SOUND_MIX))); if (_wFlags & SOUND_WAVE) { if (_wFlags & SOUND_QUEUE) waitWaveSounds(); else stopWaveSounds(); } else if (_wFlags & SOUND_MIDI) { if (_wFlags & SOUND_QUEUE) waitMidiSounds(); else stopMidiSounds(); } // Make sure this sound is still valid assert(_pSoundChain != nullptr); if (_pFileBuf == nullptr) { if ((_wFlags & (SOUND_MIX | SOUND_QUEUE)) == (SOUND_MIX | SOUND_QUEUE)) { // Don't pre-load it } else { loadSound(); } } if (_wFlags & SOUND_MIDI) { if (_wFlags & SOUND_LOOP) _wLoops = 0; g_engine->_midi->play(this); _bPlaying = true; } else if (_wFlags & SOUND_WAVE) { playWAV(); if (_bPlaying) { if (!(_wFlags & SOUND_ASYNCH)) { EventLoop eventLoop; auto *mixer = g_system->getMixer(); while (mixer->isSoundHandleActive(_handle)) { if (eventLoop.frame()) { stop(); break; } } _handle = {}; _bPlaying = false; } } } else if (_wFlags & SOUND_MIX) { if (!(_wFlags & SOUND_QUEUE)) { playWAV(); } else { assert(_iQSlot >= 0 && _iQSlot < NUM_QUEUES); _cQueue[_iQSlot]->addItem(this); _bPlaying = true; _bInQueue = true; setVolume(_nSlotVol[_iQSlot]); } } } return bSuccess; } bool CBofSound::midiLoopPlaySegment(uint32 dwLoopFrom, uint32 dwLoopTo, uint32 dwBegin, uint32 TimeFmt) { assert(isValidObject(this)); _wFlags |= SOUND_LOOP; _wLoops = 0; _dwRePlayStart = dwLoopFrom; _dwRePlayEnd = dwLoopTo; _bExtensionsUsed = true; bool bSuccess = play(dwBegin, TimeFmt); return bSuccess; } bool CBofSound::pauseSounds() { bool bSuccess = true; // Thumb through all the sounds CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { // If one is playing and not paused try to suspend it if (pSound->playing() && (pSound->_bPaused == false)) { bool bStatus = pSound->pause(); if (bStatus) pSound->_bPaused = true; else bSuccess = false; } pSound = (CBofSound *)pSound->getNext(); } return bSuccess; } bool CBofSound::pause() { assert(isValidObject(this)); bool bSuccess = false; // Must be playing to be paused and not already paused if (playing() && (_bPaused == false)) { bSuccess = true; if (_wFlags & SOUND_MIDI) { g_engine->_midi->pause(); } else { g_system->getMixer()->pauseHandle(_handle, true); } } if (bSuccess) _bPaused = true; return bSuccess; } bool CBofSound::resumeSounds() { bool bSuccess = true; CBofSound *pSound = _pSoundChain; // Thumb through all the sounds while (pSound != nullptr) { if (pSound->_bPaused) { // If one is paused bool bStatus = pSound->resume(); // ... try to get it going again if (bStatus) pSound->_bPaused = false; // success else bSuccess = false; // failure } pSound = (CBofSound *)pSound->getNext(); } return bSuccess; } bool CBofSound::resume() { assert(isValidObject(this)); bool bSuccess = false; if (_bPaused) { // must be paused to resume bSuccess = true; if (_wFlags & SOUND_MIDI) { g_engine->_midi->resume(); } else { g_system->getMixer()->pauseHandle(_handle, false); } } if (bSuccess) _bPaused = false; return bSuccess; } void CBofSound::stopSounds() { CBofSound *pSound = _pSoundChain; // Thumb through all the sounds while (pSound != nullptr) { if (pSound->playing()) { // If one is playing pSound->_bPaused = false; // ... its no longer paused pSound->stop(); // Stop it } pSound = (CBofSound *)pSound->getNext(); } } void CBofSound::stopWaveSounds() { CBofSound *pSound = _pSoundChain; // Find this Sound is the queue while (pSound != nullptr) { CBofSound *pNextSound = (CBofSound *)pSound->getNext(); if (pSound->playing() && ((pSound->_wFlags & SOUND_WAVE) || (pSound->_wFlags & SOUND_MIX))) { pSound->stop(); if (pSound->_wFlags & SOUND_AUTODELETE) delete pSound; } pSound = pNextSound; } } void CBofSound::stopMidiSounds() { CBofSound *pSound = _pSoundChain; // Find this Sound is the queue while (pSound != nullptr) { CBofSound *pNextSound = (CBofSound *)pSound->getNext(); if (pSound->playing() && (pSound->_wFlags & SOUND_MIDI)) { pSound->stop(); if (pSound->_wFlags & SOUND_AUTODELETE) delete pSound; } pSound = pNextSound; } } void CBofSound::stop() { assert(isValidObject(this)); // If this sound is currently playing if (_wFlags & SOUND_MIDI) { g_engine->_midi->stop(); } else { g_system->getMixer()->stopHandle(_handle); _handle = {}; } if (_bInQueue) { assert(_iQSlot >= 0 && _iQSlot < NUM_QUEUES); _cQueue[_iQSlot]->deleteItem(this); _bInQueue = false; } _bPlaying = false; _bStarted = false; _bPaused = false; // One less audio playing _nCount -= 1; } void CBofSound::clearSounds() { stopSounds(); // Stop all active sounds CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { // Delete all sound entries CBofSound *pNextSound = pSound->getNext(); delete pSound; pSound = pNextSound; } assert(_pSoundChain == nullptr); } void CBofSound::clearWaveSounds() { CBofSound *pSound = _pSoundChain; // Find this Sound in the queue while (pSound != nullptr) { CBofSound *pNextSound = pSound->getNext(); if ((pSound->_wFlags & SOUND_WAVE) || (pSound->_wFlags & SOUND_MIX)) delete pSound; pSound = pNextSound; } } void CBofSound::clearMidiSounds() { CBofSound *pSound = _pSoundChain; // find this Sound is the queue while (pSound != nullptr) { CBofSound *pNextSound = (CBofSound *)pSound->getNext(); if (pSound->_wFlags & SOUND_MIDI) delete pSound; pSound = pNextSound; } } void CBofSound::waitSounds() { waitWaveSounds(); waitMidiSounds(); } void CBofSound::waitWaveSounds() { uint32 dwTickCount = 0; for (;;) { audioTask(); CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->playing() && (pSound->_wFlags & SOUND_WAVE || pSound->_wFlags & SOUND_MIX)) { break; } pSound = (CBofSound *)pSound->getNext(); } if (pSound == nullptr) break; if (handleMessages()) break; // If 3 minutes go by, then just bolt if (dwTickCount == 0) dwTickCount = g_system->getMillis() + 1000 * 60 * 3; if (g_system->getMillis() > dwTickCount) { pSound->stop(); pSound->_bPlaying = false; pSound->reportError(ERR_UNKNOWN, "CBofSound::waitWaveSounds() timeout!"); } } } bool CBofSound::soundsPlaying() { audioTask(); CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->playing() && (pSound->_wFlags & SOUND_WAVE || pSound->_wFlags & SOUND_MIX)) { return true; } pSound = (CBofSound *)pSound->getNext(); } return false; } void CBofSound::waitMidiSounds() { uint32 dwTickCount = 0; for (;;) { CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->playing() && (pSound->_wFlags & SOUND_MIDI)) { break; } pSound = (CBofSound *)pSound->getNext(); } if (pSound == nullptr) break; if (handleMessages()) break; if (dwTickCount == 0) dwTickCount = g_system->getMillis() + 1000 * 60; if (g_system->getMillis() > dwTickCount) { pSound->stop(); pSound->reportError(ERR_UNKNOWN, "CBofSound::waitMidiSounds() timeout"); break; } } } bool CBofSound::handleMessages() { if (g_engine->isSpaceBar()) { Common::Event e; while (g_system->getEventManager()->pollEvent(e)) { } g_system->delayMillis(10); } else { AfxGetApp()->pause(); } return g_engine->shouldQuit(); } bool CBofSound::bofSleep(uint32 wait) { uint32 goal = wait + g_system->getMillis(); while (goal > g_system->getMillis()) { if (handleMessages()) return true; } return false; } bool BofPlaySound(const char *pszSoundFile, uint32 nFlags, int iQSlot) { // Assume failure bool bSuccess; if (pszSoundFile != nullptr) { nFlags |= SOUND_AUTODELETE; if (nFlags & SND_MEMORY) { // Hardcoded for wave files nFlags |= SOUND_WAVE; } else if (!fileExists(pszSoundFile)) { logWarning(buildString("Sound File '%s' not found", pszSoundFile)); return false; } // WORKAROUND: Hodj may not specify sound type if (!(nFlags & (SOUND_WAVE | SOUND_MIDI))) { Common::String name(pszSoundFile); if (name.hasSuffixIgnoreCase(".wav")) nFlags |= SOUND_WAVE; else if (name.hasSuffixIgnoreCase(".mid")) nFlags |= SOUND_MIDI; } // Take care of any last minute cleanup before we start this new sound CBofSound::audioTask(); CBofSound::stopWaveSounds(); CBofSound *pSound = new CBofSound(/*pWnd*/nullptr, pszSoundFile, (uint16)nFlags); if ((nFlags & SOUND_QUEUE) == SOUND_QUEUE) { pSound->setQSlot(iQSlot); } bSuccess = pSound->play(); } else { bSuccess = true; CBofSound::stopWaveSounds(); } return bSuccess; } bool BofPlaySoundEx(const char *pszSoundFile, uint32 nFlags, int iQSlot, bool bWait) { // Assume failure bool bSuccess = false; if (pszSoundFile != nullptr) { if ((nFlags & SOUND_MIX) == 0) { bWait = false; } if (!bWait) { nFlags |= SOUND_AUTODELETE; } if (!fileExists(pszSoundFile)) { logWarning(buildString("Sound File '%s' not found", pszSoundFile)); return false; } //CBofWindow *pWnd = CBofApp::getApp()->getMainWindow(); // Take care of any last minute cleanup before we start this new sound CBofSound::audioTask(); CBofSound *pSound = new CBofSound(/*pWnd*/nullptr, pszSoundFile, (uint16)nFlags); if ((nFlags & SOUND_QUEUE) == SOUND_QUEUE) { pSound->setQSlot(iQSlot); } bSuccess = pSound->play(); if (bWait) { while (pSound->isPlaying()) { CBofSound::audioTask(); } delete pSound; } } return bSuccess; } bool CBofSound::loadSound() { assert(isValidObject(this)); assert(_szFileName[0] != '\0'); bool bSuccess = true; if (_pFileBuf == nullptr) { bSuccess = false; Common::File in; if (in.open(_szFileName)) { _iFileSize = in.size(); _pFileBuf = (byte *)malloc(_iFileSize); if (in.read(_pFileBuf, _iFileSize) == _iFileSize) bSuccess = true; } } return bSuccess; } bool CBofSound::releaseSound() { assert(isValidObject(this)); if (!(_wFlags & SND_MEMORY)) free(_pFileBuf); _pFileBuf = nullptr; return true; } bool CBofSound::soundsPlayingNotOver() { // Assume no wave sounds are playing bool bPlaying = false; // Walk through sound list, and check for sounds that need attention CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->playing() && (pSound->_wFlags & SOUND_WAVE || pSound->_wFlags & SOUND_MIX) && !(pSound->_wFlags & SOUND_OVEROK)) { bPlaying = true; break; } pSound = (CBofSound *)pSound->getNext(); } return bPlaying; } bool CBofSound::waveSoundPlaying() { // Assume no wave sounds are playing bool bPlaying = false; // Walk through sound list, and check for sounds that need attention CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->playing() && (pSound->_wFlags & SOUND_WAVE || pSound->_wFlags & SOUND_MIX)) { bPlaying = true; break; } pSound = (CBofSound *)pSound->getNext(); } return bPlaying; } bool CBofSound::midiSoundPlaying() { // Assume no wave sounds are playing bool bPlaying = false; // Walk through sound list, and check for sounds that need attention CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->playing() && (pSound->_wFlags & SOUND_MIDI)) { bPlaying = true; break; } pSound = (CBofSound *)pSound->getNext(); } return bPlaying; } void CBofSound::audioTask() { static bool bAlready = false; // Don't allow recursion here assert(!bAlready); bAlready = true; // Walk through sound list, and check for sounds that need attention CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (!pSound->paused()) { if ((pSound->_wFlags & SOUND_WAVE) || (pSound->_wFlags & SOUND_MIX)) { // Has this sound been played? if (pSound->_bStarted) { // And, Is it done? if (!g_system->getMixer()->isSoundHandleActive(pSound->_handle)) { // Kill it pSound->stop(); } } else if (pSound->_bInQueue) { // If this is a Queued sound, and has not already started // And it is time to play if ((CBofSound *)_cQueue[pSound->_iQSlot]->getQItem() == pSound) { pSound->playWAV(); } } } else if ((pSound->_wFlags & SOUND_MIDI) && pSound->_bPlaying) { // And, Is it done? if (!g_engine->_midi->isPlaying()) { // Kill it pSound->stop(); } } } pSound = (CBofSound *)pSound->getNext(); } bAlready = false; } ErrorCode CBofSound::playWAV() { assert(isValidObject(this)); if (!errorOccurred()) { // If it's not yet loaded, then load it now if (_pFileBuf == nullptr) { loadSound(); } assert(_pFileBuf != nullptr); if (_pFileBuf != nullptr) { if (_bInQueue) { setVolume(_nSlotVol[_iQSlot]); } // Then, Play it Common::SeekableReadStream *stream = new Common::MemoryReadStream(_pFileBuf, _iFileSize); Audio::AudioStream *audio = Audio::makeLoopingAudioStream(Audio::makeWAVStream(stream, DisposeAfterUse::YES), _wLoops); if (audio == nullptr) { reportError(ERR_UNKNOWN, "Could not allocate audio stream."); } g_system->getMixer()->playStream(Audio::Mixer::kSFXSoundType, &_handle, audio, -1, VOLUME_SVM(_nVol)); _bPlaying = true; _bStarted = true; } } return _errCode; } ErrorCode CBofSound::flushQueue(int nSlot) { assert(nSlot >= 0 && nSlot < NUM_QUEUES); // Assume no error ErrorCode errorCode = ERR_NONE; // Remove all queued sounds _cQueue[nSlot]->flush(); // Including any that are currently playing CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { // Prefetch next sound in case stop() deletes this one CBofSound *pNextSound = pSound->getNext(); // If this sound is playing from specified queue if (pSound->isPlaying() && pSound->_bInQueue && pSound->_iQSlot == nSlot) { // Then chuck it pSound->stop(); } // Next pSound = pNextSound; } return errorCode; } void CBofSound::setQVol(int nSlot, int nVol) { // Validate input assert(nSlot >= 0 && nSlot < NUM_QUEUES); assert(nVol >= 0 && nVol <= VOLUME_INDEX_MAX); _nSlotVol[nSlot] = nVol; // Set all Queued sounds in specified slot to this volume CBofSound *pSound = _pSoundChain; while (pSound != nullptr) { if (pSound->_bInQueue && pSound->_iQSlot == nSlot) { pSound->setVolume(nVol); } pSound = pSound->getNext(); } } bool MessageBeep(int uType) { warning("TODO: beep"); return true; } } // namespace Bagel