Files
scummvm/engines/lastexpress/sound/entry.cpp
T

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