/* 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 "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(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 samples; SeekableAudioStream *seekStream = dynamic_cast(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) }