mirror of
https://github.com/scummvm/scummvm.git
synced 2026-06-20 05:45:29 +00:00
518 lines
13 KiB
C++
518 lines
13 KiB
C++
/* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "lastexpress/sound/entry.h"
|
|
|
|
#include "lastexpress/game/logic.h"
|
|
#include "lastexpress/game/savepoint.h"
|
|
#include "lastexpress/game/state.h"
|
|
|
|
#include "lastexpress/sound/queue.h"
|
|
#include "lastexpress/sound/sound.h"
|
|
|
|
#include "lastexpress/graphics.h"
|
|
#include "lastexpress/lastexpress.h"
|
|
#include "lastexpress/resource.h"
|
|
|
|
namespace LastExpress {
|
|
|
|
#define SOUNDCACHE_ENTRY_SIZE 92160
|
|
#define FILTER_BUFFER_SIZE 2940
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// SoundEntry
|
|
//////////////////////////////////////////////////////////////////////////
|
|
SoundEntry::SoundEntry(LastExpressEngine *engine) : _engine(engine) {
|
|
_status = 0;
|
|
_tag = kSoundTagNone;
|
|
|
|
_blockCount = 0;
|
|
_startTime = 0;
|
|
|
|
_stream = nullptr;
|
|
|
|
_volumeWithoutNIS = 0;
|
|
_entity = kEntityPlayer;
|
|
_initTimeMS = 0;
|
|
_activateDelayMS = 0;
|
|
_priority = 0;
|
|
|
|
_subtitle = nullptr;
|
|
|
|
_soundStream = nullptr;
|
|
}
|
|
|
|
SoundEntry::~SoundEntry() {
|
|
// Entries that have been queued will have their streamed disposed automatically
|
|
if (!_soundStream)
|
|
SAFE_DELETE(_stream);
|
|
|
|
SAFE_DELETE(_soundStream);
|
|
|
|
_subtitle = nullptr;
|
|
_stream = nullptr;
|
|
|
|
// Zero passed pointers
|
|
_engine = nullptr;
|
|
}
|
|
|
|
void SoundEntry::open(Common::String name, SoundFlag flag, int priority) {
|
|
_priority = priority;
|
|
setupTag(flag);
|
|
setupStatus(flag);
|
|
loadStream(name);
|
|
}
|
|
|
|
void SoundEntry::close() {
|
|
if (_soundStream) {
|
|
delete _soundStream; // stops the sound in destructor
|
|
_soundStream = nullptr;
|
|
_stream = nullptr; // disposed by _soundStream
|
|
}
|
|
_status |= kSoundFlagClosed;
|
|
|
|
// The original game remove the entry from the cache here,
|
|
// but since we are called from within an iterator loop
|
|
// we will remove the entry there
|
|
// removeFromCache(entry);
|
|
|
|
if (_subtitle) {
|
|
_subtitle->close();
|
|
SAFE_DELETE(_subtitle);
|
|
}
|
|
|
|
if (_entity) {
|
|
if (_entity == kEntitySteam)
|
|
getSound()->playAmbientSound(2);
|
|
else if (_entity != kEntityTrain)
|
|
getSavePoints()->push(kEntityPlayer, _entity, kActionEndSound);
|
|
}
|
|
}
|
|
|
|
void SoundEntry::play(uint32 startTime) {
|
|
if (_status & kSoundFlagClosed)
|
|
return; // failed to load sound file
|
|
|
|
if (!_stream)
|
|
error("[SoundEntry::play] stream has been disposed");
|
|
|
|
// Prepare sound stream
|
|
if (_soundStream)
|
|
error("[SoundEntry::play] already playing");
|
|
|
|
// BUG: the original game never checks for sound type when loading subtitles.
|
|
// NIS files and LNK files have the same base name,
|
|
// so without extra caution NIS subtitles would be loaded for LNK sounds as well.
|
|
// The original game instead separates calls to play() and setSubtitles()
|
|
// and does not call setSubtitles() for linked-after sounds.
|
|
// Unfortunately, that does not work well with save/load.
|
|
if ((_status & kSoundTypeMask) != kSoundTypeLink && (_status & kSoundTypeMask) != kSoundTypeConcert) {
|
|
// Get subtitles name
|
|
uint32 size = (_name.size() > 4 ? _name.size() - 4 : _name.size());
|
|
setSubtitles(Common::String(_name.c_str(), size));
|
|
}
|
|
|
|
_soundStream = new StreamedSound();
|
|
|
|
_stream->seek(0);
|
|
|
|
// Load the stream and start playing
|
|
_soundStream->load(_stream, _status & kSoundVolumeMask, (_status & kSoundFlagLooped) != 0, startTime);
|
|
|
|
_status |= kSoundFlagPlaying;
|
|
}
|
|
|
|
void SoundEntry::setupTag(SoundFlag flag) {
|
|
switch (flag & kSoundTypeMask) {
|
|
case kSoundTypeNormal:
|
|
_tag = getSoundQueue()->generateNextTag();
|
|
break;
|
|
|
|
case kSoundTypeAmbient: {
|
|
SoundEntry *previous2 = getSoundQueue()->getEntry(kSoundTagOldAmbient);
|
|
if (previous2)
|
|
previous2->fade();
|
|
|
|
SoundEntry *previous = getSoundQueue()->getEntry(kSoundTagAmbient);
|
|
if (previous) {
|
|
previous->_tag = kSoundTagOldAmbient;
|
|
previous->fade();
|
|
}
|
|
|
|
_tag = kSoundTagAmbient;
|
|
}
|
|
break;
|
|
|
|
case kSoundTypeWalla: {
|
|
SoundEntry *previous = getSoundQueue()->getEntry(kSoundTagWalla);
|
|
if (previous) {
|
|
previous->_tag = kSoundTagOldWalla;
|
|
previous->fade();
|
|
}
|
|
|
|
_tag = kSoundTagWalla;
|
|
}
|
|
break;
|
|
|
|
case kSoundTypeLink: {
|
|
SoundEntry *previous = getSoundQueue()->getEntry(kSoundTagLink);
|
|
if (previous)
|
|
previous->_tag = kSoundTagOldLink;
|
|
|
|
_tag = kSoundTagLink;
|
|
}
|
|
break;
|
|
|
|
case kSoundTypeNIS: {
|
|
SoundEntry *previous = getSoundQueue()->getEntry(kSoundTagNIS);
|
|
if (previous)
|
|
previous->_tag = kSoundTagOldNIS;
|
|
|
|
_tag = kSoundTagNIS;
|
|
}
|
|
break;
|
|
|
|
case kSoundTypeIntro: {
|
|
SoundEntry *previous = getSoundQueue()->getEntry(kSoundTagIntro);
|
|
if (previous)
|
|
previous->_tag = kSoundTagOldMenu;
|
|
|
|
_tag = kSoundTagIntro;
|
|
}
|
|
break;
|
|
|
|
case kSoundTypeMenu: {
|
|
SoundEntry *previous = getSoundQueue()->getEntry(kSoundTagMenu);
|
|
if (previous)
|
|
previous->_tag = kSoundTagOldMenu;
|
|
|
|
_tag = kSoundTagMenu;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SoundEntry::setupStatus(SoundFlag flag) {
|
|
_status = flag;
|
|
|
|
// set the flag for the case that our savefiles
|
|
// will be ever loaded by the original game
|
|
if (!(_status & kSoundFlagLooped))
|
|
_status |= kSoundFlagCloseOnDataEnd;
|
|
}
|
|
|
|
void SoundEntry::loadStream(Common::String name) {
|
|
_name = name;
|
|
|
|
// Load sound data
|
|
_stream = getArchive(name);
|
|
|
|
if (!_stream)
|
|
_stream = getArchive("DEFAULT.SND");
|
|
|
|
if (!_stream)
|
|
_status = kSoundFlagClosed;
|
|
|
|
// read total count of sound blocks for the case that our savefiles
|
|
// will be ever loaded by the original game
|
|
if (_stream) {
|
|
_stream->readUint32LE();
|
|
_blockCount = _stream->readUint16LE();
|
|
_status |= kSoundFlagHeaderProcessed;
|
|
}
|
|
}
|
|
|
|
void SoundEntry::setVolumeSmoothly(SoundFlag newVolume) {
|
|
assert((newVolume & kSoundVolumeMask) == newVolume);
|
|
|
|
if (_status & kSoundFlagFading)
|
|
return;
|
|
|
|
// the original game sets kSoundFlagVolumeChanging here
|
|
uint32 requestedVolume = (uint32)newVolume;
|
|
|
|
if (newVolume == kVolumeNone) {
|
|
_status |= kSoundFlagFading;
|
|
} else if (getSoundQueue()->getFlag() & 32) {
|
|
_volumeWithoutNIS = requestedVolume;
|
|
requestedVolume = requestedVolume / 2 + 1;
|
|
}
|
|
|
|
_status = (_status & ~kSoundVolumeMask) | requestedVolume;
|
|
if (_soundStream)
|
|
_soundStream->setVolumeSmoothly(requestedVolume);
|
|
}
|
|
|
|
bool SoundEntry::update() {
|
|
if (_soundStream && _soundStream->isFinished())
|
|
_status |= kSoundFlagClosed;
|
|
|
|
if (_status & kSoundFlagClosed)
|
|
return false;
|
|
|
|
if (_status & kSoundFlagDelayedActivate) {
|
|
// counter overflow is processed correctly
|
|
if (_engine->_system->getMillis() - _initTimeMS >= _activateDelayMS) {
|
|
_status &= ~kSoundFlagDelayedActivate;
|
|
play();
|
|
}
|
|
} else {
|
|
if (!(getSoundQueue()->getFlag() & 0x20)) {
|
|
if (!(_status & kSoundFlagFixedVolume)) {
|
|
if (_entity) {
|
|
if (_entity < 0x80) {
|
|
setVolume(getSound()->getSoundFlag(_entity));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//if (_status & kSoundFlagHasUnreadData && !(_status & kSoundFlagMute) && v1->soundBuffer)
|
|
// Sound_FillSoundBuffer(v1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SoundEntry::setVolume(SoundFlag newVolume) {
|
|
assert((newVolume & kSoundVolumeMask) == newVolume);
|
|
|
|
if (newVolume == kVolumeNone) {
|
|
_volumeWithoutNIS = 0;
|
|
} else if (getSoundQueue()->getFlag() & 0x20 && _tag != kSoundTagNIS && _tag != kSoundTagLink) {
|
|
setVolumeSmoothly(newVolume);
|
|
return;
|
|
}
|
|
|
|
_status = (_status & ~kSoundVolumeMask) | newVolume;
|
|
if (_soundStream)
|
|
_soundStream->setVolume(_status & kSoundVolumeMask);
|
|
}
|
|
|
|
void SoundEntry::adjustVolumeIfNISPlaying() {
|
|
if (getSoundQueue()->getFlag() & 32) {
|
|
if (_tag != kSoundTagNIS && _tag != kSoundTagLink && _tag != kSoundTagConcert) {
|
|
uint32 baseVolume = _status & kSoundVolumeMask;
|
|
uint32 actualVolume = baseVolume / 2 + 1;
|
|
|
|
assert((actualVolume & kSoundVolumeMask) == actualVolume);
|
|
|
|
_volumeWithoutNIS = baseVolume;
|
|
_status &= ~kSoundVolumeMask;
|
|
_status |= actualVolume;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SoundEntry::initDelayedActivate(unsigned activateDelay) {
|
|
_initTimeMS = _engine->_system->getMillis();
|
|
_activateDelayMS = activateDelay * 1000 / 15;
|
|
_status |= kSoundFlagDelayedActivate;
|
|
}
|
|
|
|
void SoundEntry::setSubtitles(Common::String filename) {
|
|
_subtitle = new SubtitleEntry(_engine);
|
|
_subtitle->load(filename, this);
|
|
|
|
if (_subtitle->getStatus() & 0x400) {
|
|
_subtitle->close();
|
|
SAFE_DELETE(_subtitle);
|
|
} else {
|
|
_status |= kSoundFlagHasSubtitles;
|
|
}
|
|
}
|
|
|
|
void SoundEntry::saveLoadWithSerializer(Common::Serializer &s) {
|
|
if (s.isLoading()) {
|
|
// load the fields
|
|
uint32 blocksLeft;
|
|
|
|
s.syncAsUint32LE(_status);
|
|
s.syncAsUint32LE(_tag);
|
|
s.syncAsUint32LE(blocksLeft);
|
|
s.syncAsUint32LE(_startTime);
|
|
uint32 unused;
|
|
s.syncAsUint32LE(unused);
|
|
s.syncAsUint32LE(unused);
|
|
s.syncAsUint32LE(_entity);
|
|
|
|
uint32 activateDelay;
|
|
s.syncAsUint32LE(activateDelay);
|
|
s.syncAsUint32LE(_priority);
|
|
|
|
char name[16];
|
|
s.syncBytes((byte *)name, 16); // _linkAfter name, should be always empty for entries in savefile
|
|
s.syncBytes((byte *)name, 16); // real name
|
|
name[15] = 0;
|
|
|
|
// load the sound
|
|
_blockCount = blocksLeft + _startTime;
|
|
|
|
// if we are loading a savefile from the original game
|
|
// and this savefile has been saved at a bizarre moment,
|
|
// we can see transient flags here.
|
|
// Let's pretend that the IRQ handler has run once.
|
|
if (_status & kSoundFlagPlayRequested)
|
|
_status |= kSoundFlagPlaying;
|
|
if (_status & (kSoundFlagCloseRequested | kSoundFlagFading))
|
|
_status |= kSoundFlagClosed;
|
|
_status &= kSoundVolumeMask
|
|
| kSoundFlagPlaying
|
|
| kSoundFlagClosed
|
|
| kSoundFlagCloseOnDataEnd
|
|
| kSoundFlagLooped
|
|
| kSoundFlagDelayedActivate
|
|
| kSoundFlagHasSubtitles
|
|
| kSoundFlagFixedVolume
|
|
| kSoundFlagHeaderProcessed
|
|
| kSoundTypeMask;
|
|
|
|
loadStream(name); // also sets _name
|
|
if (_status & kSoundFlagPlaying)
|
|
play((_status & kSoundFlagLooped) ? 0 : _startTime); // also loads subtitles
|
|
|
|
_initTimeMS = _engine->_system->getMillis();
|
|
_activateDelayMS = activateDelay * 1000 / 30;
|
|
|
|
} else {
|
|
assert(_name.size() < 16);
|
|
assert(needSaving());
|
|
// we can save our flags as is
|
|
// the original game can reconstruct kSoundFlagMute, kSoundFlagCyclicBuffer, kSoundFlagHasUnreadData,
|
|
// and we set other important flags correctly
|
|
s.syncAsUint32LE(_status);
|
|
s.syncAsUint32LE(_tag);
|
|
uint32 time = getTime();
|
|
uint32 blocksLeft = _blockCount - time;
|
|
s.syncAsUint32LE(blocksLeft);
|
|
s.syncAsUint32LE(time);
|
|
uint32 unused = 0;
|
|
s.syncAsUint32LE(unused);
|
|
s.syncAsUint32LE(unused);
|
|
s.syncAsUint32LE(_entity);
|
|
|
|
uint32 deltaMS = _initTimeMS + _activateDelayMS - _engine->_system->getMillis();
|
|
if (deltaMS > 0x8000000u) // sanity check against overflow
|
|
deltaMS = 0;
|
|
uint32 delta = deltaMS * 30 / 1000;
|
|
s.syncAsUint32LE(delta);
|
|
s.syncAsUint32LE(_priority);
|
|
|
|
char name[16] = {0};
|
|
s.syncBytes((byte *)name, 16);
|
|
|
|
Common::strcpy_s(name, _name.c_str());
|
|
s.syncBytes((byte *)name, 16);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// SubtitleEntry
|
|
//////////////////////////////////////////////////////////////////////////
|
|
SubtitleEntry::SubtitleEntry(LastExpressEngine *engine) : _engine(engine) {
|
|
_status = 0;
|
|
_sound = nullptr;
|
|
_data = nullptr;
|
|
}
|
|
|
|
SubtitleEntry::~SubtitleEntry() {
|
|
SAFE_DELETE(_data);
|
|
|
|
// Zero-out passed pointers
|
|
_sound = nullptr;
|
|
_engine = nullptr;
|
|
}
|
|
|
|
void SubtitleEntry::load(Common::String filename, SoundEntry *soundEntry) {
|
|
// Add ourselves to the list of active subtitles
|
|
getSoundQueue()->addSubtitle(this);
|
|
|
|
// Set sound entry and filename
|
|
_filename = filename + ".SBE";
|
|
_sound = soundEntry;
|
|
|
|
// Load subtitle data
|
|
if (_engine->getResourceManager()->hasFile(_filename)) {
|
|
if (getSoundQueue()->getSubtitleFlag() & 2)
|
|
return;
|
|
|
|
loadData();
|
|
} else {
|
|
_status = kSoundFlagClosed;
|
|
}
|
|
}
|
|
|
|
void SubtitleEntry::loadData() {
|
|
_data = new SubtitleManager(_engine->getFont());
|
|
_data->load(getArchive(_filename));
|
|
|
|
getSoundQueue()->setSubtitleFlag(getSoundQueue()->getSubtitleFlag() | 2);
|
|
getSoundQueue()->setCurrentSubtitle(this);
|
|
}
|
|
|
|
void SubtitleEntry::setupAndDraw() {
|
|
if (!_sound)
|
|
error("[SubtitleEntry::setupAndDraw] Sound entry not initialized");
|
|
|
|
if (!_data) {
|
|
_data = new SubtitleManager(_engine->getFont());
|
|
_data->load(getArchive(_filename));
|
|
}
|
|
|
|
if (_data->getMaxTime() > _sound->getTime()) {
|
|
_status = kSoundFlagClosed;
|
|
} else {
|
|
_data->setTime((uint16)_sound->getTime());
|
|
|
|
if (getSoundQueue()->getSubtitleFlag() & 1)
|
|
drawOnScreen();
|
|
}
|
|
|
|
getSoundQueue()->setCurrentSubtitle(this);
|
|
|
|
// TODO Missing code
|
|
}
|
|
|
|
void SubtitleEntry::close() {
|
|
// Remove ourselves from the queue
|
|
getSoundQueue()->removeSubtitle(this);
|
|
|
|
if (this == getSoundQueue()->getCurrentSubtitle()) {
|
|
drawOnScreen();
|
|
|
|
getSoundQueue()->setCurrentSubtitle(nullptr);
|
|
getSoundQueue()->setSubtitleFlag(0);
|
|
}
|
|
}
|
|
|
|
void SubtitleEntry::drawOnScreen() {
|
|
if (_data == nullptr)
|
|
return;
|
|
|
|
getSoundQueue()->setSubtitleFlag(getSoundQueue()->getSubtitleFlag() & -2);
|
|
_engine->getGraphicsManager()->draw(_data, GraphicsManager::kBackgroundOverlay);
|
|
}
|
|
|
|
} // End of namespace LastExpress
|