/* 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 "common/debug.h" #include "common/endian.h" #include "common/memstream.h" #include "common/stream.h" #include "audio/audiostream.h" #include "audio/decoders/aiff.h" #include "audio/decoders/raw.h" #include "audio/mixer.h" #include "dgds/decompress.h" #include "dgds/dgds.h" #include "dgds/includes.h" #include "dgds/resource.h" #include "dgds/sound.h" #include "dgds/sound/music.h" #include "dgds/sound/resource/sci_resource.h" namespace Dgds { static const uint16 SIGNAL_OFFSET = 0xffff; // Offsets from sound/music num to playing ID. // This is to make a fake SCI-style "resource ID" static const int SND_RESOURCE_OFFSET = 4096; static const int MUSIC_RESOURCE_OFFSET = 8192; static const uint16 FLAG_LOOP = 1; static void _readHeader(const byte* &pos, uint32 &sci_header) { sci_header = 0; if (READ_LE_UINT16(pos) == 0x0084) sci_header = 2; pos += sci_header; if (pos[0] == 0xF0) { debug(1, "SysEx transfer = %d bytes", pos[1]); pos += 2; pos += 6; } } static void _readPartHeader(const byte* &pos, uint16 &off, uint16 &siz) { pos += 2; off = READ_LE_UINT16(pos); pos += 2; siz = READ_LE_UINT16(pos); pos += 2; } static void _skipPartHeader(const byte* &pos) { pos += 6; } static uint32 _availableSndTracks(const byte *data, uint32 size) { const byte *pos = data; uint32 sci_header; _readHeader(pos, sci_header); uint32 tracks = 0; while (pos[0] != 0xFF) { byte drv = *pos++; //debug(1, "(%d)", drv); while (pos[0] != 0xFF) { uint16 off, siz; _readPartHeader(pos, off, siz); off += sci_header; //debug(1, "%06d:%d ", off, siz); //debug(1, "Header bytes"); //debug(1, "[%06X] ", data[off]); //debug(1, "[%02X] ", data[off+0]); //debug(1, "[%02X] ", data[off+1]); bool digital_pcm = false; if (READ_LE_UINT16(&data[off]) == 0x00FE) { digital_pcm = true; } switch (drv) { case 0: if (digital_pcm) { //debug(1, "- Soundblaster"); tracks |= DIGITAL_PCM; } else { //debug(1, "- Adlib"); tracks |= TRACK_ADLIB; } break; case 7: //debug(1, "- General MIDI"); tracks |= TRACK_GM; break; case 9: //debug(1, "- CMS"); tracks |= TRACK_CMS; break; case 12: //debug(1, "- MT-32"); tracks |= TRACK_MT32; break; case 18: //debug(1, "- PC Speaker"); tracks |= TRACK_PCSPK; break; case 19: //debug(1, "- Tandy 1000"); tracks |= TRACK_TANDY; break; default: //debug(1, "- Unknown %d", drv); warning("Unknown music type %d", drv); break; } } pos++; } pos++; return tracks; } static byte _loadSndTrack(uint32 track, const byte** trackPtr, uint16* trackSiz, const byte *data, uint32 size) { byte matchDrv; switch (track) { case DIGITAL_PCM: case TRACK_ADLIB: matchDrv = 0; break; case TRACK_GM: matchDrv = 7; break; case TRACK_MT32: matchDrv = 12; break; default: return 0; } const byte *pos = data; uint32 sci_header; _readHeader(pos, sci_header); while (pos[0] != 0xFF) { byte drv = *pos++; byte part; const byte *ptr; part = 0; for (ptr = pos; *ptr != 0xFF; _skipPartHeader(ptr)) part++; if (matchDrv == drv) { part = 0; while (pos[0] != 0xFF) { uint16 off, siz; _readPartHeader(pos, off, siz); off += sci_header; trackPtr[part] = data + off; trackSiz[part] = siz; part++; } debug(1, "- (%d) Play parts = %d", drv, part); return part; } else { pos = ptr; } pos++; } pos++; return 0; } Sound::Sound(Audio::Mixer *mixer, ResourceManager *resource, Decompressor *decompressor) : _mixer(mixer), _resource(resource), _decompressor(decompressor), _music(nullptr), _isMusicMuted(false), _isSfxMuted(false) { ARRAYCLEAR(_channels); _music = new SciMusic(true); _music->init(); } Sound::~Sound() { unloadMusic(); for (auto &data: _sfxData) delete [] data._data; delete _music; } void Sound::playAmigaSfx(const Common::String &filename, byte channel, byte volume) { if (!filename.hasSuffixIgnoreCase(".ins")) error("Unhandled SFX file type: %s", filename.c_str()); Common::SeekableReadStream *sfxStream = _resource->getResource(filename); if (!sfxStream) { warning("SFX file %s not found", filename.c_str()); return; } byte *dest = new byte[sfxStream->size()]; sfxStream->read(dest, sfxStream->size()); Common::MemoryReadStream *soundData = new Common::MemoryReadStream(dest, sfxStream->size(), DisposeAfterUse::YES); delete sfxStream; stopSfxForChannel(channel); Channel *ch = &_channels[channel]; Audio::AudioStream *input = Audio::makeAIFFStream(soundData, DisposeAfterUse::YES); _mixer->playStream(Audio::Mixer::kSFXSoundType, &ch->handle, input, -1, volume); } void Sound::stopAllSfx() { _music->stopSFX(); for (uint i = 0; i < ARRAYSIZE(_channels); i++) stopSfxForChannel(i); } void Sound::stopSfxForChannel(byte channel) { if (_mixer->isSoundHandleActive(_channels[channel].handle)) { _mixer->stopHandle(_channels[channel].handle); _channels[channel].stream = 0; } } bool Sound::playPCM(const byte *data, uint32 size) { _mixer->stopAll(); if (!data) return false; const byte *trackPtr[0xFF]; uint16 trackSiz[0xFF]; byte numParts = _loadSndTrack(DIGITAL_PCM, trackPtr, trackSiz, data, size); if (numParts == 0) return false; for (byte part = 0; part < numParts; part++) { const byte *ptr = trackPtr[part]; bool digital_pcm = false; if (READ_LE_UINT16(ptr) == 0x00FE) { digital_pcm = true; } ptr += 2; if (!digital_pcm) continue; uint16 rate, length, first, last; rate = READ_LE_UINT16(ptr); length = READ_LE_UINT16(ptr + 2); first = READ_LE_UINT16(ptr + 4); last = READ_LE_UINT16(ptr + 6); ptr += 8; ptr += first; debug(1, " - Digital PCM: %u Hz, [%u]=%u:%u", rate, length, first, last); trackPtr[part] = ptr; trackSiz[part] = length; Channel *ch = &_channels[part]; byte volume = 127; Audio::AudioStream *input = Audio::makeRawStream(trackPtr[part], trackSiz[part], rate, Audio::FLAG_UNSIGNED, DisposeAfterUse::NO); _mixer->playStream(Audio::Mixer::kSFXSoundType, &ch->handle, input, -1, volume, 0, DisposeAfterUse::YES); } return true; } static void _readStrings(Common::SeekableReadStream *stream) { uint16 count = stream->readUint16LE(); debug(1, " %u strs:", count); for (uint16 k = 0; k < count; k++) { uint16 idx = stream->readUint16LE(); Common::String str = stream->readString(); debug(1, " %2u: %2u, \"%s\"", k, idx, str.c_str()); } } bool Sound::loadSXSoundData(const Common::String &filename, Common::Array &dataArray, Common::HashMap &idMap) { if (!filename.hasSuffixIgnoreCase(".sx")) error("Unhandled SX file type: %s", filename.c_str()); Common::SeekableReadStream *resStream = _resource->getResource(filename); if (!resStream) { warning("SX file %s not found", filename.c_str()); return false; } DgdsChunkReader chunk(resStream); while (chunk.readNextHeader(EX_SX, filename)) { if (chunk.isContainer()) { continue; } chunk.readContent(_decompressor); Common::SeekableReadStream *stream = chunk.getContent(); if (chunk.isSection(ID_INF)) { uint16 type = stream->readUint16LE(); uint16 count = stream->readUint16LE(); debug(1, " SX INF %u [%u entries]: (%s)", type, count, filename.c_str()); for (uint16 k = 0; k < count; k++) { uint16 idx = stream->readUint16LE(); debug(10, " %2u: %u", k, idx); idMap[idx] = k; } } else if (chunk.isSection(ID_TAG) || chunk.isSection(ID_FNM)) { _readStrings(stream); } else if (chunk.isSection(ID_DAT)) { // TODO: Should we record the indexes? /*uint16 idx = */ stream->readUint16LE(); /*uint16 type = */ stream->readUint16LE(); SoundData soundData; soundData._data = _decompressor->decompress(stream, stream->size() - stream->pos(), soundData._size); dataArray.push_back(soundData); } } delete resStream; return true; } bool Sound::loadMusic(const Common::String &filename) { if (filename == _currentMusic) return false; unloadMusic(); if (filename.hasSuffixIgnoreCase(".sx")) { loadSXSoundData(filename, _musicData, _musicIdMap); } else if (filename.hasSuffixIgnoreCase(".sng")) { _musicIdMap.clear(); loadSNGSoundData(filename, _musicData); } else { error("Unhandled music file type: %s", filename.c_str()); } _currentMusic = filename; debug(1, "Sound: Loaded music %s with %d entries", filename.c_str(), _musicData.size()); return true; } void Sound::loadSFX(const Common::String &filename) { if (_sfxData.size()) error("Sound: SFX data should only be loaded once"); if (filename.hasSuffixIgnoreCase(".sx")) { loadSXSoundData(filename, _sfxData, _sfxIdMap); } else if (filename.hasSuffixIgnoreCase(".sng")) { loadSNGSoundData(filename, _sfxData); } else { error("Unhandled SFX file type: %s", filename.c_str()); } debug(1, "Sound: Loaded sfx %s with %d entries", filename.c_str(), _sfxData.size()); } void Sound::loadSNGSoundData(const Common::String &filename, Common::Array &dataArray) { if (!filename.hasSuffixIgnoreCase(".sng")) error("Unhandled SNG file type: %s", filename.c_str()); Common::SeekableReadStream *resStream = _resource->getResource(filename); if (!resStream) error("Music file %s not found", filename.c_str()); DgdsChunkReader chunk(resStream); while (chunk.readNextHeader(EX_SNG, filename)) { if (chunk.isContainer()) { continue; } chunk.readContent(_decompressor); Common::SeekableReadStream *stream = chunk.getContent(); if (chunk.isSection(ID_SNG)) { SoundData soundData; soundData._size = stream->size(); byte *data = new byte[soundData._size]; stream->read(data, soundData._size); soundData._data = data; dataArray.push_back(soundData); } else if (chunk.isSection(ID_INF)) { uint32 count = stream->size() / 2; if (count > dataArray.size()) error("Sound: %s has more flags in INF than SNG entries.", filename.c_str()); debug(1, " SNG INF [%u entries]", count); for (uint32 k = 0; k < count; k++) { uint16 flags = stream->readUint16LE(); debug(10, " %2u: 0x%04x", k, flags); dataArray[k]._flags = flags; } } else { warning("loadPCSound: skip unused chunk %s in %s", chunk.getIdStr(), filename.c_str()); } } delete resStream; } int Sound::mapSfxNum(int num) const { // Fixed offset in Dragon and HoC? if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON || DgdsEngine::getInstance()->getGameId() == GID_HOC) return num - 24; else if (_sfxIdMap.contains(num)) return _sfxIdMap[num]; return num; } int Sound::mapMusicNum(int num) const { if (_musicIdMap.contains(num)) return _musicIdMap[num]; return num; } void Sound::playSFX(int num) { int mappedNum = mapSfxNum(num); debug(1, "Sound: Play SFX %d (-> %d), have %d entries", num, mappedNum, _sfxData.size()); playPCSound(mappedNum, _sfxData, Audio::Mixer::kSFXSoundType); } void Sound::stopSfxByNum(int num) { int mappedNum = mapSfxNum(num); debug(1, "Sound: Stop SFX %d (-> %d)", num, mappedNum); MusicEntry *musicSlot = _music->getSlot(mappedNum + SND_RESOURCE_OFFSET); if (!musicSlot) { debug(1, "stopSfxByNum: Slot for sfx num %d not found.", mappedNum); return; } musicSlot->dataInc = 0; musicSlot->signal = SIGNAL_OFFSET; _music->soundStop(musicSlot); } void Sound::playMusic(int num) { int mappedNum = mapMusicNum(num); debug(1, "Sound: Play music %d (-> %d, %s), have %d entries", num, mappedNum, _currentMusic.c_str(), _musicData.size()); playPCSound(mappedNum, _musicData, Audio::Mixer::kMusicSoundType); } void Sound::playMusicOrSFX(int num) { if (_musicIdMap.contains(num)) { playMusic(num); } else { playSFX(num); } } void Sound::stopMusicOrSFX(int num) { if (_musicIdMap.contains(num)) { stopMusic(); } else { stopSfxByNum(num); } } void Sound::pauseMusicOrSFX(int num) { if (_musicIdMap.contains(num)) { // NOTE: We assume there is only ever one music here.. _music->pauseMusic(); } else { warning("Sound: TODO: Implement pause SFX %d", num); } } void Sound::unpauseMusicOrSFX(int num) { if (_musicIdMap.contains(num)) { // NOTE: We assume there is only ever one music here.. _music->resumeMusic(); } else { warning("Sound: TODO: Implement unpause SFX %d", num); } } void Sound::processInitSound(uint32 obj, const SoundData &data, Audio::Mixer::SoundType soundType) { // Check if a track with the same sound object is already playing MusicEntry *oldSound = _music->getSlot(obj); if (oldSound) { processDisposeSound(obj); } MusicEntry *newSound = new MusicEntry(); newSound->resourceId = obj; newSound->soundObj = obj; newSound->loop = 0; // set in processPlaySound newSound->overridePriority = true; newSound->priority = 255; newSound->volume = MUSIC_VOLUME_DEFAULT; newSound->reverb = -1; // initialize to SCI invalid, it'll be set correctly in soundInitSnd() below //debug(10, "processInitSound: %08x number %d, loop %d, prio %d, vol %d", obj, // obj, newSound->loop, newSound->priority, newSound->volume); initSoundResource(newSound, data, soundType); _music->pushBackSlot(newSound); } void Sound::initSoundResource(MusicEntry *newSound, const SoundData &data, Audio::Mixer::SoundType soundType) { if (newSound->resourceId) { // Skip the header. const byte *dataPtr = data._data; uint32 hdrSize = 0; _readHeader(dataPtr, hdrSize); newSound->soundRes = new SoundResource(newSound->resourceId, dataPtr, data._size - hdrSize); if (!newSound->soundRes->exists()) { delete newSound->soundRes; newSound->soundRes = nullptr; } } else { newSound->soundRes = nullptr; } if (!newSound->isSample && newSound->soundRes) _music->soundInitSnd(newSound); newSound->soundType = soundType; } void Sound::processDisposeSound(uint32 obj) { // Mostly copied from SCI soundcmd. MusicEntry *musicSlot = _music->getSlot(obj); if (!musicSlot) { warning("processDisposeSound: Slot not found (%08x)", obj); return; } processStopSound(obj, false); _music->soundKill(musicSlot); } void Sound::processStopSound(uint32 obj, bool sampleFinishedPlaying) { // Mostly copied from SCI soundcmd. MusicEntry *musicSlot = _music->getSlot(obj); if (!musicSlot) { warning("processStopSound: Slot not found (%08x)", obj); return; } musicSlot->dataInc = 0; musicSlot->signal = SIGNAL_OFFSET; _music->soundStop(musicSlot); } void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const SoundData &data) { // Mostly copied from SCI soundcmd. MusicEntry *musicSlot = _music->getSlot(obj); if (!musicSlot) { error("kDoSound(play): Slot not found (%08x)", obj); } int32 resourceId; if (!restoring) resourceId = 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, data, Audio::Mixer::kSFXSoundType); // Find slot again :) musicSlot = _music->getSlot(obj); } assert(musicSlot); musicSlot->loop = (data._flags & FLAG_LOOP) ? 1 : 0; // 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 { // Set higher priority on music than sounds. musicSlot->priority = (musicSlot->soundType == Audio::Mixer::kMusicSoundType ? 255 : 127); } // 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; musicSlot->volume = MUSIC_VOLUME_DEFAULT; debug(10, "processPlaySound: %08x number %d, sz %d, loop %d, prio %d, vol %d, bed %d", obj, resourceId, data._size, musicSlot->loop, musicSlot->priority, musicSlot->volume, playBed ? 1 : 0); _music->soundPlay(musicSlot, restoring); // Reset any left-over signals musicSlot->signal = 0; musicSlot->fadeStep = 0; } void Sound::playPCSound(int num, const Common::Array &dataArray, Audio::Mixer::SoundType soundType) { if (num >= 0 && num < (int)dataArray.size()) { const SoundData &data = dataArray[num]; uint32 tracks = _availableSndTracks(data._data, data._size); if (tracks & DIGITAL_PCM) { playPCM(data._data, data._size); } else { int idOffset = soundType == Audio::Mixer::kSFXSoundType ? SND_RESOURCE_OFFSET : MUSIC_RESOURCE_OFFSET; int soundId = num + idOffset; // Only play one music at a time, don't play sfx if sfx muted. if (soundType == Audio::Mixer::kMusicSoundType) { MusicEntry *currentMusic = _music->getSlot(soundId); // // Don't change music if we are already playing the same track. This happens // when walking through the house in Willy Beamish where all the rooms have // the same music track. // if (currentMusic && currentMusic->status == kSoundPlaying) return; stopMusic(); } else if (soundType == Audio::Mixer::kSFXSoundType && _isSfxMuted) { return; } processInitSound(soundId, data, soundType); processPlaySound(soundId, false, false, data); // Immediately pause new music if muted if (_isMusicMuted && soundType == Audio::Mixer::kMusicSoundType) _music->pauseMusic(); } } else { warning("Sound: Requested to play %d but only have %d tracks", num, dataArray.size()); } } void Sound::stopMusic() { debug(1, "Sound: Stop music."); _music->stopMusic(); } void Sound::muteSoundType(Audio::Mixer::SoundType soundType) { if (soundType == Audio::Mixer::kMusicSoundType) { _isMusicMuted = true; _music->pauseMusic(); } else if (soundType == Audio::Mixer::kSFXSoundType) { _isSfxMuted = true; stopAllSfx(); } else { error("Sound: Can only mute music or sfx, not sound type %d", soundType); } } void Sound::unmuteSoundType(Audio::Mixer::SoundType soundType) { if (soundType == Audio::Mixer::kMusicSoundType) { _isMusicMuted = false; _music->resumeMusic(); } else if (soundType == Audio::Mixer::kSFXSoundType) { _isSfxMuted = false; } else { error("Sound: Can only unmute music or sfx, not sound type %d", soundType); } } void Sound::unloadMusic() { stopMusic(); for (auto &data: _musicData) delete [] data._data; _musicData.clear(); _currentMusic.clear(); // Don't unload sfxData. } } // End of namespace Dgds