scummvm/engines/sci/sound/soundcmd.cpp
2024-10-21 23:18:54 -07:00

1038 lines
38 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 "common/config-manager.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
#include "sci/resource/resource.h"
#include "sci/sound/audio.h"
#include "sci/sound/music.h"
#include "sci/sound/soundcmd.h"
#include "sci/engine/features.h"
#include "sci/engine/guest_additions.h"
#include "sci/engine/kernel.h"
#include "sci/engine/object.h"
#include "sci/engine/selector.h"
namespace Sci {
enum SoundFlags {
kSoundFlagFixedPriority = 2,
kSoundFlagPreload = 4
};
SoundCommandParser::SoundCommandParser(ResourceManager *resMan, SegManager *segMan, Kernel *kernel, AudioPlayer *audio, SciVersion soundVersion) :
_resMan(resMan), _segMan(segMan), _kernel(kernel), _audio(audio), _soundVersion(soundVersion) {
// Check if the user wants synthesized or digital sound effects in SCI1.1
// games based on the prefer_digitalsfx config setting
// In SCI2 and later games, this check should always be true - there was
// always only one version of each sound effect or digital music track
// (e.g. the menu music in GK1 - there is a sound effect with the same
// resource number, but it's totally unrelated to the menu music).
// The GK1 demo (very late SCI1.1) does the same thing
// TODO: Check the QFG4 demo
_useDigitalSFX = (_soundVersion >= SCI_VERSION_2 || g_sci->getGameId() == GID_GK1DEMO || ConfMan.getBool("prefer_digitalsfx"));
_music = new SciMusic(_soundVersion, _useDigitalSFX);
_music->init();
}
SoundCommandParser::~SoundCommandParser() {
delete _music;
}
reg_t SoundCommandParser::kDoSoundInit(EngineState *s, int argc, reg_t *argv) {
debugC(kDebugLevelSound, "kDoSound(init): %04x:%04x", PRINT_REG(argv[0]));
processInitSound(argv[0]);
return s->r_acc;
}
uint16 SoundCommandParser::getSoundResourceId(reg_t obj) {
uint16 resourceId = readSelectorValue(_segMan, obj, SELECTOR(number));
// Modify the resourceId for the Windows versions that have an alternate MIDI soundtrack, like SSCI did.
if (g_sci->_features->useAltWinGMSound()) {
// Check if the alternate MIDI song actually exists...
// There are cases where it just doesn't exist (e.g. SQ4, room 530 -
// bug #5829). In these cases, use the DOS tracks instead.
if (resourceId && _resMan->testResource(ResourceId(kResourceTypeSound, resourceId + 1000)))
resourceId += 1000;
}
if (g_sci->getGameId() == GID_SQ4 &&
g_sci->getPlatform() == Common::kPlatformWindows &&
_useDigitalSFX &&
resourceId < 1000) {
// SQ4 CD Windows played Windows-exclusive digital samples instead of MIDI sounds
// when available. It did this when the "sound1000" was set to true in sierra.ini,
// which was always the case. If there was no audio resource with the given id,
// it would add 1000 and use that if it existed. Otherwise, it would fall back on
// the MIDI sound resource. Although this behavior existed in other Windows
// SCI 1.1 interpreters, SQ4 CD is the only game known to enable sound1000 in
// sierra.ini and include Windows-exclusive audio resources.
if (!_resMan->testResource(ResourceId(kResourceTypeAudio, resourceId)) &&
_resMan->testResource(ResourceId(kResourceTypeAudio, resourceId + 1000))) {
resourceId += 1000;
}
}
return resourceId;
}
void SoundCommandParser::initSoundResource(MusicEntry *newSound) {
if (newSound->resourceId) {
newSound->soundRes = new SoundResource(newSound->resourceId, _resMan, _soundVersion);
if (!newSound->soundRes->exists()) {
delete newSound->soundRes;
newSound->soundRes = nullptr;
}
} else {
newSound->soundRes = nullptr;
}
// In SCI1.1 games, sound effects are started from here. If we can find
// a relevant audio resource, play it, otherwise switch to synthesized
// effects. If the resource exists, play it using map 65535 (sound
// effects map)
if (getSciVersion() >= SCI_VERSION_1_1 && _resMan->testResource(ResourceId(kResourceTypeAudio, newSound->resourceId))) {
// Found a relevant audio resource, create an audio stream if there is
// no associated sound resource, or if both resources exist and the
// user wants the digital version.
if (_useDigitalSFX || !newSound->soundRes) {
int sampleLen;
#ifdef ENABLE_SCI32
if (_soundVersion >= SCI_VERSION_2) {
newSound->isSample = g_sci->getResMan()->testResource(ResourceId(kResourceTypeAudio, newSound->resourceId)) != nullptr;
} else {
#endif
newSound->pStreamAud = _audio->getAudioStream(newSound->resourceId, 65535, &sampleLen);
newSound->soundType = Audio::Mixer::kSFXSoundType;
newSound->isSample = newSound->pStreamAud != nullptr;
#ifdef ENABLE_SCI32
}
#endif
}
}
if (!newSound->isSample && newSound->soundRes)
_music->soundInitSnd(newSound);
}
void SoundCommandParser::processInitSound(reg_t obj) {
uint16 resourceId = getSoundResourceId(obj);
// Check if a track with the same sound object is already playing
MusicEntry *oldSound = _music->getSlot(obj);
if (oldSound) {
if (_soundVersion <= SCI_VERSION_0_LATE)
_music->soundKill(oldSound);
else
processDisposeSound(obj);
}
MusicEntry *newSound = new MusicEntry();
newSound->resourceId = resourceId;
newSound->soundObj = obj;
newSound->loop = readSelectorValue(_segMan, obj, SELECTOR(loop));
newSound->overridePriority = false;
if (_soundVersion <= SCI_VERSION_0_LATE)
newSound->priority = readSelectorValue(_segMan, obj, SELECTOR(priority));
else
newSound->priority = readSelectorValue(_segMan, obj, SELECTOR(priority)) & 0xFF;
if (_soundVersion >= SCI_VERSION_1_EARLY)
newSound->volume = CLIP<int>(readSelectorValue(_segMan, obj, SELECTOR(vol)), 0, MUSIC_VOLUME_MAX);
newSound->reverb = -1; // initialize to SCI invalid, it'll be set correctly in soundInitSnd() below
debugC(kDebugLevelSound, "kDoSound(init): %04x:%04x number %d, loop %d, prio %d, vol %d", PRINT_REG(obj),
resourceId, newSound->loop, newSound->priority, newSound->volume);
initSoundResource(newSound);
_music->pushBackSlot(newSound);
if (newSound->soundRes || newSound->isSample) {
// Notify the engine
if (_soundVersion <= SCI_VERSION_0_LATE)
writeSelectorValue(_segMan, obj, SELECTOR(state), kSoundInitialized);
else
writeSelector(_segMan, obj, SELECTOR(nodePtr), obj);
}
}
reg_t SoundCommandParser::kDoSoundPlay(EngineState *s, int argc, reg_t *argv) {
debugC(kDebugLevelSound, "kDoSound(play): %04x:%04x", PRINT_REG(argv[0]));
bool playBed = false;
if (argc >= 2 && !argv[1].isNull())
playBed = true;
processPlaySound(argv[0], playBed);
return s->r_acc;
}
void SoundCommandParser::processPlaySound(reg_t obj, bool playBed, bool restoring) {
MusicEntry *musicSlot = _music->getSlot(obj);
if (!restoring && musicSlot && isUninterruptableSoundPlaying(obj)) {
debugC(kDebugLevelSound, "kDoSound(play): sound %d already playing", musicSlot->resourceId);
return;
}
if (!musicSlot) {
warning("kDoSound(play): Slot not found (%04x:%04x), initializing it manually", PRINT_REG(obj));
// The sound hasn't been initialized for some reason, so initialize it
// here. Happens in KQ6, room 460, when giving the creature (child) to
// the bookworm. Fixes bugs #5849 and #5868.
processInitSound(obj);
musicSlot = _music->getSlot(obj);
if (!musicSlot)
error("Failed to initialize uninitialized sound slot");
}
uint16 resourceId;
if (!restoring)
resourceId = getSoundResourceId(obj);
else
// Handle cases where a game was saved while track A was playing, but track B was initialized, waiting to be played later.
// In such cases, musicSlot->resourceId contains the actual track that was playing (A), while getSoundResourceId(obj)
// contains the track that's waiting to be played later (B) - bug #10907.
resourceId = musicSlot->resourceId;
if (musicSlot->resourceId != resourceId) { // another sound loaded into struct
processDisposeSound(obj);
processInitSound(obj);
// Find slot again :)
musicSlot = _music->getSlot(obj);
}
writeSelector(_segMan, obj, SELECTOR(handle), obj);
if (_soundVersion >= SCI_VERSION_1_EARLY) {
writeSelector(_segMan, obj, SELECTOR(nodePtr), obj);
writeSelectorValue(_segMan, obj, SELECTOR(min), 0);
writeSelectorValue(_segMan, obj, SELECTOR(sec), 0);
writeSelectorValue(_segMan, obj, SELECTOR(frame), 0);
writeSelectorValue(_segMan, obj, SELECTOR(signal), 0);
} else {
writeSelectorValue(_segMan, obj, SELECTOR(state), kSoundPlaying);
}
musicSlot->loop = readSelectorValue(_segMan, obj, SELECTOR(loop));
// Get song priority from either obj or soundRes
byte resourcePriority = 0xFF;
if (musicSlot->soundRes)
resourcePriority = musicSlot->soundRes->getSoundPriority();
if (!musicSlot->overridePriority && resourcePriority != 0xFF) {
musicSlot->priority = resourcePriority;
} else {
musicSlot->priority = readSelectorValue(_segMan, obj, SELECTOR(priority));
}
// Reset hold when starting a new song. kDoSoundSetHold is always called after
// kDoSoundPlay to set it properly, if needed. Fixes bug #5851.
musicSlot->hold = -1;
musicSlot->playBed = playBed;
if (_soundVersion >= SCI_VERSION_1_EARLY)
musicSlot->volume = readSelectorValue(_segMan, obj, SELECTOR(vol));
debugC(kDebugLevelSound, "kDoSound(play): %04x:%04x number %d, loop %d, prio %d, vol %d, bed %d", PRINT_REG(obj),
resourceId, musicSlot->loop, musicSlot->priority, musicSlot->volume, playBed ? 1 : 0);
_music->soundPlay(musicSlot, restoring);
// Increment the sample play counter used by Pharkas CD.
// SSCI calls kDoAudio(Play) which did this. See kDoAudio(13).
if (_audio != nullptr && musicSlot->isSample) {
_audio->incrementPlayCounter();
}
if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
// When playing a sample without the preload flag, set the
// handle to -1 instead of the object. LSL6HIRES depends on
// this to play certain death messages. Fixes bug #13500
uint16 flags = readSelectorValue(_segMan, obj, SELECTOR(flags));
if (!(flags & kSoundFlagPreload)) {
writeSelectorValue(_segMan, obj, SELECTOR(handle), 0xffff);
}
}
// Reset any left-over signals
musicSlot->signal = 0;
musicSlot->fadeStep = 0;
}
reg_t SoundCommandParser::kDoSoundDispose(EngineState *s, int argc, reg_t *argv) {
debugC(kDebugLevelSound, "kDoSound(dispose): %04x:%04x", PRINT_REG(argv[0]));
processDisposeSound(argv[0]);
return s->r_acc;
}
void SoundCommandParser::processDisposeSound(reg_t obj) {
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
warning("kDoSound(dispose): Slot not found (%04x:%04x)", PRINT_REG(obj));
return;
}
processStopSound(obj, false);
_music->soundKill(musicSlot);
writeSelectorValue(_segMan, obj, SELECTOR(handle), 0);
if (_soundVersion >= SCI_VERSION_1_EARLY)
writeSelector(_segMan, obj, SELECTOR(nodePtr), NULL_REG);
else
writeSelectorValue(_segMan, obj, SELECTOR(state), kSoundStopped);
}
reg_t SoundCommandParser::kDoSoundStop(EngineState *s, int argc, reg_t *argv) {
debugC(kDebugLevelSound, "kDoSound(stop): %04x:%04x", PRINT_REG(argv[0]));
processStopSound(argv[0], false);
return s->r_acc;
}
void SoundCommandParser::processStopSound(reg_t obj, bool sampleFinishedPlaying) {
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
warning("kDoSound(stop): Slot not found (%04x:%04x)", PRINT_REG(obj));
return;
}
if (_soundVersion <= SCI_VERSION_0_LATE) {
writeSelectorValue(_segMan, obj, SELECTOR(state), kSoundStopped);
} else {
writeSelectorValue(_segMan, obj, SELECTOR(handle), 0);
}
// Set signal selector in sound SCI0 games only, when the sample has
// finished playing. If we don't set it at all, we get a problem when using
// vaporizer on the 2 guys. If we set it all the time, we get no music in
// sq3new and kq1.
// FIXME: This *may* be wrong, it's impossible to find out in Sierra DOS
// SCI, because SCI0 under DOS didn't have sfx drivers included.
// We need to set signal in sound SCI1+ games all the time.
if ((_soundVersion > SCI_VERSION_0_LATE) || sampleFinishedPlaying)
writeSelectorValue(_segMan, obj, SELECTOR(signal), SIGNAL_OFFSET);
musicSlot->dataInc = 0;
musicSlot->signal = SIGNAL_OFFSET;
_music->soundStop(musicSlot);
if (_soundVersion <= SCI_VERSION_0_LATE && (musicSlot = _music->getFirstSlotWithStatus(kSoundPlaying)))
writeSelectorValue(_segMan, musicSlot->soundObj, SELECTOR(state), kSoundPlaying);
}
reg_t SoundCommandParser::kDoSoundPause(EngineState *s, int argc, reg_t *argv) {
if (argc == 1)
debugC(kDebugLevelSound, "kDoSound(pause): %04x:%04x", PRINT_REG(argv[0]));
else
debugC(kDebugLevelSound, "kDoSound(pause): %04x:%04x, %04x:%04x", PRINT_REG(argv[0]), PRINT_REG(argv[1]));
if (_soundVersion <= SCI_VERSION_0_LATE) {
// SCI0 games give us 0/1 for either resuming or pausing the current music
// this one doesn't count, so pausing 2 times and resuming once means here that we are supposed to resume
uint16 value = argv[0].toUint16();
MusicEntry *musicSlot = _music->getFirstSlotWithStatus(kSoundPlaying);
switch (value) {
case 1:
if (musicSlot) {
_music->soundPause(musicSlot);
writeSelectorValue(_segMan, musicSlot->soundObj, SELECTOR(state), kSoundPaused);
}
return make_reg(0, 0);
case 0:
if (!musicSlot && (musicSlot = _music->getFirstSlotWithStatus(kSoundPaused))) {
_music->soundResume(musicSlot);
writeSelectorValue(_segMan, musicSlot->soundObj, SELECTOR(state), kSoundPlaying);
return make_reg(0, 1);
}
return make_reg(0, 0);
default:
error("kDoSound(pause): parameter 0 is invalid for sound-sci0");
}
}
reg_t obj = argv[0];
const bool shouldPause = argc > 1 ? argv[1].toUint16() : false;
if (
(_soundVersion < SCI_VERSION_2 && !obj.getSegment()) ||
(_soundVersion >= SCI_VERSION_2 && obj.isNull())
) {
// Don't unpause here, if the sound is already unpaused. The negative global pause counter
// that we introduced to accommodate our special needs during GMM save/load and autosave
// operations is not meant to be used here and will cause glitches.
if (_music->isAllPaused() || shouldPause)
_music->pauseAll(shouldPause);
#ifdef ENABLE_SCI32
if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
if (shouldPause) {
g_sci->_audio32->pause(kAllChannels);
} else {
g_sci->_audio32->resume(kAllChannels);
}
}
#endif
} else {
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
// This happens quite frequently
debugC(kDebugLevelSound, "kDoSound(pause): Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
#ifdef ENABLE_SCI32
// SSCI also expected a global "kernel call" flag to be true in order to
// perform this action, but the architecture of the ScummVM
// implementation is so different that it doesn't matter here
if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
// LSL6HIRES didn't support pausing samples with kDoSoundFade. Its interpreter's
// pause code didn't call kDoAudio. This feature appeared in PQ4 CD's interpreter
// a month later, according to the date strings, even though they have the same
// version string. LSL6HIRES door sounds depend on these pause calls not having
// any effect. Bug #13555
if (g_sci->getGameId() != GID_LSL6HIRES) {
if (shouldPause) {
g_sci->_audio32->pause(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj);
} else {
g_sci->_audio32->resume(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj);
}
}
} else
#endif
_music->soundToggle(musicSlot, shouldPause);
}
return s->r_acc;
}
// SCI0 only command
// It's called right after restoring a game - it's responsible to kick off playing music again
// we don't need this at all, so we don't do anything here
reg_t SoundCommandParser::kDoSoundResumeAfterRestore(EngineState *s, int argc, reg_t *argv) {
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundMute(EngineState *s, int argc, reg_t *argv) {
uint16 previousState = _music->soundGetSoundOn();
if (argc > 0) {
debugC(kDebugLevelSound, "kDoSound(mute): %d", argv[0].toUint16());
_music->soundSetSoundOn(argv[0].toUint16());
}
return make_reg(0, previousState);
}
reg_t SoundCommandParser::kDoSoundMasterVolume(EngineState *s, int argc, reg_t *argv) {
s->r_acc = make_reg(0, _music->soundGetMasterVolume());
if (argc > 0) {
debugC(kDebugLevelSound, "kDoSound(masterVolume): %d", argv[0].toSint16());
int vol = CLIP<int16>(argv[0].toSint16(), 0, MUSIC_MASTERVOLUME_MAX);
if (!g_sci->_guestAdditions->kDoSoundMasterVolumeHook(vol)) {
setMasterVolume(vol);
}
}
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundFade(EngineState *s, int argc, reg_t *argv) {
reg_t obj = argv[0];
// The object can be null in several SCI0 games (e.g. Camelot, KQ1, KQ4, MUMG).
// Check bugs #4984, #5045 and #6163.
// In this case, we just ignore the call.
if (obj.isNull() && argc == 1)
return s->r_acc;
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
debugC(kDebugLevelSound, "kDoSound(fade): Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
int volume = musicSlot->volume;
#ifdef ENABLE_SCI32
if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
g_sci->_audio32->fadeChannel(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, argv[1].toSint16(), argv[2].toSint16(), argv[3].toSint16(), argc > 4 ? (bool)argv[4].toSint16() : false);
return s->r_acc;
}
#endif
switch (argc) {
case 1: // SCI0
// If sound is not playing currently, set signal directly
if (musicSlot->status != kSoundPlaying) {
debugC(kDebugLevelSound, "kDoSound(fade): %04x:%04x fading requested, but sound is currently not playing", PRINT_REG(obj));
writeSelectorValue(_segMan, obj, SELECTOR(signal), SIGNAL_OFFSET);
return s->r_acc;
}
// SCI0 fades out all the time and when fadeout is done it will also
// stop the music from playing
musicSlot->fadeTo = 0;
musicSlot->fadeStep = -5;
musicSlot->fadeTickerStep = 10 * 16667 / _music->soundGetTempo();
musicSlot->fadeTicker = 0;
break;
case 4: // SCI01+
case 5: { // SCI1+ (SCI1 late sound scheme), with fade and continue
byte fadeTo = CLIP<uint16>(argv[1].toUint16(), 0, MUSIC_VOLUME_MAX);
// argv[4] is a boolean. Scripts sometimes pass strange values,
// but SSCI only checks for zero/non-zero. (Verified in KQ6).
// KQ6 room 460 even passes an object, but treating this as 'true'
// seems fine in that case. Bug #6120
bool stopAfterFading = (argc == 5) && !argv[4].isNull();
// Ignore the fade request if the song is already at the requested volume and
// the stopAfterFading flag isn't being set. SSCI stored this flag in the upper
// bit of the target volume before comparing it to the current volume.
// Longbow relies on this in its introduction. Bug #5239
if (musicSlot->volume == fadeTo && !stopAfterFading) {
return s->r_acc;
}
musicSlot->fadeTo = fadeTo;
musicSlot->fadeStep = volume > musicSlot->fadeTo ? -argv[3].toUint16() : argv[3].toUint16();
musicSlot->fadeTickerStep = argv[2].toUint16() * 16667 / _music->soundGetTempo();
musicSlot->fadeTicker = 0;
musicSlot->stopAfterFading = stopAfterFading;
break;
}
default:
error("kDoSound(fade): unsupported argc %d", argc);
}
debugC(kDebugLevelSound, "kDoSound(fade): %04x:%04x to %d, step %d, ticker %d", PRINT_REG(obj), musicSlot->fadeTo, musicSlot->fadeStep, musicSlot->fadeTickerStep);
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundGetPolyphony(EngineState *s, int argc, reg_t *argv) {
return make_reg(0, _music->soundGetVoices()); // Get the number of voices
}
reg_t SoundCommandParser::kDoSoundUpdate(EngineState *s, int argc, reg_t *argv) {
reg_t obj = argv[0];
debugC(kDebugLevelSound, "kDoSound(update): %04x:%04x", PRINT_REG(argv[0]));
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
warning("kDoSound(update): Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
musicSlot->loop = readSelectorValue(_segMan, obj, SELECTOR(loop));
int16 objVol = CLIP<int>(readSelectorValue(_segMan, obj, SELECTOR(vol)), 0, 255);
if (objVol != musicSlot->volume)
_music->soundSetVolume(musicSlot, objVol);
int16 objPrio = readSelectorValue(_segMan, obj, SELECTOR(priority));
if (objPrio != musicSlot->priority)
_music->soundSetPriority(musicSlot, objPrio);
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundUpdateCues(EngineState *s, int argc, reg_t *argv) {
processUpdateCues(argv[0]);
return s->r_acc;
}
void SoundCommandParser::processUpdateCues(reg_t obj) {
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
warning("kDoSound(updateCues): Slot not found (%04x:%04x)", PRINT_REG(obj));
return;
}
if (musicSlot->isSample) {
#ifdef ENABLE_SCI32
if (_soundVersion >= SCI_VERSION_2) {
const ResourceId audioId = ResourceId(kResourceTypeAudio, musicSlot->resourceId);
if (getSciVersion() == SCI_VERSION_3) {
// In SSCI the volume is first set to -1 and then reset later if
// a sample is playing in the audio player, but since our audio
// code returns -1 for not-found samples, the extra check is not
// needed and we can just always set it to the return value of
// the getVolume call
const int16 volume = g_sci->_audio32->getVolume(audioId, musicSlot->soundObj);
writeSelectorValue(_segMan, musicSlot->soundObj, SELECTOR(vol), volume);
}
const int16 position = g_sci->_audio32->getPosition(audioId, musicSlot->soundObj);
if (position == -1) {
processStopSound(musicSlot->soundObj, true);
}
return;
}
#endif
// SCI1.1 stopped the sample if the audio driver reported that no audio was playing.
// We have two separate components that could be playing digital audio instead of
// one driver, so we must check them both. This behavior is necessary to handle cases
// where scripts played a sample with kDoSound and speech with kDoAudio at the same
// time, and relied on them both signaling even though one interrupts the other.
// Fixes LSL6 CD Electroshock scene from freezing in room 380, sHookUpLarry state 5.
// This script only worked by accident, and these lines were rewritten for SCI32.
// LB2 CD's "Ra Ra Amon Ra" chant in room 710 also accidentally relies on this.
if (getSciVersion() == SCI_VERSION_1_1) {
// SSCI queried the audio driver's AudioLoc function. If it returned -1 then
// it stopped the sound, but it could have been playing any sample or speech.
int audioPosition = _audio->getAudioPosition();
if (audioPosition == -1) {
if (!isDigitalSamplePlaying()) {
processStopSound(obj, true);
return;
}
}
}
// Update digital sound effect slots
uint currentLoopCounter = 0;
if (musicSlot->pLoopStream)
currentLoopCounter = musicSlot->pLoopStream->getCompleteIterations();
if (currentLoopCounter != musicSlot->sampleLoopCounter) {
// during last time we looped at least one time, update loop accordingly
musicSlot->loop -= currentLoopCounter - musicSlot->sampleLoopCounter;
musicSlot->sampleLoopCounter = currentLoopCounter;
}
if (musicSlot->status == kSoundPlaying) {
if (!_music->isSoundActive(musicSlot)) {
processStopSound(obj, true);
} else {
_music->updateAudioStreamTicker(musicSlot);
}
} else if (musicSlot->status == kSoundPaused) {
_music->updateAudioStreamTicker(musicSlot);
}
// We get a flag from MusicEntry::doFade() here to set volume for the stream
if (musicSlot->fadeSetVolume) {
_music->soundSetSampleVolume(musicSlot, musicSlot->volume);
musicSlot->fadeSetVolume = false;
}
} else if (musicSlot->pMidiParser) {
// Update MIDI slots
if (musicSlot->signal == 0) {
if (musicSlot->dataInc != readSelectorValue(_segMan, obj, SELECTOR(dataInc))) {
if (SELECTOR(dataInc) > -1)
writeSelectorValue(_segMan, obj, SELECTOR(dataInc), musicSlot->dataInc);
writeSelectorValue(_segMan, obj, SELECTOR(signal), musicSlot->dataInc + 127);
}
} else {
// Sync the signal of the sound object
writeSelectorValue(_segMan, obj, SELECTOR(signal), musicSlot->signal);
// We need to do this especially because state selector needs to get updated
if (musicSlot->signal == SIGNAL_OFFSET)
processStopSound(obj, false);
}
} else {
// The sound slot has no data for the currently selected sound card.
// An example can be found during the mud wrestling scene in LSL5, room
// 730: sound 744 (a splat sound heard when Lana Luscious jumps in the
// mud) only contains MIDI channel data. If a non-MIDI sound card is
// selected (like Adlib), then the scene freezes. We also need to stop
// the sound at this point, otherwise KQ6 Mac breaks because the rest
// of the object needs to be reset to avoid a continuous stream of
// sound cues.
processStopSound(obj, true); // this also sets the signal selector
}
if (musicSlot->fadeCompleted) {
musicSlot->fadeCompleted = false;
// We need signal for sci0 at least in iceman as well (room 14,
// fireworks).
// It is also needed in other games, e.g. LSL6 when talking to the
// receptionist (bug #5601).
// TODO: More thoroughly check the different SCI version:
// * SCI1late sets signal to 0xFE here. (With signal 0xFF
// duplicate music plays in LauraBow2CD - bug #6462)
// SCI1middle LSL1 1.000.510 does not have the 0xFE;
// SCI1late CastleDrBrain demo 1.000.005 does have the 0xFE.
// * Other SCI1 games seem to rely on processStopSound to set the signal
// * Need to check SCI0 behaviour.
uint16 sig;
if (getSciVersion() >= SCI_VERSION_1_LATE)
sig = 0x00fe;
else
sig = SIGNAL_OFFSET;
writeSelectorValue(_segMan, obj, SELECTOR(signal), sig);
if (_soundVersion <= SCI_VERSION_0_LATE) {
processStopSound(obj, false);
} else {
if (musicSlot->stopAfterFading)
processStopSound(obj, false);
}
}
// Sync loop selector for SCI0
if (_soundVersion <= SCI_VERSION_0_LATE)
writeSelectorValue(_segMan, obj, SELECTOR(loop), musicSlot->loop);
musicSlot->signal = 0;
if (_soundVersion >= SCI_VERSION_1_EARLY) {
writeSelectorValue(_segMan, obj, SELECTOR(min), musicSlot->ticker / 3600);
writeSelectorValue(_segMan, obj, SELECTOR(sec), musicSlot->ticker % 3600 / 60);
writeSelectorValue(_segMan, obj, SELECTOR(frame), musicSlot->ticker % 60 / 2);
if (_soundVersion >= SCI_VERSION_1_MIDDLE) {
writeSelectorValue(_segMan, obj, SELECTOR(vol), musicSlot->volume);
}
}
}
reg_t SoundCommandParser::kDoSoundSendMidi(EngineState *s, int argc, reg_t *argv) {
// The 4 parameter variant of this call is used in at least LSL1VGA, room
// 110 (Lefty's bar), to distort the music when Larry is drunk and stands
// up - bug #6349.
reg_t obj = argv[0];
byte channel = argv[1].toUint16() & 0xf;
byte midiCmd = (argc == 5) ? argv[2].toUint16() & 0xff : 0xB0; // 0xB0: controller
uint16 controller = (argc == 5) ? argv[3].toUint16() : argv[2].toUint16();
uint16 param = (argc == 5) ? argv[4].toUint16() : argv[3].toUint16();
// This call is made in Hoyle 5 when toggling the music from the main menu.
// Ignore it for this game, since it doesn't use MIDI audio, and this call
// looks to be a leftover in Sound::mute (script 64989).
if (g_sci->getGameId() == GID_HOYLE5)
return s->r_acc;
if (argc == 4 && controller == 0xFF) {
midiCmd = 0xE0; // 0xE0: pitch wheel
uint16 pitch = CLIP<uint16>(argv[3].toSint16() + 0x2000, 0x0000, 0x3FFF);
controller = pitch & 0x7F;
param = pitch >> 7;
}
debugC(kDebugLevelSound, "kDoSound(sendMidi): %04x:%04x, %d, %d, %d, %d", PRINT_REG(obj), channel, midiCmd, controller, param);
if (channel)
channel--; // channel is given 1-based, we are using 0-based
uint32 midiCommand = (channel | midiCmd) | ((uint32)controller << 8) | ((uint32)param << 16);
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
// TODO: maybe it's possible to call this with obj == 0:0 and send directly?!
// if so, allow it
//_music->sendMidiCommand(_midiCommand);
warning("kDoSound(sendMidi): Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
_music->sendMidiCommand(musicSlot, midiCommand);
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundGlobalReverb(EngineState *s, int argc, reg_t *argv) {
byte prevReverb = _music->getCurrentReverb();
byte reverb = argv[0].toUint16() & 0xF;
if (argc == 1) {
debugC(kDebugLevelSound, "doSoundGlobalReverb: %d", argv[0].toUint16() & 0xF);
if (reverb <= 10)
_music->setGlobalReverb(reverb);
}
return make_reg(0, prevReverb);
}
reg_t SoundCommandParser::kDoSoundSetHold(EngineState *s, int argc, reg_t *argv) {
reg_t obj = argv[0];
debugC(kDebugLevelSound, "doSoundSetHold: %04x:%04x, %d", PRINT_REG(argv[0]), argv[1].toUint16());
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
warning("kDoSound(setHold): Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
// Set the special hold marker ID where the song should be looped at.
musicSlot->hold = argv[1].toSint16();
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundGetAudioCapability(EngineState *s, int argc, reg_t *argv) {
// Tests for digital audio support
return make_reg(0, 1);
}
reg_t SoundCommandParser::kDoSoundStopAll(EngineState *s, int argc, reg_t *argv) {
// TODO: this can't be right, this gets called in kq1 - e.g. being in witch house, getting the note
// now the point jingle plays and after a messagebox they call this - and would stop the background effects with it
// this doesn't make sense, so i disable it for now
return s->r_acc;
#if 0
Common::StackLock(_music->_mutex);
const MusicList::iterator end = _music->getPlayListEnd();
for (MusicList::iterator i = _music->getPlayListStart(); i != end; ++i) {
if (_soundVersion <= SCI_VERSION_0_LATE) {
writeSelectorValue(_segMan, (*i)->soundObj, SELECTOR(state), kSoundStopped);
} else {
writeSelectorValue(_segMan, (*i)->soundObj, SELECTOR(handle), 0);
writeSelectorValue(_segMan, (*i)->soundObj, SELECTOR(signal), SIGNAL_OFFSET);
}
(*i)->dataInc = 0;
_music->soundStop(*i);
}
return s->r_acc;
#endif
}
reg_t SoundCommandParser::kDoSoundSetVolume(EngineState *s, int argc, reg_t *argv) {
reg_t obj = argv[0];
int16 value = argv[1].toSint16();
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
// Do not throw a warning if the sound can't be found, as in some games
// this is called before the actual sound is loaded (e.g. SQ4CD, with
// the drum sounds of the energizer bunny at the beginning), so this is
// normal behavior.
//warning("cmdSetSoundVolume: Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
debugC(kDebugLevelSound, "kDoSound(setVolume): %d", value);
value = CLIP<int>(value, 0, MUSIC_VOLUME_MAX);
#ifdef ENABLE_SCI32
// SSCI unconditionally sets volume if it is digital audio
if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
g_sci->_audio32->setVolume(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, value);
}
#endif
if (musicSlot->volume != value) {
musicSlot->volume = value;
_music->soundSetVolume(musicSlot, value);
writeSelectorValue(_segMan, obj, SELECTOR(vol), value);
#ifdef ENABLE_SCI32
g_sci->_guestAdditions->kDoSoundSetVolumeHook(obj, value);
#endif
}
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundSetPriority(EngineState *s, int argc, reg_t *argv) {
reg_t obj = argv[0];
int16 value = argv[1].toSint16();
debugC(kDebugLevelSound, "kDoSound(setPriority): %04x:%04x, %d", PRINT_REG(obj), value);
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
debugC(kDebugLevelSound, "kDoSound(setPriority): Slot not found (%04x:%04x)", PRINT_REG(obj));
return s->r_acc;
}
if (value == -1) {
musicSlot->overridePriority = false;
musicSlot->priority = 0;
// NB: It seems SSCI doesn't actually reset the priority here.
writeSelectorValue(_segMan, obj, SELECTOR(flags), readSelectorValue(_segMan, obj, SELECTOR(flags)) & ~kSoundFlagFixedPriority);
} else {
// Scripted priority
musicSlot->overridePriority = true;
writeSelectorValue(_segMan, obj, SELECTOR(flags), readSelectorValue(_segMan, obj, SELECTOR(flags)) | kSoundFlagFixedPriority);
_music->soundSetPriority(musicSlot, value);
}
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundSetLoop(EngineState *s, int argc, reg_t *argv) {
reg_t obj = argv[0];
int16 value = argv[1].toSint16();
debugC(kDebugLevelSound, "kDoSound(setLoop): %04x:%04x, %d", PRINT_REG(obj), value);
const uint16 loopCount = value == -1 ? 0xFFFF : 1;
writeSelectorValue(_segMan, obj, SELECTOR(loop), loopCount);
MusicEntry *musicSlot = _music->getSlot(obj);
if (!musicSlot) {
// Apparently, it's perfectly normal for a game to call cmdSetSoundLoop
// before actually initializing the sound and adding it to the playlist
// with cmdInitSound. Usually, it doesn't matter if the game doesn't
// request to loop the sound, so in this case, don't throw any warning,
// otherwise do, because the sound won't be looped.
if (value == -1) {
warning("kDoSound(setLoop): Slot not found (%04x:%04x) and the song was requested to be looped", PRINT_REG(obj));
} else {
// Doesn't really matter
}
return s->r_acc;
}
#ifdef ENABLE_SCI32
if (_soundVersion >= SCI_VERSION_2_1_MIDDLE && musicSlot->isSample) {
g_sci->_audio32->setLoop(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, value == -1);
} else
#endif
musicSlot->loop = loopCount;
return s->r_acc;
}
reg_t SoundCommandParser::kDoSoundSuspend(EngineState *s, int argc, reg_t *argv) {
// TODO
warning("kDoSound(suspend): STUB");
return s->r_acc;
}
void SoundCommandParser::updateSci0Cues() {
for (MusicList::iterator i = _music->getPlayListStart(); i != _music->getPlayListEnd(); ++i) {
if ((*i)->status == kSoundPlaying || (*i)->signal)
processUpdateCues((*i)->soundObj);
}
}
bool SoundCommandParser::isDigitalSamplePlaying() const {
return _music->isDigitalSamplePlaying();
}
void SoundCommandParser::clearPlayList() {
_music->clearPlayList();
}
void SoundCommandParser::printPlayList(Console *con) {
_music->printPlayList(con);
}
void SoundCommandParser::printSongInfo(reg_t obj, Console *con) {
_music->printSongInfo(obj, con);
}
void SoundCommandParser::stopAllSounds() {
_music->stopAll();
}
void SoundCommandParser::stopAllSamples() {
_music->stopAllSamples();
}
void SoundCommandParser::startNewSound(int number) {
// NB: This is only used by the debugging console.
Common::StackLock lock(_music->_mutex);
// Overwrite the first sound in the playlist
MusicEntry *song = *_music->getPlayListStart();
reg_t soundObj = song->soundObj;
processDisposeSound(soundObj);
writeSelectorValue(_segMan, soundObj, SELECTOR(number), number);
processInitSound(soundObj);
processPlaySound(soundObj, false);
}
void SoundCommandParser::setMasterVolume(int vol) {
// 0...15
_music->soundSetMasterVolume(vol);
}
#ifdef ENABLE_SCI32
void SoundCommandParser::setVolume(const reg_t obj, const int volume) {
MusicEntry *slot = _music->getSlot(obj);
if (slot != nullptr) {
slot->volume = volume;
writeSelectorValue(_segMan, obj, SELECTOR(vol), volume);
_music->soundSetVolume(slot, volume);
}
}
#endif
void SoundCommandParser::pauseAll(bool pause) {
_music->pauseAll(pause);
}
void SoundCommandParser::resetGlobalPauseCounter() {
_music->resetGlobalPauseCounter();
}
bool SoundCommandParser::isGlobalPauseActive() const {
return _music->isAllPaused();
}
MusicType SoundCommandParser::getMusicType() const {
assert(_music);
return _music->soundGetMusicType();
}
bool SoundCommandParser::isUninterruptableSoundPlaying(reg_t obj) {
#ifdef ENABLE_SCI32
// WORKAROUND
// Lighthouse has a buggy script pattern in several places that causes
// stuttering audio. The scripts poll game state on each game cycle and
// play a sound when a condition is met. Usually that's when a robot or
// view is on a particular frame. But since these conditions stay true for
// multiple cycles, each subsequent iteration plays the song again and
// restarts it, creating a stuttering effect. In the original, this usually
// worked because of the time it took the interpreter to load and begin
// playing a sound, but it could still occur on subsequent plays.
// Sierra included the necessary checks in some scripts to prevent this bug,
// but not all of them. Unfortunately there are too many functions to patch.
// Each is different and would require finding a way to add new code, so
// instead we detect the known sounds and ignore requests to play them if
// their Sound object is already playing.
const uint16 lighthouseSoundIds[] = {
// script 270
802, 838, // powerLever:doit
813, // trainBot:doit (bug #10231)
828, // elevLever:doit (bug #10238)
911, 912, // railProp:doit (bug #10232)
913, 914, 915, // switchProp:doit (bug #10237)
2037, // steam:doit
// script 470
443, // birdMan:doit (bug #10224)
44001, 44005, 44006, // birdManSmash:doit
// script 700
45905, 45906, // closeHatch:doit
// script 870
851, 852, 879, 880, 881, 882, 883 // ValveProp:doit
};
if (g_sci->getGameId() == GID_LIGHTHOUSE) {
const reg_t handle = readSelector(_segMan, obj, SELECTOR(handle));
if (handle != NULL_REG) { // handle can be an object or -1 when playing
const uint16 soundId = readSelectorValue(_segMan, obj, SELECTOR(number));
for (int i = 0; i < ARRAYSIZE(lighthouseSoundIds); ++i) {
if (lighthouseSoundIds[i] == soundId) {
return true;
}
}
}
}
#endif
return false;
}
} // End of namespace Sci