Files
2026-02-02 04:50:13 +01:00

398 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 "alcachofa/sounds.h"
#include "alcachofa/rooms.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/detection.h"
#include "common/file.h"
#include "common/substream.h"
#include "audio/audiostream.h"
#include "audio/decoders/wave.h"
#include "audio/decoders/adpcm.h"
#include "audio/decoders/raw.h"
using namespace Common;
using namespace Audio;
namespace Alcachofa {
void Sounds::Playback::fadeOut(uint32 duration) {
_fadeStart = g_system->getMillis();
_fadeDuration = MAX<uint32>(duration, 1);
}
Sounds::Sounds()
: _mixer(g_system->getMixer())
, _musicSemaphore("music") {
assert(_mixer != nullptr);
}
Sounds::~Sounds() {
_mixer->stopAll();
}
Sounds::Playback *Sounds::getPlaybackById(SoundHandle id) {
auto itPlayback = find_if(_playbacks.begin(), _playbacks.end(),
[&] (const Playback &playback) { return playback._handle == id; });
return itPlayback == _playbacks.end() ? nullptr : itPlayback;
}
void Sounds::update() {
if (_isMusicPlaying && !isAlive(_musicSoundID)) {
if (_nextMusicID < 0)
fadeMusic();
else
startMusic(_nextMusicID);
}
for (uint i = _playbacks.size(); i > 0; i--) {
Playback &playback = _playbacks[i - 1];
if (!_mixer->isSoundHandleActive(playback._handle))
_playbacks.erase(_playbacks.begin() + i - 1);
else if (playback._fadeDuration != 0) {
if (g_system->getMillis() >= playback._fadeStart + playback._fadeDuration) {
_mixer->stopHandle(playback._handle);
_playbacks.erase(_playbacks.begin() + i - 1);
} else {
byte newVolume = (g_system->getMillis() - playback._fadeStart) * Mixer::kMaxChannelVolume / playback._fadeDuration;
_mixer->setChannelVolume(playback._handle, Mixer::kMaxChannelVolume - newVolume);
}
}
}
}
static AudioStream *loadSND(File *file) {
// SND files are just WAV files with removed headers
const uint32 endOfFormat = file->readUint32LE() + 2 * sizeof(uint32);
if (endOfFormat < 24)
error("Invalid SND format size");
uint16 format = file->readUint16LE();
uint16 channels = file->readUint16LE();
uint32 freq = file->readUint32LE();
file->skip(sizeof(uint32)); // bytesPerSecond, unnecessary for us
uint16 bytesPerBlock = file->readUint16LE();
uint16 bitsPerSample = file->readUint16LE();
if (endOfFormat >= 2 * sizeof(uint32) + 20) {
file->skip(sizeof(uint16)); // size of extra data
uint16 extra = file->readUint16LE();
bytesPerBlock = 4 * channels * ((extra + 14) / 8);
}
file->seek(endOfFormat, SEEK_SET);
auto subStream = new SeekableSubReadStream(file, (uint32)file->pos(), (uint32)file->size(), DisposeAfterUse::YES);
if (format == 1 && channels <= 2 && (bitsPerSample == 8 || bitsPerSample == 16))
return makeRawStream(subStream, (int)freq,
(channels == 2 ? FLAG_STEREO : 0) |
(bitsPerSample == 16 ? FLAG_16BITS | FLAG_LITTLE_ENDIAN : FLAG_UNSIGNED));
else if (format == 17 && channels <= 2)
return makeADPCMStream(subStream, DisposeAfterUse::YES, 0, kADPCMMSIma, (int)freq, (int)channels, (uint32)bytesPerBlock);
else {
delete subStream;
g_engine->game().invalidSNDFormat(format, channels, freq, bitsPerSample);
return nullptr;
}
}
static AudioStream *openAudio(const char *fileName) {
String path = String::format("Sonidos/%s.SND", fileName);
File *file = new File();
if (file->open(path.c_str()))
return file->size() == 0 // Movie Adventure has some null-size audio files, they are treated like infinite silence
? makeSilentAudioStream(8000, false)
: loadSND(file);
path.setChar('W', path.size() - 3);
path.setChar('A', path.size() - 2);
path.setChar('V', path.size() - 1);
if (file->open(path.c_str()))
return makeWAVStream(file, DisposeAfterUse::YES);
delete file;
g_engine->game().missingSound(fileName);
return nullptr;
}
SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
AudioStream *stream = openAudio(fileName);
if (stream == nullptr && (type == Mixer::kSpeechSoundType || type == Mixer::kMusicSoundType)) {
/* If voice files are missing, the player could still read the subtitle
* For this we return infinite silent audio which the user has to skip
* But only do this for speech as there is no skipping for sound effects
* so those would live on forever and block up mixer channels
* Music is fine as well as we clean up the music playack explicitly
*/
stream = makeSilentAudioStream(8000, false);
}
if (stream == nullptr)
return {};
Array<int16> samples;
SeekableAudioStream *seekStream = dynamic_cast<SeekableAudioStream *>(stream);
if (type == Mixer::kSpeechSoundType && seekStream != nullptr) {
// for lip-sync we need access to the samples so we decode the entire stream now
int sampleCount = seekStream->getLength().totalNumberOfFrames();
if (sampleCount > 0) {
// we actually got a length
samples.resize((uint)sampleCount);
sampleCount = seekStream->readBuffer(samples.data(), sampleCount);
if (sampleCount < 0)
samples.clear();
samples.resize((uint)sampleCount); // we might have gotten less samples
} else {
// we did not, now it is getting inefficient
const int bufferSize = 2048;
int16 buffer[bufferSize];
int chunkSampleCount;
do {
chunkSampleCount = seekStream->readBuffer(buffer, bufferSize);
if (chunkSampleCount <= 0)
break;
samples.resize(samples.size() + chunkSampleCount);
copy(buffer, buffer + chunkSampleCount, samples.data() + sampleCount);
sampleCount += chunkSampleCount;
} while (chunkSampleCount >= bufferSize);
}
if (sampleCount > 0) {
stream = makeRawStream(
(byte *)samples.data(),
samples.size() * sizeof(int16),
seekStream->getRate(),
FLAG_16BITS |
#ifdef SCUMM_LITTLE_ENDIAN
FLAG_LITTLE_ENDIAN | // readBuffer returns native endian
#endif
(seekStream->isStereo() ? FLAG_STEREO : 0),
DisposeAfterUse::NO);
delete seekStream;
}
}
SoundHandle handle;
_mixer->playStream(type, &handle, stream, -1, volume);
Playback playback;
playback._handle = handle;
playback._type = type;
playback._inputRate = stream->getRate();
playback._samples = Common::move(samples);
_playbacks.push_back(Common::move(playback));
return handle;
}
SoundHandle Sounds::playVoice(const String &fileName, byte volume) {
debugC(1, kDebugSounds, "Play voice: %s at %d", fileName.c_str(), (int)volume);
return playSoundInternal(fileName.c_str(), volume, Mixer::kSpeechSoundType);
}
SoundHandle Sounds::playSFX(const String &fileName, byte volume) {
debugC(1, kDebugSounds, "Play SFX: %s at %d", fileName.c_str(), (int)volume);
return playSoundInternal(fileName.c_str(), volume, Mixer::kSFXSoundType);
}
void Sounds::stopAll() {
debugC(1, kDebugSounds, "Stop all sounds");
_mixer->stopAll();
_playbacks.clear();
}
void Sounds::stopVoice() {
debugC(1, kDebugSounds, "Stop all voices");
for (uint i = _playbacks.size(); i > 0; i--) {
if (_playbacks[i - 1]._type == Mixer::kSpeechSoundType) {
_mixer->stopHandle(_playbacks[i - 1]._handle);
_playbacks.erase(_playbacks.begin() + i - 1);
}
}
}
void Sounds::pauseAll(bool paused) {
_mixer->pauseAll(paused);
}
bool Sounds::isAlive(SoundHandle id) {
Playback *playback = getPlaybackById(id);
return playback != nullptr && _mixer->isSoundHandleActive(playback->_handle);
}
void Sounds::setVolume(SoundHandle id, byte volume) {
Playback *playback = getPlaybackById(id);
if (playback != nullptr)
_mixer->setChannelVolume(playback->_handle, volume);
}
void Sounds::setAppropriateVolume(SoundHandle id,
MainCharacterKind processCharacterKind,
Character *speakingCharacter) {
static constexpr byte kAlmostMaxVolume = Mixer::kMaxChannelVolume * 9 / 10;
auto &player = g_engine->player();
auto processCharacter = processCharacterKind == MainCharacterKind::None ? nullptr
: &g_engine->world().getMainCharacterByKind(processCharacterKind);
byte newVolume;
if (processCharacter == nullptr || processCharacter == player.activeCharacter())
newVolume = Mixer::kMaxChannelVolume;
else if (speakingCharacter != nullptr && speakingCharacter->room() == player.currentRoom())
newVolume = kAlmostMaxVolume;
else if (processCharacter->room() == player.currentRoom())
newVolume = kAlmostMaxVolume;
else
newVolume = 0;
setVolume(id, newVolume);
}
void Sounds::fadeOut(SoundHandle id, uint32 duration) {
Playback *playback = getPlaybackById(id);
if (playback != nullptr)
playback->fadeOut(duration);
}
void Sounds::fadeOutVoiceAndSFX(uint32 duration) {
for (auto &playback : _playbacks) {
if (playback._type == Mixer::kSpeechSoundType || playback._type == Mixer::kSFXSoundType)
playback.fadeOut(duration);
}
}
bool Sounds::isNoisy(SoundHandle id, float windowSize, float minDifferences) {
assert(windowSize > 0 && minDifferences > 0);
const Playback *playback = getPlaybackById(id);
if (playback == nullptr ||
playback->_samples.empty() ||
!_mixer->isSoundHandleActive(playback->_handle))
return false;
minDifferences *= windowSize;
uint windowSizeInSamples = (uint)(windowSize * 0.001f * playback->_inputRate);
uint samplePosition = (uint)_mixer->getElapsedTime(playback->_handle)
.convertToFramerate(playback->_inputRate)
.totalNumberOfFrames();
uint endPosition = MIN(playback->_samples.size(), samplePosition + windowSizeInSamples);
if (samplePosition >= endPosition)
return false;
/* While both ScummVM and the original engine use signed int16 samples
* For this noise detection the samples are reinterpret as uint16
* This causes changes going through zero to be much more significant.
*/
float sumOfDifferences = 0;
const uint16 *samplePtr = (const uint16 *)playback->_samples.data();
for (uint i = samplePosition; i < endPosition - 1; i++)
// cast to int before to not be constrained by uint16
sumOfDifferences += ABS((int)samplePtr[i + 1] - samplePtr[i]);
return sumOfDifferences / 256.0f >= minDifferences;
}
void Sounds::startMusic(int musicId) {
debugC(2, kDebugSounds, "startMusic %d", musicId);
assert(musicId >= 0);
fadeMusic();
constexpr size_t kBufferSize = 16;
char filenameBuffer[kBufferSize];
snprintf(filenameBuffer, kBufferSize, "T%d", musicId);
_musicSoundID = playSoundInternal(filenameBuffer, Mixer::kMaxChannelVolume, Mixer::kMusicSoundType);
_isMusicPlaying = true;
_nextMusicID = musicId;
}
void Sounds::queueMusic(int musicId) {
debugC(2, kDebugSounds, "queueMusic %d", musicId);
_nextMusicID = musicId;
}
void Sounds::fadeMusic(uint32 duration) {
debugC(2, kDebugSounds, "fadeMusic");
fadeOut(_musicSoundID, duration);
_isMusicPlaying = false;
_nextMusicID = -1;
_musicSoundID = {};
}
void Sounds::setMusicToRoom(int roomMusicId) {
// Alcachofa Soft used IDs > 200 to mean "no change in music"
if (roomMusicId == _nextMusicID || roomMusicId > 200) {
debugC(1, kDebugSounds, "setMusicToRoom: from %d to %d, not executed", _nextMusicID, roomMusicId);
return;
}
debugC(1, kDebugSounds, "setMusicToRoom: from %d to %d", _nextMusicID, roomMusicId);
if (roomMusicId > 0)
startMusic(roomMusicId);
else
fadeMusic();
}
Task *Sounds::waitForMusicToEnd(Process &process) {
return new WaitForMusicTask(process);
}
PlaySoundTask::PlaySoundTask(Process &process, SoundHandle SoundHandle)
: Task(process)
, _soundHandle(SoundHandle) {}
PlaySoundTask::PlaySoundTask(Process &process, Serializer &s)
: Task(process)
, _soundHandle({}) {
// playing sounds are not persisted in the savestates,
// this task will stop at the next frame
syncGame(s);
}
TaskReturn PlaySoundTask::run() {
auto &sounds = g_engine->sounds();
if (sounds.isAlive(_soundHandle)) {
sounds.setAppropriateVolume(_soundHandle, process().character(), nullptr);
return TaskReturn::yield();
} else
return TaskReturn::finish(1);
}
void PlaySoundTask::debugPrint() {
// unfortunately SoundHandle is not castable to something we could display here safely
g_engine->console().debugPrintf("PlaySound\n");
}
DECLARE_TASK(PlaySoundTask)
WaitForMusicTask::WaitForMusicTask(Process &process)
: Task(process)
, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) {}
WaitForMusicTask::WaitForMusicTask(Process &process, Serializer &s)
: Task(process)
, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) {
syncGame(s);
}
TaskReturn WaitForMusicTask::run() {
g_engine->sounds().queueMusic(-1);
return g_engine->sounds().isMusicPlaying()
? TaskReturn::yield()
: TaskReturn::finish(0);
}
void WaitForMusicTask::debugPrint() {
g_engine->console().debugPrintf("WaitForMusic\n");
}
DECLARE_TASK(WaitForMusicTask)
}